| 1 | /* (c) Shereef Marzouk. See "licence DDRace.txt" and the readme.txt in the root of the distribution for more information. */ |
| 2 | /* Based on Race mod stuff and tweaked by GreYFoX@GTi and others to fit our DDRace needs. */ |
| 3 | #include "DDRace.h" |
| 4 | |
| 5 | #include <engine/server.h> |
| 6 | #include <engine/shared/config.h> |
| 7 | #include <engine/shared/protocol.h> |
| 8 | #include <engine/shared/protocol7.h> |
| 9 | |
| 10 | #include <game/mapitems.h> |
| 11 | #include <game/server/entities/character.h> |
| 12 | #include <game/server/gamecontext.h> |
| 13 | #include <game/server/player.h> |
| 14 | #include <game/server/score.h> |
| 15 | #include <game/version.h> |
| 16 | |
| 17 | #define GAME_TYPE_NAME "DDraceNetwork" |
| 18 | #define TEST_TYPE_NAME "TestDDraceNetwork" |
| 19 | |
| 20 | CGameControllerDDRace::CGameControllerDDRace(class CGameContext *pGameServer) : |
| 21 | IGameController(pGameServer) |
| 22 | { |
| 23 | m_pGameType = g_Config.m_SvTestingCommands ? TEST_TYPE_NAME : GAME_TYPE_NAME; |
| 24 | m_GameFlags = protocol7::GAMEFLAG_RACE; |
| 25 | } |
| 26 | |
| 27 | CGameControllerDDRace::~CGameControllerDDRace() = default; |
| 28 | |
| 29 | CScore *CGameControllerDDRace::Score() |
| 30 | { |
| 31 | return GameServer()->Score(); |
| 32 | } |
| 33 | |
| 34 | void CGameControllerDDRace::HandleCharacterTiles(CCharacter *pChr, int MapIndex) |
| 35 | { |
| 36 | CPlayer *pPlayer = pChr->GetPlayer(); |
| 37 | const int ClientId = pPlayer->GetCid(); |
| 38 | |
| 39 | int TileIndex = GameServer()->Collision()->GetTileIndex(Index: MapIndex); |
| 40 | int TileFIndex = GameServer()->Collision()->GetFrontTileIndex(Index: MapIndex); |
| 41 | |
| 42 | //Sensitivity |
| 43 | int S1 = GameServer()->Collision()->GetPureMapIndex(Pos: vec2(pChr->GetPos().x + pChr->GetProximityRadius() / 3.f, pChr->GetPos().y - pChr->GetProximityRadius() / 3.f)); |
| 44 | int S2 = GameServer()->Collision()->GetPureMapIndex(Pos: vec2(pChr->GetPos().x + pChr->GetProximityRadius() / 3.f, pChr->GetPos().y + pChr->GetProximityRadius() / 3.f)); |
| 45 | int S3 = GameServer()->Collision()->GetPureMapIndex(Pos: vec2(pChr->GetPos().x - pChr->GetProximityRadius() / 3.f, pChr->GetPos().y - pChr->GetProximityRadius() / 3.f)); |
| 46 | int S4 = GameServer()->Collision()->GetPureMapIndex(Pos: vec2(pChr->GetPos().x - pChr->GetProximityRadius() / 3.f, pChr->GetPos().y + pChr->GetProximityRadius() / 3.f)); |
| 47 | int Tile1 = GameServer()->Collision()->GetTileIndex(Index: S1); |
| 48 | int Tile2 = GameServer()->Collision()->GetTileIndex(Index: S2); |
| 49 | int Tile3 = GameServer()->Collision()->GetTileIndex(Index: S3); |
| 50 | int Tile4 = GameServer()->Collision()->GetTileIndex(Index: S4); |
| 51 | int FTile1 = GameServer()->Collision()->GetFrontTileIndex(Index: S1); |
| 52 | int FTile2 = GameServer()->Collision()->GetFrontTileIndex(Index: S2); |
| 53 | int FTile3 = GameServer()->Collision()->GetFrontTileIndex(Index: S3); |
| 54 | int FTile4 = GameServer()->Collision()->GetFrontTileIndex(Index: S4); |
| 55 | |
| 56 | const ERaceState PlayerDDRaceState = pChr->m_DDRaceState; |
| 57 | bool IsOnStartTile = (TileIndex == TILE_START) || (TileFIndex == TILE_START) || FTile1 == TILE_START || FTile2 == TILE_START || FTile3 == TILE_START || FTile4 == TILE_START || Tile1 == TILE_START || Tile2 == TILE_START || Tile3 == TILE_START || Tile4 == TILE_START; |
| 58 | // start |
| 59 | if(IsOnStartTile && PlayerDDRaceState != ERaceState::CHEATED) |
| 60 | { |
| 61 | const int Team = GameServer()->GetDDRaceTeam(ClientId); |
| 62 | if(Teams().GetSaving(TeamId: Team)) |
| 63 | { |
| 64 | GameServer()->SendStartWarning(ClientId, pMessage: "You can't start while loading/saving of team is in progress" ); |
| 65 | pChr->Die(Killer: ClientId, Weapon: WEAPON_WORLD); |
| 66 | return; |
| 67 | } |
| 68 | if(g_Config.m_SvTeam == SV_TEAM_MANDATORY && (Team == TEAM_FLOCK || Teams().Count(Team) <= 1)) |
| 69 | { |
| 70 | GameServer()->SendStartWarning(ClientId, pMessage: "You have to be in a team with other tees to start" ); |
| 71 | pChr->Die(Killer: ClientId, Weapon: WEAPON_WORLD); |
| 72 | return; |
| 73 | } |
| 74 | if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && Team != TEAM_FLOCK && Teams().IsValidTeamNumber(Team) && Teams().Count(Team) < g_Config.m_SvMinTeamSize && !Teams().TeamFlock(Team)) |
| 75 | { |
| 76 | char aBuf[128]; |
| 77 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Your team has fewer than %d players, so your team rank won't count" , g_Config.m_SvMinTeamSize); |
| 78 | GameServer()->SendStartWarning(ClientId, pMessage: aBuf); |
| 79 | } |
| 80 | if(g_Config.m_SvResetPickups) |
| 81 | { |
| 82 | pChr->ResetPickups(); |
| 83 | } |
| 84 | |
| 85 | Teams().OnCharacterStart(ClientId); |
| 86 | pChr->m_LastTimeCp = -1; |
| 87 | pChr->m_LastTimeCpBroadcasted = -1; |
| 88 | for(float &CurrentTimeCp : pChr->m_aCurrentTimeCp) |
| 89 | { |
| 90 | CurrentTimeCp = 0.0f; |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | // finish |
| 95 | if(((TileIndex == TILE_FINISH) || (TileFIndex == TILE_FINISH) || FTile1 == TILE_FINISH || FTile2 == TILE_FINISH || FTile3 == TILE_FINISH || FTile4 == TILE_FINISH || Tile1 == TILE_FINISH || Tile2 == TILE_FINISH || Tile3 == TILE_FINISH || Tile4 == TILE_FINISH) && PlayerDDRaceState == ERaceState::STARTED) |
| 96 | Teams().OnCharacterFinish(ClientId); |
| 97 | |
| 98 | // unlock team |
| 99 | else if(((TileIndex == TILE_UNLOCK_TEAM) || (TileFIndex == TILE_UNLOCK_TEAM)) && Teams().TeamLocked(Team: GameServer()->GetDDRaceTeam(ClientId))) |
| 100 | { |
| 101 | Teams().SetTeamLock(Team: GameServer()->GetDDRaceTeam(ClientId), Lock: false); |
| 102 | GameServer()->SendChatTeam(Team: GameServer()->GetDDRaceTeam(ClientId), pText: "Your team was unlocked by an unlock team tile" ); |
| 103 | } |
| 104 | |
| 105 | // solo part |
| 106 | if(((TileIndex == TILE_SOLO_ENABLE) || (TileFIndex == TILE_SOLO_ENABLE)) && !Teams().m_Core.GetSolo(ClientId)) |
| 107 | { |
| 108 | GameServer()->SendChatTarget(To: ClientId, pText: "You are now in a solo part" ); |
| 109 | pChr->SetSolo(true); |
| 110 | } |
| 111 | else if(((TileIndex == TILE_SOLO_DISABLE) || (TileFIndex == TILE_SOLO_DISABLE)) && Teams().m_Core.GetSolo(ClientId)) |
| 112 | { |
| 113 | GameServer()->SendChatTarget(To: ClientId, pText: "You are now out of the solo part" ); |
| 114 | pChr->SetSolo(false); |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | void CGameControllerDDRace::SetArmorProgress(CCharacter *pCharacter, int Progress) |
| 119 | { |
| 120 | pCharacter->SetArmor(std::clamp(val: 10 - (Progress / 15), lo: 0, hi: 10)); |
| 121 | } |
| 122 | |
| 123 | int CGameControllerDDRace::SnapPlayerScore(int SnappingClient, CPlayer *pPlayer) |
| 124 | { |
| 125 | bool HideScore = g_Config.m_SvHideScore && SnappingClient != pPlayer->GetCid(); |
| 126 | std::optional<float> Score = GameServer()->Score()->PlayerData(Id: pPlayer->GetCid())->m_BestTime; |
| 127 | |
| 128 | if(Server()->IsSixup(ClientId: SnappingClient)) |
| 129 | { |
| 130 | if(!Score.has_value() || HideScore) |
| 131 | return protocol7::FinishTime::NOT_FINISHED; |
| 132 | |
| 133 | // Times are in milliseconds for 0.7 |
| 134 | return Score.value() * 1000.0f; |
| 135 | } |
| 136 | |
| 137 | // This is the time sent to the player while ingame (do not confuse to the one reported to the master server). |
| 138 | // Due to clients expecting this as a negative value, we have to make sure it's negative. |
| 139 | // Special numbers: |
| 140 | // -9999 or FinishTime::NOT_FINISHED_TIMESCORE: means no time and isn't displayed in the scoreboard. |
| 141 | if(!Score.has_value() || HideScore) |
| 142 | return FinishTime::NOT_FINISHED_TIMESCORE; |
| 143 | |
| 144 | // Times are in seconds for 0.6 |
| 145 | int ScoreSeconds = Score.value(); |
| 146 | |
| 147 | // shift the time by a second if the player actually took 9999 |
| 148 | // seconds to finish the map. |
| 149 | if(-ScoreSeconds == FinishTime::NOT_FINISHED_TIMESCORE) |
| 150 | return -ScoreSeconds - 1; |
| 151 | return -ScoreSeconds; |
| 152 | } |
| 153 | |
| 154 | IGameController::CFinishTime CGameControllerDDRace::SnapPlayerTime(int SnappingClient, CPlayer *pPlayer) |
| 155 | { |
| 156 | std::optional<float> BestTime = GameServer()->Score()->PlayerData(Id: pPlayer->GetCid())->m_BestTime; |
| 157 | if(BestTime.has_value() && (!g_Config.m_SvHideScore || SnappingClient == pPlayer->GetCid())) |
| 158 | { |
| 159 | // same as in str_time_float |
| 160 | int64_t TimeMilliseconds = static_cast<int64_t>(std::roundf(x: BestTime.value() * 1000.0f)); |
| 161 | int Seconds = static_cast<int>(TimeMilliseconds / 1000); |
| 162 | int Millis = static_cast<int>(TimeMilliseconds % 1000); |
| 163 | return CFinishTime(Seconds, Millis); |
| 164 | } |
| 165 | return CFinishTime::NotFinished(); |
| 166 | } |
| 167 | |
| 168 | void CGameControllerDDRace::OnPlayerConnect(CPlayer *pPlayer) |
| 169 | { |
| 170 | IGameController::OnPlayerConnect(pPlayer); |
| 171 | int ClientId = pPlayer->GetCid(); |
| 172 | |
| 173 | // init the player |
| 174 | Score()->PlayerData(Id: ClientId)->Reset(); |
| 175 | |
| 176 | // Can't set score here as LoadScore() is threaded, run it in |
| 177 | // LoadScoreThreaded() instead |
| 178 | Score()->LoadPlayerData(ClientId); |
| 179 | |
| 180 | if(!Server()->ClientPrevIngame(ClientId)) |
| 181 | { |
| 182 | char aBuf[512]; |
| 183 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' entered and joined the %s" , Server()->ClientName(ClientId), GetTeamName(Team: pPlayer->GetTeam())); |
| 184 | GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf, SpamProtectionClientId: -1, VersionFlags: CGameContext::FLAG_SIX); |
| 185 | |
| 186 | GameServer()->SendChatTarget(To: ClientId, pText: "DDraceNetwork Mod. Version: " GAME_VERSION); |
| 187 | GameServer()->SendChatTarget(To: ClientId, pText: "please visit DDNet.org or say /info and make sure to read our /rules" ); |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | void CGameControllerDDRace::OnPlayerDisconnect(CPlayer *pPlayer, const char *pReason) |
| 192 | { |
| 193 | int ClientId = pPlayer->GetCid(); |
| 194 | bool WasModerator = pPlayer->m_Moderating && Server()->ClientIngame(ClientId); |
| 195 | |
| 196 | IGameController::OnPlayerDisconnect(pPlayer, pReason); |
| 197 | |
| 198 | if(!GameServer()->PlayerModerating() && WasModerator) |
| 199 | GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: "Server kick/spec votes are no longer actively moderated." ); |
| 200 | |
| 201 | if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO) |
| 202 | Teams().SetForceCharacterTeam(ClientId, Team: TEAM_FLOCK); |
| 203 | |
| 204 | for(int Team = TEAM_FLOCK + 1; Team < TEAM_SUPER; Team++) |
| 205 | if(Teams().IsInvited(Team, ClientId)) |
| 206 | Teams().SetClientInvited(Team, ClientId, Invited: false); |
| 207 | } |
| 208 | |
| 209 | void CGameControllerDDRace::OnReset() |
| 210 | { |
| 211 | IGameController::OnReset(); |
| 212 | Teams().Reset(); |
| 213 | } |
| 214 | |
| 215 | void CGameControllerDDRace::Tick() |
| 216 | { |
| 217 | IGameController::Tick(); |
| 218 | Teams().ProcessSaveTeam(); |
| 219 | Teams().Tick(); |
| 220 | } |
| 221 | |
| 222 | void CGameControllerDDRace::DoTeamChange(class CPlayer *pPlayer, int Team, bool DoChatMsg) |
| 223 | { |
| 224 | if(!IsValidTeam(Team)) |
| 225 | return; |
| 226 | |
| 227 | if(Team == pPlayer->GetTeam()) |
| 228 | return; |
| 229 | |
| 230 | CCharacter *pCharacter = pPlayer->GetCharacter(); |
| 231 | |
| 232 | if(Team == TEAM_SPECTATORS) |
| 233 | { |
| 234 | if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && pCharacter) |
| 235 | { |
| 236 | Teams().OnCharacterDeath(ClientId: pPlayer->GetCid(), Weapon: WEAPON_GAME); |
| 237 | // Joining spectators should not kill a locked team, but should still |
| 238 | // check if the team finished by you leaving it. |
| 239 | int DDRTeam = pCharacter->Team(); |
| 240 | Teams().SetForceCharacterTeam(ClientId: pPlayer->GetCid(), Team: TEAM_FLOCK); |
| 241 | Teams().CheckTeamFinished(Team: DDRTeam); |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | IGameController::DoTeamChange(pPlayer, Team, DoChatMsg); |
| 246 | } |
| 247 | |