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