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