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