1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "gamecontroller.h"
4
5#include "entities/character.h"
6#include "entities/door.h"
7#include "entities/dragger.h"
8#include "entities/gun.h"
9#include "entities/light.h"
10#include "entities/pickup.h"
11#include "entities/projectile.h"
12#include "gamecontext.h"
13#include "player.h"
14
15#include <engine/shared/config.h>
16#include <engine/shared/protocolglue.h>
17
18#include <generated/protocol.h>
19
20#include <game/mapitems.h>
21#include <game/server/score.h>
22#include <game/teamscore.h>
23
24IGameController::IGameController(class CGameContext *pGameServer) :
25 m_Teams(pGameServer), m_pLoadBestTimeResult(nullptr)
26{
27 m_pGameServer = pGameServer;
28 m_pConfig = m_pGameServer->Config();
29 m_pServer = m_pGameServer->Server();
30 m_pGameType = "unknown";
31
32 //
33 DoWarmup(Seconds: g_Config.m_SvWarmup);
34 m_GameOverTick = -1;
35 m_SuddenDeath = 0;
36 m_RoundStartTick = Server()->Tick();
37 m_RoundCount = 0;
38 m_GameFlags = 0;
39 m_aMapWish[0] = 0;
40
41 m_CurrentRecord.reset();
42}
43
44IGameController::~IGameController() = default;
45
46void IGameController::DoActivityCheck()
47{
48 if(g_Config.m_SvInactiveKickTime == 0)
49 return;
50
51 for(int i = 0; i < MAX_CLIENTS; ++i)
52 {
53 if(GameServer()->m_apPlayers[i] && GameServer()->m_apPlayers[i]->GetTeam() != TEAM_SPECTATORS && !Server()->IsRconAuthed(ClientId: i))
54 {
55 if(Server()->Tick() > GameServer()->m_apPlayers[i]->m_LastActionTick + g_Config.m_SvInactiveKickTime * Server()->TickSpeed() * 60)
56 {
57 switch(g_Config.m_SvInactiveKick)
58 {
59 case 0:
60 {
61 // move player to spectator
62 DoTeamChange(pPlayer: GameServer()->m_apPlayers[i], Team: TEAM_SPECTATORS);
63 }
64 break;
65 case 1:
66 {
67 // move player to spectator if the reserved slots aren't filled yet, kick him otherwise
68 int Spectators = 0;
69 for(auto &pPlayer : GameServer()->m_apPlayers)
70 if(pPlayer && pPlayer->GetTeam() == TEAM_SPECTATORS)
71 ++Spectators;
72 if(Spectators >= g_Config.m_SvSpectatorSlots)
73 Server()->Kick(ClientId: i, pReason: "Kicked for inactivity");
74 else
75 DoTeamChange(pPlayer: GameServer()->m_apPlayers[i], Team: TEAM_SPECTATORS);
76 }
77 break;
78 case 2:
79 {
80 // kick the player
81 Server()->Kick(ClientId: i, pReason: "Kicked for inactivity");
82 }
83 }
84 }
85 }
86 }
87}
88
89float IGameController::EvaluateSpawnPos(CSpawnEval *pEval, vec2 Pos, int ClientId)
90{
91 float Score = 0.0f;
92 CCharacter *pC = static_cast<CCharacter *>(GameServer()->m_World.FindFirst(Type: CGameWorld::ENTTYPE_CHARACTER));
93 for(; pC; pC = (CCharacter *)pC->TypeNext())
94 {
95 if(!pC->CanCollide(ClientId))
96 continue;
97
98 float d = distance(a: Pos, b: pC->m_Pos);
99 Score += d == 0 ? 1000000000.0f : 1.0f / d;
100 }
101
102 return Score;
103}
104
105void IGameController::EvaluateSpawnType(CSpawnEval *pEval, ESpawnType SpawnType, int ClientId)
106{
107 const bool PlayerCollision = GameServer()->GlobalTuning()->m_PlayerCollision;
108
109 bool PlayerCollisionDisabled = false;
110 CCharacter *pPlayerCharacter = GameServer()->GetPlayerChar(ClientId);
111 if(pPlayerCharacter)
112 PlayerCollisionDisabled = pPlayerCharacter->GetCore().m_CollisionDisabled;
113
114 // make sure players keep spawning at the same tile
115 // on race maps no matter what
116 if(!PlayerCollision && pEval->m_Got)
117 return;
118
119 // j == 0: Find an empty slot, j == 1: Take any slot if no empty one found
120 for(int j = 0; j < 2; j++)
121 {
122 // get spawn point
123 for(const vec2 &SpawnPoint : m_avSpawnPoints[SpawnType])
124 {
125 vec2 P = SpawnPoint;
126 if(j == 0)
127 {
128 // check if the position is occupado
129 CEntity *apEnts[MAX_CLIENTS];
130 int Num = GameServer()->m_World.FindEntities(Pos: SpawnPoint, Radius: 64, ppEnts: apEnts, Max: MAX_CLIENTS, Type: CGameWorld::ENTTYPE_CHARACTER);
131 vec2 aPositions[5] = {vec2(0.0f, 0.0f), vec2(-32.0f, 0.0f), vec2(0.0f, -32.0f), vec2(32.0f, 0.0f), vec2(0.0f, 32.0f)}; // start, left, up, right, down
132 int Result = -1;
133 for(int Index = 0; Index < 5 && Result == -1; ++Index)
134 {
135 Result = Index;
136 if(!PlayerCollision || PlayerCollisionDisabled)
137 break;
138 for(int c = 0; c < Num; ++c)
139 {
140 CCharacter *pChr = static_cast<CCharacter *>(apEnts[c]);
141 const bool CanCollide = pChr->CanCollide(ClientId) && !pChr->GetCore().m_CollisionDisabled;
142
143 if(GameServer()->Collision()->CheckPoint(Pos: SpawnPoint + aPositions[Index]) ||
144 (CanCollide && distance(a: pChr->m_Pos, b: SpawnPoint + aPositions[Index]) <= pChr->GetProximityRadius()))
145 {
146 Result = -1;
147 break;
148 }
149 }
150 }
151 if(Result == -1)
152 continue; // try next spawn point
153
154 P += aPositions[Result];
155 }
156
157 float S = EvaluateSpawnPos(pEval, Pos: P, ClientId);
158 if(!pEval->m_Got || (j == 0 && pEval->m_Score > S))
159 {
160 pEval->m_Got = true;
161 pEval->m_Score = S;
162 pEval->m_Pos = P;
163 }
164 }
165 }
166}
167
168bool IGameController::CanSpawn(int Team, vec2 *pOutPos, int ClientId)
169{
170 // spectators can't spawn
171 if(Team == TEAM_SPECTATORS)
172 return false;
173
174 CSpawnEval Eval;
175 EvaluateSpawnType(pEval: &Eval, SpawnType: SPAWNTYPE_DEFAULT, ClientId);
176 EvaluateSpawnType(pEval: &Eval, SpawnType: SPAWNTYPE_RED, ClientId);
177 EvaluateSpawnType(pEval: &Eval, SpawnType: SPAWNTYPE_BLUE, ClientId);
178
179 *pOutPos = Eval.m_Pos;
180 return Eval.m_Got;
181}
182
183bool IGameController::OnEntity(int Index, int x, int y, int Layer, int Flags, bool Initial, int Number)
184{
185 dbg_assert(Index >= 0, "Invalid entity index");
186
187 const vec2 Pos(x * 32.0f + 16.0f, y * 32.0f + 16.0f);
188
189 int aSides[8];
190 aSides[0] = GameServer()->Collision()->Entity(x, y: y + 1, Layer);
191 aSides[1] = GameServer()->Collision()->Entity(x: x + 1, y: y + 1, Layer);
192 aSides[2] = GameServer()->Collision()->Entity(x: x + 1, y, Layer);
193 aSides[3] = GameServer()->Collision()->Entity(x: x + 1, y: y - 1, Layer);
194 aSides[4] = GameServer()->Collision()->Entity(x, y: y - 1, Layer);
195 aSides[5] = GameServer()->Collision()->Entity(x: x - 1, y: y - 1, Layer);
196 aSides[6] = GameServer()->Collision()->Entity(x: x - 1, y, Layer);
197 aSides[7] = GameServer()->Collision()->Entity(x: x - 1, y: y + 1, Layer);
198
199 if(Index >= ENTITY_SPAWN && Index <= ENTITY_SPAWN_BLUE && Initial)
200 {
201 const int SpawnType = Index - ENTITY_SPAWN;
202 m_avSpawnPoints[SpawnType].push_back(x: Pos);
203 }
204 else if(Index == ENTITY_DOOR)
205 {
206 for(int i = 0; i < 8; i++)
207 {
208 if(aSides[i] >= ENTITY_LASER_SHORT && aSides[i] <= ENTITY_LASER_LONG)
209 {
210 new CDoor(
211 &GameServer()->m_World, //GameWorld
212 Pos, //Pos
213 pi / 4 * i, //Rotation
214 32 * 3 + 32 * (aSides[i] - ENTITY_LASER_SHORT) * 3, //Length
215 Number //Number
216 );
217 }
218 }
219 }
220 else if(Index == ENTITY_CRAZY_SHOTGUN_EX)
221 {
222 int Dir;
223 if(!Flags)
224 Dir = 0;
225 else if(Flags == ROTATION_90)
226 Dir = 1;
227 else if(Flags == ROTATION_180)
228 Dir = 2;
229 else
230 Dir = 3;
231 float Deg = Dir * (pi / 2);
232 CProjectile *pBullet = new CProjectile(
233 &GameServer()->m_World,
234 WEAPON_SHOTGUN, //Type
235 -1, //Owner
236 Pos, //Pos
237 vec2(std::sin(x: Deg), std::cos(x: Deg)), //Dir
238 -2, //Span
239 true, //Freeze
240 true, //Explosive
241 (g_Config.m_SvShotgunBulletSound) ? SOUND_GRENADE_EXPLODE : -1, //SoundImpact
242 vec2(std::sin(x: Deg), std::cos(x: Deg)), // InitDir
243 Layer,
244 Number);
245 pBullet->SetBouncing(2 - (Dir % 2));
246 }
247 else if(Index == ENTITY_CRAZY_SHOTGUN)
248 {
249 int Dir;
250 if(!Flags)
251 Dir = 0;
252 else if(Flags == (TILEFLAG_ROTATE))
253 Dir = 1;
254 else if(Flags == (TILEFLAG_XFLIP | TILEFLAG_YFLIP))
255 Dir = 2;
256 else
257 Dir = 3;
258 float Deg = Dir * (pi / 2);
259 CProjectile *pBullet = new CProjectile(
260 &GameServer()->m_World,
261 WEAPON_SHOTGUN, //Type
262 -1, //Owner
263 Pos, //Pos
264 vec2(std::sin(x: Deg), std::cos(x: Deg)), //Dir
265 -2, //Span
266 true, //Freeze
267 false, //Explosive
268 SOUND_GRENADE_EXPLODE,
269 vec2(std::sin(x: Deg), std::cos(x: Deg)), // InitDir
270 Layer,
271 Number);
272 pBullet->SetBouncing(2 - (Dir % 2));
273 }
274
275 int Type = -1;
276 int SubType = 0;
277
278 if(Index == ENTITY_ARMOR_1)
279 Type = POWERUP_ARMOR;
280 else if(Index == ENTITY_ARMOR_SHOTGUN)
281 Type = POWERUP_ARMOR_SHOTGUN;
282 else if(Index == ENTITY_ARMOR_GRENADE)
283 Type = POWERUP_ARMOR_GRENADE;
284 else if(Index == ENTITY_ARMOR_NINJA)
285 Type = POWERUP_ARMOR_NINJA;
286 else if(Index == ENTITY_ARMOR_LASER)
287 Type = POWERUP_ARMOR_LASER;
288 else if(Index == ENTITY_HEALTH_1)
289 Type = POWERUP_HEALTH;
290 else if(Index == ENTITY_WEAPON_SHOTGUN)
291 {
292 Type = POWERUP_WEAPON;
293 SubType = WEAPON_SHOTGUN;
294 }
295 else if(Index == ENTITY_WEAPON_GRENADE)
296 {
297 Type = POWERUP_WEAPON;
298 SubType = WEAPON_GRENADE;
299 }
300 else if(Index == ENTITY_WEAPON_LASER)
301 {
302 Type = POWERUP_WEAPON;
303 SubType = WEAPON_LASER;
304 }
305 else if(Index == ENTITY_POWERUP_NINJA)
306 {
307 Type = POWERUP_NINJA;
308 SubType = WEAPON_NINJA;
309 }
310 else if(Index >= ENTITY_LASER_FAST_CCW && Index <= ENTITY_LASER_FAST_CW)
311 {
312 int aSides2[8];
313 aSides2[0] = GameServer()->Collision()->Entity(x, y: y + 2, Layer);
314 aSides2[1] = GameServer()->Collision()->Entity(x: x + 2, y: y + 2, Layer);
315 aSides2[2] = GameServer()->Collision()->Entity(x: x + 2, y, Layer);
316 aSides2[3] = GameServer()->Collision()->Entity(x: x + 2, y: y - 2, Layer);
317 aSides2[4] = GameServer()->Collision()->Entity(x, y: y - 2, Layer);
318 aSides2[5] = GameServer()->Collision()->Entity(x: x - 2, y: y - 2, Layer);
319 aSides2[6] = GameServer()->Collision()->Entity(x: x - 2, y, Layer);
320 aSides2[7] = GameServer()->Collision()->Entity(x: x - 2, y: y + 2, Layer);
321
322 int Ind = Index - ENTITY_LASER_STOP;
323 int M;
324 if(Ind < 0)
325 {
326 Ind = -Ind;
327 M = 1;
328 }
329 else if(Ind == 0)
330 M = 0;
331 else
332 M = -1;
333
334 float AngularSpeed = 0.0f;
335 if(Ind == 0)
336 AngularSpeed = 0.0f;
337 else if(Ind == 1)
338 AngularSpeed = pi / 360;
339 else if(Ind == 2)
340 AngularSpeed = pi / 180;
341 else if(Ind == 3)
342 AngularSpeed = pi / 90;
343 AngularSpeed *= M;
344
345 for(int i = 0; i < 8; i++)
346 {
347 if(aSides[i] >= ENTITY_LASER_SHORT && aSides[i] <= ENTITY_LASER_LONG)
348 {
349 CLight *pLight = new CLight(&GameServer()->m_World, Pos, pi / 4 * i, 32 * 3 + 32 * (aSides[i] - ENTITY_LASER_SHORT) * 3, Layer, Number);
350 pLight->m_AngularSpeed = AngularSpeed;
351 if(aSides2[i] >= ENTITY_LASER_C_SLOW && aSides2[i] <= ENTITY_LASER_C_FAST)
352 {
353 pLight->m_Speed = 1 + (aSides2[i] - ENTITY_LASER_C_SLOW) * 2;
354 pLight->m_CurveLength = pLight->m_Length;
355 }
356 else if(aSides2[i] >= ENTITY_LASER_O_SLOW && aSides2[i] <= ENTITY_LASER_O_FAST)
357 {
358 pLight->m_Speed = 1 + (aSides2[i] - ENTITY_LASER_O_SLOW) * 2;
359 pLight->m_CurveLength = 0;
360 }
361 else
362 pLight->m_CurveLength = pLight->m_Length;
363 }
364 }
365 }
366 else if(Index >= ENTITY_DRAGGER_WEAK && Index <= ENTITY_DRAGGER_STRONG)
367 {
368 new CDragger(&GameServer()->m_World, Pos, Index - ENTITY_DRAGGER_WEAK + 1, false, Layer, Number);
369 }
370 else if(Index >= ENTITY_DRAGGER_WEAK_NW && Index <= ENTITY_DRAGGER_STRONG_NW)
371 {
372 new CDragger(&GameServer()->m_World, Pos, Index - ENTITY_DRAGGER_WEAK_NW + 1, true, Layer, Number);
373 }
374 else if(Index == ENTITY_PLASMAE)
375 {
376 new CGun(&GameServer()->m_World, Pos, false, true, Layer, Number);
377 }
378 else if(Index == ENTITY_PLASMAF)
379 {
380 new CGun(&GameServer()->m_World, Pos, true, false, Layer, Number);
381 }
382 else if(Index == ENTITY_PLASMA)
383 {
384 new CGun(&GameServer()->m_World, Pos, true, true, Layer, Number);
385 }
386 else if(Index == ENTITY_PLASMAU)
387 {
388 new CGun(&GameServer()->m_World, Pos, false, false, Layer, Number);
389 }
390
391 if(Type != -1) // NOLINT(clang-analyzer-unix.Malloc)
392 {
393 int PickupFlags = TileFlagsToPickupFlags(TileFlags: Flags);
394 CPickup *pPickup = new CPickup(&GameServer()->m_World, Type, SubType, Layer, Number, PickupFlags);
395 pPickup->m_Pos = Pos;
396 return true; // NOLINT(clang-analyzer-unix.Malloc)
397 }
398
399 return false;
400}
401
402void IGameController::OnPlayerConnect(CPlayer *pPlayer)
403{
404 int ClientId = pPlayer->GetCid();
405 pPlayer->Respawn();
406
407 if(!Server()->ClientPrevIngame(ClientId))
408 {
409 char aBuf[128];
410 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "team_join player='%d:%s' team=%d", ClientId, Server()->ClientName(ClientId), pPlayer->GetTeam());
411 GameServer()->Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "game", pStr: aBuf);
412 }
413
414 if(Server()->IsSixup(ClientId))
415 {
416 {
417 protocol7::CNetMsg_Sv_GameInfo Msg;
418 Msg.m_GameFlags = m_GameFlags;
419 Msg.m_MatchCurrent = 1;
420 Msg.m_MatchNum = 0;
421 Msg.m_ScoreLimit = 0;
422 Msg.m_TimeLimit = 0;
423 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
424 }
425
426 // /team is essential
427 {
428 protocol7::CNetMsg_Sv_CommandInfoRemove Msg;
429 Msg.m_pName = "team";
430 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId);
431 }
432 }
433}
434
435void IGameController::OnPlayerDisconnect(class CPlayer *pPlayer, const char *pReason)
436{
437 pPlayer->OnDisconnect();
438 int ClientId = pPlayer->GetCid();
439 if(Server()->ClientIngame(ClientId))
440 {
441 char aBuf[512];
442 if(pReason && *pReason)
443 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' has left the game (%s)", Server()->ClientName(ClientId), pReason);
444 else
445 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' has left the game", Server()->ClientName(ClientId));
446 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf, SpamProtectionClientId: -1, VersionFlags: CGameContext::FLAG_SIX);
447
448 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "leave player='%d:%s'", ClientId, Server()->ClientName(ClientId));
449 GameServer()->Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "game", pStr: aBuf);
450 }
451}
452
453void IGameController::EndRound()
454{
455 if(m_Warmup) // game can't end when we are running warmup
456 return;
457
458 GameServer()->m_World.m_Paused = true;
459 m_GameOverTick = Server()->Tick();
460 m_SuddenDeath = 0;
461}
462
463void IGameController::ResetGame()
464{
465 GameServer()->m_World.m_ResetRequested = true;
466}
467
468bool IGameController::IsValidTeam(int Team)
469{
470 return Team == TEAM_SPECTATORS || Team == TEAM_GAME;
471}
472
473const char *IGameController::GetTeamName(int Team)
474{
475 switch(Team)
476 {
477 case TEAM_SPECTATORS:
478 return "spectators";
479 case TEAM_GAME:
480 return "game";
481 default:
482 dbg_assert_failed("Invalid Team: %d", Team);
483 }
484}
485
486void IGameController::StartRound()
487{
488 ResetGame();
489
490 m_RoundStartTick = Server()->Tick();
491 m_SuddenDeath = 0;
492 m_GameOverTick = -1;
493 GameServer()->m_World.m_Paused = false;
494 Server()->DemoRecorder_HandleAutoStart();
495 char aBuf[256];
496 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "start round type='%s' teamplay='%d'", m_pGameType, m_GameFlags & GAMEFLAG_TEAMS);
497 GameServer()->Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "game", pStr: aBuf);
498}
499
500void IGameController::ChangeMap(const char *pToMap)
501{
502 Server()->ChangeMap(pMap: pToMap);
503}
504
505void IGameController::OnReset()
506{
507 for(auto &pPlayer : GameServer()->m_apPlayers)
508 if(pPlayer)
509 pPlayer->Respawn();
510}
511
512int IGameController::OnCharacterDeath(class CCharacter *pVictim, class CPlayer *pKiller, int Weapon)
513{
514 return 0;
515}
516
517void IGameController::OnCharacterSpawn(class CCharacter *pChr)
518{
519 pChr->SetTeams(&Teams());
520 Teams().OnCharacterSpawn(ClientId: pChr->GetPlayer()->GetCid());
521
522 // default health
523 pChr->IncreaseHealth(Amount: 10);
524
525 // give default weapons
526 pChr->GiveWeapon(Weapon: WEAPON_HAMMER);
527 pChr->GiveWeapon(Weapon: WEAPON_GUN);
528}
529
530void IGameController::HandleCharacterTiles(CCharacter *pChr, int MapIndex)
531{
532 // Do nothing by default
533}
534
535void IGameController::DoWarmup(int Seconds)
536{
537 if(Seconds < 0)
538 m_Warmup = 0;
539 else
540 m_Warmup = Seconds * Server()->TickSpeed();
541}
542
543void IGameController::Tick()
544{
545 // do warmup
546 if(m_Warmup)
547 {
548 m_Warmup--;
549 if(!m_Warmup)
550 StartRound();
551 }
552
553 if(m_GameOverTick != -1)
554 {
555 // game over.. wait for restart
556 if(Server()->Tick() > m_GameOverTick + Server()->TickSpeed() * 10)
557 {
558 StartRound();
559 m_RoundCount++;
560 }
561 }
562
563 if(m_pLoadBestTimeResult != nullptr && m_pLoadBestTimeResult->m_Completed)
564 {
565 if(m_pLoadBestTimeResult->m_Success)
566 {
567 m_CurrentRecord = m_pLoadBestTimeResult->m_CurrentRecord;
568
569 for(int i = 0; i < MAX_CLIENTS; i++)
570 {
571 if(GameServer()->m_apPlayers[i] && GameServer()->m_apPlayers[i]->GetClientVersion() >= VERSION_DDRACE)
572 {
573 GameServer()->SendRecord(ClientId: i);
574 }
575 }
576 }
577 m_pLoadBestTimeResult = nullptr;
578 }
579
580 DoActivityCheck();
581}
582
583void IGameController::Snap(int SnappingClient)
584{
585 CNetObj_GameInfo *pGameInfoObj = Server()->SnapNewItem<CNetObj_GameInfo>(Id: 0);
586 if(!pGameInfoObj)
587 return;
588
589 pGameInfoObj->m_GameFlags = GameFlags_ClampToSix(Flags: m_GameFlags);
590 pGameInfoObj->m_GameStateFlags = 0;
591 if(m_GameOverTick != -1)
592 pGameInfoObj->m_GameStateFlags |= GAMESTATEFLAG_GAMEOVER;
593 if(m_SuddenDeath)
594 pGameInfoObj->m_GameStateFlags |= GAMESTATEFLAG_SUDDENDEATH;
595 if(GameServer()->m_World.m_Paused)
596 pGameInfoObj->m_GameStateFlags |= GAMESTATEFLAG_PAUSED;
597 pGameInfoObj->m_RoundStartTick = m_RoundStartTick;
598 pGameInfoObj->m_WarmupTimer = m_Warmup;
599
600 pGameInfoObj->m_RoundNum = 0;
601 pGameInfoObj->m_RoundCurrent = m_RoundCount + 1;
602
603 CCharacter *pChr;
604 CPlayer *pPlayer = SnappingClient != SERVER_DEMO_CLIENT ? GameServer()->m_apPlayers[SnappingClient] : nullptr;
605 CPlayer *pPlayer2;
606
607 if(pPlayer && (pPlayer->m_TimerType == CPlayer::TIMERTYPE_GAMETIMER || pPlayer->m_TimerType == CPlayer::TIMERTYPE_GAMETIMER_AND_BROADCAST) && pPlayer->GetClientVersion() >= VERSION_DDNET_GAMETICK)
608 {
609 if((pPlayer->GetTeam() == TEAM_SPECTATORS || pPlayer->IsPaused()) && pPlayer->SpectatorId() != SPEC_FREEVIEW && (pPlayer2 = GameServer()->m_apPlayers[pPlayer->SpectatorId()]))
610 {
611 if((pChr = pPlayer2->GetCharacter()) && pChr->m_DDRaceState == ERaceState::STARTED)
612 {
613 pGameInfoObj->m_WarmupTimer = -pChr->m_StartTime;
614 pGameInfoObj->m_GameStateFlags |= GAMESTATEFLAG_RACETIME;
615 }
616 }
617 else if((pChr = pPlayer->GetCharacter()) && pChr->m_DDRaceState == ERaceState::STARTED)
618 {
619 pGameInfoObj->m_WarmupTimer = -pChr->m_StartTime;
620 pGameInfoObj->m_GameStateFlags |= GAMESTATEFLAG_RACETIME;
621 }
622 }
623
624 CNetObj_GameInfoEx *pGameInfoEx = Server()->SnapNewItem<CNetObj_GameInfoEx>(Id: 0);
625 if(!pGameInfoEx)
626 return;
627
628 pGameInfoEx->m_Flags =
629 GAMEINFOFLAG_TIMESCORE |
630 GAMEINFOFLAG_GAMETYPE_RACE |
631 GAMEINFOFLAG_GAMETYPE_DDRACE |
632 GAMEINFOFLAG_GAMETYPE_DDNET |
633 GAMEINFOFLAG_UNLIMITED_AMMO |
634 GAMEINFOFLAG_RACE_RECORD_MESSAGE |
635 GAMEINFOFLAG_ALLOW_EYE_WHEEL |
636 GAMEINFOFLAG_ALLOW_HOOK_COLL |
637 GAMEINFOFLAG_ALLOW_ZOOM |
638 GAMEINFOFLAG_BUG_DDRACE_GHOST |
639 GAMEINFOFLAG_BUG_DDRACE_INPUT |
640 GAMEINFOFLAG_PREDICT_DDRACE |
641 GAMEINFOFLAG_PREDICT_DDRACE_TILES |
642 GAMEINFOFLAG_ENTITIES_DDNET |
643 GAMEINFOFLAG_ENTITIES_DDRACE |
644 GAMEINFOFLAG_ENTITIES_RACE |
645 GAMEINFOFLAG_RACE;
646 pGameInfoEx->m_Flags2 = GAMEINFOFLAG2_HUD_DDRACE | GAMEINFOFLAG2_DDRACE_TEAM;
647 if(g_Config.m_SvNoWeakHook)
648 pGameInfoEx->m_Flags2 |= GAMEINFOFLAG2_NO_WEAK_HOOK;
649 pGameInfoEx->m_Version = GAMEINFO_CURVERSION;
650
651 if(Server()->IsSixup(ClientId: SnappingClient))
652 {
653 protocol7::CNetObj_GameData *pGameData = Server()->SnapNewItem<protocol7::CNetObj_GameData>(Id: 0);
654 if(!pGameData)
655 return;
656
657 pGameData->m_GameStartTick = m_RoundStartTick;
658 pGameData->m_GameStateFlags = 0;
659 if(m_GameOverTick != -1)
660 pGameData->m_GameStateFlags |= protocol7::GAMESTATEFLAG_GAMEOVER;
661 if(m_SuddenDeath)
662 pGameData->m_GameStateFlags |= protocol7::GAMESTATEFLAG_SUDDENDEATH;
663 if(GameServer()->m_World.m_Paused)
664 pGameData->m_GameStateFlags |= protocol7::GAMESTATEFLAG_PAUSED;
665
666 pGameData->m_GameStateEndTick = 0;
667
668 protocol7::CNetObj_GameDataRace *pRaceData = Server()->SnapNewItem<protocol7::CNetObj_GameDataRace>(Id: 0);
669 if(!pRaceData)
670 return;
671
672 pRaceData->m_BestTime = m_CurrentRecord.has_value() ? round_to_int(f: m_CurrentRecord.value() * 1000) : -1;
673 pRaceData->m_Precision = 2;
674 pRaceData->m_RaceFlags = protocol7::RACEFLAG_KEEP_WANTED_WEAPON;
675 }
676
677 GameServer()->SnapSwitchers(SnappingClient);
678}
679
680int IGameController::GetAutoTeam(int NotThisId)
681{
682 int Team = TEAM_GAME;
683
684 if(CanJoinTeam(Team, NotThisId, pErrorReason: nullptr, ErrorReasonSize: 0))
685 return Team;
686 return -1;
687}
688
689bool IGameController::CanJoinTeam(int Team, int NotThisId, char *pErrorReason, int ErrorReasonSize)
690{
691 const CPlayer *pPlayer = GameServer()->m_apPlayers[NotThisId];
692 if(pPlayer && pPlayer->IsPaused())
693 {
694 if(pErrorReason)
695 str_copy(dst: pErrorReason, src: "Use /pause first then you can kill", dst_size: ErrorReasonSize);
696 return false;
697 }
698 if(Team == TEAM_SPECTATORS || (pPlayer && pPlayer->GetTeam() != TEAM_SPECTATORS))
699 return true;
700
701 int aNumplayers[2] = {0, 0};
702 for(int i = 0; i < MAX_CLIENTS; i++)
703 {
704 if(GameServer()->m_apPlayers[i] && i != NotThisId)
705 {
706 if(GameServer()->m_apPlayers[i]->GetTeam() >= TEAM_RED && GameServer()->m_apPlayers[i]->GetTeam() <= TEAM_BLUE)
707 aNumplayers[GameServer()->m_apPlayers[i]->GetTeam()]++;
708 }
709 }
710
711 if((aNumplayers[0] + aNumplayers[1]) < Server()->MaxClients() - g_Config.m_SvSpectatorSlots)
712 return true;
713
714 if(pErrorReason)
715 str_format(buffer: pErrorReason, buffer_size: ErrorReasonSize, format: "Only %d active players are allowed", Server()->MaxClients() - g_Config.m_SvSpectatorSlots);
716 return false;
717}
718
719CClientMask IGameController::GetMaskForPlayerWorldEvent(int Asker, int ExceptId)
720{
721 if(Asker == -1)
722 return CClientMask().set().reset(position: ExceptId);
723
724 return Teams().TeamMask(Team: GameServer()->GetDDRaceTeam(ClientId: Asker), ExceptId, Asker);
725}
726
727void IGameController::DoTeamChange(CPlayer *pPlayer, int Team, bool DoChatMsg)
728{
729 if(!IsValidTeam(Team))
730 return;
731
732 if(Team == pPlayer->GetTeam())
733 return;
734
735 pPlayer->SetTeam(Team);
736 int ClientId = pPlayer->GetCid();
737
738 char aBuf[128];
739 DoChatMsg = false;
740 if(DoChatMsg)
741 {
742 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' joined the %s", Server()->ClientName(ClientId), GameServer()->m_pController->GetTeamName(Team));
743 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf);
744 }
745
746 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "team_join player='%d:%s' m_Team=%d", ClientId, Server()->ClientName(ClientId), Team);
747 GameServer()->Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "game", pStr: aBuf);
748
749 // OnPlayerInfoChange(pPlayer);
750}
751
752int IGameController::TileFlagsToPickupFlags(int TileFlags) const
753{
754 int PickupFlags = 0;
755 if(TileFlags & TILEFLAG_XFLIP)
756 PickupFlags |= PICKUPFLAG_XFLIP;
757 if(TileFlags & TILEFLAG_YFLIP)
758 PickupFlags |= PICKUPFLAG_YFLIP;
759 if(TileFlags & TILEFLAG_ROTATE)
760 PickupFlags |= PICKUPFLAG_ROTATE;
761 return PickupFlags;
762}
763