1/* (c) Shereef Marzouk. See "licence DDRace.txt" and the readme.txt in the root of the distribution for more information. */
2#include "teams.h"
3
4#include "gamecontroller.h"
5#include "player.h"
6#include "score.h"
7#include "teehistorian.h"
8
9#include <base/dbg.h>
10#include <base/str.h>
11#include <base/time.h>
12
13#include <engine/shared/config.h>
14
15#include <game/mapitems.h>
16#include <game/server/entities/character.h>
17#include <game/server/gamecontext.h>
18#include <game/server/interactions.h>
19#include <game/team_state.h>
20
21CGameTeams::CGameTeams(CGameContext *pGameContext) :
22 m_pGameContext(pGameContext)
23{
24 Reset();
25}
26
27void CGameTeams::Reset()
28{
29 m_Core.Reset();
30 for(int i = 0; i < MAX_CLIENTS; ++i)
31 {
32 m_aTeeStarted[i] = false;
33 m_aTeeFinished[i] = false;
34 m_aLastChat[i] = 0;
35 SendTeamsState(ClientId: i);
36 }
37
38 for(int i = 0; i < NUM_DDRACE_TEAMS; ++i)
39 {
40 m_aTeamState[i] = ETeamState::EMPTY;
41 m_aTeamLocked[i] = false;
42 m_aTeamFlock[i] = false;
43 m_apSaveTeamResult[i] = nullptr;
44 m_aTeamSentStartWarning[i] = false;
45 if(m_pGameContext->PracticeByDefault())
46 m_aPractice[i] = true;
47 ResetRoundState(Team: i);
48 }
49}
50
51void CGameTeams::ResetRoundState(int Team)
52{
53 ResetInvited(Team);
54 if(Team != TEAM_SUPER)
55 ResetSwitchers(Team);
56
57 if(!m_pGameContext->PracticeByDefault())
58 m_aPractice[Team] = false;
59 m_aTeamUnfinishableKillTick[Team] = -1;
60 for(int i = 0; i < MAX_CLIENTS; i++)
61 {
62 if(m_Core.Team(ClientId: i) == Team && GameServer()->m_apPlayers[i])
63 {
64 GameServer()->m_apPlayers[i]->m_VotedForPractice = false;
65 GameServer()->m_apPlayers[i]->m_SwapTargetsClientId = -1;
66 m_aLastSwap[i] = 0;
67 }
68 }
69}
70
71void CGameTeams::ResetSwitchers(int Team)
72{
73 for(auto &Switcher : GameServer()->Switchers())
74 {
75 Switcher.m_aStatus[Team] = Switcher.m_Initial;
76 Switcher.m_aEndTick[Team] = 0;
77 Switcher.m_aType[Team] = TILE_SWITCHOPEN;
78 }
79}
80
81void CGameTeams::OnCharacterStart(int ClientId)
82{
83 int Tick = Server()->Tick();
84 CCharacter *pStartingChar = Character(ClientId);
85 if(!pStartingChar)
86 return;
87 if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO && pStartingChar->m_DDRaceState == ERaceState::STARTED)
88 return;
89 if((g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO || (m_Core.Team(ClientId) != TEAM_FLOCK && !m_aTeamFlock[m_Core.Team(ClientId)])) && pStartingChar->m_DDRaceState == ERaceState::FINISHED)
90 return;
91 if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO &&
92 (m_Core.Team(ClientId) == TEAM_FLOCK || TeamFlock(Team: m_Core.Team(ClientId)) || m_Core.Team(ClientId) == TEAM_SUPER))
93 {
94 if(TeamFlock(Team: m_Core.Team(ClientId)) && (m_aTeamState[m_Core.Team(ClientId)] < ETeamState::STARTED))
95 ChangeTeamState(Team: m_Core.Team(ClientId), State: ETeamState::STARTED);
96
97 m_aTeeStarted[ClientId] = true;
98 pStartingChar->m_DDRaceState = ERaceState::STARTED;
99 pStartingChar->m_StartTime = Tick;
100 return;
101 }
102 bool Waiting = false;
103 for(int i = 0; i < MAX_CLIENTS; ++i)
104 {
105 if(m_Core.Team(ClientId) != m_Core.Team(ClientId: i))
106 continue;
107 CPlayer *pPlayer = GetPlayer(ClientId: i);
108 if(!pPlayer || !pPlayer->IsPlaying())
109 continue;
110 if(GetDDRaceState(Player: pPlayer) != ERaceState::FINISHED)
111 continue;
112
113 Waiting = true;
114 pStartingChar->m_DDRaceState = ERaceState::NONE;
115
116 if(m_aLastChat[ClientId] + Server()->TickSpeed() + g_Config.m_SvChatDelay < Tick)
117 {
118 char aBuf[128];
119 str_format(
120 buffer: aBuf,
121 buffer_size: sizeof(aBuf),
122 format: "%s has finished and didn't go through start yet, wait for them or join another team.",
123 Server()->ClientName(ClientId: i));
124 GameServer()->SendChatTarget(To: ClientId, pText: aBuf);
125 m_aLastChat[ClientId] = Tick;
126 }
127 if(m_aLastChat[i] + Server()->TickSpeed() + g_Config.m_SvChatDelay < Tick)
128 {
129 char aBuf[128];
130 str_format(
131 buffer: aBuf,
132 buffer_size: sizeof(aBuf),
133 format: "%s wants to start a new round, kill or walk to start.",
134 Server()->ClientName(ClientId));
135 GameServer()->SendChatTarget(To: i, pText: aBuf);
136 m_aLastChat[i] = Tick;
137 }
138 }
139
140 if(!Waiting)
141 {
142 m_aTeeStarted[ClientId] = true;
143 }
144
145 if(m_aTeamState[m_Core.Team(ClientId)] < ETeamState::STARTED && !Waiting)
146 {
147 ChangeTeamState(Team: m_Core.Team(ClientId), State: ETeamState::STARTED);
148 m_aTeamSentStartWarning[m_Core.Team(ClientId)] = false;
149 m_aTeamUnfinishableKillTick[m_Core.Team(ClientId)] = -1;
150
151 int NumPlayers = TeamSize(Team: m_Core.Team(ClientId));
152
153 char aBuf[512];
154 str_format(
155 buffer: aBuf,
156 buffer_size: sizeof(aBuf),
157 format: "Team %d started with %d player%s: ",
158 m_Core.Team(ClientId),
159 NumPlayers,
160 NumPlayers == 1 ? "" : "s");
161
162 bool First = true;
163
164 for(int i = 0; i < MAX_CLIENTS; ++i)
165 {
166 if(m_Core.Team(ClientId) == m_Core.Team(ClientId: i))
167 {
168 CPlayer *pPlayer = GetPlayer(ClientId: i);
169 // TODO: THE PROBLEM IS THAT THERE IS NO CHARACTER SO START TIME CAN'T BE SET!
170 if(pPlayer && (pPlayer->IsPlaying() || TeamLocked(Team: m_Core.Team(ClientId))))
171 {
172 SetDDRaceState(Player: pPlayer, DDRaceState: ERaceState::STARTED);
173 SetStartTime(Player: pPlayer, StartTime: Tick);
174
175 if(First)
176 First = false;
177 else
178 str_append(dst&: aBuf, src: ", ");
179
180 str_append(dst&: aBuf, src: GameServer()->Server()->ClientName(ClientId: i));
181 }
182 }
183 }
184
185 if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && g_Config.m_SvMaxTeamSize != 2 && g_Config.m_SvPauseable)
186 {
187 for(int i = 0; i < MAX_CLIENTS; ++i)
188 {
189 CPlayer *pPlayer = GetPlayer(ClientId: i);
190 if(m_Core.Team(ClientId) == m_Core.Team(ClientId: i) && pPlayer && (pPlayer->IsPlaying() || TeamLocked(Team: m_Core.Team(ClientId))))
191 {
192 GameServer()->SendChatTarget(To: i, pText: aBuf);
193 }
194 }
195 }
196 }
197}
198
199void CGameTeams::OnCharacterFinish(int ClientId)
200{
201 if(((m_Core.Team(ClientId) == TEAM_FLOCK || m_aTeamFlock[m_Core.Team(ClientId)]) && g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO) || m_Core.Team(ClientId) == TEAM_SUPER)
202 {
203 CPlayer *pPlayer = GetPlayer(ClientId);
204 if(pPlayer && pPlayer->IsPlaying())
205 {
206 int TimeTicks = Server()->Tick() - GetStartTime(Player: pPlayer);
207 if(TimeTicks <= 0)
208 return;
209 char aTimestamp[TIMESTAMP_STR_LENGTH];
210 str_timestamp_format(buffer: aTimestamp, buffer_size: sizeof(aTimestamp), format: TimestampFormat::SPACE); // 2019-04-02 19:41:58
211
212 OnFinish(Player: pPlayer, TimeTicks, pTimestamp: aTimestamp);
213 }
214 }
215 else
216 {
217 if(m_aTeeStarted[ClientId])
218 {
219 m_aTeeFinished[ClientId] = true;
220 }
221 CheckTeamFinished(Team: m_Core.Team(ClientId));
222 }
223}
224
225void CGameTeams::Tick()
226{
227 int Now = Server()->Tick();
228
229 for(int i = 0; i < MAX_CLIENTS; i++)
230 {
231 CPlayerData *pData = GameServer()->Score()->PlayerData(Id: i);
232 if(!Server()->IsRecording(ClientId: i))
233 continue;
234
235 if(Now >= pData->m_RecordStopTick && pData->m_RecordStopTick != -1)
236 {
237 Server()->SaveDemo(ClientId: i, Time: pData->m_RecordFinishTime);
238 pData->m_RecordStopTick = -1;
239 }
240 }
241
242 for(int i = 0; i < TEAM_SUPER; i++)
243 {
244 if(m_aTeamUnfinishableKillTick[i] == -1 || m_aTeamState[i] != ETeamState::STARTED_UNFINISHABLE)
245 {
246 continue;
247 }
248 if(Now >= m_aTeamUnfinishableKillTick[i])
249 {
250 if(m_aPractice[i])
251 {
252 m_aTeamUnfinishableKillTick[i] = -1;
253 continue;
254 }
255 GameServer()->SendChatTeam(Team: i, pText: "Your team was killed because it couldn't finish anymore and hasn't entered /practice mode");
256 KillTeam(Team: i, NewStrongId: -1);
257 }
258 }
259
260 int Frequency = Server()->TickSpeed() * 60;
261 int Remainder = Server()->TickSpeed() * 30;
262 uint64_t TeamHasWantedStartTime = 0;
263 for(int i = 0; i < MAX_CLIENTS; i++)
264 {
265 CCharacter *pChar = GameServer()->m_apPlayers[i] ? GameServer()->m_apPlayers[i]->GetCharacter() : nullptr;
266 int Team = m_Core.Team(ClientId: i);
267 if(!pChar || m_aTeamState[Team] != ETeamState::STARTED || m_aTeamFlock[Team] || m_aTeeStarted[i] || m_aPractice[m_Core.Team(ClientId: i)])
268 {
269 continue;
270 }
271 if((Now - pChar->m_StartTime) % Frequency == Remainder)
272 {
273 TeamHasWantedStartTime |= ((uint64_t)1) << m_Core.Team(ClientId: i);
274 }
275 }
276 TeamHasWantedStartTime &= ~(uint64_t)1;
277 if(!TeamHasWantedStartTime)
278 {
279 return;
280 }
281 for(int i = 0; i < MAX_CLIENTS; i++)
282 {
283 if(((TeamHasWantedStartTime >> i) & 1) == 0)
284 {
285 continue;
286 }
287 if(TeamSize(Team: i) <= 1)
288 {
289 continue;
290 }
291 bool TeamHasCheatCharacter = false;
292 int NumPlayersNotStarted = 0;
293 char aPlayerNames[256];
294 aPlayerNames[0] = 0;
295 for(int j = 0; j < MAX_CLIENTS; j++)
296 {
297 if(Character(ClientId: j) && Character(ClientId: j)->m_DDRaceState == ERaceState::CHEATED)
298 TeamHasCheatCharacter = true;
299 if(m_Core.Team(ClientId: j) == i && !m_aTeeStarted[j])
300 {
301 if(aPlayerNames[0])
302 {
303 str_append(dst&: aPlayerNames, src: ", ");
304 }
305 str_append(dst&: aPlayerNames, src: Server()->ClientName(ClientId: j));
306 NumPlayersNotStarted += 1;
307 }
308 }
309 if(!aPlayerNames[0] || TeamHasCheatCharacter)
310 {
311 continue;
312 }
313 char aBuf[512];
314 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
315 format: "Your team has %d %s not started yet, they need "
316 "to touch the start before this team can finish: %s",
317 NumPlayersNotStarted,
318 NumPlayersNotStarted == 1 ? "player that has" : "players that have",
319 aPlayerNames);
320 GameServer()->SendChatTeam(Team: i, pText: aBuf);
321 }
322}
323
324void CGameTeams::CheckTeamFinished(int Team)
325{
326 if(TeamFinished(Team))
327 {
328 CPlayer *apTeamPlayers[MAX_CLIENTS];
329 unsigned int PlayersCount = 0;
330
331 for(int i = 0; i < MAX_CLIENTS; ++i)
332 {
333 if(Team == m_Core.Team(ClientId: i))
334 {
335 CPlayer *pPlayer = GetPlayer(ClientId: i);
336 if(pPlayer && pPlayer->IsPlaying())
337 {
338 m_aTeeStarted[i] = false;
339 m_aTeeFinished[i] = false;
340
341 apTeamPlayers[PlayersCount++] = pPlayer;
342 }
343 }
344 }
345
346 if(PlayersCount > 0)
347 {
348 int TimeTicks = Server()->Tick() - GetStartTime(Player: apTeamPlayers[0]);
349 float Time = (float)TimeTicks / (float)Server()->TickSpeed();
350 if(TimeTicks <= 0)
351 {
352 return;
353 }
354
355 if(m_aPractice[Team])
356 {
357 ChangeTeamState(Team, State: ETeamState::FINISHED);
358
359 const int Minutes = (int)Time / 60;
360 const float Seconds = Time - (Minutes * 60.0f);
361
362 char aBuf[256];
363 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
364 format: "Your team would've finished in: %d minute(s) %5.2f second(s). Since you had practice mode enabled your rank doesn't count.",
365 Minutes, Seconds);
366 GameServer()->SendChatTeam(Team, pText: aBuf);
367
368 for(unsigned int i = 0; i < PlayersCount; ++i)
369 {
370 SetDDRaceState(Player: apTeamPlayers[i], DDRaceState: ERaceState::FINISHED);
371 }
372
373 return;
374 }
375
376 char aTimestamp[TIMESTAMP_STR_LENGTH];
377 str_timestamp_format(buffer: aTimestamp, buffer_size: sizeof(aTimestamp), format: TimestampFormat::SPACE); // 2019-04-02 19:41:58
378
379 for(unsigned int i = 0; i < PlayersCount; ++i)
380 OnFinish(Player: apTeamPlayers[i], TimeTicks, pTimestamp: aTimestamp);
381 ChangeTeamState(Team, State: ETeamState::FINISHED); // TODO: Make it better
382 OnTeamFinish(Team, Players: apTeamPlayers, Size: PlayersCount, TimeTicks, pTimestamp: aTimestamp);
383 }
384 }
385}
386
387bool CGameTeams::CanJoinTeam(int ClientId, int Team, char *pError, int ErrorSize) const
388{
389 int CurrentTeam = m_Core.Team(ClientId);
390
391 if(ClientId < 0 || ClientId >= MAX_CLIENTS)
392 {
393 str_format(buffer: pError, buffer_size: ErrorSize, format: "Invalid client ID: %d", ClientId);
394 return false;
395 }
396 if(!IsValidTeamNumber(Team) && Team != TEAM_SUPER)
397 {
398 str_format(buffer: pError, buffer_size: ErrorSize, format: "Invalid team number: %d", Team);
399 return false;
400 }
401 if(Team != TEAM_SUPER && m_aTeamState[Team] > ETeamState::OPEN && !m_aPractice[Team] && !m_aTeamFlock[Team])
402 {
403 str_copy(dst: pError, src: "This team started already", dst_size: ErrorSize);
404 return false;
405 }
406 if(CurrentTeam == Team)
407 {
408 str_copy(dst: pError, src: "You are in this team already", dst_size: ErrorSize);
409 return false;
410 }
411 if(!Character(ClientId))
412 {
413 str_copy(dst: pError, src: "You can't change teams while you are dead/a spectator.", dst_size: ErrorSize);
414 return false;
415 }
416 if(Team == TEAM_SUPER && !Character(ClientId)->IsSuper())
417 {
418 str_copy(dst: pError, src: "You can't join super team if you don't have super rights", dst_size: ErrorSize);
419 return false;
420 }
421 if(Team != TEAM_SUPER && Character(ClientId)->m_DDRaceState != ERaceState::NONE && (m_aTeamState[CurrentTeam] < ETeamState::FINISHED || Team != 0))
422 {
423 str_copy(dst: pError, src: "You have started racing already", dst_size: ErrorSize);
424 return false;
425 }
426 // No cheating through noob filter with practice and then leaving team
427 if(m_aPractice[CurrentTeam] && !m_pGameContext->PracticeByDefault())
428 {
429 str_copy(dst: pError, src: "You have used practice mode already", dst_size: ErrorSize);
430 return false;
431 }
432
433 // you can not join a team which is currently in the process of saving,
434 // because the save-process can fail and then the team is reset into the game
435 if(Team != TEAM_SUPER && GetSaving(TeamId: Team))
436 {
437 str_copy(dst: pError, src: "This team is currently saving", dst_size: ErrorSize);
438 return false;
439 }
440 if(CurrentTeam != TEAM_SUPER && GetSaving(TeamId: CurrentTeam))
441 {
442 str_copy(dst: pError, src: "Your team is currently saving", dst_size: ErrorSize);
443 return false;
444 }
445
446 return true;
447}
448
449bool CGameTeams::SetCharacterTeam(int ClientId, int Team, char *pError, int ErrorSize)
450{
451 if(!CanJoinTeam(ClientId, Team, pError, ErrorSize))
452 return false;
453
454 SetForceCharacterTeam(ClientId, Team);
455 return true;
456}
457
458void CGameTeams::SetForceCharacterTeam(int ClientId, int Team)
459{
460 m_aTeeStarted[ClientId] = false;
461 m_aTeeFinished[ClientId] = false;
462 int OldTeam = m_Core.Team(ClientId);
463
464 if(Team != OldTeam && (OldTeam != TEAM_FLOCK || g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) && OldTeam != TEAM_SUPER && m_aTeamState[OldTeam] != ETeamState::EMPTY)
465 {
466 bool NoElseInOldTeam = TeamSize(Team: OldTeam) <= 1;
467 if(NoElseInOldTeam)
468 {
469 m_aTeamState[OldTeam] = ETeamState::EMPTY;
470
471 // unlock team when last player leaves
472 SetTeamLock(Team: OldTeam, Lock: false);
473 SetTeamFlock(Team: OldTeam, Mode: false);
474 ResetRoundState(Team: OldTeam);
475 // do not reset SaveTeamResult, because it should be logged into teehistorian even if the team leaves
476 }
477 }
478
479 m_Core.Team(ClientId, Team);
480
481 if(OldTeam != Team)
482 {
483 for(int LoopClientId = 0; LoopClientId < MAX_CLIENTS; ++LoopClientId)
484 if(GetPlayer(ClientId: LoopClientId))
485 SendTeamsState(ClientId: LoopClientId);
486
487 if(GetPlayer(ClientId))
488 {
489 GetPlayer(ClientId)->m_VotedForPractice = false;
490 GetPlayer(ClientId)->m_SwapTargetsClientId = -1;
491 }
492 m_pGameContext->m_World.RemoveEntitiesFromPlayer(PlayerId: ClientId);
493 }
494
495 if(Team != TEAM_SUPER && (m_aTeamState[Team] == ETeamState::EMPTY || (m_aTeamLocked[Team] && !m_aTeamFlock[Team])))
496 {
497 if(!m_aTeamLocked[Team])
498 ChangeTeamState(Team, State: ETeamState::OPEN);
499
500 ResetSwitchers(Team);
501 }
502}
503
504int CGameTeams::TeamSize(int Team) const
505{
506 if(Team == TEAM_SUPER)
507 return -1;
508
509 int Count = 0;
510
511 for(int i = 0; i < MAX_CLIENTS; ++i)
512 if(m_Core.Team(ClientId: i) == Team)
513 Count++;
514
515 return Count;
516}
517
518void CGameTeams::ChangeTeamState(int Team, ETeamState State)
519{
520 m_aTeamState[Team] = State;
521}
522
523void CGameTeams::KillTeam(int Team, int NewStrongId, int ExceptId)
524{
525 for(int i = 0; i < MAX_CLIENTS; i++)
526 {
527 if(m_Core.Team(ClientId: i) == Team && GameServer()->m_apPlayers[i])
528 {
529 GameServer()->m_apPlayers[i]->m_VotedForPractice = false;
530 if(i != ExceptId)
531 {
532 GameServer()->m_apPlayers[i]->KillCharacter(Weapon: WEAPON_SELF, SendKillMsg: false);
533 if(NewStrongId != -1 && i != NewStrongId)
534 {
535 GameServer()->m_apPlayers[i]->Respawn(WeakHook: true); // spawn the rest of team with weak hook on the killer
536 }
537 }
538 }
539 }
540
541 // send the team kill message
542 CNetMsg_Sv_KillMsgTeam Msg;
543 Msg.m_Team = Team;
544 Msg.m_First = NewStrongId;
545 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId: -1);
546}
547
548bool CGameTeams::TeamFinished(int Team)
549{
550 if(m_aTeamState[Team] != ETeamState::STARTED)
551 {
552 return false;
553 }
554 for(int i = 0; i < MAX_CLIENTS; ++i)
555 if(m_Core.Team(ClientId: i) == Team && !m_aTeeFinished[i])
556 return false;
557 return true;
558}
559
560CClientMask CGameTeams::TeamMask(int Team, int ExceptId, int Asker, int VersionFlags)
561{
562 if(Team == TEAM_SUPER)
563 {
564 if(ExceptId == -1)
565 return CClientMask().set();
566 return CClientMask().set().reset(pos: ExceptId);
567 }
568
569 CPlayer *pAsker = GetPlayer(ClientId: Asker);
570 CInteractions Interact;
571 Interact.Init(OwnerId: Asker, UniqueOwnerId: pAsker ? pAsker->GetUniqueCid() : 0);
572 Interact.FillOwnerConnected(
573 OwnerAlive: pAsker && pAsker->GetCharacter() && pAsker->GetCharacter()->IsAlive(),
574 DDRaceTeam: m_Core.Team(ClientId: Asker),
575 Solo: m_Core.GetSolo(ClientId: Asker),
576 NoHitOthers: false,
577 NoHitSelf: false); // TODO: these false values make little sense
578
579 CClientMask Mask;
580 for(int i = 0; i < MAX_CLIENTS; ++i)
581 {
582 if(i == ExceptId)
583 continue; // Explicitly excluded
584 if(!GetPlayer(ClientId: i))
585 continue; // Player doesn't exist
586 if(!((Server()->IsSixup(ClientId: i) && (VersionFlags & CGameContext::FLAG_SIXUP)) ||
587 (!Server()->IsSixup(ClientId: i) && (VersionFlags & CGameContext::FLAG_SIX))))
588 continue;
589 if(!Interact.CanSee(pGameServer: GameServer(), ClientId: i))
590 continue;
591
592 Mask.set(pos: i);
593 }
594 return Mask;
595}
596
597void CGameTeams::SendTeamsState(int ClientId)
598{
599 if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO)
600 return;
601
602 if(!m_pGameContext->m_apPlayers[ClientId])
603 return;
604
605 CMsgPacker Msg(NETMSGTYPE_SV_TEAMSSTATE);
606 CMsgPacker MsgLegacy(NETMSGTYPE_SV_TEAMSSTATELEGACY);
607
608 for(unsigned i = 0; i < MAX_CLIENTS; i++)
609 {
610 Msg.AddInt(i: m_Core.Team(ClientId: i));
611 MsgLegacy.AddInt(i: m_Core.Team(ClientId: i));
612 }
613
614 Server()->SendMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId);
615 int ClientVersion = m_pGameContext->m_apPlayers[ClientId]->GetClientVersion();
616 if(!Server()->IsSixup(ClientId) && VERSION_DDRACE < ClientVersion && ClientVersion < VERSION_DDNET_MSG_LEGACY)
617 {
618 Server()->SendMsg(pMsg: &MsgLegacy, Flags: MSGFLAG_VITAL, ClientId);
619 }
620}
621
622ERaceState CGameTeams::GetDDRaceState(const CPlayer *Player) const
623{
624 if(!Player)
625 return ERaceState::NONE;
626
627 const CCharacter *pChar = Player->GetCharacter();
628 if(pChar)
629 return pChar->m_DDRaceState;
630 return ERaceState::NONE;
631}
632
633void CGameTeams::SetDDRaceState(CPlayer *Player, ERaceState DDRaceState)
634{
635 if(!Player)
636 return;
637
638 CCharacter *pChar = Player->GetCharacter();
639 if(pChar)
640 pChar->m_DDRaceState = DDRaceState;
641}
642
643int CGameTeams::GetStartTime(CPlayer *Player)
644{
645 if(!Player)
646 return 0;
647
648 CCharacter *pChar = Player->GetCharacter();
649 if(pChar)
650 return pChar->m_StartTime;
651 return 0;
652}
653
654void CGameTeams::SetStartTime(CPlayer *Player, int StartTime)
655{
656 if(!Player)
657 return;
658
659 CCharacter *pChar = Player->GetCharacter();
660 if(pChar)
661 pChar->m_StartTime = StartTime;
662}
663
664void CGameTeams::SetLastTimeCp(CPlayer *Player, int LastTimeCp)
665{
666 if(!Player)
667 return;
668
669 CCharacter *pChar = Player->GetCharacter();
670 if(pChar)
671 pChar->m_LastTimeCp = LastTimeCp;
672}
673
674float *CGameTeams::GetCurrentTimeCp(CPlayer *Player)
675{
676 if(!Player)
677 return nullptr;
678
679 CCharacter *pChar = Player->GetCharacter();
680 if(pChar)
681 return pChar->m_aCurrentTimeCp;
682 return nullptr;
683}
684
685void CGameTeams::OnTeamFinish(int Team, CPlayer **Players, unsigned int Size, int TimeTicks, const char *pTimestamp)
686{
687 int aPlayerCids[MAX_CLIENTS];
688
689 for(unsigned int i = 0; i < Size; i++)
690 {
691 aPlayerCids[i] = Players[i]->GetCid();
692
693 if(g_Config.m_SvRejoinTeam0 && g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && (!IsValidTeamNumber(Team: m_Core.Team(ClientId: Players[i]->GetCid())) || !m_aTeamLocked[m_Core.Team(ClientId: Players[i]->GetCid())]))
694 {
695 SetForceCharacterTeam(ClientId: Players[i]->GetCid(), Team: TEAM_FLOCK);
696 char aBuf[512];
697 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' joined team 0",
698 GameServer()->Server()->ClientName(ClientId: Players[i]->GetCid()));
699 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf);
700 }
701 }
702
703 if(Size >= (unsigned int)g_Config.m_SvMinTeamSize)
704 GameServer()->Score()->SaveTeamScore(Team, pClientIds: aPlayerCids, Size, TimeTicks, pTimestamp);
705}
706
707void CGameTeams::OnFinish(CPlayer *pPlayer, int TimeTicks, const char *pTimestamp)
708{
709 if(!pPlayer || !pPlayer->IsPlaying())
710 return;
711
712 float Time = TimeTicks / (float)Server()->TickSpeed();
713
714 // TODO:DDRace:btd: this ugly
715 const int ClientId = pPlayer->GetCid();
716 CPlayerData *pData = GameServer()->Score()->PlayerData(Id: ClientId);
717
718 char aBuf[128];
719 SetLastTimeCp(Player: pPlayer, LastTimeCp: -1);
720 // Note that the "finished in" message is parsed by the client
721 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
722 format: "%s finished in: %d minute(s) %5.2f second(s)",
723 Server()->ClientName(ClientId), (int)Time / 60,
724 Time - ((int)Time / 60 * 60));
725 if(g_Config.m_SvHideScore)
726 GameServer()->SendChatTarget(To: ClientId, pText: aBuf, VersionFlags: CGameContext::FLAG_SIX);
727 else
728 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf, SpamProtectionClientId: -1., VersionFlags: CGameContext::FLAG_SIX);
729
730 float Diff = absolute(a: Time - pData->m_BestTime.value_or(u: 0.0f));
731
732 if(Time - pData->m_BestTime.value_or(u: 0.0f) < 0)
733 {
734 // new record \o/
735 pData->m_RecordStopTick = Server()->Tick() + Server()->TickSpeed();
736 pData->m_RecordFinishTime = Time;
737
738 if(Diff >= 60)
739 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "New record: %d minute(s) %5.2f second(s) better.",
740 (int)Diff / 60, Diff - ((int)Diff / 60 * 60));
741 else
742 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "New record: %5.2f second(s) better.",
743 Diff);
744 if(g_Config.m_SvHideScore)
745 GameServer()->SendChatTarget(To: ClientId, pText: aBuf, VersionFlags: CGameContext::FLAG_SIX);
746 else
747 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf, SpamProtectionClientId: -1, VersionFlags: CGameContext::FLAG_SIX);
748 }
749 else if(pData->m_BestTime.has_value()) // tee has already finished?
750 {
751 Server()->StopRecord(ClientId);
752
753 if(Diff <= 0.005f)
754 {
755 GameServer()->SendChatTarget(To: ClientId,
756 pText: "You finished with your best time.");
757 }
758 else
759 {
760 if(Diff >= 60)
761 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d minute(s) %5.2f second(s) worse, better luck next time.",
762 (int)Diff / 60, Diff - ((int)Diff / 60 * 60));
763 else
764 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
765 format: "%5.2f second(s) worse, better luck next time.",
766 Diff);
767 GameServer()->SendChatTarget(To: ClientId, pText: aBuf, VersionFlags: CGameContext::FLAG_SIX); // this is private, sent only to the tee
768 }
769 }
770 else
771 {
772 pData->m_RecordStopTick = Server()->Tick() + Server()->TickSpeed();
773 pData->m_RecordFinishTime = Time;
774 }
775
776 GameServer()->SendFinish(ClientId, Time, PreviousBestTime: pData->m_BestTime);
777 bool CallSaveScore = g_Config.m_SvSaveWorseScores;
778 bool NeedToSendNewPersonalRecord = false;
779 if(!pData->m_BestTime || Time < pData->m_BestTime)
780 {
781 // update the score
782 pData->Set(Time, aTimeCp: GetCurrentTimeCp(Player: pPlayer));
783 CallSaveScore = true;
784 NeedToSendNewPersonalRecord = true;
785 }
786
787 if(CallSaveScore)
788 if(g_Config.m_SvNamelessScore || !str_startswith(str: Server()->ClientName(ClientId), prefix: "nameless tee"))
789 GameServer()->Score()->SaveScore(ClientId, TimeTicks, pTimestamp,
790 aTimeCp: GetCurrentTimeCp(Player: pPlayer), NotEligible: pPlayer->m_NotEligibleForFinish);
791
792 bool NeedToSendNewServerRecord = false;
793 // update server best time
794 if(!GameServer()->m_pController->m_CurrentRecord.has_value())
795 {
796 GameServer()->Score()->LoadBestTime();
797 }
798 else if(Time < GameServer()->m_pController->m_CurrentRecord)
799 {
800 // check for nameless
801 if(g_Config.m_SvNamelessScore || !str_startswith(str: Server()->ClientName(ClientId), prefix: "nameless tee"))
802 {
803 GameServer()->m_pController->m_CurrentRecord = Time;
804 NeedToSendNewServerRecord = true;
805 }
806 }
807
808 SetDDRaceState(Player: pPlayer, DDRaceState: ERaceState::FINISHED);
809 if(NeedToSendNewServerRecord)
810 {
811 for(int i = 0; i < MAX_CLIENTS; i++)
812 {
813 if(GameServer()->m_apPlayers[i] && GameServer()->m_apPlayers[i]->GetClientVersion() >= VERSION_DDRACE)
814 {
815 GameServer()->SendRecord(ClientId: i);
816 }
817 }
818 }
819 if(!NeedToSendNewServerRecord && NeedToSendNewPersonalRecord && pPlayer->GetClientVersion() >= VERSION_DDRACE)
820 {
821 GameServer()->SendRecord(ClientId);
822 }
823
824 int TTime = (int)Time;
825 std::optional<float> Score = GameServer()->Score()->PlayerData(Id: ClientId)->m_BestTime;
826 if(!Score.has_value() || TTime < Score.value())
827 {
828 Server()->SetClientScore(ClientId, Score: TTime);
829 }
830
831 // Confetti
832 CCharacter *pChar = pPlayer->GetCharacter();
833 m_pGameContext->CreateFinishEffect(Pos: pChar->m_Pos, Mask: pChar->TeamMask());
834}
835
836CCharacter *CGameTeams::Character(int ClientId)
837{
838 return GameServer()->GetPlayerChar(ClientId);
839}
840
841const CCharacter *CGameTeams::Character(int ClientId) const
842{
843 return GameServer()->GetPlayerChar(ClientId);
844}
845
846CPlayer *CGameTeams::GetPlayer(int ClientId)
847{
848 return GameServer()->m_apPlayers[ClientId];
849}
850
851CGameContext *CGameTeams::GameServer()
852{
853 return m_pGameContext;
854}
855
856const CGameContext *CGameTeams::GameServer() const
857{
858 return m_pGameContext;
859}
860
861class IServer *CGameTeams::Server()
862{
863 return m_pGameContext->Server();
864}
865
866void CGameTeams::RequestTeamSwap(CPlayer *pPlayer, CPlayer *pTargetPlayer, int Team)
867{
868 if(!pPlayer || !pTargetPlayer)
869 return;
870
871 char aBuf[512];
872 if(pPlayer->m_SwapTargetsClientId == pTargetPlayer->GetCid())
873 {
874 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
875 format: "You have already requested to swap with %s.", Server()->ClientName(ClientId: pTargetPlayer->GetCid()));
876
877 GameServer()->SendChatTarget(To: pPlayer->GetCid(), pText: aBuf);
878 return;
879 }
880
881 // Notification for the swap initiator
882 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
883 format: "You have requested to swap with %s. Use /cancelswap to cancel the request.",
884 Server()->ClientName(ClientId: pTargetPlayer->GetCid()));
885 GameServer()->SendChatTarget(To: pPlayer->GetCid(), pText: aBuf);
886
887 // Notification to the target swap player
888 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
889 format: "%s has requested to swap with you. To complete the swap process please wait %d seconds and then type /swap %s.",
890 Server()->ClientName(ClientId: pPlayer->GetCid()), g_Config.m_SvSaveSwapGamesDelay, Server()->ClientName(ClientId: pPlayer->GetCid()));
891 GameServer()->SendChatTarget(To: pTargetPlayer->GetCid(), pText: aBuf);
892
893 // Notification for the remaining team
894 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
895 format: "%s has requested to swap with %s.",
896 Server()->ClientName(ClientId: pPlayer->GetCid()), Server()->ClientName(ClientId: pTargetPlayer->GetCid()));
897 // Do not send the team notification for team 0
898 if(Team != 0)
899 {
900 for(int i = 0; i < MAX_CLIENTS; i++)
901 {
902 if(m_Core.Team(ClientId: i) == Team && i != pTargetPlayer->GetCid() && i != pPlayer->GetCid())
903 {
904 GameServer()->SendChatTarget(To: i, pText: aBuf);
905 }
906 }
907 }
908
909 pPlayer->m_SwapTargetsClientId = pTargetPlayer->GetCid();
910 m_aLastSwap[pPlayer->GetCid()] = Server()->Tick();
911}
912
913void CGameTeams::SwapTeamCharacters(CPlayer *pPrimaryPlayer, CPlayer *pTargetPlayer, int Team)
914{
915 if(!pPrimaryPlayer || !pTargetPlayer)
916 return;
917
918 char aBuf[128];
919
920 int Since = (Server()->Tick() - m_aLastSwap[pTargetPlayer->GetCid()]) / Server()->TickSpeed();
921 if(Since < g_Config.m_SvSaveSwapGamesDelay)
922 {
923 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
924 format: "You have to wait %d seconds until you can swap.",
925 g_Config.m_SvSaveSwapGamesDelay - Since);
926
927 GameServer()->SendChatTarget(To: pPrimaryPlayer->GetCid(), pText: aBuf);
928
929 return;
930 }
931
932 pPrimaryPlayer->m_SwapTargetsClientId = -1;
933 pTargetPlayer->m_SwapTargetsClientId = -1;
934
935 int TimeoutAfterDelay = g_Config.m_SvSaveSwapGamesDelay + g_Config.m_SvSwapTimeout;
936 if(Since >= TimeoutAfterDelay)
937 {
938 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
939 format: "Your swap request timed out %d seconds ago. Use /swap again to re-initiate it.",
940 Since - g_Config.m_SvSwapTimeout);
941
942 GameServer()->SendChatTarget(To: pPrimaryPlayer->GetCid(), pText: aBuf);
943
944 return;
945 }
946
947 CSaveTee PrimarySavedTee;
948 PrimarySavedTee.Save(pChr: pPrimaryPlayer->GetCharacter());
949
950 CSaveTee SecondarySavedTee;
951 SecondarySavedTee.Save(pChr: pTargetPlayer->GetCharacter());
952
953 PrimarySavedTee.Load(pChr: pTargetPlayer->GetCharacter());
954 SecondarySavedTee.Load(pChr: pPrimaryPlayer->GetCharacter());
955
956 if(Team >= 1 && !m_aTeamFlock[Team])
957 {
958 for(const auto &pPlayer : GameServer()->m_apPlayers)
959 {
960 CCharacter *pChar = pPlayer ? pPlayer->GetCharacter() : nullptr;
961 if(pChar && pChar->Team() == Team && pChar != pPrimaryPlayer->GetCharacter() && pChar != pTargetPlayer->GetCharacter())
962 pChar->m_StartTime = pPrimaryPlayer->GetCharacter()->m_StartTime;
963 }
964 }
965 std::swap(a&: m_aTeeStarted[pPrimaryPlayer->GetCid()], b&: m_aTeeStarted[pTargetPlayer->GetCid()]);
966 std::swap(a&: m_aTeeFinished[pPrimaryPlayer->GetCid()], b&: m_aTeeFinished[pTargetPlayer->GetCid()]);
967 std::swap(a&: pPrimaryPlayer->GetCharacter()->GetLastRescueTeeRef(Mode: RESCUEMODE_AUTO), b&: pTargetPlayer->GetCharacter()->GetLastRescueTeeRef(Mode: RESCUEMODE_AUTO));
968 std::swap(a&: pPrimaryPlayer->GetCharacter()->GetLastRescueTeeRef(Mode: RESCUEMODE_MANUAL), b&: pTargetPlayer->GetCharacter()->GetLastRescueTeeRef(Mode: RESCUEMODE_MANUAL));
969
970 GameServer()->m_World.SwapClients(Client1: pPrimaryPlayer->GetCid(), Client2: pTargetPlayer->GetCid());
971
972 if(GameServer()->TeeHistorianActive())
973 {
974 GameServer()->TeeHistorian()->RecordPlayerSwap(ClientId1: pPrimaryPlayer->GetCid(), ClientId2: pTargetPlayer->GetCid());
975 }
976
977 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
978 format: "%s has swapped with %s.",
979 Server()->ClientName(ClientId: pPrimaryPlayer->GetCid()), Server()->ClientName(ClientId: pTargetPlayer->GetCid()));
980
981 GameServer()->SendChatTeam(Team, pText: aBuf);
982}
983
984void CGameTeams::CancelTeamSwap(CPlayer *pPlayer, int Team)
985{
986 if(!pPlayer)
987 return;
988
989 char aBuf[128];
990
991 // Notification for the swap initiator
992 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
993 format: "You have canceled swap with %s.",
994 Server()->ClientName(ClientId: pPlayer->m_SwapTargetsClientId));
995 GameServer()->SendChatTarget(To: pPlayer->GetCid(), pText: aBuf);
996
997 // Notification to the target swap player
998 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
999 format: "%s has canceled swap with you.",
1000 Server()->ClientName(ClientId: pPlayer->GetCid()));
1001 GameServer()->SendChatTarget(To: pPlayer->m_SwapTargetsClientId, pText: aBuf);
1002
1003 // Notification for the remaining team
1004 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1005 format: "%s has canceled swap with %s.",
1006 Server()->ClientName(ClientId: pPlayer->GetCid()), Server()->ClientName(ClientId: pPlayer->m_SwapTargetsClientId));
1007 // Do not send the team notification for team 0
1008 if(Team != 0)
1009 {
1010 for(int i = 0; i < MAX_CLIENTS; i++)
1011 {
1012 if(m_Core.Team(ClientId: i) == Team && i != pPlayer->m_SwapTargetsClientId && i != pPlayer->GetCid())
1013 {
1014 GameServer()->SendChatTarget(To: i, pText: aBuf);
1015 }
1016 }
1017 }
1018
1019 pPlayer->m_SwapTargetsClientId = -1;
1020}
1021
1022void CGameTeams::ProcessSaveTeam()
1023{
1024 for(int Team = 0; Team < NUM_DDRACE_TEAMS; Team++)
1025 {
1026 if(m_apSaveTeamResult[Team] == nullptr || !m_apSaveTeamResult[Team]->m_Completed)
1027 continue;
1028
1029 int Size = m_apSaveTeamResult[Team]->m_SavedTeam.GetMembersCount();
1030 int State = -1;
1031
1032 switch(m_apSaveTeamResult[Team]->m_Status)
1033 {
1034 case CScoreSaveResult::SAVE_FALLBACKFILE:
1035 State = SAVESTATE_FALLBACKFILE;
1036 break;
1037 case CScoreSaveResult::SAVE_WARNING:
1038 State = SAVESTATE_WARNING;
1039 break;
1040 case CScoreSaveResult::SAVE_SUCCESS:
1041 State = SAVESTATE_DONE;
1042 break;
1043 case CScoreSaveResult::SAVE_FAILED:
1044 State = SAVESTATE_ERROR;
1045 break;
1046 case CScoreSaveResult::LOAD_FAILED:
1047 case CScoreSaveResult::LOAD_SUCCESS:
1048 State = -1;
1049 break;
1050 }
1051
1052 if(State != -1)
1053 {
1054 GameServer()->SendSaveCode(
1055 Team,
1056 TeamSize: Size,
1057 State,
1058 pError: (State == SAVESTATE_DONE) ? "" : m_apSaveTeamResult[Team]->m_aMessage,
1059 pSaveRequester: m_apSaveTeamResult[Team]->m_aRequestingPlayer,
1060 pServerName: m_apSaveTeamResult[Team]->m_aServer,
1061 pGeneratedCode: m_apSaveTeamResult[Team]->m_aGeneratedCode,
1062 pCode: m_apSaveTeamResult[Team]->m_aCode);
1063 }
1064
1065 if(m_apSaveTeamResult[Team]->m_aBroadcast[0] != '\0')
1066 GameServer()->SendBroadcast(pText: m_apSaveTeamResult[Team]->m_aBroadcast, ClientId: -1);
1067
1068 switch(m_apSaveTeamResult[Team]->m_Status)
1069 {
1070 case CScoreSaveResult::SAVE_FALLBACKFILE:
1071 case CScoreSaveResult::SAVE_WARNING:
1072 case CScoreSaveResult::SAVE_SUCCESS:
1073 {
1074 if(GameServer()->TeeHistorianActive())
1075 {
1076 GameServer()->TeeHistorian()->RecordTeamSaveSuccess(
1077 Team,
1078 SaveId: m_apSaveTeamResult[Team]->m_SaveId,
1079 pTeamSave: m_apSaveTeamResult[Team]->m_SavedTeam.GetString());
1080 }
1081 for(int i = 0; i < Size; i++)
1082 {
1083 if(m_apSaveTeamResult[Team]->m_SavedTeam.m_pSavedTees->IsHooking())
1084 {
1085 int ClientId = m_apSaveTeamResult[Team]->m_SavedTeam.m_pSavedTees->GetClientId();
1086 if(GameServer()->m_apPlayers[ClientId] != nullptr)
1087 GameServer()->SendChatTarget(To: ClientId, pText: "Start holding the hook before loading the savegame to keep the hook");
1088 }
1089 }
1090 ResetSavedTeam(ClientId: m_apSaveTeamResult[Team]->m_RequestingPlayer, Team);
1091 char aSaveId[UUID_MAXSTRSIZE];
1092 FormatUuid(Uuid: m_apSaveTeamResult[Team]->m_SaveId, pBuffer: aSaveId, BufferLength: UUID_MAXSTRSIZE);
1093 dbg_msg(sys: "save", fmt: "Save successful: %s", aSaveId);
1094 break;
1095 }
1096 case CScoreSaveResult::SAVE_FAILED:
1097 if(GameServer()->TeeHistorianActive())
1098 GameServer()->TeeHistorian()->RecordTeamSaveFailure(Team);
1099 if(TeamSize(Team) > 0)
1100 {
1101 // load weak/strong order to prevent switching weak/strong while saving
1102 m_apSaveTeamResult[Team]->m_SavedTeam.Load(pGameServer: GameServer(), Team, KeepCurrentWeakStrong: false);
1103 }
1104 break;
1105 case CScoreSaveResult::LOAD_SUCCESS:
1106 {
1107 if(GameServer()->TeeHistorianActive())
1108 {
1109 GameServer()->TeeHistorian()->RecordTeamLoadSuccess(
1110 Team,
1111 SaveId: m_apSaveTeamResult[Team]->m_SaveId,
1112 pTeamSave: m_apSaveTeamResult[Team]->m_SavedTeam.GetString());
1113 }
1114
1115 bool TeamValid = false;
1116 if(TeamSize(Team) > 0)
1117 {
1118 // keep current weak/strong order as on some maps there is no other way of switching
1119 TeamValid = m_apSaveTeamResult[Team]->m_SavedTeam.Load(pGameServer: GameServer(), Team, KeepCurrentWeakStrong: true);
1120 }
1121
1122 if(!TeamValid)
1123 {
1124 GameServer()->SendChatTeam(Team, pText: "Your team has been killed because it contains an invalid tee state");
1125 KillTeam(Team, NewStrongId: -1);
1126 }
1127
1128 char aSaveId[UUID_MAXSTRSIZE];
1129 FormatUuid(Uuid: m_apSaveTeamResult[Team]->m_SaveId, pBuffer: aSaveId, BufferLength: UUID_MAXSTRSIZE);
1130 dbg_msg(sys: "save", fmt: "Load successful: %s", aSaveId);
1131 break;
1132 }
1133 case CScoreSaveResult::LOAD_FAILED:
1134 if(GameServer()->TeeHistorianActive())
1135 GameServer()->TeeHistorian()->RecordTeamLoadFailure(Team);
1136 if(m_apSaveTeamResult[Team]->m_aMessage[0] != '\0')
1137 GameServer()->SendChatTarget(To: m_apSaveTeamResult[Team]->m_RequestingPlayer, pText: m_apSaveTeamResult[Team]->m_aMessage);
1138 break;
1139 }
1140 m_apSaveTeamResult[Team] = nullptr;
1141 }
1142}
1143
1144void CGameTeams::OnCharacterSpawn(int ClientId)
1145{
1146 m_Core.SetSolo(ClientId, Value: false);
1147 int Team = m_Core.Team(ClientId);
1148
1149 if(GetSaving(TeamId: Team))
1150 return;
1151
1152 if(!IsValidTeamNumber(Team) || !m_aTeamLocked[Team])
1153 {
1154 if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO)
1155 SetForceCharacterTeam(ClientId, Team: TEAM_FLOCK);
1156 else
1157 SetForceCharacterTeam(ClientId, Team: ClientId); // initialize team
1158 if(!m_aTeamFlock[Team])
1159 CheckTeamFinished(Team);
1160 }
1161}
1162
1163void CGameTeams::OnCharacterDeath(int ClientId, int Weapon)
1164{
1165 m_Core.SetSolo(ClientId, Value: false);
1166
1167 int Team = m_Core.Team(ClientId);
1168 if(GetSaving(TeamId: Team))
1169 return;
1170 bool Locked = TeamLocked(Team) && Weapon != WEAPON_GAME;
1171
1172 if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO && Team != TEAM_SUPER)
1173 {
1174 ChangeTeamState(Team, State: ETeamState::OPEN);
1175 if(m_aPractice[Team])
1176 {
1177 if(Weapon != WEAPON_WORLD)
1178 {
1179 ResetRoundState(Team);
1180 }
1181 else
1182 {
1183 GameServer()->SendChatTeam(Team, pText: "You died, but will stay in practice until you use kill.");
1184 }
1185 }
1186 else
1187 {
1188 ResetRoundState(Team);
1189 }
1190 }
1191 else if(Locked)
1192 {
1193 SetForceCharacterTeam(ClientId, Team);
1194
1195 if(GetTeamState(Team) != ETeamState::OPEN && !m_aTeamFlock[m_Core.Team(ClientId)])
1196 {
1197 ChangeTeamState(Team, State: ETeamState::OPEN);
1198
1199 if(!m_pGameContext->PracticeByDefault())
1200 {
1201 if(!g_Config.m_SvPauseable)
1202 {
1203 for(int ClientId1 = 0; ClientId1 < MAX_CLIENTS; ClientId1++)
1204 {
1205 if(m_Core.Team(ClientId: ClientId1) == Team)
1206 {
1207 CPlayer *pPlayer = GameServer()->m_apPlayers[ClientId1];
1208 if(pPlayer && pPlayer->IsPaused() == -1 * CPlayer::PAUSE_SPEC)
1209 pPlayer->Pause(State: CPlayer::PAUSE_PAUSED, Force: true);
1210 }
1211 }
1212 }
1213 m_aPractice[Team] = false;
1214 }
1215
1216 if(TeamSize(Team) > 1)
1217 {
1218 // Disband team if the team has more players than allowed.
1219 if(TeamSize(Team) > g_Config.m_SvMaxTeamSize)
1220 {
1221 GameServer()->SendChatTeam(Team, pText: "This team was disbanded because there are more players than allowed in the team.");
1222 SetTeamLock(Team, Lock: false);
1223 KillTeam(Team, NewStrongId: Weapon == WEAPON_SELF ? ClientId : -1, ExceptId: ClientId);
1224 return;
1225 }
1226
1227 KillTeam(Team, NewStrongId: Weapon == WEAPON_SELF ? ClientId : -1, ExceptId: ClientId);
1228
1229 char aBuf[512];
1230 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Everyone in your locked team was killed because '%s' %s.", Server()->ClientName(ClientId), Weapon == WEAPON_SELF ? "killed" : "died");
1231
1232 GameServer()->SendChatTeam(Team, pText: aBuf);
1233 }
1234 }
1235 }
1236 else
1237 {
1238 if(m_aTeamState[m_Core.Team(ClientId)] == ETeamState::STARTED && !m_aTeeStarted[ClientId] && !m_aTeamFlock[m_Core.Team(ClientId)])
1239 {
1240 char aBuf[128];
1241 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "This team cannot finish anymore because '%s' left the team before hitting the start", Server()->ClientName(ClientId));
1242 GameServer()->SendChatTeam(Team, pText: aBuf);
1243 GameServer()->SendChatTeam(Team, pText: "Enter /practice mode or restart to avoid the entire team being killed in 60 seconds");
1244
1245 m_aTeamUnfinishableKillTick[Team] = Server()->Tick() + 60 * Server()->TickSpeed();
1246 ChangeTeamState(Team, State: ETeamState::STARTED_UNFINISHABLE);
1247 }
1248 SetForceCharacterTeam(ClientId, Team: TEAM_FLOCK);
1249 if(!m_aTeamFlock[m_Core.Team(ClientId)])
1250 CheckTeamFinished(Team);
1251 }
1252}
1253
1254void CGameTeams::SetTeamLock(int Team, bool Lock)
1255{
1256 if(Team != TEAM_FLOCK && IsValidTeamNumber(Team))
1257 m_aTeamLocked[Team] = Lock;
1258}
1259
1260void CGameTeams::SetTeamFlock(int Team, bool Mode)
1261{
1262 if(Team != TEAM_FLOCK && IsValidTeamNumber(Team))
1263 m_aTeamFlock[Team] = Mode;
1264}
1265
1266void CGameTeams::ResetInvited(int Team)
1267{
1268 m_aInvited[Team].reset();
1269}
1270
1271void CGameTeams::SetClientInvited(int Team, int ClientId, bool Invited)
1272{
1273 if(Team != TEAM_FLOCK && IsValidTeamNumber(Team))
1274 {
1275 if(Invited)
1276 m_aInvited[Team].set(pos: ClientId);
1277 else
1278 m_aInvited[Team].reset(pos: ClientId);
1279 }
1280}
1281
1282void CGameTeams::KillCharacterOrTeam(int ClientId, int Team)
1283{
1284 if(g_Config.m_SvSoloServer || !g_Config.m_SvTeam)
1285 {
1286 GameServer()->m_apPlayers[ClientId]->KillCharacter(Weapon: WEAPON_SELF, SendKillMsg: true);
1287 }
1288 else
1289 {
1290 KillTeam(Team, NewStrongId: -1);
1291 }
1292}
1293
1294void CGameTeams::ResetSavedTeam(int ClientId, int Team)
1295{
1296 if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO)
1297 {
1298 ChangeTeamState(Team, State: ETeamState::OPEN);
1299 ResetRoundState(Team);
1300 }
1301 else
1302 {
1303 for(int i = 0; i < MAX_CLIENTS; i++)
1304 {
1305 if(m_Core.Team(ClientId: i) == Team && GameServer()->m_apPlayers[i])
1306 {
1307 SetForceCharacterTeam(ClientId: i, Team: TEAM_FLOCK);
1308 }
1309 }
1310 }
1311}
1312
1313std::optional<int> CGameTeams::GetFirstEmptyTeam() const
1314{
1315 for(int i = 1; i < TEAM_SUPER; i++)
1316 if(m_aTeamState[i] == ETeamState::EMPTY)
1317 return i;
1318 return std::nullopt;
1319}
1320
1321bool CGameTeams::TeeStarted(int ClientId) const
1322{
1323 return m_aTeeStarted[ClientId];
1324}
1325
1326bool CGameTeams::TeeFinished(int ClientId) const
1327{
1328 return m_aTeeFinished[ClientId];
1329}
1330
1331ETeamState CGameTeams::GetTeamState(int Team) const
1332{
1333 return m_aTeamState[Team];
1334}
1335
1336bool CGameTeams::TeamLocked(int Team) const
1337{
1338 if(Team == TEAM_FLOCK || !IsValidTeamNumber(Team))
1339 return false;
1340
1341 return m_aTeamLocked[Team];
1342}
1343
1344bool CGameTeams::TeamFlock(int Team) const
1345{
1346 // this is for team0mode, TEAM_FLOCK is handled differently
1347 if(Team == TEAM_FLOCK || !IsValidTeamNumber(Team))
1348 return false;
1349
1350 return m_aTeamFlock[Team];
1351}
1352
1353bool CGameTeams::IsInvited(int Team, int ClientId) const
1354{
1355 return m_aInvited[Team].test(position: ClientId);
1356}
1357
1358bool CGameTeams::IsStarted(int Team) const
1359{
1360 return m_aTeamState[Team] == ETeamState::STARTED;
1361}
1362
1363void CGameTeams::SetStarted(int ClientId, bool Started)
1364{
1365 m_aTeeStarted[ClientId] = Started;
1366}
1367
1368void CGameTeams::SetFinished(int ClientId, bool Finished)
1369{
1370 m_aTeeFinished[ClientId] = Finished;
1371}
1372
1373void CGameTeams::SetSaving(int TeamId, std::shared_ptr<CScoreSaveResult> &SaveResult)
1374{
1375 m_apSaveTeamResult[TeamId] = SaveResult;
1376}
1377
1378bool CGameTeams::GetSaving(int TeamId) const
1379{
1380 if(!IsValidTeamNumber(Team: TeamId))
1381 return false;
1382 if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && TeamId == TEAM_FLOCK)
1383 return false;
1384
1385 return m_apSaveTeamResult[TeamId] != nullptr;
1386}
1387
1388void CGameTeams::SetPractice(int Team, bool Enabled)
1389{
1390 if(!IsValidTeamNumber(Team))
1391 return;
1392 if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && Team == TEAM_FLOCK)
1393 {
1394 // allow to enable practice in team 0, for practice by default
1395 if(!g_Config.m_SvTestingCommands)
1396 return;
1397 }
1398
1399 m_aPractice[Team] = Enabled;
1400}
1401
1402bool CGameTeams::IsPractice(int Team)
1403{
1404 if(!IsValidTeamNumber(Team))
1405 return false;
1406 if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && Team == TEAM_FLOCK)
1407 {
1408 if(GameServer()->PracticeByDefault())
1409 return true;
1410
1411 return false;
1412 }
1413
1414 return m_aPractice[Team];
1415}
1416
1417bool CGameTeams::IsValidTeamNumber(int Team) const
1418{
1419 return Team >= TEAM_FLOCK && Team < NUM_DDRACE_TEAMS - 1; // no TEAM_SUPER
1420}
1421