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 *Player, int TimeTicks, const char *pTimestamp)
745{
746 if(!Player || !Player->IsPlaying())
747 return;
748
749 float Time = TimeTicks / (float)Server()->TickSpeed();
750
751 // TODO:DDRace:btd: this ugly
752 const int ClientId = Player->GetCid();
753 CPlayerData *pData = GameServer()->Score()->PlayerData(Id: ClientId);
754
755 char aBuf[128];
756 SetLastTimeCp(Player, 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);
768
769 if(Time - pData->m_BestTime < 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 != 0) // 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));
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), NotEligible: Player->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, 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 && Player->GetClientVersion() >= VERSION_DDRACE)
857 {
858 GameServer()->SendRecord(ClientId);
859 }
860
861 int TTime = (int)Time;
862 if(!Player->m_Score.has_value() || TTime < Player->m_Score.value())
863 {
864 Player->m_Score = TTime;
865 }
866
867 // Confetti
868 CCharacter *pChar = Player->GetCharacter();
869 m_pGameContext->CreateFinishEffect(Pos: pChar->m_Pos, Mask: pChar->TeamMask());
870}
871
872CCharacter *CGameTeams::Character(int ClientId)
873{
874 return GameServer()->GetPlayerChar(ClientId);
875}
876
877const CCharacter *CGameTeams::Character(int ClientId) const
878{
879 return GameServer()->GetPlayerChar(ClientId);
880}
881
882CPlayer *CGameTeams::GetPlayer(int ClientId)
883{
884 return GameServer()->m_apPlayers[ClientId];
885}
886
887CGameContext *CGameTeams::GameServer()
888{
889 return m_pGameContext;
890}
891
892const CGameContext *CGameTeams::GameServer() const
893{
894 return m_pGameContext;
895}
896
897class IServer *CGameTeams::Server()
898{
899 return m_pGameContext->Server();
900}
901
902void CGameTeams::RequestTeamSwap(CPlayer *pPlayer, CPlayer *pTargetPlayer, int Team)
903{
904 if(!pPlayer || !pTargetPlayer)
905 return;
906
907 char aBuf[512];
908 if(pPlayer->m_SwapTargetsClientId == pTargetPlayer->GetCid())
909 {
910 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
911 format: "You have already requested to swap with %s.", Server()->ClientName(ClientId: pTargetPlayer->GetCid()));
912
913 GameServer()->SendChatTarget(To: pPlayer->GetCid(), pText: aBuf);
914 return;
915 }
916
917 // Notification for the swap initiator
918 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
919 format: "You have requested to swap with %s. Use /cancelswap to cancel the request.",
920 Server()->ClientName(ClientId: pTargetPlayer->GetCid()));
921 GameServer()->SendChatTarget(To: pPlayer->GetCid(), pText: aBuf);
922
923 // Notification to the target swap player
924 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
925 format: "%s has requested to swap with you. To complete the swap process please wait %d seconds and then type /swap %s.",
926 Server()->ClientName(ClientId: pPlayer->GetCid()), g_Config.m_SvSaveSwapGamesDelay, Server()->ClientName(ClientId: pPlayer->GetCid()));
927 GameServer()->SendChatTarget(To: pTargetPlayer->GetCid(), pText: aBuf);
928
929 // Notification for the remaining team
930 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
931 format: "%s has requested to swap with %s.",
932 Server()->ClientName(ClientId: pPlayer->GetCid()), Server()->ClientName(ClientId: pTargetPlayer->GetCid()));
933 // Do not send the team notification for team 0
934 if(Team != 0)
935 {
936 for(int i = 0; i < MAX_CLIENTS; i++)
937 {
938 if(m_Core.Team(ClientId: i) == Team && i != pTargetPlayer->GetCid() && i != pPlayer->GetCid())
939 {
940 GameServer()->SendChatTarget(To: i, pText: aBuf);
941 }
942 }
943 }
944
945 pPlayer->m_SwapTargetsClientId = pTargetPlayer->GetCid();
946 m_aLastSwap[pPlayer->GetCid()] = Server()->Tick();
947}
948
949void CGameTeams::SwapTeamCharacters(CPlayer *pPrimaryPlayer, CPlayer *pTargetPlayer, int Team)
950{
951 if(!pPrimaryPlayer || !pTargetPlayer)
952 return;
953
954 char aBuf[128];
955
956 int Since = (Server()->Tick() - m_aLastSwap[pTargetPlayer->GetCid()]) / Server()->TickSpeed();
957 if(Since < g_Config.m_SvSaveSwapGamesDelay)
958 {
959 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
960 format: "You have to wait %d seconds until you can swap.",
961 g_Config.m_SvSaveSwapGamesDelay - Since);
962
963 GameServer()->SendChatTarget(To: pPrimaryPlayer->GetCid(), pText: aBuf);
964
965 return;
966 }
967
968 pPrimaryPlayer->m_SwapTargetsClientId = -1;
969 pTargetPlayer->m_SwapTargetsClientId = -1;
970
971 int TimeoutAfterDelay = g_Config.m_SvSaveSwapGamesDelay + g_Config.m_SvSwapTimeout;
972 if(Since >= TimeoutAfterDelay)
973 {
974 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
975 format: "Your swap request timed out %d seconds ago. Use /swap again to re-initiate it.",
976 Since - g_Config.m_SvSwapTimeout);
977
978 GameServer()->SendChatTarget(To: pPrimaryPlayer->GetCid(), pText: aBuf);
979
980 return;
981 }
982
983 CSaveTee PrimarySavedTee;
984 PrimarySavedTee.Save(pChr: pPrimaryPlayer->GetCharacter());
985
986 CSaveTee SecondarySavedTee;
987 SecondarySavedTee.Save(pChr: pTargetPlayer->GetCharacter());
988
989 PrimarySavedTee.Load(pChr: pTargetPlayer->GetCharacter());
990 SecondarySavedTee.Load(pChr: pPrimaryPlayer->GetCharacter());
991
992 if(Team >= 1 && !m_aTeamFlock[Team])
993 {
994 for(const auto &pPlayer : GameServer()->m_apPlayers)
995 {
996 CCharacter *pChar = pPlayer ? pPlayer->GetCharacter() : nullptr;
997 if(pChar && pChar->Team() == Team && pChar != pPrimaryPlayer->GetCharacter() && pChar != pTargetPlayer->GetCharacter())
998 pChar->m_StartTime = pPrimaryPlayer->GetCharacter()->m_StartTime;
999 }
1000 }
1001 std::swap(a&: m_aTeeStarted[pPrimaryPlayer->GetCid()], b&: m_aTeeStarted[pTargetPlayer->GetCid()]);
1002 std::swap(a&: m_aTeeFinished[pPrimaryPlayer->GetCid()], b&: m_aTeeFinished[pTargetPlayer->GetCid()]);
1003 std::swap(a&: pPrimaryPlayer->GetCharacter()->GetLastRescueTeeRef(Mode: RESCUEMODE_AUTO), b&: pTargetPlayer->GetCharacter()->GetLastRescueTeeRef(Mode: RESCUEMODE_AUTO));
1004 std::swap(a&: pPrimaryPlayer->GetCharacter()->GetLastRescueTeeRef(Mode: RESCUEMODE_MANUAL), b&: pTargetPlayer->GetCharacter()->GetLastRescueTeeRef(Mode: RESCUEMODE_MANUAL));
1005
1006 GameServer()->m_World.SwapClients(Client1: pPrimaryPlayer->GetCid(), Client2: pTargetPlayer->GetCid());
1007
1008 if(GameServer()->TeeHistorianActive())
1009 {
1010 GameServer()->TeeHistorian()->RecordPlayerSwap(ClientId1: pPrimaryPlayer->GetCid(), ClientId2: pTargetPlayer->GetCid());
1011 }
1012
1013 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1014 format: "%s has swapped with %s.",
1015 Server()->ClientName(ClientId: pPrimaryPlayer->GetCid()), Server()->ClientName(ClientId: pTargetPlayer->GetCid()));
1016
1017 GameServer()->SendChatTeam(Team, pText: aBuf);
1018}
1019
1020void CGameTeams::CancelTeamSwap(CPlayer *pPlayer, int Team)
1021{
1022 if(!pPlayer)
1023 return;
1024
1025 char aBuf[128];
1026
1027 // Notification for the swap initiator
1028 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1029 format: "You have canceled swap with %s.",
1030 Server()->ClientName(ClientId: pPlayer->m_SwapTargetsClientId));
1031 GameServer()->SendChatTarget(To: pPlayer->GetCid(), pText: aBuf);
1032
1033 // Notification to the target swap player
1034 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1035 format: "%s has canceled swap with you.",
1036 Server()->ClientName(ClientId: pPlayer->GetCid()));
1037 GameServer()->SendChatTarget(To: pPlayer->m_SwapTargetsClientId, pText: aBuf);
1038
1039 // Notification for the remaining team
1040 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
1041 format: "%s has canceled swap with %s.",
1042 Server()->ClientName(ClientId: pPlayer->GetCid()), Server()->ClientName(ClientId: pPlayer->m_SwapTargetsClientId));
1043 // Do not send the team notification for team 0
1044 if(Team != 0)
1045 {
1046 for(int i = 0; i < MAX_CLIENTS; i++)
1047 {
1048 if(m_Core.Team(ClientId: i) == Team && i != pPlayer->m_SwapTargetsClientId && i != pPlayer->GetCid())
1049 {
1050 GameServer()->SendChatTarget(To: i, pText: aBuf);
1051 }
1052 }
1053 }
1054
1055 pPlayer->m_SwapTargetsClientId = -1;
1056}
1057
1058void CGameTeams::ProcessSaveTeam()
1059{
1060 for(int Team = 0; Team < NUM_DDRACE_TEAMS; Team++)
1061 {
1062 if(m_apSaveTeamResult[Team] == nullptr || !m_apSaveTeamResult[Team]->m_Completed)
1063 continue;
1064
1065 int TeamSize = m_apSaveTeamResult[Team]->m_SavedTeam.GetMembersCount();
1066 int State = -1;
1067
1068 switch(m_apSaveTeamResult[Team]->m_Status)
1069 {
1070 case CScoreSaveResult::SAVE_FALLBACKFILE:
1071 State = SAVESTATE_FALLBACKFILE;
1072 break;
1073 case CScoreSaveResult::SAVE_WARNING:
1074 State = SAVESTATE_WARNING;
1075 break;
1076 case CScoreSaveResult::SAVE_SUCCESS:
1077 State = SAVESTATE_DONE;
1078 break;
1079 case CScoreSaveResult::SAVE_FAILED:
1080 State = SAVESTATE_ERROR;
1081 break;
1082 case CScoreSaveResult::LOAD_FAILED:
1083 case CScoreSaveResult::LOAD_SUCCESS:
1084 State = -1;
1085 break;
1086 }
1087
1088 if(State != -1)
1089 {
1090 GameServer()->SendSaveCode(
1091 Team,
1092 TeamSize,
1093 State,
1094 pError: (State == SAVESTATE_DONE) ? "" : m_apSaveTeamResult[Team]->m_aMessage,
1095 pSaveRequester: m_apSaveTeamResult[Team]->m_aRequestingPlayer,
1096 pServerName: m_apSaveTeamResult[Team]->m_aServer,
1097 pGeneratedCode: m_apSaveTeamResult[Team]->m_aGeneratedCode,
1098 pCode: m_apSaveTeamResult[Team]->m_aCode);
1099 }
1100
1101 if(m_apSaveTeamResult[Team]->m_aBroadcast[0] != '\0')
1102 GameServer()->SendBroadcast(pText: m_apSaveTeamResult[Team]->m_aBroadcast, ClientId: -1);
1103
1104 switch(m_apSaveTeamResult[Team]->m_Status)
1105 {
1106 case CScoreSaveResult::SAVE_FALLBACKFILE:
1107 case CScoreSaveResult::SAVE_WARNING:
1108 case CScoreSaveResult::SAVE_SUCCESS:
1109 {
1110 if(GameServer()->TeeHistorianActive())
1111 {
1112 GameServer()->TeeHistorian()->RecordTeamSaveSuccess(
1113 Team,
1114 SaveId: m_apSaveTeamResult[Team]->m_SaveId,
1115 pTeamSave: m_apSaveTeamResult[Team]->m_SavedTeam.GetString());
1116 }
1117 for(int i = 0; i < TeamSize; i++)
1118 {
1119 if(m_apSaveTeamResult[Team]->m_SavedTeam.m_pSavedTees->IsHooking())
1120 {
1121 int ClientId = m_apSaveTeamResult[Team]->m_SavedTeam.m_pSavedTees->GetClientId();
1122 if(GameServer()->m_apPlayers[ClientId] != nullptr)
1123 GameServer()->SendChatTarget(To: ClientId, pText: "Start holding the hook before loading the savegame to keep the hook");
1124 }
1125 }
1126 ResetSavedTeam(ClientId: m_apSaveTeamResult[Team]->m_RequestingPlayer, Team);
1127 char aSaveId[UUID_MAXSTRSIZE];
1128 FormatUuid(Uuid: m_apSaveTeamResult[Team]->m_SaveId, pBuffer: aSaveId, BufferLength: UUID_MAXSTRSIZE);
1129 dbg_msg(sys: "save", fmt: "Save successful: %s", aSaveId);
1130 break;
1131 }
1132 case CScoreSaveResult::SAVE_FAILED:
1133 if(GameServer()->TeeHistorianActive())
1134 GameServer()->TeeHistorian()->RecordTeamSaveFailure(Team);
1135 if(Count(Team) > 0)
1136 {
1137 // load weak/strong order to prevent switching weak/strong while saving
1138 m_apSaveTeamResult[Team]->m_SavedTeam.Load(pGameServer: GameServer(), Team, KeepCurrentWeakStrong: false);
1139 }
1140 break;
1141 case CScoreSaveResult::LOAD_SUCCESS:
1142 {
1143 if(GameServer()->TeeHistorianActive())
1144 {
1145 GameServer()->TeeHistorian()->RecordTeamLoadSuccess(
1146 Team,
1147 SaveId: m_apSaveTeamResult[Team]->m_SaveId,
1148 pTeamSave: m_apSaveTeamResult[Team]->m_SavedTeam.GetString());
1149 }
1150
1151 bool TeamValid = false;
1152 if(Count(Team) > 0)
1153 {
1154 // keep current weak/strong order as on some maps there is no other way of switching
1155 TeamValid = m_apSaveTeamResult[Team]->m_SavedTeam.Load(pGameServer: GameServer(), Team, KeepCurrentWeakStrong: true);
1156 }
1157
1158 if(!TeamValid)
1159 {
1160 GameServer()->SendChatTeam(Team, pText: "Your team has been killed because it contains an invalid tee state");
1161 KillTeam(Team, NewStrongId: -1, ExceptId: -1);
1162 }
1163
1164 char aSaveId[UUID_MAXSTRSIZE];
1165 FormatUuid(Uuid: m_apSaveTeamResult[Team]->m_SaveId, pBuffer: aSaveId, BufferLength: UUID_MAXSTRSIZE);
1166 dbg_msg(sys: "save", fmt: "Load successful: %s", aSaveId);
1167 break;
1168 }
1169 case CScoreSaveResult::LOAD_FAILED:
1170 if(GameServer()->TeeHistorianActive())
1171 GameServer()->TeeHistorian()->RecordTeamLoadFailure(Team);
1172 if(m_apSaveTeamResult[Team]->m_aMessage[0] != '\0')
1173 GameServer()->SendChatTarget(To: m_apSaveTeamResult[Team]->m_RequestingPlayer, pText: m_apSaveTeamResult[Team]->m_aMessage);
1174 break;
1175 }
1176 m_apSaveTeamResult[Team] = nullptr;
1177 }
1178}
1179
1180void CGameTeams::OnCharacterSpawn(int ClientId)
1181{
1182 m_Core.SetSolo(ClientId, Value: false);
1183 int Team = m_Core.Team(ClientId);
1184
1185 if(GetSaving(TeamId: Team))
1186 return;
1187
1188 if(!IsValidTeamNumber(Team) || !m_aTeamLocked[Team])
1189 {
1190 if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO)
1191 SetForceCharacterTeam(ClientId, Team: TEAM_FLOCK);
1192 else
1193 SetForceCharacterTeam(ClientId, Team: ClientId); // initialize team
1194 if(!m_aTeamFlock[Team])
1195 CheckTeamFinished(Team);
1196 }
1197}
1198
1199void CGameTeams::OnCharacterDeath(int ClientId, int Weapon)
1200{
1201 m_Core.SetSolo(ClientId, Value: false);
1202
1203 int Team = m_Core.Team(ClientId);
1204 if(GetSaving(TeamId: Team))
1205 return;
1206 bool Locked = TeamLocked(Team) && Weapon != WEAPON_GAME;
1207
1208 if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO && Team != TEAM_SUPER)
1209 {
1210 ChangeTeamState(Team, State: ETeamState::OPEN);
1211 if(m_aPractice[Team])
1212 {
1213 if(Weapon != WEAPON_WORLD)
1214 {
1215 ResetRoundState(Team);
1216 }
1217 else
1218 {
1219 GameServer()->SendChatTeam(Team, pText: "You died, but will stay in practice until you use kill.");
1220 }
1221 }
1222 else
1223 {
1224 ResetRoundState(Team);
1225 }
1226 }
1227 else if(Locked)
1228 {
1229 SetForceCharacterTeam(ClientId, Team);
1230
1231 if(GetTeamState(Team) != ETeamState::OPEN && !m_aTeamFlock[m_Core.Team(ClientId)])
1232 {
1233 ChangeTeamState(Team, State: ETeamState::OPEN);
1234
1235 if(!m_pGameContext->PracticeByDefault())
1236 m_aPractice[Team] = false;
1237
1238 if(Count(Team) > 1)
1239 {
1240 // Disband team if the team has more players than allowed.
1241 if(Count(Team) > g_Config.m_SvMaxTeamSize)
1242 {
1243 GameServer()->SendChatTeam(Team, pText: "This team was disbanded because there are more players than allowed in the team.");
1244 SetTeamLock(Team, Lock: false);
1245 KillTeam(Team, NewStrongId: Weapon == WEAPON_SELF ? ClientId : -1, ExceptId: ClientId);
1246 return;
1247 }
1248
1249 KillTeam(Team, NewStrongId: Weapon == WEAPON_SELF ? ClientId : -1, ExceptId: ClientId);
1250
1251 char aBuf[512];
1252 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");
1253
1254 GameServer()->SendChatTeam(Team, pText: aBuf);
1255 }
1256 }
1257 }
1258 else
1259 {
1260 if(m_aTeamState[m_Core.Team(ClientId)] == ETeamState::STARTED && !m_aTeeStarted[ClientId] && !m_aTeamFlock[m_Core.Team(ClientId)])
1261 {
1262 char aBuf[128];
1263 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));
1264 GameServer()->SendChatTeam(Team, pText: aBuf);
1265 GameServer()->SendChatTeam(Team, pText: "Enter /practice mode or restart to avoid the entire team being killed in 60 seconds");
1266
1267 m_aTeamUnfinishableKillTick[Team] = Server()->Tick() + 60 * Server()->TickSpeed();
1268 ChangeTeamState(Team, State: ETeamState::STARTED_UNFINISHABLE);
1269 }
1270 SetForceCharacterTeam(ClientId, Team: TEAM_FLOCK);
1271 if(!m_aTeamFlock[m_Core.Team(ClientId)])
1272 CheckTeamFinished(Team);
1273 }
1274}
1275
1276void CGameTeams::SetTeamLock(int Team, bool Lock)
1277{
1278 if(Team != TEAM_FLOCK && IsValidTeamNumber(Team))
1279 m_aTeamLocked[Team] = Lock;
1280}
1281
1282void CGameTeams::SetTeamFlock(int Team, bool Mode)
1283{
1284 if(Team != TEAM_FLOCK && IsValidTeamNumber(Team))
1285 m_aTeamFlock[Team] = Mode;
1286}
1287
1288void CGameTeams::ResetInvited(int Team)
1289{
1290 m_aInvited[Team].reset();
1291}
1292
1293void CGameTeams::SetClientInvited(int Team, int ClientId, bool Invited)
1294{
1295 if(Team != TEAM_FLOCK && IsValidTeamNumber(Team))
1296 {
1297 if(Invited)
1298 m_aInvited[Team].set(position: ClientId);
1299 else
1300 m_aInvited[Team].reset(position: ClientId);
1301 }
1302}
1303
1304void CGameTeams::KillCharacterOrTeam(int ClientId, int Team)
1305{
1306 if(g_Config.m_SvSoloServer || !g_Config.m_SvTeam)
1307 {
1308 GameServer()->m_apPlayers[ClientId]->KillCharacter(Weapon: WEAPON_SELF, SendKillMsg: true);
1309 }
1310 else
1311 {
1312 KillTeam(Team, NewStrongId: -1);
1313 }
1314}
1315
1316void CGameTeams::ResetSavedTeam(int ClientId, int Team)
1317{
1318 if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO)
1319 {
1320 ChangeTeamState(Team, State: ETeamState::OPEN);
1321 ResetRoundState(Team);
1322 }
1323 else
1324 {
1325 for(int i = 0; i < MAX_CLIENTS; i++)
1326 {
1327 if(m_Core.Team(ClientId: i) == Team && GameServer()->m_apPlayers[i])
1328 {
1329 SetForceCharacterTeam(ClientId: i, Team: TEAM_FLOCK);
1330 }
1331 }
1332 }
1333}
1334
1335std::optional<int> CGameTeams::GetFirstEmptyTeam() const
1336{
1337 for(int i = 1; i < TEAM_SUPER; i++)
1338 if(m_aTeamState[i] == ETeamState::EMPTY)
1339 return i;
1340 return std::nullopt;
1341}
1342
1343bool CGameTeams::TeeStarted(int ClientId) const
1344{
1345 return m_aTeeStarted[ClientId];
1346}
1347
1348bool CGameTeams::TeeFinished(int ClientId) const
1349{
1350 return m_aTeeFinished[ClientId];
1351}
1352
1353ETeamState CGameTeams::GetTeamState(int Team) const
1354{
1355 return m_aTeamState[Team];
1356}
1357
1358bool CGameTeams::TeamLocked(int Team) const
1359{
1360 if(Team == TEAM_FLOCK || !IsValidTeamNumber(Team))
1361 return false;
1362
1363 return m_aTeamLocked[Team];
1364}
1365
1366bool CGameTeams::TeamFlock(int Team) const
1367{
1368 // this is for team0mode, TEAM_FLOCK is handled differently
1369 if(Team == TEAM_FLOCK || !IsValidTeamNumber(Team))
1370 return false;
1371
1372 return m_aTeamFlock[Team];
1373}
1374
1375bool CGameTeams::IsInvited(int Team, int ClientId) const
1376{
1377 return m_aInvited[Team].test(position: ClientId);
1378}
1379
1380bool CGameTeams::IsStarted(int Team) const
1381{
1382 return m_aTeamState[Team] == ETeamState::STARTED;
1383}
1384
1385void CGameTeams::SetStarted(int ClientId, bool Started)
1386{
1387 m_aTeeStarted[ClientId] = Started;
1388}
1389
1390void CGameTeams::SetFinished(int ClientId, bool Finished)
1391{
1392 m_aTeeFinished[ClientId] = Finished;
1393}
1394
1395void CGameTeams::SetSaving(int TeamId, std::shared_ptr<CScoreSaveResult> &SaveResult)
1396{
1397 m_apSaveTeamResult[TeamId] = SaveResult;
1398}
1399
1400bool CGameTeams::GetSaving(int TeamId) const
1401{
1402 if(!IsValidTeamNumber(Team: TeamId))
1403 return false;
1404 if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && TeamId == TEAM_FLOCK)
1405 return false;
1406
1407 return m_apSaveTeamResult[TeamId] != nullptr;
1408}
1409
1410void CGameTeams::SetPractice(int Team, bool Enabled)
1411{
1412 if(!IsValidTeamNumber(Team))
1413 return;
1414 if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && Team == TEAM_FLOCK)
1415 {
1416 // allow to enable practice in team 0, for practice by default
1417 if(!g_Config.m_SvTestingCommands)
1418 return;
1419 }
1420
1421 m_aPractice[Team] = Enabled;
1422}
1423
1424bool CGameTeams::IsPractice(int Team)
1425{
1426 if(!IsValidTeamNumber(Team))
1427 return false;
1428 if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && Team == TEAM_FLOCK)
1429 {
1430 if(GameServer()->PracticeByDefault())
1431 return true;
1432
1433 return false;
1434 }
1435
1436 return m_aPractice[Team];
1437}
1438
1439bool CGameTeams::IsValidTeamNumber(int Team) const
1440{
1441 return Team >= TEAM_FLOCK && Team < NUM_DDRACE_TEAMS - 1; // no TEAM_SUPER
1442}
1443