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