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