1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2005, Sven2
6 * Copyright (c) 2017-2022, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18// player team management for teamwork melees
19
20#include <C4Include.h>
21#include <C4Teams.h>
22
23#include <C4Game.h>
24#include <C4Random.h>
25#include <C4Components.h>
26#include <C4Wrappers.h>
27#include <C4Player.h>
28
29// C4Team
30
31C4Team::C4Team(const C4Team &rCopy)
32 : piPlayers(new int32_t[rCopy.GetPlayerCount()]),
33 iPlayerCount(rCopy.GetPlayerCount()),
34 iPlayerCapacity(rCopy.GetPlayerCount()),
35 iID(rCopy.GetID()), iPlrStartIndex(rCopy.iPlrStartIndex), dwClr(rCopy.dwClr),
36 sIconSpec(rCopy.GetIconSpec()), iMaxPlayer(rCopy.iMaxPlayer)
37{
38 // copy name
39 SCopy(szSource: rCopy.GetName(), sTarget: Name, iMaxL: C4MaxName);
40 // copy players
41 for (int32_t i = 0; i < iPlayerCount; i++)
42 piPlayers[i] = rCopy.GetIndexedPlayer(iIndex: i);
43}
44
45void C4Team::Clear()
46{
47 delete[] piPlayers; piPlayers = nullptr;
48 iPlayerCount = iPlayerCapacity = iMaxPlayer = 0;
49 iID = 0; *Name = 0;
50 sIconSpec.Clear();
51}
52
53void C4Team::AddPlayer(C4PlayerInfo &rInfo, bool fAdjustPlayer)
54{
55 // must not happen!
56 assert(rInfo.GetID());
57 if (!rInfo.GetID()) return;
58 // add player; grow vector if necessary
59 if (iPlayerCount >= iPlayerCapacity)
60 {
61 int32_t *piNewPlayers = new int32_t[iPlayerCapacity = iPlayerCount + 4 & ~3];
62 if (iPlayerCount) memcpy(dest: piNewPlayers, src: piPlayers, n: iPlayerCount * sizeof(int32_t));
63 delete[] piPlayers; piPlayers = piNewPlayers;
64 }
65 // store new player
66 piPlayers[iPlayerCount++] = rInfo.GetID();
67 if (!fAdjustPlayer) return;
68 // set values in info
69 rInfo.SetTeam(GetID());
70 if (Game.Teams.IsTeamColors()) rInfo.SetColor(GetColor());
71 // and in actual player, if it is joined already
72 if (rInfo.IsJoined())
73 {
74 C4Player *pJoinedPlr = Game.Players.GetByInfoID(iInfoID: rInfo.GetID());
75 assert(pJoinedPlr);
76 if (pJoinedPlr)
77 {
78 pJoinedPlr->Team = GetID();
79 if (Game.Teams.IsTeamColors()) pJoinedPlr->SetPlayerColor(GetColor());
80 }
81 }
82}
83
84void C4Team::RemoveIndexedPlayer(int32_t iIndex)
85{
86 // safety
87 assert(Inside<int32_t>(iIndex, 0, iPlayerCount - 1));
88 if (!Inside<int32_t>(ival: iIndex, lbound: 0, rbound: iPlayerCount - 1)) return;
89 // move other players done
90 for (int32_t i = iIndex + 1; i < iPlayerCount; ++i)
91 piPlayers[i - 1] = piPlayers[i];
92 --iPlayerCount;
93}
94
95void C4Team::RemovePlayerByID(int32_t iID)
96{
97 // get index
98 int32_t i;
99 for (i = 0; i < iPlayerCount; ++i)
100 if (piPlayers[i] == iID) break;
101 if (i == iPlayerCount) { assert(false); return; } // ID not found
102 // remove it
103 RemoveIndexedPlayer(iIndex: i);
104}
105
106bool C4Team::IsPlayerIDInTeam(int32_t iID)
107{
108 int32_t i = iPlayerCount, *piPlr = piPlayers;
109 while (i--) if (*piPlr++ == iID) return true;
110 return false;
111}
112
113int32_t C4Team::GetFirstUnjoinedPlayerID() const
114{
115 // search for a player that does not have the join-flag set
116 int32_t i = iPlayerCount, idPlr, *piPlr = piPlayers;
117 C4PlayerInfo *pInfo;
118 while (i--)
119 if (pInfo = Game.PlayerInfos.GetPlayerInfoByID(id: idPlr = *piPlr++))
120 if (!pInfo->HasJoinIssued())
121 return idPlr;
122 // none found
123 return 0;
124}
125
126int32_t C4Team::GetFirstActivePlayerID() const
127{
128 // search for a player that is currently in the game
129 int32_t i = iPlayerCount, idPlr, *piPlr = piPlayers;
130 C4Player *pPlr;
131 while (i--)
132 if (pPlr = Game.Players.GetByInfoID(iInfoID: (idPlr = *piPlr++)))
133 return idPlr;
134 // none found
135 return 0;
136}
137
138void C4Team::CompileFunc(StdCompiler *pComp)
139{
140 if (pComp->isCompiler()) Clear();
141 pComp->Value(rStruct: mkNamingAdapt(rValue&: iID, szName: "id", rDefault: 0));
142 pComp->Value(rStruct: mkNamingAdapt(mkStringAdaptMA(Name), szName: "Name", rDefault: ""));
143 pComp->Value(rStruct: mkNamingAdapt(rValue&: iPlrStartIndex, szName: "PlrStartIndex", rDefault: 0));
144 pComp->Value(rStruct: mkNamingAdapt(rValue&: iPlayerCount, szName: "PlayerCount", rDefault: 0));
145 if (pComp->isCompiler()) { delete[] piPlayers; piPlayers = new int32_t[iPlayerCapacity = iPlayerCount]{}; }
146 pComp->Value(rStruct: mkNamingAdapt(rValue: mkArrayAdaptS(array: piPlayers, size: iPlayerCount, default_: -1), szName: "Players"));
147 pComp->Value(rStruct: mkNamingAdapt(rValue&: dwClr, szName: "Color", rDefault: 0u));
148 pComp->Value(rStruct: mkNamingAdapt(rValue&: sIconSpec, szName: "IconSpec", rDefault: StdStrBuf()));
149 pComp->Value(rStruct: mkNamingAdapt(rValue&: iMaxPlayer, szName: "MaxPlayer", rDefault: 0));
150}
151
152void C4Team::RecheckPlayers()
153{
154 // check all players within the team
155 for (int32_t i = 0; i < iPlayerCount; ++i)
156 {
157 bool fIsValid = false; int32_t id; C4PlayerInfo *pInfo;
158 if (id = piPlayers[i])
159 if (pInfo = Game.PlayerInfos.GetPlayerInfoByID(id))
160 if (pInfo->GetTeam() == GetID())
161 if (pInfo->IsUsingTeam())
162 fIsValid = true;
163 // removal will decrease iPlayerCount, which will abort the loop earlier
164 if (!fIsValid) RemoveIndexedPlayer(iIndex: i--);
165 }
166 // now check for any new players in the team
167 int32_t id = 0; C4PlayerInfo *pInfo;
168 while (pInfo = Game.PlayerInfos.GetNextPlayerInfoByID(id))
169 {
170 id = pInfo->GetID();
171 if (pInfo->GetTeam() == GetID())
172 if (pInfo->IsUsingTeam())
173 if (!IsPlayerIDInTeam(iID: id))
174 AddPlayer(rInfo&: *pInfo, fAdjustPlayer: false);
175 }
176}
177
178uint32_t GenerateRandomPlayerColor(int32_t iTry); // C4PlayerInfo.cpp
179bool IsColorConflict(uint32_t dwClr1, uint32_t dwClr2); // C4PlayerInfo.cpp
180
181void C4Team::RecheckColor(C4TeamList &rForList)
182{
183 // number of times trying new player colors
184 const int32_t C4MaxTeamColorChangeTries = 100;
185 if (!dwClr)
186 {
187 const int defTeamColorCount = 10;
188 uint32_t defTeamColorRGB[defTeamColorCount] =
189 {
190 0xF40000, 0x00C800, 0xFCF41C, 0x2020FF, // red, green, yellow, blue,
191 0xC48444, 0xFFFFFF, 0x848484, 0xFF00EF, // brown, white, grey, pink,
192 0x00FFFF, 0x784830
193 }; // cyan, dk brown
194 // no color assigned yet: Generate by team ID
195 if (iID >= 1 && iID <= defTeamColorCount + 1)
196 {
197 // default colors
198 dwClr = defTeamColorRGB[iID - 1];
199 }
200 else
201 {
202 // find a new, unused color
203 for (int32_t iTry = 1; iTry < C4MaxTeamColorChangeTries; ++iTry)
204 {
205 dwClr = GenerateRandomPlayerColor(iTry);
206 int32_t iIdx = 0; C4Team *pTeam; bool fOK = true;
207 while (pTeam = rForList.GetTeamByIndex(iIndex: iIdx++))
208 if (pTeam != this)
209 if (IsColorConflict(dwClr1: pTeam->GetColor(), dwClr2: dwClr))
210 {
211 fOK = false;
212 break;
213 }
214 // color is fine?
215 if (fOK) return;
216 // it's not; try next color
217 }
218 // Giving up: Use last generated color
219 }
220 }
221}
222
223StdStrBuf C4Team::GetNameWithParticipants() const
224{
225 // compose team name like "Team 1 (boni, GhostBear, Clonko)"
226 // or just "Team 1" for empty team
227 StdStrBuf sTeamName;
228 sTeamName.Copy(pnData: GetName());
229 if (GetPlayerCount())
230 {
231 sTeamName.Append(pnData: " (");
232 int32_t iTeamPlrCount = 0;
233 for (int32_t j = 0; j < GetPlayerCount(); ++j)
234 {
235 int32_t iPlr = GetIndexedPlayer(iIndex: j);
236 C4PlayerInfo *pPlrInfo;
237 if (iPlr) if (pPlrInfo = Game.PlayerInfos.GetPlayerInfoByID(id: iPlr))
238 {
239 if (iTeamPlrCount++) sTeamName.Append(pnData: ", ");
240 sTeamName.Append(pnData: pPlrInfo->GetName());
241 }
242 }
243 sTeamName.AppendChar(cChar: ')');
244 }
245 return sTeamName;
246}
247
248bool C4Team::HasWon() const
249{
250 // return true if any member player of the team has won
251 bool fHasWon = false;
252 for (int32_t i = 0; i < iPlayerCount; ++i)
253 {
254 int32_t id; C4PlayerInfo *pInfo;
255 if (id = piPlayers[i])
256 if (pInfo = Game.PlayerInfos.GetPlayerInfoByID(id))
257 if (pInfo->HasWon())
258 {
259 fHasWon = true;
260 break;
261 }
262 }
263 return fHasWon;
264}
265
266// C4TeamList
267
268void C4TeamList::Clear()
269{
270 // del all teams
271 ClearTeams();
272 // del player team vector
273 delete[] ppList; ppList = nullptr;
274 iTeamCapacity = 0;
275 fAllowHostilityChange = true;
276 fAllowTeamSwitch = false;
277 fCustom = false;
278 fActive = true;
279 fTeamColors = false;
280 eTeamDist = TEAMDIST_Free;
281 fAutoGenerateTeams = false;
282 iMaxScriptPlayers = 0;
283 sScriptPlayerNames.Clear();
284 randomTeamCount = 0;
285}
286
287C4TeamList &C4TeamList::operator=(const C4TeamList &rCopy)
288{
289 Clear();
290 if (iTeamCount = iTeamCapacity = rCopy.iTeamCount)
291 ppList = new C4Team *[iTeamCapacity];
292 for (int i = 0; i < iTeamCount; i++)
293 ppList[i] = new C4Team(*rCopy.ppList[i]);
294 iLastTeamID = rCopy.iLastTeamID;
295 fAllowHostilityChange = rCopy.fAllowHostilityChange;
296 fAllowTeamSwitch = rCopy.fAllowTeamSwitch;
297 fCustom = rCopy.fCustom;
298 fActive = rCopy.fActive;
299 eTeamDist = rCopy.eTeamDist;
300 fTeamColors = rCopy.fTeamColors;
301 fAutoGenerateTeams = rCopy.fAutoGenerateTeams;
302 sScriptPlayerNames.Copy(Buf2: rCopy.sScriptPlayerNames);
303 randomTeamCount = rCopy.randomTeamCount;
304 return *this;
305}
306
307bool C4TeamList::CanLocalChooseTeam() const
308{
309 // only if there are any teams
310 if (!fActive) return false;
311 // check by mode
312 switch (eTeamDist)
313 {
314 case TEAMDIST_Free: return true;
315 case TEAMDIST_Host: return Game.Control.isCtrlHost();
316 case TEAMDIST_None:
317 case TEAMDIST_Random:
318 case TEAMDIST_RandomInv:
319 return false;
320 }
321
322 assert(false);
323 return false;
324}
325
326bool C4TeamList::CanLocalChooseTeam(int32_t idPlayer) const
327{
328 // must be possible at all
329 if (!CanLocalChooseTeam()) return false;
330 // there must be space in a target team
331 // always possible if teams are generated on the fly
332 if (IsAutoGenerateTeams()) return true;
333 // also possible if one of the teams that's not the player's is not full
334 C4Team *pCurrentTeam = nullptr, *pCheck;
335 if (idPlayer) pCurrentTeam = GetTeamByPlayerID(iID: idPlayer);
336 int32_t iCheckTeam = 0;
337 while (pCheck = GetTeamByIndex(iIndex: iCheckTeam++))
338 if (pCheck != pCurrentTeam)
339 if (!pCheck->IsFull())
340 break;
341 return !!pCheck;
342}
343
344bool C4TeamList::CanLocalSeeTeam() const
345{
346 if (!fActive) return false;
347 // invisible teams aren't revealed before game start
348 if (eTeamDist != TEAMDIST_RandomInv) return true;
349 return !!Game.IsRunning;
350}
351
352void C4TeamList::AddTeam(C4Team *pNewTeam)
353{
354 // add team; grow vector if necessary
355 if (iTeamCount >= iTeamCapacity)
356 {
357 C4Team **ppNewTeams = new C4Team *[iTeamCapacity = iTeamCount + 4 & ~3];
358 if (iTeamCount) memcpy(dest: ppNewTeams, src: ppList, n: iTeamCount * sizeof(C4Team *));
359 delete[] ppList; ppList = ppNewTeams;
360 }
361 // store new team
362 ppList[iTeamCount++] = pNewTeam;
363 // adjust ID
364 iLastTeamID = (std::max)(a: pNewTeam->iID, b: iLastTeamID);
365}
366
367void C4TeamList::ClearTeams()
368{
369 // delete all teams
370 C4Team **ppTeam = ppList;
371 if (iTeamCount) { while (iTeamCount--) delete *(ppTeam++); iTeamCount = 0; }
372 iLastTeamID = 0;
373}
374
375C4Team *C4TeamList::CreateTeam(const char *szName)
376{
377 // custom team
378 C4Team *pNewTeam = new C4Team();
379 pNewTeam->iID = iLastTeamID + 1;
380 SCopy(szSource: szName, sTarget: pNewTeam->Name, iMaxL: C4MaxName);
381 AddTeam(pNewTeam);
382 pNewTeam->RecheckColor(rForList&: *this);
383 return pNewTeam;
384}
385
386bool C4TeamList::GenerateDefaultTeams(int32_t iUpToID)
387{
388 // generate until last team ID matches given
389 while (iLastTeamID < iUpToID)
390 {
391 if (!CreateTeam(szName: LoadResStr(id: C4ResStrTableKey::IDS_MSG_TEAM, args: iLastTeamID + 1).c_str())) return false;
392 }
393 return true;
394}
395
396int32_t C4TeamList::GetGenerateTeamCount() const
397{
398 // default is 2, only used for random teams
399 return randomTeamCount > 1 ? randomTeamCount : 2;
400}
401
402C4Team *C4TeamList::GetTeamByID(int32_t iID) const
403{
404 C4Team **ppCheck = ppList; int32_t iCnt = iTeamCount;
405 for (; iCnt--; ++ppCheck) if ((*ppCheck)->GetID() == iID) return *ppCheck;
406 return nullptr;
407}
408
409C4Team *C4TeamList::GetGenerateTeamByID(int32_t iID)
410{
411 // only if enabled
412 if (!IsMultiTeams()) return nullptr;
413 // new team?
414 if (iID == TEAMID_New) iID = GetLargestTeamID() + 1;
415 // find in list
416 C4Team *pTeam = GetTeamByID(iID);
417 if (pTeam) return pTeam;
418 // not found: Generate
419 GenerateDefaultTeams(iUpToID: iID);
420 return GetTeamByID(iID);
421}
422
423C4Team *C4TeamList::GetTeamByIndex(int32_t iIndex) const
424{
425 // safety
426 if (!Inside<int32_t>(ival: iIndex, lbound: 0, rbound: iTeamCount - 1)) return nullptr;
427 // direct list access
428 return ppList[iIndex];
429}
430
431C4Team *C4TeamList::GetTeamByPlayerID(int32_t iID) const
432{
433 C4Team **ppCheck = ppList; int32_t iCnt = iTeamCount;
434 for (; iCnt--; ++ppCheck) if ((*ppCheck)->IsPlayerIDInTeam(iID)) return *ppCheck;
435 return nullptr;
436}
437
438int32_t C4TeamList::GetLargestTeamID() const
439{
440 int32_t iLargest = 0;
441 C4Team **ppCheck = ppList; int32_t iCnt = iTeamCount;
442 for (; iCnt--; ++ppCheck) iLargest = (std::max)(a: (*ppCheck)->GetID(), b: iLargest);
443 return iLargest;
444}
445
446C4Team *C4TeamList::GetRandomSmallestTeam(bool limitRandomTeamCount) const
447{
448 C4Team *pLowestTeam = nullptr; int iLowestTeamCount = 0;
449 C4Team **ppCheck = ppList; int32_t iCnt = limitRandomTeamCount && randomTeamCount > 1 ? randomTeamCount : iTeamCount;
450 for (; iCnt--; ++ppCheck)
451 {
452 if ((*ppCheck)->IsFull()) continue; // do not join into full teams
453 if (!pLowestTeam || pLowestTeam->GetPlayerCount() > (*ppCheck)->GetPlayerCount())
454 {
455 pLowestTeam = *ppCheck;
456 iLowestTeamCount = 1;
457 }
458 else if (pLowestTeam->GetPlayerCount() == (*ppCheck)->GetPlayerCount())
459 if (!SafeRandom(range: ++iLowestTeamCount))
460 pLowestTeam = *ppCheck;
461 }
462 return pLowestTeam;
463}
464
465bool C4TeamList::IsTeamVisible() const
466{
467 // teams invisible during lobby time if random surprise teams
468 if (eTeamDist == TEAMDIST_RandomInv)
469 if (Game.Network.isLobbyActive())
470 return false;
471 return true;
472}
473
474bool C4TeamList::RecheckPlayerInfoTeams(C4PlayerInfo &rNewJoin, bool fByHost)
475{
476 // only if enabled
477 assert(IsMultiTeams());
478 if (!IsMultiTeams()) return false;
479 // check whether a new team is to be assigned first
480 C4Team *pCurrentTeam = GetTeamByPlayerID(iID: rNewJoin.GetID());
481 int32_t idCurrentTeam = pCurrentTeam ? pCurrentTeam->GetID() : 0;
482 if (rNewJoin.GetTeam())
483 {
484 // was that team a change to the current team?
485 // no change anyway: OK, skip this info
486 if (idCurrentTeam == rNewJoin.GetTeam()) return true;
487 // the player had a different team assigned: Check if changes are allowed at all
488 if (eTeamDist == TEAMDIST_Free || (eTeamDist == TEAMDIST_Host && fByHost))
489 // also make sure that selecting this team is allowed, e.g. doesn't break the team limit
490 // this also checks whether the team number is a valid team - but it would accept TEAMID_New, which shouldn't be used in player infos!
491 if (rNewJoin.GetTeam() != TEAMID_New && IsJoin2TeamAllowed(idTeam: rNewJoin.GetTeam()))
492 // okay; accept change
493 return true;
494 // Reject change by reassigning the current team
495 rNewJoin.SetTeam(idCurrentTeam);
496 // and determine a new team, if none has been assigned yet
497 if (idCurrentTeam) return true;
498 }
499 // new team assignment
500 // teams are always needed in the lobby, so there's a team preset to change
501 // for runtime joins, teams are needed if specified by teams.txt or if any teams have been created before (to avoid mixed team-noteam-scenarios)
502 // but only assign teams in runtime join if the player won't pick it himself
503 bool fWillHaveLobby = Game.Network.isEnabled() && !Game.Network.Status.isPastLobby() && Game.fLobby;
504 bool fHasOrWillHaveLobby = Game.Network.isLobbyActive() || fWillHaveLobby;
505 bool fCanPickTeamAtRuntime = !IsRandomTeam() && (rNewJoin.GetType() == C4PT_User) && IsRuntimeJoinTeamChoice();
506 bool fIsTeamNeeded = IsRuntimeJoinTeamChoice() || GetTeamCount();
507 if (!fHasOrWillHaveLobby && (!fIsTeamNeeded || fCanPickTeamAtRuntime)) return false;
508 // get least-used team
509 C4Team *pAssignTeam = nullptr;
510 C4Team *pLowestTeam = GetRandomSmallestTeam(limitRandomTeamCount: IsRandomTeam());
511 // melee mode
512 if (IsAutoGenerateTeams() && !IsRandomTeam())
513 {
514 // reuse old team only if it's empty
515 if (pLowestTeam && !pLowestTeam->GetPlayerCount())
516 pAssignTeam = pLowestTeam;
517 else
518 {
519 // no empty team: generate new
520 GenerateDefaultTeams(iUpToID: iLastTeamID + 1);
521 pAssignTeam = GetTeamByID(iID: iLastTeamID);
522 }
523 }
524 else
525 {
526 if (!pLowestTeam)
527 {
528 // not enough teams defined in teamwork mode?
529 // then create two teams as default
530 if (!GetTeamByIndex(iIndex: 1))
531 GenerateDefaultTeams(iUpToID: 2);
532 else
533 // otherwise, all defined teams are full. This is a scenario error, because MaxPlayer should have been adjusted
534 return false;
535 pLowestTeam = GetTeamByIndex(iIndex: 0);
536 }
537 pAssignTeam = pLowestTeam;
538 }
539 // assign it
540 if (!pAssignTeam) return false;
541 pAssignTeam->AddPlayer(rInfo&: rNewJoin, fAdjustPlayer: true);
542 return true;
543}
544
545bool C4TeamList::IsJoin2TeamAllowed(int32_t idTeam)
546{
547 // join to new team: Only if new teams can be created
548 if (idTeam == TEAMID_New) return IsAutoGenerateTeams();
549 // team number must be valid
550 C4Team *pTeam = GetTeamByID(iID: idTeam);
551 if (!pTeam) return false;
552 // team player count must not exceed the limit
553 return !pTeam->IsFull();
554}
555
556void C4TeamList::CompileFunc(StdCompiler *pComp)
557{
558 pComp->Value(rStruct: mkNamingAdapt(rValue&: fActive, szName: "Active", rDefault: true));
559 pComp->Value(rStruct: mkNamingAdapt(rValue&: fCustom, szName: "Custom", rDefault: true));
560 pComp->Value(rStruct: mkNamingAdapt(rValue&: fAllowHostilityChange, szName: "AllowHostilityChange", rDefault: false));
561 pComp->Value(rStruct: mkNamingAdapt(rValue&: fAllowTeamSwitch, szName: "AllowTeamSwitch", rDefault: false));
562 pComp->Value(rStruct: mkNamingAdapt(rValue&: fAutoGenerateTeams, szName: "AutoGenerateTeams", rDefault: false));
563 pComp->Value(rStruct: mkNamingAdapt(rValue&: iLastTeamID, szName: "LastTeamID", rDefault: 0));
564
565 StdEnumEntry<TeamDist> TeamDistEntries[] =
566 {
567 { .Name: "Free", .Val: TEAMDIST_Free },
568 { .Name: "Host", .Val: TEAMDIST_Host },
569 { .Name: "None", .Val: TEAMDIST_None },
570 { .Name: "Random", .Val: TEAMDIST_Random },
571 { .Name: "RandomInv", .Val: TEAMDIST_RandomInv },
572 };
573 pComp->Value(rStruct: mkNamingAdapt(rValue: mkEnumAdaptT<uint8_t>(rVal&: eTeamDist, pNames: TeamDistEntries), szName: "TeamDistribution", rDefault: TEAMDIST_Free));
574
575 pComp->Value(rStruct: mkNamingAdapt(rValue&: fTeamColors, szName: "TeamColors", rDefault: false));
576 pComp->Value(rStruct: mkNamingAdapt(rValue&: iMaxScriptPlayers, szName: "MaxScriptPlayers", rDefault: 0));
577 pComp->Value(rStruct: mkNamingAdapt(rValue&: sScriptPlayerNames, szName: "ScriptPlayerNames", rDefault: StdStrBuf()));
578 pComp->Value(rStruct: mkNamingAdapt(rValue&: randomTeamCount, szName: "RandomTeamCount", rDefault: 0));
579
580 int32_t iOldTeamCount = iTeamCount;
581 pComp->Value(rStruct: mkNamingCountAdapt(iCount&: iTeamCount, szName: "Team"));
582
583 if (pComp->isCompiler())
584 {
585 while (iOldTeamCount--) delete ppList[iOldTeamCount];
586 delete[] ppList;
587 if (iTeamCapacity = iTeamCount)
588 {
589 ppList = new C4Team *[iTeamCapacity]{};
590 }
591 else
592 ppList = nullptr;
593 }
594
595 if (iTeamCount)
596 {
597 // Force compiler to spezialize
598 mkPtrAdaptNoNull(rpObj&: *ppList);
599 // Save team list, using map-function.
600 pComp->Value(rStruct: mkNamingAdapt(
601 rValue: mkArrayAdaptMapS(array: ppList, size: iTeamCount, map: mkPtrAdaptNoNull<C4Team>),
602 szName: "Team"));
603 }
604
605 if (pComp->isCompiler())
606 {
607 // adjust last team ID, which may not be set properly for player-generated team files
608 iLastTeamID = (std::max)(a: GetLargestTeamID(), b: iLastTeamID);
609 // force automatic generation of teams if none are defined
610 if (!iTeamCount) fAutoGenerateTeams = true;
611 }
612}
613
614bool C4TeamList::Load(C4Group &hGroup, class C4Scenario *pInitDefault, class C4LangStringTable *pLang)
615{
616 // clear previous
617 Clear();
618 // load file contents
619 StdStrBuf Buf;
620 if (!hGroup.LoadEntryString(C4CFN_Teams, Buf))
621 {
622 // no teams: Try default init
623 if (!pInitDefault) return false;
624 // no teams defined: Activate default melee teams if a melee rule is found
625 C4ID C4ID_Melee = C4Id(str: "MELE");
626 C4ID C4ID_TeamworkMelee = C4Id(str: "MEL2"); // deprecated
627 C4ID C4ID_Rivalry = C4Id(str: "RVLR");
628 // default: FFA for anything that looks like melee
629 if (pInitDefault->Game.Goals.GetIDCount(id: C4ID_Melee, zeroDefVal: 1)
630 || pInitDefault->Game.Rules.GetIDCount(id: C4ID_Rivalry, zeroDefVal: 1)
631 || pInitDefault->Game.Goals.GetIDCount(id: C4ID_TeamworkMelee, zeroDefVal: 1)
632 || Game.C4S.Game.Mode == C4S_Melee || Game.C4S.Game.Mode == C4S_MeleeTeamwork)
633 {
634 fAllowHostilityChange = true;
635 fActive = true;
636 fAutoGenerateTeams = true;
637 }
638 else
639 {
640 // No goals/rules whatsoever: They could be present in the objects.txt, but parsing that would be a bit of
641 // overkill
642 // So just keep the old behaviour here, and disallow teams
643 fAllowHostilityChange = true;
644 fActive = false;
645 }
646 fCustom = false;
647 }
648 else
649 {
650 // team definition file may be localized
651 if (pLang) pLang->ReplaceStrings(rBuf&: Buf);
652 // compile
653 if (!CompileFromBuf_LogWarn<StdCompilerINIRead>(TargetStruct: mkNamingAdapt(rValue&: *this, szName: "Teams"), SrcBuf: Buf, C4CFN_Teams)) return false;
654 }
655 // post-initialization: Generate default team colors
656 int32_t iTeam = 0; C4Team *pTeam;
657 while (pTeam = GetTeamByIndex(iIndex: iTeam++))
658 pTeam->RecheckColor(rForList&: *this);
659 return true;
660}
661
662bool C4TeamList::Save(C4Group &hGroup)
663{
664 // remove previous entry from group
665 hGroup.DeleteEntry(C4CFN_Teams);
666 // decompile
667 try
668 {
669 const std::string buf{DecompileToBuf<StdCompilerINIWrite>(SrcStruct: mkNamingAdapt(rValue&: *this, szName: "Teams"))};
670 // save it
671 StdStrBuf copy{buf.c_str(), buf.size()};
672 hGroup.Add(C4CFN_Teams, pBuffer&: copy, fChild: false, fHoldBuffer: true);
673 }
674 catch (const StdCompiler::Exception &)
675 {
676 return false;
677 }
678 // done, success
679 return true;
680}
681
682void C4TeamList::RecheckPlayers()
683{
684 C4Team **ppCheck = ppList; int32_t iCnt = iTeamCount;
685 for (; iCnt--; ++ppCheck)(*ppCheck)->RecheckPlayers();
686}
687
688void C4TeamList::RecheckTeams()
689{
690 // automatic team distributions only
691 if (!IsRandomTeam()) return;
692 // host decides random teams
693 if (!Game.Control.isCtrlHost()) return;
694 // random teams in auto generate mode? Make sure there are as many as set up
695 if (IsAutoGenerateTeams() && GetTeamCount() != GetGenerateTeamCount())
696 {
697 ReassignAllTeams();
698 return;
699 }
700 // redistribute players of largest team that has relocatable players left towards smaller teams
701 for (;;)
702 {
703 C4Team *pLowestTeam = GetRandomSmallestTeam(limitRandomTeamCount: true);
704 if (!pLowestTeam) break; // no teams: Nothing to re-distribute.
705 // get largest team that has relocateable players
706 C4Team *pLargestTeam = nullptr;
707 C4Team **ppCheck = ppList; int32_t iCnt = randomTeamCount > 1 ? randomTeamCount : iTeamCount;
708 for (; iCnt--; ++ppCheck) if (!pLargestTeam || (*ppCheck)->GetPlayerCount() > pLargestTeam->GetPlayerCount())
709 if ((*ppCheck)->GetFirstUnjoinedPlayerID())
710 pLargestTeam = *ppCheck;
711 // no team can redistribute?
712 if (!pLargestTeam) break;
713 // redistribution won't help much?
714 if (pLargestTeam->GetPlayerCount() - pLowestTeam->GetPlayerCount() <= 1) break;
715 // okay; redistribute one player!
716 int32_t idRedistPlayer = pLargestTeam->GetFirstUnjoinedPlayerID();
717 C4PlayerInfo *pInfo = Game.PlayerInfos.GetPlayerInfoByID(id: idRedistPlayer);
718 assert(pInfo);
719 if (!pInfo) break; // umn...serious problems
720 pLargestTeam->RemovePlayerByID(iID: idRedistPlayer);
721 pLowestTeam->AddPlayer(rInfo&: *pInfo, fAdjustPlayer: true);
722 C4ClientPlayerInfos *pClrInfo = Game.PlayerInfos.GetClientInfoByPlayerID(id: idRedistPlayer);
723 assert(pClrInfo);
724 // player info change: mark updated to remote clients get information
725 if (pClrInfo)
726 {
727 pClrInfo->SetUpdated();
728 }
729 }
730}
731
732void C4TeamList::ReassignAllTeams()
733{
734 assert(Game.Control.isCtrlHost());
735 if (!Game.Control.isCtrlHost()) return;
736 // go through all player infos; reset team in them
737 int32_t idStart = -1; C4PlayerInfo *pNfo;
738 while (pNfo = Game.PlayerInfos.GetNextPlayerInfoByID(id: idStart))
739 {
740 idStart = pNfo->GetID();
741 if (pNfo->HasJoinIssued()) continue;
742 pNfo->SetTeam(0);
743 // mark changed info as updated
744 C4ClientPlayerInfos *pCltInfo = Game.PlayerInfos.GetClientInfoByPlayerID(id: idStart);
745 assert(pCltInfo);
746 if (pCltInfo)
747 {
748 pCltInfo->SetUpdated();
749 }
750 }
751 // clear players from team lists
752 RecheckPlayers();
753 const auto generateTeamCount = GetGenerateTeamCount();
754 if (IsRandomTeam())
755 if (IsAutoGenerateTeams() && GetTeamCount() != generateTeamCount)
756 {
757 ClearTeams();
758 GenerateDefaultTeams(iUpToID: generateTeamCount);
759 }
760 // reassign them
761 idStart = -1;
762 while (pNfo = Game.PlayerInfos.GetNextPlayerInfoByID(id: idStart))
763 {
764 idStart = pNfo->GetID();
765 if (pNfo->HasJoinIssued()) continue;
766 assert(!pNfo->GetTeam());
767 RecheckPlayerInfoTeams(rNewJoin&: *pNfo, fByHost: true);
768 }
769}
770
771std::string C4TeamList::GetTeamDistName(TeamDist eTeamDist) const
772{
773 switch (eTeamDist)
774 {
775 case TEAMDIST_Free: return LoadResStr(id: C4ResStrTableKey::IDS_MSG_TEAMDIST_FREE);
776 case TEAMDIST_Host: return LoadResStr(id: C4ResStrTableKey::IDS_MSG_TEAMDIST_HOST);
777 case TEAMDIST_None: return LoadResStr(id: C4ResStrTableKey::IDS_MSG_TEAMDIST_NONE);
778 case TEAMDIST_Random: return LoadResStr(id: C4ResStrTableKey::IDS_MSG_TEAMDIST_RND);
779 case TEAMDIST_RandomInv: return LoadResStr(id: C4ResStrTableKey::IDS_MSG_TEAMDIST_RNDINV);
780 }
781 return std::format(fmt: "TEAMDIST_undefined({})", args: std::to_underlying(value: eTeamDist));
782}
783
784void C4TeamList::FillTeamDistOptions(C4GUI::ComboBox_FillCB *pFiller) const
785{
786 // no teams if disabled
787 if (!fActive) return;
788 // team distribution options
789 pFiller->AddEntry(szText: GetTeamDistName(eTeamDist: TEAMDIST_Free).c_str(), id: TEAMDIST_Free);
790 pFiller->AddEntry(szText: GetTeamDistName(eTeamDist: TEAMDIST_Host).c_str(), id: TEAMDIST_Host);
791 if (IsAutoGenerateTeams()) pFiller->AddEntry(szText: GetTeamDistName(eTeamDist: TEAMDIST_None).c_str(), id: TEAMDIST_None); // no teams: only for regular melees
792 pFiller->AddEntry(szText: GetTeamDistName(eTeamDist: TEAMDIST_Random).c_str(), id: TEAMDIST_Random);
793 pFiller->AddEntry(szText: GetTeamDistName(eTeamDist: TEAMDIST_RandomInv).c_str(), id: TEAMDIST_RandomInv);
794}
795
796void C4TeamList::SendSetTeamDist(TeamDist eNewTeamDist)
797{
798 assert(Game.Control.isCtrlHost());
799 // set it for all clients
800 Game.Control.DoInput(eCtrlType: CID_Set, pPkt: new C4ControlSet(C4CVT_TeamDistribution, eNewTeamDist), eDelivery: CDT_Sync);
801}
802
803std::string C4TeamList::GetTeamDistString() const
804{
805 // return name of current team distribution setting
806 return GetTeamDistName(eTeamDist);
807}
808
809bool C4TeamList::HasTeamDistOptions() const
810{
811 // team distribution can be changed if teams are enabled
812 return fActive;
813}
814
815void C4TeamList::SetTeamDistribution(TeamDist eToVal)
816{
817 if (!Inside(ival: eToVal, lbound: TEAMDIST_First, rbound: TEAMDIST_Last)) { assert(false); return; }
818 eTeamDist = eToVal;
819 // team distribution mode changed: Host may beed to redistribute
820 if (Game.Control.isCtrlHost())
821 {
822 // if a random team mode was set, reassign all teams so it's really random. Also reassign in no-team-mode so enough teams for all players exist
823 if (IsRandomTeam() || eTeamDist == TEAMDIST_None)
824 ReassignAllTeams();
825 else
826 {
827 // otherwise, it's sufficient to just reassign any teams that are incorrect for the current mode
828 RecheckTeams();
829 }
830 // send updates to other clients and reset flags
831 if (Game.Network.isEnabled())
832 {
833 Game.Network.Players.SendUpdatedPlayers();
834 }
835 }
836}
837
838void C4TeamList::SendSetTeamColors(bool fEnabled)
839{
840 // set it for all clients
841 Game.Control.DoInput(eCtrlType: CID_Set, pPkt: new C4ControlSet(C4CVT_TeamColors, fEnabled), eDelivery: CDT_Sync);
842}
843
844void C4TeamList::SetTeamColors(bool fEnabled)
845{
846 // change only
847 if (fEnabled == fTeamColors) return;
848 // reflect change
849 fTeamColors = fEnabled;
850 // update colors of all players
851 if (!Game.Control.isCtrlHost()) return;
852 // go through all player infos; reset color in them
853 Game.PlayerInfos.UpdatePlayerAttributes(); // sets team and savegame colors
854 if (Game.Network.isEnabled())
855 {
856 // sends color updates to all clients
857 Game.Network.Players.SendUpdatedPlayers();
858 }
859}
860
861void C4TeamList::SetRandomTeamCount(int32_t count)
862{
863 randomTeamCount = count;
864 if (IsRandomTeam())
865 {
866 ReassignAllTeams();
867 }
868}
869
870void C4TeamList::EnforceLeagueRules()
871{
872 // enforce some league settings
873 fAllowTeamSwitch = false; // switching teams in league games? Yeah, sure...
874}
875
876int32_t C4TeamList::GetForcedTeamSelection(int32_t idForPlayer) const
877{
878 // if there's only one team for the player to join, return that team ID
879 C4Team *pOKTeam = nullptr, *pCheck;
880 if (idForPlayer) pOKTeam = GetTeamByPlayerID(iID: idForPlayer); // curent team is always possible, even if full
881 int32_t iCheckTeam = 0;
882 while (pCheck = GetTeamByIndex(iIndex: iCheckTeam++))
883 if (!pCheck->IsFull())
884 {
885 // this team could be joined
886 if (pOKTeam && pOKTeam != pCheck)
887 {
888 // there already was a team that could be joined
889 // two alternatives -> team selection is not forced
890 return 0;
891 }
892 pOKTeam = pCheck;
893 }
894 // was there a team that could be joined?
895 if (pOKTeam)
896 {
897 // if teams are generated on the fly, there would always be the possibility of creating a new team
898 if (IsAutoGenerateTeams()) return 0;
899 // otherwise, this team is forced!
900 return pOKTeam->GetID();
901 }
902 // no team could be joined: Teams auto generated?
903 if (IsAutoGenerateTeams())
904 {
905 // then the only possible way is to join a new team
906 return TEAMID_New;
907 }
908 // otherwise, nothing can be done...
909 return 0;
910}
911
912StdStrBuf C4TeamList::GetScriptPlayerName() const
913{
914 // get a name to assign to a new script player. Try to avoid name conflicts
915 if (!sScriptPlayerNames.getLength()) return StdStrBuf::MakeRef(str: LoadResStr(id: C4ResStrTableKey::IDS_TEXT_COMPUTER)); // default name
916 // test available script names
917 int32_t iNameIdx = 0; StdStrBuf sOut;
918 while (sScriptPlayerNames.GetSection(idx: iNameIdx++, psOutSection: &sOut, cSeparator: '|'))
919 if (!Game.PlayerInfos.GetActivePlayerInfoByName(szName: sOut.getData()))
920 return sOut;
921 // none are available: Return a random name
922 sScriptPlayerNames.GetSection(idx: SafeRandom(range: iNameIdx - 1), psOutSection: &sOut, cSeparator: '|');
923 return sOut;
924}
925