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