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_DDRACE_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 int TimeTicks = Server()->Tick() - GetStartTime(Player: pPlayer);
197 if(TimeTicks <= 0)
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, TimeTicks, 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 int TimeTicks = Server()->Tick() - GetStartTime(Player: apTeamPlayers[0]);
339 float Time = (float)TimeTicks / (float)Server()->TickSpeed();
340 if(TimeTicks <= 0)
341 {
342 return;
343 }
344
345 if(m_aPractice[Team])
346 {
347 ChangeTeamState(Team, State: TEAMSTATE_FINISHED);
348
349 char aBuf[256];
350 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
351 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.",
352 (int)Time / 50 / 60, Time - ((int)Time / 60 * 60));
353 GameServer()->SendChatTeam(Team, pText: aBuf);
354
355 for(unsigned int i = 0; i < PlayersCount; ++i)
356 {
357 SetDDRaceState(Player: apTeamPlayers[i], DDRaceState: DDRACE_FINISHED);
358 }
359
360 return;
361 }
362
363 char aTimestamp[TIMESTAMP_STR_LENGTH];
364 str_timestamp_format(buffer: aTimestamp, buffer_size: sizeof(aTimestamp), FORMAT_SPACE); // 2019-04-02 19:41:58
365
366 for(unsigned int i = 0; i < PlayersCount; ++i)
367 OnFinish(Player: apTeamPlayers[i], TimeTicks, pTimestamp: aTimestamp);
368 ChangeTeamState(Team, State: TEAMSTATE_FINISHED); // TODO: Make it better
369 OnTeamFinish(Team, Players: apTeamPlayers, Size: PlayersCount, TimeTicks, pTimestamp: aTimestamp);
370 }
371 }
372}
373
374const char *CGameTeams::SetCharacterTeam(int ClientId, int Team)
375{
376 if(ClientId < 0 || ClientId >= MAX_CLIENTS)
377 return "Invalid client ID";
378 if(Team < 0 || Team >= MAX_CLIENTS + 1)
379 return "Invalid team number";
380 if(Team != TEAM_SUPER && m_aTeamState[Team] > TEAMSTATE_OPEN && !m_aPractice[Team] && !m_aTeamFlock[Team])
381 return "This team started already";
382 if(m_Core.Team(ClientId) == Team)
383 return "You are in this team already";
384 if(!Character(ClientId))
385 return "Your character is not valid";
386 if(Team == TEAM_SUPER && !Character(ClientId)->IsSuper())
387 return "You can't join super team if you don't have super rights";
388 if(Team != TEAM_SUPER && Character(ClientId)->m_DDRaceState != DDRACE_NONE)
389 return "You have started racing already";
390 // No cheating through noob filter with practice and then leaving team
391 if(m_aPractice[m_Core.Team(ClientId)])
392 return "You have used practice mode already";
393
394 // you can not join a team which is currently in the process of saving,
395 // because the save-process can fail and then the team is reset into the game
396 if(Team != TEAM_SUPER && GetSaving(TeamId: Team))
397 return "Your team is currently saving";
398 if(m_Core.Team(ClientId) != TEAM_SUPER && GetSaving(TeamId: m_Core.Team(ClientId)))
399 return "This team is currently saving";
400
401 SetForceCharacterTeam(ClientId, Team);
402 return nullptr;
403}
404
405void CGameTeams::SetForceCharacterTeam(int ClientId, int Team)
406{
407 m_aTeeStarted[ClientId] = false;
408 m_aTeeFinished[ClientId] = false;
409 int OldTeam = m_Core.Team(ClientId);
410
411 if(Team != OldTeam && (OldTeam != TEAM_FLOCK || g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) && OldTeam != TEAM_SUPER && m_aTeamState[OldTeam] != TEAMSTATE_EMPTY)
412 {
413 bool NoElseInOldTeam = Count(Team: OldTeam) <= 1;
414 if(NoElseInOldTeam)
415 {
416 m_aTeamState[OldTeam] = TEAMSTATE_EMPTY;
417
418 // unlock team when last player leaves
419 SetTeamLock(Team: OldTeam, Lock: false);
420 SetTeamFlock(Team: OldTeam, Mode: false);
421 ResetRoundState(Team: OldTeam);
422 // do not reset SaveTeamResult, because it should be logged into teehistorian even if the team leaves
423 }
424 }
425
426 m_Core.Team(ClientId, Team);
427
428 if(OldTeam != Team)
429 {
430 for(int LoopClientId = 0; LoopClientId < MAX_CLIENTS; ++LoopClientId)
431 if(GetPlayer(ClientId: LoopClientId))
432 SendTeamsState(ClientId: LoopClientId);
433
434 if(GetPlayer(ClientId))
435 {
436 GetPlayer(ClientId)->m_VotedForPractice = false;
437 GetPlayer(ClientId)->m_SwapTargetsClientId = -1;
438 }
439 m_pGameContext->m_World.RemoveEntitiesFromPlayer(PlayerId: ClientId);
440 }
441
442 if(Team != TEAM_SUPER && (m_aTeamState[Team] == TEAMSTATE_EMPTY || (m_aTeamLocked[Team] && !m_aTeamFlock[Team])))
443 {
444 if(!m_aTeamLocked[Team])
445 ChangeTeamState(Team, State: TEAMSTATE_OPEN);
446
447 ResetSwitchers(Team);
448 }
449}
450
451int CGameTeams::Count(int Team) const
452{
453 if(Team == TEAM_SUPER)
454 return -1;
455
456 int Count = 0;
457
458 for(int i = 0; i < MAX_CLIENTS; ++i)
459 if(m_Core.Team(ClientId: i) == Team)
460 Count++;
461
462 return Count;
463}
464
465void CGameTeams::ChangeTeamState(int Team, int State)
466{
467 m_aTeamState[Team] = State;
468}
469
470void CGameTeams::KillTeam(int Team, int NewStrongId, int ExceptId)
471{
472 for(int i = 0; i < MAX_CLIENTS; i++)
473 {
474 if(m_Core.Team(ClientId: i) == Team && GameServer()->m_apPlayers[i])
475 {
476 GameServer()->m_apPlayers[i]->m_VotedForPractice = false;
477 if(i != ExceptId)
478 {
479 GameServer()->m_apPlayers[i]->KillCharacter(Weapon: WEAPON_SELF, SendKillMsg: false);
480 if(NewStrongId != -1 && i != NewStrongId)
481 {
482 GameServer()->m_apPlayers[i]->Respawn(WeakHook: true); // spawn the rest of team with weak hook on the killer
483 }
484 }
485 }
486 }
487
488 // send the team kill message
489 CNetMsg_Sv_KillMsgTeam Msg;
490 Msg.m_Team = Team;
491 Msg.m_First = NewStrongId;
492 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId: -1);
493}
494
495bool CGameTeams::TeamFinished(int Team)
496{
497 if(m_aTeamState[Team] != TEAMSTATE_STARTED)
498 {
499 return false;
500 }
501 for(int i = 0; i < MAX_CLIENTS; ++i)
502 if(m_Core.Team(ClientId: i) == Team && !m_aTeeFinished[i])
503 return false;
504 return true;
505}
506
507CClientMask CGameTeams::TeamMask(int Team, int ExceptId, int Asker)
508{
509 if(Team == TEAM_SUPER)
510 {
511 if(ExceptId == -1)
512 return CClientMask().set();
513 return CClientMask().set().reset(position: ExceptId);
514 }
515
516 CClientMask Mask;
517 for(int i = 0; i < MAX_CLIENTS; ++i)
518 {
519 if(i == ExceptId)
520 continue; // Explicitly excluded
521 if(!GetPlayer(ClientId: i))
522 continue; // Player doesn't exist
523
524 if(!(GetPlayer(ClientId: i)->GetTeam() == TEAM_SPECTATORS || GetPlayer(ClientId: i)->IsPaused()))
525 { // Not spectator
526 if(i != Asker)
527 { // Actions of other players
528 if(!Character(ClientId: i))
529 continue; // Player is currently dead
530 if(GetPlayer(ClientId: i)->m_ShowOthers == SHOW_OTHERS_ONLY_TEAM)
531 {
532 if(m_Core.Team(ClientId: i) != Team && m_Core.Team(ClientId: i) != TEAM_SUPER)
533 continue; // In different teams
534 }
535 else if(GetPlayer(ClientId: i)->m_ShowOthers == SHOW_OTHERS_OFF)
536 {
537 if(m_Core.GetSolo(ClientId: Asker))
538 continue; // When in solo part don't show others
539 if(m_Core.GetSolo(ClientId: i))
540 continue; // When in solo part don't show others
541 if(m_Core.Team(ClientId: i) != Team && m_Core.Team(ClientId: i) != TEAM_SUPER)
542 continue; // In different teams
543 }
544 } // See everything of yourself
545 }
546 else if(GetPlayer(ClientId: i)->m_SpectatorId != SPEC_FREEVIEW)
547 { // Spectating specific player
548 if(GetPlayer(ClientId: i)->m_SpectatorId != Asker)
549 { // Actions of other players
550 if(!Character(ClientId: GetPlayer(ClientId: i)->m_SpectatorId))
551 continue; // Player is currently dead
552 if(GetPlayer(ClientId: i)->m_ShowOthers == SHOW_OTHERS_ONLY_TEAM)
553 {
554 if(m_Core.Team(ClientId: GetPlayer(ClientId: i)->m_SpectatorId) != Team && m_Core.Team(ClientId: GetPlayer(ClientId: i)->m_SpectatorId) != TEAM_SUPER)
555 continue; // In different teams
556 }
557 else if(GetPlayer(ClientId: i)->m_ShowOthers == SHOW_OTHERS_OFF)
558 {
559 if(m_Core.GetSolo(ClientId: Asker))
560 continue; // When in solo part don't show others
561 if(m_Core.GetSolo(ClientId: GetPlayer(ClientId: i)->m_SpectatorId))
562 continue; // When in solo part don't show others
563 if(m_Core.Team(ClientId: GetPlayer(ClientId: i)->m_SpectatorId) != Team && m_Core.Team(ClientId: GetPlayer(ClientId: i)->m_SpectatorId) != TEAM_SUPER)
564 continue; // In different teams
565 }
566 } // See everything of player you're spectating
567 }
568 else
569 { // Freeview
570 if(GetPlayer(ClientId: i)->m_SpecTeam)
571 { // Show only players in own team when spectating
572 if(m_Core.Team(ClientId: i) != Team && m_Core.Team(ClientId: i) != TEAM_SUPER)
573 continue; // in different teams
574 }
575 }
576
577 Mask.set(position: i);
578 }
579 return Mask;
580}
581
582void CGameTeams::SendTeamsState(int ClientId)
583{
584 if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO)
585 return;
586
587 if(!m_pGameContext->m_apPlayers[ClientId])
588 return;
589
590 CMsgPacker Msg(NETMSGTYPE_SV_TEAMSSTATE);
591 CMsgPacker MsgLegacy(NETMSGTYPE_SV_TEAMSSTATELEGACY);
592
593 for(unsigned i = 0; i < MAX_CLIENTS; i++)
594 {
595 Msg.AddInt(i: m_Core.Team(ClientId: i));
596 MsgLegacy.AddInt(i: m_Core.Team(ClientId: i));
597 }
598
599 Server()->SendMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId);
600 int ClientVersion = m_pGameContext->m_apPlayers[ClientId]->GetClientVersion();
601 if(!Server()->IsSixup(ClientId) && VERSION_DDRACE < ClientVersion && ClientVersion < VERSION_DDNET_MSG_LEGACY)
602 {
603 Server()->SendMsg(pMsg: &MsgLegacy, Flags: MSGFLAG_VITAL, ClientId);
604 }
605}
606
607int CGameTeams::GetDDRaceState(CPlayer *Player)
608{
609 if(!Player)
610 return DDRACE_NONE;
611
612 CCharacter *pChar = Player->GetCharacter();
613 if(pChar)
614 return pChar->m_DDRaceState;
615 return DDRACE_NONE;
616}
617
618void CGameTeams::SetDDRaceState(CPlayer *Player, int DDRaceState)
619{
620 if(!Player)
621 return;
622
623 CCharacter *pChar = Player->GetCharacter();
624 if(pChar)
625 pChar->m_DDRaceState = DDRaceState;
626}
627
628int CGameTeams::GetStartTime(CPlayer *Player)
629{
630 if(!Player)
631 return 0;
632
633 CCharacter *pChar = Player->GetCharacter();
634 if(pChar)
635 return pChar->m_StartTime;
636 return 0;
637}
638
639void CGameTeams::SetStartTime(CPlayer *Player, int StartTime)
640{
641 if(!Player)
642 return;
643
644 CCharacter *pChar = Player->GetCharacter();
645 if(pChar)
646 pChar->m_StartTime = StartTime;
647}
648
649void CGameTeams::SetLastTimeCp(CPlayer *Player, int LastTimeCp)
650{
651 if(!Player)
652 return;
653
654 CCharacter *pChar = Player->GetCharacter();
655 if(pChar)
656 pChar->m_LastTimeCp = LastTimeCp;
657}
658
659float *CGameTeams::GetCurrentTimeCp(CPlayer *Player)
660{
661 if(!Player)
662 return NULL;
663
664 CCharacter *pChar = Player->GetCharacter();
665 if(pChar)
666 return pChar->m_aCurrentTimeCp;
667 return NULL;
668}
669
670void CGameTeams::OnTeamFinish(int Team, CPlayer **Players, unsigned int Size, int TimeTicks, const char *pTimestamp)
671{
672 int aPlayerCids[MAX_CLIENTS];
673
674 for(unsigned int i = 0; i < Size; i++)
675 {
676 aPlayerCids[i] = Players[i]->GetCid();
677
678 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())]))
679 {
680 SetForceCharacterTeam(ClientId: Players[i]->GetCid(), Team: TEAM_FLOCK);
681 char aBuf[512];
682 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "'%s' joined team 0",
683 GameServer()->Server()->ClientName(ClientId: Players[i]->GetCid()));
684 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf);
685 }
686 }
687
688 if(Size >= (unsigned int)g_Config.m_SvMinTeamSize)
689 GameServer()->Score()->SaveTeamScore(Team, pClientIds: aPlayerCids, Size, TimeTicks, pTimestamp);
690}
691
692void CGameTeams::OnFinish(CPlayer *Player, int TimeTicks, const char *pTimestamp)
693{
694 if(!Player || !Player->IsPlaying())
695 return;
696
697 float Time = TimeTicks / (float)Server()->TickSpeed();
698
699 // TODO:DDRace:btd: this ugly
700 const int ClientId = Player->GetCid();
701 CPlayerData *pData = GameServer()->Score()->PlayerData(Id: ClientId);
702
703 char aBuf[128];
704 SetLastTimeCp(Player, LastTimeCp: -1);
705 // Note that the "finished in" message is parsed by the client
706 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
707 format: "%s finished in: %d minute(s) %5.2f second(s)",
708 Server()->ClientName(ClientId), (int)Time / 60,
709 Time - ((int)Time / 60 * 60));
710 if(g_Config.m_SvHideScore || !g_Config.m_SvSaveWorseScores)
711 GameServer()->SendChatTarget(To: ClientId, pText: aBuf, Flags: CGameContext::CHAT_SIX);
712 else
713 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf, SpamProtectionClientId: -1., Flags: CGameContext::CHAT_SIX);
714
715 float Diff = absolute(a: Time - pData->m_BestTime);
716
717 if(Time - pData->m_BestTime < 0)
718 {
719 // new record \o/
720 pData->m_RecordStopTick = Server()->Tick() + Server()->TickSpeed();
721 pData->m_RecordFinishTime = Time;
722
723 if(Diff >= 60)
724 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "New record: %d minute(s) %5.2f second(s) better.",
725 (int)Diff / 60, Diff - ((int)Diff / 60 * 60));
726 else
727 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "New record: %5.2f second(s) better.",
728 Diff);
729 if(g_Config.m_SvHideScore || !g_Config.m_SvSaveWorseScores)
730 GameServer()->SendChatTarget(To: ClientId, pText: aBuf, Flags: CGameContext::CHAT_SIX);
731 else
732 GameServer()->SendChat(ClientId: -1, Team: TEAM_ALL, pText: aBuf, SpamProtectionClientId: -1, Flags: CGameContext::CHAT_SIX);
733 }
734 else if(pData->m_BestTime != 0) // tee has already finished?
735 {
736 Server()->StopRecord(ClientId);
737
738 if(Diff <= 0.005f)
739 {
740 GameServer()->SendChatTarget(To: ClientId,
741 pText: "You finished with your best time.");
742 }
743 else
744 {
745 if(Diff >= 60)
746 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d minute(s) %5.2f second(s) worse, better luck next time.",
747 (int)Diff / 60, Diff - ((int)Diff / 60 * 60));
748 else
749 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
750 format: "%5.2f second(s) worse, better luck next time.",
751 Diff);
752 GameServer()->SendChatTarget(To: ClientId, pText: aBuf, Flags: CGameContext::CHAT_SIX); // this is private, sent only to the tee
753 }
754 }
755 else
756 {
757 pData->m_RecordStopTick = Server()->Tick() + Server()->TickSpeed();
758 pData->m_RecordFinishTime = Time;
759 }
760
761 if(!Server()->IsSixup(ClientId))
762 {
763 CNetMsg_Sv_DDRaceTime Msg;
764 CNetMsg_Sv_DDRaceTimeLegacy MsgLegacy;
765 MsgLegacy.m_Time = Msg.m_Time = (int)(Time * 100.0f);
766 MsgLegacy.m_Check = Msg.m_Check = 0;
767 MsgLegacy.m_Finish = Msg.m_Finish = 1;
768
769 if(pData->m_BestTime)
770 {
771 float Diff100 = (Time - pData->m_BestTime) * 100;
772 MsgLegacy.m_Check = Msg.m_Check = (int)Diff100;
773 }
774 if(VERSION_DDRACE <= Player->GetClientVersion())
775 {
776 if(Player->GetClientVersion() < VERSION_DDNET_MSG_LEGACY)
777 {
778 Server()->SendPackMsg(pMsg: &Msg, Flags: MSGFLAG_VITAL, ClientId);
779 }
780 else
781 {
782 Server()->SendPackMsg(pMsg: &MsgLegacy, Flags: MSGFLAG_VITAL, ClientId);
783 }
784 }
785 }
786
787 CNetMsg_Sv_RaceFinish RaceFinishMsg;
788 RaceFinishMsg.m_ClientId = ClientId;
789 RaceFinishMsg.m_Time = Time * 1000;
790 RaceFinishMsg.m_Diff = 0;
791 if(pData->m_BestTime)
792 {
793 RaceFinishMsg.m_Diff = Diff * 1000 * (Time < pData->m_BestTime ? -1 : 1);
794 }
795 RaceFinishMsg.m_RecordPersonal = (Time < pData->m_BestTime || !pData->m_BestTime);
796 RaceFinishMsg.m_RecordServer = Time < GameServer()->m_pController->m_CurrentRecord;
797 Server()->SendPackMsg(pMsg: &RaceFinishMsg, Flags: MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId: -1);
798
799 bool CallSaveScore = g_Config.m_SvSaveWorseScores;
800 bool NeedToSendNewPersonalRecord = false;
801 if(!pData->m_BestTime || Time < pData->m_BestTime)
802 {
803 // update the score
804 pData->Set(Time, aTimeCp: GetCurrentTimeCp(Player));
805 CallSaveScore = true;
806 NeedToSendNewPersonalRecord = true;
807 }
808
809 if(CallSaveScore)
810 if(g_Config.m_SvNamelessScore || !str_startswith(str: Server()->ClientName(ClientId), prefix: "nameless tee"))
811 GameServer()->Score()->SaveScore(ClientId, TimeTicks, pTimestamp,
812 aTimeCp: GetCurrentTimeCp(Player), NotEligible: Player->m_NotEligibleForFinish);
813
814 bool NeedToSendNewServerRecord = false;
815 // update server best time
816 if(GameServer()->m_pController->m_CurrentRecord == 0)
817 {
818 GameServer()->Score()->LoadBestTime();
819 }
820 else if(Time < GameServer()->m_pController->m_CurrentRecord)
821 {
822 // check for nameless
823 if(g_Config.m_SvNamelessScore || !str_startswith(str: Server()->ClientName(ClientId), prefix: "nameless tee"))
824 {
825 GameServer()->m_pController->m_CurrentRecord = Time;
826 NeedToSendNewServerRecord = true;
827 }
828 }
829
830 SetDDRaceState(Player, DDRaceState: DDRACE_FINISHED);
831 if(NeedToSendNewServerRecord)
832 {
833 for(int i = 0; i < MAX_CLIENTS; i++)
834 {
835 if(GameServer()->m_apPlayers[i] && GameServer()->m_apPlayers[i]->GetClientVersion() >= VERSION_DDRACE)
836 {
837 GameServer()->SendRecord(ClientId: i);
838 }
839 }
840 }
841 if(!NeedToSendNewServerRecord && NeedToSendNewPersonalRecord && Player->GetClientVersion() >= VERSION_DDRACE)
842 {
843 GameServer()->SendRecord(ClientId);
844 }
845
846 int TTime = (int)Time;
847 if(!Player->m_Score.has_value() || TTime < Player->m_Score.value())
848 {
849 Player->m_Score = TTime;
850 }
851
852 // Confetti
853 CCharacter *pChar = Player->GetCharacter();
854 m_pGameContext->CreateFinishConfetti(Pos: pChar->m_Pos, Mask: pChar->TeamMask());
855}
856
857void CGameTeams::RequestTeamSwap(CPlayer *pPlayer, CPlayer *pTargetPlayer, int Team)
858{
859 if(!pPlayer || !pTargetPlayer)
860 return;
861
862 char aBuf[512];
863 if(pPlayer->m_SwapTargetsClientId == pTargetPlayer->GetCid())
864 {
865 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
866 format: "You have already requested to swap with %s.", Server()->ClientName(ClientId: pTargetPlayer->GetCid()));
867
868 GameServer()->SendChatTarget(To: pPlayer->GetCid(), pText: aBuf);
869 return;
870 }
871
872 // Notification for the swap initiator
873 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
874 format: "You have requested to swap with %s.",
875 Server()->ClientName(ClientId: pTargetPlayer->GetCid()));
876 GameServer()->SendChatTarget(To: pPlayer->GetCid(), pText: aBuf);
877
878 // Notification to the target swap player
879 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
880 format: "%s has requested to swap with you. To complete the swap process please wait %d seconds and then type /swap %s.",
881 Server()->ClientName(ClientId: pPlayer->GetCid()), g_Config.m_SvSaveSwapGamesDelay, Server()->ClientName(ClientId: pPlayer->GetCid()));
882 GameServer()->SendChatTarget(To: pTargetPlayer->GetCid(), pText: aBuf);
883
884 // Notification for the remaining team
885 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
886 format: "%s has requested to swap with %s.",
887 Server()->ClientName(ClientId: pPlayer->GetCid()), Server()->ClientName(ClientId: pTargetPlayer->GetCid()));
888 // Do not send the team notification for team 0
889 if(Team != 0)
890 {
891 for(int i = 0; i < MAX_CLIENTS; i++)
892 {
893 if(m_Core.Team(ClientId: i) == Team && i != pTargetPlayer->GetCid() && i != pPlayer->GetCid())
894 {
895 GameServer()->SendChatTarget(To: i, pText: aBuf);
896 }
897 }
898 }
899
900 pPlayer->m_SwapTargetsClientId = pTargetPlayer->GetCid();
901 m_aLastSwap[pPlayer->GetCid()] = Server()->Tick();
902}
903
904void CGameTeams::SwapTeamCharacters(CPlayer *pPrimaryPlayer, CPlayer *pTargetPlayer, int Team)
905{
906 if(!pPrimaryPlayer || !pTargetPlayer)
907 return;
908
909 char aBuf[128];
910
911 int Since = (Server()->Tick() - m_aLastSwap[pTargetPlayer->GetCid()]) / Server()->TickSpeed();
912 if(Since < g_Config.m_SvSaveSwapGamesDelay)
913 {
914 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
915 format: "You have to wait %d seconds until you can swap.",
916 g_Config.m_SvSaveSwapGamesDelay - Since);
917
918 GameServer()->SendChatTarget(To: pPrimaryPlayer->GetCid(), pText: aBuf);
919
920 return;
921 }
922
923 pPrimaryPlayer->m_SwapTargetsClientId = -1;
924 pTargetPlayer->m_SwapTargetsClientId = -1;
925
926 int TimeoutAfterDelay = g_Config.m_SvSaveSwapGamesDelay + g_Config.m_SvSwapTimeout;
927 if(Since >= TimeoutAfterDelay)
928 {
929 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
930 format: "Your swap request timed out %d seconds ago. Use /swap again to re-initiate it.",
931 Since - g_Config.m_SvSwapTimeout);
932
933 GameServer()->SendChatTarget(To: pPrimaryPlayer->GetCid(), pText: aBuf);
934
935 return;
936 }
937
938 CSaveTee PrimarySavedTee;
939 PrimarySavedTee.Save(pchr: pPrimaryPlayer->GetCharacter());
940
941 CSaveTee SecondarySavedTee;
942 SecondarySavedTee.Save(pchr: pTargetPlayer->GetCharacter());
943
944 PrimarySavedTee.Load(pchr: pTargetPlayer->GetCharacter(), Team, IsSwap: true);
945 SecondarySavedTee.Load(pchr: pPrimaryPlayer->GetCharacter(), Team, IsSwap: true);
946
947 if(Team >= 1 && !m_aTeamFlock[Team])
948 {
949 for(const auto &pPlayer : GameServer()->m_apPlayers)
950 {
951 CCharacter *pChar = pPlayer ? pPlayer->GetCharacter() : nullptr;
952 if(pChar && pChar->Team() == Team && pChar != pPrimaryPlayer->GetCharacter() && pChar != pTargetPlayer->GetCharacter())
953 pChar->m_StartTime = pPrimaryPlayer->GetCharacter()->m_StartTime;
954 }
955 }
956 std::swap(a&: m_aTeeStarted[pPrimaryPlayer->GetCid()], b&: m_aTeeStarted[pTargetPlayer->GetCid()]);
957 std::swap(a&: m_aTeeFinished[pPrimaryPlayer->GetCid()], b&: m_aTeeFinished[pTargetPlayer->GetCid()]);
958 std::swap(a&: pPrimaryPlayer->GetCharacter()->GetLastRescueTeeRef(Mode: RESCUEMODE_AUTO), b&: pTargetPlayer->GetCharacter()->GetLastRescueTeeRef(Mode: RESCUEMODE_AUTO));
959 std::swap(a&: pPrimaryPlayer->GetCharacter()->GetLastRescueTeeRef(Mode: RESCUEMODE_MANUAL), b&: pTargetPlayer->GetCharacter()->GetLastRescueTeeRef(Mode: RESCUEMODE_MANUAL));
960
961 GameServer()->m_World.SwapClients(Client1: pPrimaryPlayer->GetCid(), Client2: pTargetPlayer->GetCid());
962
963 if(GameServer()->TeeHistorianActive())
964 {
965 GameServer()->TeeHistorian()->RecordPlayerSwap(ClientId1: pPrimaryPlayer->GetCid(), ClientId2: pTargetPlayer->GetCid());
966 }
967
968 str_format(buffer: aBuf, buffer_size: sizeof(aBuf),
969 format: "%s has swapped with %s.",
970 Server()->ClientName(ClientId: pPrimaryPlayer->GetCid()), Server()->ClientName(ClientId: pTargetPlayer->GetCid()));
971
972 GameServer()->SendChatTeam(Team, pText: aBuf);
973}
974
975void CGameTeams::ProcessSaveTeam()
976{
977 for(int Team = 0; Team < NUM_DDRACE_TEAMS; Team++)
978 {
979 if(m_apSaveTeamResult[Team] == nullptr || !m_apSaveTeamResult[Team]->m_Completed)
980 continue;
981 if(m_apSaveTeamResult[Team]->m_aBroadcast[0] != '\0')
982 GameServer()->SendBroadcast(pText: m_apSaveTeamResult[Team]->m_aBroadcast, ClientId: -1);
983 if(m_apSaveTeamResult[Team]->m_aMessage[0] != '\0' && m_apSaveTeamResult[Team]->m_Status != CScoreSaveResult::LOAD_FAILED)
984 GameServer()->SendChatTeam(Team, pText: m_apSaveTeamResult[Team]->m_aMessage);
985 switch(m_apSaveTeamResult[Team]->m_Status)
986 {
987 case CScoreSaveResult::SAVE_SUCCESS:
988 {
989 if(GameServer()->TeeHistorianActive())
990 {
991 GameServer()->TeeHistorian()->RecordTeamSaveSuccess(
992 Team,
993 SaveId: m_apSaveTeamResult[Team]->m_SaveId,
994 pTeamSave: m_apSaveTeamResult[Team]->m_SavedTeam.GetString());
995 }
996 for(int i = 0; i < m_apSaveTeamResult[Team]->m_SavedTeam.GetMembersCount(); i++)
997 {
998 if(m_apSaveTeamResult[Team]->m_SavedTeam.m_pSavedTees->IsHooking())
999 {
1000 int ClientId = m_apSaveTeamResult[Team]->m_SavedTeam.m_pSavedTees->GetClientId();
1001 if(GameServer()->m_apPlayers[ClientId] != nullptr)
1002 GameServer()->SendChatTarget(To: ClientId, pText: "Start holding the hook before loading the savegame to keep the hook");
1003 }
1004 }
1005 ResetSavedTeam(ClientId: m_apSaveTeamResult[Team]->m_RequestingPlayer, Team);
1006 char aSaveId[UUID_MAXSTRSIZE];
1007 FormatUuid(Uuid: m_apSaveTeamResult[Team]->m_SaveId, pBuffer: aSaveId, BufferLength: UUID_MAXSTRSIZE);
1008 dbg_msg(sys: "save", fmt: "Save successful: %s", aSaveId);
1009 break;
1010 }
1011 case CScoreSaveResult::SAVE_FAILED:
1012 if(GameServer()->TeeHistorianActive())
1013 GameServer()->TeeHistorian()->RecordTeamSaveFailure(Team);
1014 if(Count(Team) > 0)
1015 {
1016 // load weak/strong order to prevent switching weak/strong while saving
1017 m_apSaveTeamResult[Team]->m_SavedTeam.Load(pGameServer: GameServer(), Team, KeepCurrentWeakStrong: false);
1018 }
1019 break;
1020 case CScoreSaveResult::LOAD_SUCCESS:
1021 {
1022 if(GameServer()->TeeHistorianActive())
1023 {
1024 GameServer()->TeeHistorian()->RecordTeamLoadSuccess(
1025 Team,
1026 SaveId: m_apSaveTeamResult[Team]->m_SaveId,
1027 pTeamSave: m_apSaveTeamResult[Team]->m_SavedTeam.GetString());
1028 }
1029 if(Count(Team) > 0)
1030 {
1031 // keep current weak/strong order as on some maps there is no other way of switching
1032 m_apSaveTeamResult[Team]->m_SavedTeam.Load(pGameServer: GameServer(), Team, KeepCurrentWeakStrong: true);
1033 }
1034 char aSaveId[UUID_MAXSTRSIZE];
1035 FormatUuid(Uuid: m_apSaveTeamResult[Team]->m_SaveId, pBuffer: aSaveId, BufferLength: UUID_MAXSTRSIZE);
1036 dbg_msg(sys: "save", fmt: "Load successful: %s", aSaveId);
1037 break;
1038 }
1039 case CScoreSaveResult::LOAD_FAILED:
1040 if(GameServer()->TeeHistorianActive())
1041 GameServer()->TeeHistorian()->RecordTeamLoadFailure(Team);
1042 if(m_apSaveTeamResult[Team]->m_aMessage[0] != '\0')
1043 GameServer()->SendChatTarget(To: m_apSaveTeamResult[Team]->m_RequestingPlayer, pText: m_apSaveTeamResult[Team]->m_aMessage);
1044 break;
1045 }
1046 m_apSaveTeamResult[Team] = nullptr;
1047 }
1048}
1049
1050void CGameTeams::OnCharacterSpawn(int ClientId)
1051{
1052 m_Core.SetSolo(ClientId, Value: false);
1053 int Team = m_Core.Team(ClientId);
1054
1055 if(GetSaving(TeamId: Team))
1056 return;
1057
1058 if(m_Core.Team(ClientId) >= TEAM_SUPER || !m_aTeamLocked[Team])
1059 {
1060 if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO)
1061 SetForceCharacterTeam(ClientId, Team: TEAM_FLOCK);
1062 else
1063 SetForceCharacterTeam(ClientId, Team: ClientId); // initialize team
1064 if(!m_aTeamFlock[Team])
1065 CheckTeamFinished(Team);
1066 }
1067}
1068
1069void CGameTeams::OnCharacterDeath(int ClientId, int Weapon)
1070{
1071 m_Core.SetSolo(ClientId, Value: false);
1072
1073 int Team = m_Core.Team(ClientId);
1074 if(GetSaving(TeamId: Team))
1075 return;
1076 bool Locked = TeamLocked(Team) && Weapon != WEAPON_GAME;
1077
1078 if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO && Team != TEAM_SUPER)
1079 {
1080 ChangeTeamState(Team, State: CGameTeams::TEAMSTATE_OPEN);
1081 if(m_aPractice[Team])
1082 {
1083 if(Weapon != WEAPON_WORLD)
1084 {
1085 ResetRoundState(Team);
1086 }
1087 else
1088 {
1089 GameServer()->SendChatTeam(Team, pText: "You died, but will stay in practice until you use kill.");
1090 }
1091 }
1092 else
1093 {
1094 ResetRoundState(Team);
1095 }
1096 }
1097 else if(Locked)
1098 {
1099 SetForceCharacterTeam(ClientId, Team);
1100
1101 if(GetTeamState(Team) != TEAMSTATE_OPEN && !m_aTeamFlock[m_Core.Team(ClientId)])
1102 {
1103 ChangeTeamState(Team, State: CGameTeams::TEAMSTATE_OPEN);
1104
1105 m_aPractice[Team] = false;
1106
1107 if(Count(Team) > 1)
1108 {
1109 // Disband team if the team has more players than allowed.
1110 if(Count(Team) > g_Config.m_SvMaxTeamSize)
1111 {
1112 GameServer()->SendChatTeam(Team, pText: "This team was disbanded because there are more players than allowed in the team.");
1113 SetTeamLock(Team, Lock: false);
1114 KillTeam(Team, NewStrongId: Weapon == WEAPON_SELF ? ClientId : -1, ExceptId: ClientId);
1115 return;
1116 }
1117
1118 KillTeam(Team, NewStrongId: Weapon == WEAPON_SELF ? ClientId : -1, ExceptId: ClientId);
1119
1120 char aBuf[512];
1121 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");
1122
1123 GameServer()->SendChatTeam(Team, pText: aBuf);
1124 }
1125 }
1126 }
1127 else
1128 {
1129 if(m_aTeamState[m_Core.Team(ClientId)] == CGameTeams::TEAMSTATE_STARTED && !m_aTeeStarted[ClientId] && !m_aTeamFlock[m_Core.Team(ClientId)])
1130 {
1131 char aBuf[128];
1132 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));
1133 GameServer()->SendChatTeam(Team, pText: aBuf);
1134 GameServer()->SendChatTeam(Team, pText: "Enter /practice mode or restart to avoid the entire team being killed in 60 seconds");
1135
1136 m_aTeamUnfinishableKillTick[Team] = Server()->Tick() + 60 * Server()->TickSpeed();
1137 ChangeTeamState(Team, State: CGameTeams::TEAMSTATE_STARTED_UNFINISHABLE);
1138 }
1139 SetForceCharacterTeam(ClientId, Team: TEAM_FLOCK);
1140 if(!m_aTeamFlock[m_Core.Team(ClientId)])
1141 CheckTeamFinished(Team);
1142 }
1143}
1144
1145void CGameTeams::SetTeamLock(int Team, bool Lock)
1146{
1147 if(Team > TEAM_FLOCK && Team < TEAM_SUPER)
1148 m_aTeamLocked[Team] = Lock;
1149}
1150
1151void CGameTeams::SetTeamFlock(int Team, bool Mode)
1152{
1153 if(Team > TEAM_FLOCK && Team < TEAM_SUPER)
1154 m_aTeamFlock[Team] = Mode;
1155}
1156
1157void CGameTeams::ResetInvited(int Team)
1158{
1159 m_aInvited[Team].reset();
1160}
1161
1162void CGameTeams::SetClientInvited(int Team, int ClientId, bool Invited)
1163{
1164 if(Team > TEAM_FLOCK && Team < TEAM_SUPER)
1165 {
1166 if(Invited)
1167 m_aInvited[Team].set(position: ClientId);
1168 else
1169 m_aInvited[Team].reset(position: ClientId);
1170 }
1171}
1172
1173void CGameTeams::KillSavedTeam(int ClientId, int Team)
1174{
1175 if(g_Config.m_SvSoloServer || !g_Config.m_SvTeam)
1176 {
1177 GameServer()->m_apPlayers[ClientId]->KillCharacter(Weapon: WEAPON_SELF, SendKillMsg: true);
1178 }
1179 else
1180 {
1181 KillTeam(Team, NewStrongId: -1);
1182 }
1183}
1184
1185void CGameTeams::ResetSavedTeam(int ClientId, int Team)
1186{
1187 if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO)
1188 {
1189 ChangeTeamState(Team, State: CGameTeams::TEAMSTATE_OPEN);
1190 ResetRoundState(Team);
1191 }
1192 else
1193 {
1194 for(int i = 0; i < MAX_CLIENTS; i++)
1195 {
1196 if(m_Core.Team(ClientId: i) == Team && GameServer()->m_apPlayers[i])
1197 {
1198 SetForceCharacterTeam(ClientId: i, Team: TEAM_FLOCK);
1199 }
1200 }
1201 }
1202}
1203
1204int CGameTeams::GetFirstEmptyTeam() const
1205{
1206 for(int i = 1; i < MAX_CLIENTS; i++)
1207 if(m_aTeamState[i] == TEAMSTATE_EMPTY)
1208 return i;
1209 return -1;
1210}
1211