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