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