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
20CGameControllerDDRace::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
27CGameControllerDDRace::~CGameControllerDDRace() = default;
28
29CScore *CGameControllerDDRace::Score()
30{
31 return GameServer()->Score();
32}
33
34void 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
118void CGameControllerDDRace::SetArmorProgress(CCharacter *pCharacter, int Progress)
119{
120 pCharacter->SetArmor(std::clamp(val: 10 - (Progress / 15), lo: 0, hi: 10));
121}
122
123int 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
154IGameController::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
168void 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
191void 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
209void CGameControllerDDRace::OnReset()
210{
211 IGameController::OnReset();
212 Teams().Reset();
213}
214
215void CGameControllerDDRace::Tick()
216{
217 IGameController::Tick();
218 Teams().ProcessSaveTeam();
219 Teams().Tick();
220}
221
222void 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