1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2004, 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// permanent player information management
19// see header for some additional information
20
21#include <C4Include.h>
22#include <C4PlayerInfo.h>
23
24#include <C4Game.h>
25#include <C4Config.h>
26#include <C4Log.h>
27#include <C4Wrappers.h>
28#include <C4Player.h>
29#include <C4FullScreen.h>
30
31#include <format>
32
33// *** C4PlayerInfo
34
35void C4PlayerInfo::Clear()
36{
37 // del temp file
38 DeleteTempFile();
39 // clear fields
40 sName.Clear(); szFilename.Clear();
41 pRes = nullptr;
42 ResCore.Clear();
43 // default fields
44 dwColor = dwOriginalColor = 0xffffff;
45 dwAlternateColor = 0;
46 dwFlags = 0;
47 iID = idSavegamePlayer = idTeam = 0;
48 iInGameNumber = iInGameJoinFrame = iInGamePartFrame = -1;
49 sLeagueAccount = ""; iLeagueScore = iLeagueRank = 0;
50 iLeagueProjectedGain = -1;
51 eType = C4PT_User;
52 idExtraData = C4ID_None;
53 iLeaguePerformance = 0;
54 sLeagueProgressData.Clear();
55}
56
57void C4PlayerInfo::DeleteTempFile()
58{
59 // is temp file?
60 if (!!szFilename && (dwFlags & PIF_TempFile))
61 {
62 // erase it
63 EraseItem(szItemName: szFilename.getData());
64 // reset flag and filename to prevent double deletion
65 dwFlags &= ~PIF_TempFile;
66 szFilename.Clear();
67 }
68}
69
70bool C4PlayerInfo::LoadFromLocalFile(const char *szFilename)
71{
72 // players should not be added in replay mode
73 assert(!Game.C4S.Head.Replay);
74 // clear previous
75 Clear();
76 // open player file group
77 C4Group Grp;
78 if (!Grp.Open(szGroupName: szFilename)) return false;
79 // read core
80 C4PlayerInfoCore C4P;
81 if (!C4P.Load(hGroup&: Grp)) return false;
82 // close group to free file handle
83 Grp.Close();
84 // set values
85 eType = C4PT_User;
86 sName.Copy(pnData: C4P.PrefName);
87 this->szFilename = szFilename;
88 dwColor = dwOriginalColor = C4P.PrefColorDw & 0xffffff; // ignore alpha
89 dwAlternateColor = C4P.PrefColor2Dw & 0xffffff; // ignore alpha
90 // network: ressource (not for replays, because everyone has the player files there...)
91 if (Game.Network.isEnabled() && !Game.C4S.Head.Replay)
92 {
93 // add ressource
94 // 2do: rejoining players need to update their ressource version when saving the player
95 // otherwise, player file versions may differ
96 pRes = Game.Network.ResList.getRefRes(szFile: Config.AtExeRelativePath(szFilename), fLocalOnly: true);
97 // not found? add
98 if (!pRes) pRes = Game.Network.ResList.AddByFile(strFilePath: Config.AtExeRelativePath(szFilename), fTemp: false, eType: NRT_Player);
99 if (!pRes) return false;
100 // set core and flag
101 ResCore = pRes->getCore();
102 dwFlags |= PIF_HasRes;
103 // filename is no longer needed in network mode, because it's stored in the res-core
104 }
105 // done, success
106 return true;
107}
108
109bool C4PlayerInfo::SetAsScriptPlayer(const char *szName, uint32_t dwColor, uint32_t dwFlags, C4ID idExtra)
110{
111 // clear previous
112 Clear();
113 // set parameters
114 eType = C4PT_Script;
115 dwColor = dwOriginalColor = dwColor & 0xffffff; // ignore alpha
116 dwAlternateColor = 0;
117 this->sName.CopyValidated(szFromVal: szName);
118 idExtraData = idExtra;
119 this->dwFlags |= dwFlags;
120 // done, success
121 return true;
122}
123
124const char *C4PlayerInfo::GetLocalJoinFilename() const
125{
126 // preferred: by ressource
127 if (pRes) return pRes->getFile();
128 // if no ressource is known (replay or non-net), return filename
129 return szFilename.getData();
130}
131
132uint32_t C4PlayerInfo::GetLobbyColor() const
133{
134 // special case if random teams and team colors are enabled in lobby:
135 // Unjoined players do not show their team! Instead, they just display their original color
136 if (Game.Teams.GetTeamDist() == C4TeamList::TEAMDIST_RandomInv)
137 if (Game.Teams.IsTeamColors())
138 if (Game.Teams.GetTeamByID(iID: GetTeam()))
139 if (!HasJoined() && !GetAssociatedSavegamePlayerID())
140 return GetOriginalColor();
141 // otherwise, just show the normal player color
142 return GetColor();
143}
144
145StdStrBuf C4PlayerInfo::GetLobbyName() const
146{
147 // return player name including colored clan/team tag if known
148 StdStrBuf sResult;
149 if (sLeagueAccount.getLength())
150 {
151 if (sClanTag.getLength())
152 {
153 // gray team tag color used in lobby and game evaluation dialog!
154 sResult.Copy(pnData: std::format(fmt: "<c afafaf>{}</c> {}", args: sClanTag.getData(), args: sLeagueAccount.getData()).c_str());
155 }
156 else
157 sResult.Ref(Buf2: sLeagueAccount);
158 }
159 else
160 {
161 // fallback to regular player name
162 sResult.Ref(Buf2: sForcedName.getLength() ? static_cast<const StdStrBuf &>(sForcedName) : static_cast<const StdStrBuf &>(sName));
163 }
164 return sResult;
165}
166
167bool C4PlayerInfo::HasTeamWon() const
168{
169 // team win/solo win
170 C4Team *pTeam;
171 if (idTeam && (pTeam = Game.Teams.GetTeamByID(iID: idTeam)))
172 return pTeam->HasWon();
173 else
174 return HasWon();
175}
176
177void C4PlayerInfo::CompileFunc(StdCompiler *pComp)
178{
179 // Names
180 pComp->Value(rStruct: mkNamingAdapt(rValue&: sName, szName: "Name", rDefault: ""));
181 pComp->Value(rStruct: mkNamingAdapt(rValue&: sForcedName, szName: "ForcedName", rDefault: ""));
182 pComp->Value(rStruct: mkNamingAdapt(rValue&: szFilename, szName: "Filename", rDefault: ""));
183
184 // Flags
185 const StdBitfieldEntry<uint16_t> Entries[] =
186 {
187 { .Name: "Joined", .Val: PIF_Joined },
188 { .Name: "Removed", .Val: PIF_Removed },
189 { .Name: "HasResource", .Val: PIF_HasRes },
190 { .Name: "JoinIssued", .Val: PIF_JoinIssued },
191 { .Name: "SavegameJoin", .Val: PIF_JoinedForSavegameOnly },
192 { .Name: "Disconnected", .Val: PIF_Disconnected },
193 { .Name: "VotedOut", .Val: PIF_VotedOut },
194 { .Name: "Won", .Val: PIF_Won },
195 { .Name: "AttributesFixed", .Val: PIF_AttributesFixed },
196 { .Name: "NoScenarioInit", .Val: PIF_NoScenarioInit },
197 { .Name: "NoEliminationCheck", .Val: PIF_NoEliminationCheck },
198 { .Name: "Invisible", .Val: PIF_Invisible },
199 { .Name: nullptr, .Val: 0 },
200 };
201 uint16_t dwSyncFlags = dwFlags & PIF_SyncFlags; // do not store local flags!
202 pComp->Value(rStruct: mkNamingAdapt(rValue: mkBitfieldAdapt(rVal&: dwSyncFlags, pNames: Entries), szName: "Flags", rDefault: 0u));
203 if (pComp->isCompiler()) dwFlags = dwSyncFlags;
204 pComp->Value(rStruct: mkNamingAdapt(rValue&: iID, szName: "ID", rDefault: 0));
205
206 // type
207 StdEnumEntry<C4PlayerType> PlayerTypes[] =
208 {
209 { .Name: "User", .Val: C4PT_User },
210 { .Name: "Script", .Val: C4PT_Script },
211 };
212 pComp->Value(rStruct: mkNamingAdapt(rValue: mkEnumAdaptT<uint8_t>(rVal&: eType, pNames: PlayerTypes), szName: "Type", rDefault: C4PT_User));
213
214 // safety: Do not allow invisible regular players
215 if (pComp->isCompiler())
216 {
217 if (eType != C4PT_Script) dwFlags &= ~PIF_Invisible;
218 }
219
220 // load colors
221 pComp->Value(rStruct: mkNamingAdapt(rValue&: dwColor, szName: "Color", rDefault: 0u));
222 pComp->Value(rStruct: mkNamingAdapt(rValue&: dwOriginalColor, szName: "OriginalColor", rDefault: dwColor));
223 // load savegame ID
224 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: idSavegamePlayer), szName: "SavgamePlayer", rDefault: 0));
225 // load team ID
226 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: idTeam), szName: "Team", rDefault: 0));
227 // load authentication ID
228 pComp->Value(rStruct: mkNamingAdapt(rValue&: szAuthID, szName: "AUID", rDefault: ""));
229
230 // InGame info
231 if (dwFlags & PIF_Joined)
232 {
233 pComp->Value(rStruct: mkNamingAdapt(rValue&: iInGameNumber, szName: "GameNumber", rDefault: -1));
234 pComp->Value(rStruct: mkNamingAdapt(rValue&: iInGameJoinFrame, szName: "GameJoinFrame", rDefault: -1));
235 }
236 else
237 iInGameNumber = iInGameJoinFrame = -1;
238
239 if (dwFlags & PIF_Removed)
240 pComp->Value(rStruct: mkNamingAdapt(rValue&: iInGamePartFrame, szName: "GamePartFrame", rDefault: -1));
241 else
242 iInGamePartFrame = -1;
243
244 // script player extra data
245 pComp->Value(rStruct: mkNamingAdapt(rValue: mkC4IDAdapt(rValue&: idExtraData), szName: "ExtraData", rDefault: C4ID_None));
246
247 // load league info
248 pComp->Value(rStruct: mkNamingAdapt(rValue&: sLeagueAccount, szName: "LeagueAccount", rDefault: ""));
249 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iLeagueScore), szName: "LeagueScore", rDefault: 0));
250 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iLeagueRank), szName: "LeagueRank", rDefault: 0));
251 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iLeagueRankSymbol), szName: "LeagueRankSymbol", rDefault: 0));
252 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iLeagueProjectedGain), szName: "ProjectedGain", rDefault: -1));
253 pComp->Value(rStruct: mkNamingAdapt(rValue: mkParAdapt(rObj&: sClanTag, rPar: StdCompiler::RCT_All), szName: "ClanTag", rDefault: ""));
254 pComp->Value(rStruct: mkNamingAdapt(rValue: mkIntPackAdapt(rVal&: iLeaguePerformance), szName: "LeaguePerformance", rDefault: 0));
255 pComp->Value(rStruct: mkNamingAdapt(rValue&: sLeagueProgressData, szName: "LeagueProgressData", rDefault: ""));
256
257 // file resource
258 if (dwFlags & PIF_HasRes)
259 {
260 // ResCore
261 if (pComp->isDecompiler() && pRes)
262 {
263 // ensure ResCore is up-to-date
264 ResCore = pRes->getCore();
265 }
266 pComp->Value(rStruct: mkNamingAdapt(rValue&: ResCore, szName: "ResCore"));
267 }
268}
269
270void C4PlayerInfo::SetFilename(const char *szToFilename)
271{
272 szFilename = szToFilename;
273}
274
275void C4PlayerInfo::LoadResource()
276{
277 // only if any resource present and not yet assigned
278 if (IsRemoved() || !(dwFlags & PIF_HasRes) || pRes) return;
279 // Ignore res if a local file is to be used
280 // the PIF_InScenarioFile is not set for startup players in initial replays,
281 // because ressources are used for player joins but emulated in playback control
282 // if there will ever be ressources in replay mode, this special case can be removed
283 if (Game.C4S.Head.Replay || (dwFlags & PIF_InScenarioFile))
284 dwFlags &= ~PIF_HasRes;
285 else
286 // create resource (will check if resource already exists)
287 if (!(pRes = Game.Network.ResList.AddByCore(Core: ResCore)))
288 {
289 dwFlags &= ~PIF_HasRes;
290 // add failed? invalid ressource??! -- TODO: may be too large to load
291 LogNTr(level: spdlog::level::err, fmt: "Could not add resource {} for player {}! Player file too large to load?", args: ResCore.getID(), args: GetFilename());
292 }
293}
294
295void C4PlayerInfo::DiscardResource()
296{
297 // del any file resource
298 if (pRes)
299 {
300 assert(dwFlags & PIF_HasRes);
301 pRes = nullptr;
302 dwFlags &= ~PIF_HasRes;
303 }
304 else assert(~dwFlags & PIF_HasRes);
305 ResCore.Clear();
306}
307
308bool C4PlayerInfo::SetSavegameResume(C4PlayerInfo *pSavegameInfo)
309{
310 // copy some data fields; but not the file fields, because the join method is determined by this player
311 if (!pSavegameInfo) return false;
312 iID = pSavegameInfo->GetID();
313 dwFlags = (dwFlags & ~PIF_SavegameTakeoverFlags) | (pSavegameInfo->GetFlags() & PIF_SavegameTakeoverFlags);
314 dwColor = pSavegameInfo->GetColor(); // redundant; should be done by host already
315 idTeam = pSavegameInfo->GetTeam();
316 return true;
317}
318
319void C4PlayerInfo::SetJoined(int32_t iNumber)
320{
321 // mark as joined in current frame
322 iInGameNumber = iNumber;
323 iInGameJoinFrame = Game.FrameCounter;
324 dwFlags |= PIF_Joined;
325}
326
327void C4PlayerInfo::SetRemoved()
328{
329 // mark as removed - always marks as previously joined, too
330 dwFlags |= PIF_Joined | PIF_Removed;
331 // remember removal frame
332 iInGamePartFrame = Game.FrameCounter;
333}
334
335bool C4PlayerInfo::LoadBigIcon(C4FacetExSurface &fctTarget)
336{
337 bool fSuccess = false;
338 // load BigIcon.png of player into target facet; return false if no bigicon present or player file not yet loaded
339 C4Group Plr;
340 C4Network2Res *pRes = nullptr;
341 bool fIncompleteRes = false;
342 if (pRes = GetRes())
343 if (!pRes->isComplete())
344 fIncompleteRes = true;
345 size_t iBigIconSize = 0;
346 if (!fIncompleteRes)
347 if (Plr.Open(szGroupName: pRes ? pRes->getFile() : GetFilename()))
348 if (Plr.AccessEntry(C4CFN_BigIcon, iSize: &iBigIconSize))
349 if (iBigIconSize <= C4NetResMaxBigicon * 1024)
350 if (fctTarget.Load(hGroup&: Plr, C4CFN_BigIcon))
351 fSuccess = true;
352 return fSuccess;
353}
354
355// *** C4ClientPlayerInfos
356
357C4ClientPlayerInfos::C4ClientPlayerInfos(const char *szJoinFilenames, bool fAdd, C4PlayerInfo *pAddInfo)
358 : iPlayerCount(0), iClientID(-1), iPlayerCapacity(0), ppPlayers(nullptr), dwFlags(0)
359{
360 // init for local client?
361 if (szJoinFilenames || pAddInfo)
362 {
363 // set local ID
364 iClientID = Game.Control.ClientID();
365 // maybe control is not preinitialized
366 if (!Game.Control.isNetwork() && iClientID < 0) iClientID = 0;
367 // join packet or initial packet?
368 if (fAdd)
369 // packet is to be added to other players
370 dwFlags |= CIF_AddPlayers;
371 else
372 // set initial flag for first-time join packet
373 dwFlags |= CIF_Initial;
374 // join all players in list
375 if (iPlayerCapacity = (szJoinFilenames ? SModuleCount(szList: szJoinFilenames) : 0) + !!pAddInfo)
376 {
377 ppPlayers = new C4PlayerInfo *[iPlayerCapacity];
378 if (szJoinFilenames)
379 {
380 char szPlrFile[_MAX_PATH + 1];
381 for (int32_t i = 0; i < iPlayerCapacity; ++i)
382 if (SGetModule(szList: szJoinFilenames, iIndex: i, sTarget: szPlrFile, _MAX_PATH))
383 {
384 C4PlayerInfo *pNewInfo = new C4PlayerInfo();
385 if (pNewInfo->LoadFromLocalFile(szFilename: szPlrFile))
386 // player def loaded; register and count it
387 ppPlayers[iPlayerCount++] = pNewInfo;
388 else
389 // loading failure; clear info class
390 delete pNewInfo;
391 }
392 }
393 if (pAddInfo)
394 ppPlayers[iPlayerCount++] = pAddInfo;
395 }
396 }
397}
398
399C4ClientPlayerInfos::C4ClientPlayerInfos(const C4ClientPlayerInfos &rCopy)
400{
401 // copy fields
402 iClientID = rCopy.iClientID;
403 if (iPlayerCount = rCopy.iPlayerCount)
404 {
405 // copy player infos
406 ppPlayers = new C4PlayerInfo *[iPlayerCapacity = rCopy.iPlayerCapacity];
407 int32_t i = iPlayerCount;
408 C4PlayerInfo **ppCurrPlrInfo = ppPlayers, **ppSrcPlrInfo = rCopy.ppPlayers;
409 while (i--) *ppCurrPlrInfo++ = new C4PlayerInfo(**ppSrcPlrInfo++);
410 }
411 // no players
412 else
413 {
414 ppPlayers = nullptr;
415 iPlayerCapacity = 0;
416 }
417 // misc fields
418 dwFlags = rCopy.dwFlags;
419}
420
421C4ClientPlayerInfos &C4ClientPlayerInfos::operator=(const C4ClientPlayerInfos &rCopy)
422{
423 Clear();
424 // copy fields
425 iClientID = rCopy.iClientID;
426 if (iPlayerCount = rCopy.iPlayerCount)
427 {
428 // copy player infos
429 ppPlayers = new C4PlayerInfo *[iPlayerCapacity = rCopy.iPlayerCapacity];
430 int32_t i = iPlayerCount;
431 C4PlayerInfo **ppCurrPlrInfo = ppPlayers, **ppSrcPlrInfo = rCopy.ppPlayers;
432 while (i--) *ppCurrPlrInfo++ = new C4PlayerInfo(**ppSrcPlrInfo++);
433 }
434 // no players
435 else
436 {
437 ppPlayers = nullptr;
438 iPlayerCapacity = 0;
439 }
440 // misc fields
441 dwFlags = rCopy.dwFlags;
442 return *this;
443}
444
445void C4ClientPlayerInfos::Clear()
446{
447 // del player infos
448 int32_t i = iPlayerCount; C4PlayerInfo **ppCurrPlrInfo = ppPlayers;
449 while (i--) delete *ppCurrPlrInfo++;
450 // del player info vector
451 delete[] ppPlayers; ppPlayers = nullptr;
452 // reset other fields
453 iPlayerCount = iPlayerCapacity = 0;
454 iClientID = -1;
455 dwFlags = 0;
456}
457
458void C4ClientPlayerInfos::GrabMergeFrom(C4ClientPlayerInfos &rFrom)
459{
460 // anything to grab?
461 if (!rFrom.iPlayerCount) return;
462 // any previous players to copy?
463 if (iPlayerCount)
464 {
465 // buffer sufficient?
466 if (iPlayerCount + rFrom.iPlayerCount > iPlayerCapacity)
467 GrowList(iByVal: rFrom.iPlayerCount);
468 // merge into new buffer
469 memcpy(dest: ppPlayers + iPlayerCount, src: rFrom.ppPlayers, n: rFrom.iPlayerCount * sizeof(C4PlayerInfo *));
470 iPlayerCount += rFrom.iPlayerCount;
471 rFrom.iPlayerCount = rFrom.iPlayerCapacity = 0;
472 delete[] rFrom.ppPlayers; rFrom.ppPlayers = nullptr;
473 }
474 else
475 {
476 // no own players: take over buffer of pFrom
477 delete[] ppPlayers;
478 ppPlayers = rFrom.ppPlayers; rFrom.ppPlayers = nullptr;
479 iPlayerCount = rFrom.iPlayerCount; rFrom.iPlayerCount = 0;
480 iPlayerCapacity = rFrom.iPlayerCapacity; rFrom.iPlayerCapacity = 0;
481 }
482}
483
484void C4ClientPlayerInfos::AddInfo(C4PlayerInfo *pAddInfo)
485{
486 // grow list if necessary
487 if (iPlayerCount == iPlayerCapacity) GrowList(iByVal: 4);
488 // add info
489 ppPlayers[iPlayerCount++] = pAddInfo;
490}
491
492void C4ClientPlayerInfos::RemoveIndexedInfo(int32_t iAtIndex)
493{
494 // bounds check
495 if (iAtIndex < 0 || iAtIndex >= iPlayerCount) return;
496 // del player info at index
497 delete ppPlayers[iAtIndex];
498 // move down last index (may self-assign a ptr)
499 ppPlayers[iAtIndex] = ppPlayers[--iPlayerCount];
500}
501
502void C4ClientPlayerInfos::RemoveInfo(int32_t idPlr)
503{
504 // check all infos; remove the one that matches
505 int32_t i = 0; C4PlayerInfo **ppCurrPlrInfo = ppPlayers;
506 while (i < iPlayerCount)
507 {
508 if ((*ppCurrPlrInfo)->GetID() == idPlr)
509 {
510 RemoveIndexedInfo(iAtIndex: i);
511 return;
512 }
513 ++ppCurrPlrInfo; ++i;
514 }
515 // none matched
516 return;
517}
518
519void C4ClientPlayerInfos::GrowList(size_t iByVal)
520{
521 // create new list (out of mem: simply returns here; info list remains in a valid state)
522 C4PlayerInfo **ppNewInfo = new C4PlayerInfo *[iPlayerCapacity += iByVal];
523 // move existing
524 if (ppPlayers)
525 {
526 memcpy(dest: ppNewInfo, src: ppPlayers, n: iPlayerCount * sizeof(C4PlayerInfo *));
527 }
528 delete[] ppPlayers;
529 // assign new
530 ppPlayers = ppNewInfo;
531}
532
533C4PlayerInfo *C4ClientPlayerInfos::GetPlayerInfo(int32_t iIndex) const
534{
535 // check range
536 if (iIndex < 0 || iIndex >= iPlayerCount) return nullptr;
537 // return indexed info
538 return ppPlayers[iIndex];
539}
540
541C4PlayerInfo *C4ClientPlayerInfos::GetPlayerInfo(int32_t iIndex, C4PlayerType eType) const
542{
543 // get indexed matching info
544 for (int32_t iCheck = 0; iCheck < iPlayerCount; ++iCheck)
545 {
546 C4PlayerInfo *pNfo = ppPlayers[iCheck];
547 if (pNfo->GetType() == eType)
548 if (!iIndex--)
549 return pNfo;
550 }
551 // nothing found
552 return nullptr;
553}
554
555C4PlayerInfo *C4ClientPlayerInfos::GetPlayerInfoByID(int32_t id) const
556{
557 // check all infos
558 int32_t i = iPlayerCount; C4PlayerInfo **ppCurrPlrInfo = ppPlayers;
559 while (i--)
560 {
561 if ((*ppCurrPlrInfo)->GetID() == id) return *ppCurrPlrInfo;
562 ++ppCurrPlrInfo;
563 }
564 // none matched
565 return nullptr;
566}
567
568C4PlayerInfo *C4ClientPlayerInfos::GetPlayerInfoByRes(int32_t idResID) const
569{
570 int32_t i = iPlayerCount; C4PlayerInfo **ppCurrPlrInfo = ppPlayers;
571 C4Network2Res *pRes;
572 while (i--)
573 {
574 if (pRes = (*ppCurrPlrInfo)->GetRes())
575 if (pRes->getResID() == idResID)
576 // only if the player is actually using the ressource
577 if ((*ppCurrPlrInfo)->IsUsingPlayerFile())
578 return *ppCurrPlrInfo;
579 ++ppCurrPlrInfo;
580 }
581 return nullptr;
582}
583
584bool C4ClientPlayerInfos::HasUnjoinedPlayers() const
585{
586 // check all players
587 int32_t i = iPlayerCount; C4PlayerInfo **ppCurrPlrInfo = ppPlayers;
588 while (i--) if (!(*ppCurrPlrInfo++)->HasJoined()) return true;
589 // all joined
590 return false;
591}
592
593int32_t C4ClientPlayerInfos::GetJoinedPlayerCount() const
594{
595 // count all players with IsJoined()
596 int32_t i = iPlayerCount; int32_t cnt = 0; C4PlayerInfo **ppCurrPlrInfo = ppPlayers;
597 while (i--) if ((*ppCurrPlrInfo++)->IsJoined()) ++cnt;
598 return cnt;
599}
600
601void C4ClientPlayerInfos::CompileFunc(StdCompiler *pComp)
602{
603 bool fCompiler = pComp->isCompiler();
604 if (fCompiler) Clear();
605 pComp->Value(rStruct: mkNamingAdapt(rValue&: iClientID, szName: "ID", rDefault: C4ClientIDUnknown));
606
607 // Flags
608 StdBitfieldEntry<uint32_t> Entries[] =
609 {
610 { .Name: "AddPlayers", .Val: CIF_AddPlayers },
611 { .Name: "Updated", .Val: CIF_Updated },
612 { .Name: "Initial", .Val: CIF_Initial },
613
614 { .Name: nullptr, .Val: 0 }
615 };
616 pComp->Value(rStruct: mkNamingAdapt(rValue: mkBitfieldAdapt(rVal&: dwFlags, pNames: Entries), szName: "Flags", rDefault: 0u));
617
618 pComp->Value(rStruct: mkNamingCountAdapt<int32_t>(iCount&: iPlayerCount, szName: "Player"));
619 if (iPlayerCount < 0 || iPlayerCount > C4MaxPlayer)
620 {
621 pComp->excCorrupt(message: "player count out of range"); return;
622 }
623 // Grow list, if necessary
624 if (fCompiler && iPlayerCount > iPlayerCapacity)
625 {
626 GrowList(iByVal: iPlayerCount - iPlayerCapacity);
627 std::fill_n(first: ppPlayers, n: iPlayerCount, value: nullptr);
628 }
629 // Compile
630 pComp->Value(rStruct: mkNamingAdapt(rValue: mkArrayAdaptMapS(array: ppPlayers, size: iPlayerCount, map: mkPtrAdaptNoNull<C4PlayerInfo>), szName: "Player"));
631 // Force specialization
632 mkPtrAdaptNoNull<C4PlayerInfo>(rpObj&: *ppPlayers);
633}
634
635void C4ClientPlayerInfos::LoadResources()
636{
637 // load for all players
638 int32_t i = iPlayerCount; C4PlayerInfo **ppCurrPlrInfo = ppPlayers;
639 while (i--)(*ppCurrPlrInfo++)->LoadResource();
640}
641
642// *** C4PlayerInfoList
643
644C4PlayerInfoList::C4PlayerInfoList() : iClientCount(0), iClientCapacity(0), ppClients(nullptr), iLastPlayerID(0)
645{
646 // no need to alloc mem yet
647}
648
649C4PlayerInfoList &C4PlayerInfoList::operator=(const C4PlayerInfoList &rCpy)
650{
651 Clear();
652 iClientCount = rCpy.iClientCount;
653 iClientCapacity = rCpy.iClientCapacity;
654 iLastPlayerID = rCpy.iLastPlayerID;
655 if (rCpy.ppClients)
656 {
657 ppClients = new C4ClientPlayerInfos *[iClientCapacity];
658 C4ClientPlayerInfos **ppInfo = ppClients, **ppCpy = rCpy.ppClients;
659 int32_t i = iClientCount;
660 while (i--) *ppInfo++ = new C4ClientPlayerInfos(**ppCpy++);
661 }
662 else
663 ppClients = nullptr;
664 return *this;
665}
666
667void C4PlayerInfoList::Clear()
668{
669 // delete client infos
670 C4ClientPlayerInfos **ppInfo = ppClients; int32_t i = iClientCount;
671 while (i--) delete *ppInfo++;
672 // clear client infos
673 delete[] ppClients; ppClients = nullptr;
674 iClientCount = iClientCapacity = 0;
675 // reset player ID counter
676 iLastPlayerID = 0;
677}
678
679void C4PlayerInfoList::GrowList(size_t iByVal)
680{
681 // create new list (out of mem: simply returns here; info list remains in a valid state)
682 C4ClientPlayerInfos **ppNewInfo = new C4ClientPlayerInfos *[iClientCapacity += iByVal];
683 // move existing
684 if (ppClients)
685 {
686 memcpy(dest: ppNewInfo, src: ppClients, n: iClientCount * sizeof(C4ClientPlayerInfos *));
687 }
688 delete[] ppClients;
689 // assign new
690 ppClients = ppNewInfo;
691}
692
693bool C4PlayerInfoList::DoLocalNonNetworkPlayerJoin(const char *szPlayerFile)
694{
695 // construct joining information
696 C4ClientPlayerInfos *pNewJoin = new C4ClientPlayerInfos(szPlayerFile, true);
697 // handle it
698 bool fSuccess = DoLocalNonNetworkPlayerInfoUpdate(pUpdate: pNewJoin);
699 // done
700 delete pNewJoin;
701 return fSuccess;
702}
703
704bool C4PlayerInfoList::DoPlayerInfoUpdate(C4ClientPlayerInfos *pUpdate)
705{
706 // never done by clients or in replay - update will be handled via queue
707 if (!Game.Control.isCtrlHost()) return false;
708 // in network game, process by host. In offline game, just create control
709 bool fSucc = true;
710 if (Game.Control.isNetwork())
711 Game.Network.Players.RequestPlayerInfoUpdate(rRequest: *pUpdate);
712 else
713 fSucc = DoLocalNonNetworkPlayerInfoUpdate(pUpdate);
714 return fSucc;
715}
716
717bool C4PlayerInfoList::DoLocalNonNetworkPlayerInfoUpdate(C4ClientPlayerInfos *pUpdate)
718{
719 // assign IDs first: Must be done early, so AssignTeams works
720 if (!AssignPlayerIDs(pNewClientInfo: pUpdate))
721 {
722 return false;
723 }
724 // set standard teams
725 AssignTeams(pNewClientInfo: pUpdate, fByHost: true);
726 // color/name change by team or savegame assignment
727 UpdatePlayerAttributes(pForInfo: pUpdate, fResolveConflicts: true);
728 // add through queue: This will add directly, do the record and put player joins into the queue
729 // in running mode, this call will also put the actual player joins into the queue
730 Game.Control.DoInput(eCtrlType: CID_PlrInfo, pPkt: new C4ControlPlayerInfo(*pUpdate), eDelivery: Game.IsRunning ? CDT_Queue : CDT_Direct);
731 // done, success
732 return true;
733}
734
735void C4PlayerInfoList::UpdatePlayerAttributes(C4ClientPlayerInfos *pForInfo, bool fResolveConflicts)
736{
737 assert(pForInfo);
738 // update colors of all players of this packet
739 C4PlayerInfo *pInfo, *pInfo2; int32_t i = 0;
740 while (pInfo = pForInfo->GetPlayerInfo(iIndex: i++))
741 if (!pInfo->HasJoined())
742 {
743 // assign savegame colors
744 int32_t idSavegameID; bool fHasForcedColor = false; uint32_t dwForceClr;
745 if (idSavegameID = pInfo->GetAssociatedSavegamePlayerID())
746 if (pInfo2 = Game.RestorePlayerInfos.GetPlayerInfoByID(id: idSavegameID))
747 {
748 dwForceClr = pInfo2->GetColor();
749 fHasForcedColor = true;
750 }
751 // assign team colors
752 if (!fHasForcedColor && Game.Teams.IsTeamColors())
753 {
754 C4Team *pPlrTeam = Game.Teams.GetTeamByID(iID: pInfo->GetTeam());
755 if (pPlrTeam)
756 {
757 dwForceClr = pPlrTeam->GetColor();
758 fHasForcedColor = true;
759 }
760 }
761 // do color change
762 if (fHasForcedColor && (dwForceClr != pInfo->GetColor()))
763 {
764 pInfo->SetColor(dwForceClr);
765 pForInfo->SetUpdated();
766 }
767 }
768 if (fResolveConflicts) ResolvePlayerAttributeConflicts(pSecPacket: pForInfo);
769}
770
771void C4PlayerInfoList::UpdatePlayerAttributes()
772{
773 // update attributes of all packets
774 int32_t iIdx = 0;
775 C4ClientPlayerInfos *pForInfo;
776 while (pForInfo = GetIndexedInfo(iIndex: iIdx++)) UpdatePlayerAttributes(pForInfo, fResolveConflicts: false);
777 // now resole all conflicts
778 ResolvePlayerAttributeConflicts(pSecPacket: nullptr);
779}
780
781bool C4PlayerInfoList::AssignPlayerIDs(C4ClientPlayerInfos *pNewClientInfo)
782{
783 // assign player IDs to those player infos without
784 C4PlayerInfo *pPlrInfo; int32_t i = 0, iJoinsGranted = 0;
785 while (pPlrInfo = pNewClientInfo->GetPlayerInfo(iIndex: i++))
786 if (!pPlrInfo->GetID())
787 {
788 // are there still any player slots free?
789 if (GetFreePlayerSlotCount() - iJoinsGranted < 1)
790 {
791 // nope - then deny this join!
792 Log(id: C4ResStrTableKey::IDS_MSG_TOOMANYPLAYERS, args: static_cast<int>(Game.Parameters.MaxPlayers));
793 pNewClientInfo->RemoveIndexedInfo(iAtIndex: --i);
794 continue;
795 }
796 // Join OK; grant an ID
797 pPlrInfo->SetID(++iLastPlayerID);
798 ++iJoinsGranted;
799 }
800 // return whether any join remains
801 return !!pNewClientInfo->GetPlayerCount();
802}
803
804int32_t C4PlayerInfoList::GetFreePlayerSlotCount()
805{
806 // number of free slots depends on max player setting
807 return std::max<int32_t>(a: Game.Parameters.MaxPlayers - GetStartupCount(), b: 0);
808}
809
810void C4PlayerInfoList::AssignTeams(C4ClientPlayerInfos *pNewClientInfo, bool fByHost)
811{
812 if (!Game.Teams.IsMultiTeams()) return;
813 // assign any unset teams (host/standalone only - fByHost determines whether the packet came from the host)
814 C4PlayerInfo *pPlrInfo; int32_t i = 0;
815 while (pPlrInfo = pNewClientInfo->GetPlayerInfo(iIndex: i++))
816 Game.Teams.RecheckPlayerInfoTeams(rNewJoin&: *pPlrInfo, fByHost);
817}
818
819void C4PlayerInfoList::RecheckAutoGeneratedTeams()
820{
821 // ensure all teams specified in the list exist
822 C4ClientPlayerInfos *pPlrInfos; int32_t j = 0;
823 while (pPlrInfos = GetIndexedInfo(iIndex: j++))
824 {
825 C4PlayerInfo *pPlrInfo; int32_t i = 0;
826 while (pPlrInfo = pPlrInfos->GetPlayerInfo(iIndex: i++))
827 {
828 int32_t idTeam = pPlrInfo->GetTeam();
829 if (idTeam) Game.Teams.GetGenerateTeamByID(iID: idTeam);
830 }
831 }
832}
833
834C4ClientPlayerInfos *C4PlayerInfoList::AddInfo(C4ClientPlayerInfos *pNewClientInfo)
835{
836 assert(pNewClientInfo);
837 // caution: also called for RestorePlayerInfos-list
838 // host: reserve new IDs for all players
839 // client: all IDs should be assigned already by host
840 if (Game.Network.isHost() || !Game.Network.isEnabled())
841 {
842 if (!AssignPlayerIDs(pNewClientInfo) && pNewClientInfo->IsAddPacket())
843 {
844 // no players could be added (probably MaxPlayer)
845 delete pNewClientInfo;
846 return nullptr;
847 }
848 }
849 // ensure all teams specified in the list exist (this should be done for savegame teams as well)
850 C4PlayerInfo *pInfo; int32_t i = 0;
851 while (pInfo = pNewClientInfo->GetPlayerInfo(iIndex: i++))
852 {
853 int32_t idTeam = pInfo->GetTeam();
854 if (idTeam) Game.Teams.GetGenerateTeamByID(iID: idTeam);
855 }
856 // add info for client; overwriting or appending to existing info if necessary
857 // try to find existing data of same client
858 C4ClientPlayerInfos **ppExistingInfo = GetInfoPtrByClientID(iClientID: pNewClientInfo->GetClientID());
859 if (ppExistingInfo)
860 {
861 // info exists: append to it?
862 if (pNewClientInfo->IsAddPacket())
863 {
864 (*ppExistingInfo)->GrabMergeFrom(rFrom&: *pNewClientInfo);
865 // info added: remove unused class
866 delete pNewClientInfo;
867 // assign existing info for further usage in this fn
868 return pNewClientInfo = *ppExistingInfo;
869 }
870 // no add packet: overwrite current info
871 delete *ppExistingInfo;
872 return *ppExistingInfo = pNewClientInfo;
873 }
874 // no existing info: add it directly
875 pNewClientInfo->ResetAdd();
876 // may need to grow list (vector) for that
877 if (iClientCount >= iClientCapacity) GrowList(iByVal: 4);
878 ppClients[iClientCount++] = pNewClientInfo;
879 // done; return actual info
880 return pNewClientInfo;
881}
882
883C4ClientPlayerInfos **C4PlayerInfoList::GetInfoPtrByClientID(int32_t iClientID) const
884{
885 // search list
886 for (int32_t i = 0; i < iClientCount; ++i) if (ppClients[i]->GetClientID() == iClientID) return ppClients + i;
887 // nothing found
888 return nullptr;
889}
890
891int32_t C4PlayerInfoList::GetPlayerCount() const
892{
893 // count players of all clients
894 int32_t iCount = 0;
895 for (int32_t i = 0; i < iClientCount; ++i)
896 iCount += ppClients[i]->GetPlayerCount();
897 // return it
898 return iCount;
899}
900
901int32_t C4PlayerInfoList::GetJoinIssuedPlayerCount() const
902{
903 // count players of all clients
904 int32_t iCount = 0;
905 for (int32_t i = 0; i < iClientCount; ++i)
906 {
907 C4ClientPlayerInfos *pClient = ppClients[i];
908 for (int32_t j = 0; j < pClient->GetPlayerCount(); ++j)
909 if (pClient->GetPlayerInfo(iIndex: j)->HasJoinIssued())
910 ++iCount;
911 }
912 // return it
913 return iCount;
914}
915
916int32_t C4PlayerInfoList::GetActivePlayerCount(bool fCountInvisible) const
917{
918 // count players of all clients
919 int32_t iCount = 0;
920 for (int32_t i = 0; i < iClientCount; ++i)
921 {
922 C4ClientPlayerInfos *pClient = ppClients[i];
923 for (int32_t j = 0; j < pClient->GetPlayerCount(); ++j)
924 {
925 C4PlayerInfo *pInfo = pClient->GetPlayerInfo(iIndex: j);
926 if (!pInfo->IsRemoved())
927 if (fCountInvisible || !pInfo->IsInvisible())
928 ++iCount;
929 }
930 }
931 // return it
932 return iCount;
933}
934
935int32_t C4PlayerInfoList::GetActiveScriptPlayerCount(bool fCountSavegameResumes, bool fCountInvisible) const
936{
937 // count players of all clients
938 int32_t iCount = 0;
939 for (int32_t i = 0; i < iClientCount; ++i)
940 {
941 C4ClientPlayerInfos *pClient = ppClients[i];
942 for (int32_t j = 0; j < pClient->GetPlayerCount(); ++j)
943 {
944 C4PlayerInfo *pNfo = pClient->GetPlayerInfo(iIndex: j);
945 if (!pNfo->IsRemoved())
946 if (pNfo->GetType() == C4PT_Script)
947 if (fCountSavegameResumes || !pNfo->GetAssociatedSavegamePlayerID())
948 if (fCountInvisible || !pNfo->IsInvisible())
949 ++iCount;
950 }
951 }
952 // return it
953 return iCount;
954}
955
956StdStrBuf C4PlayerInfoList::GetActivePlayerNames(bool fCountInvisible, int32_t iAtClientID) const
957{
958 // add up players of all clients
959 StdStrBuf sPlr;
960 int32_t iCount = 0;
961 for (int32_t i = 0; i < iClientCount; ++i)
962 {
963 C4ClientPlayerInfos *pClient = ppClients[i];
964 if (iAtClientID != -1 && pClient->GetClientID() != iAtClientID) continue;
965 for (int32_t j = 0; j < pClient->GetPlayerCount(); ++j)
966 {
967 C4PlayerInfo *pInfo = pClient->GetPlayerInfo(iIndex: j);
968 if (!pInfo->IsRemoved()) if (fCountInvisible || !pInfo->IsInvisible())
969 {
970 if (iCount++)
971 {
972 // not first name: Add separator
973 sPlr.Append(pnData: ", ");
974 }
975 sPlr.Append(pnData: pInfo->GetName());
976 }
977 }
978 }
979 // return it
980 return sPlr;
981}
982
983C4PlayerInfo *C4PlayerInfoList::GetPlayerInfoByIndex(int32_t index) const
984{
985 // check all packets for a player
986 for (int32_t i = 0; i < iClientCount; ++i)
987 {
988 int32_t j = 0; C4PlayerInfo *pInfo;
989 while (pInfo = ppClients[i]->GetPlayerInfo(iIndex: j++))
990 if (index-- <= 0)
991 return pInfo;
992 }
993 // nothing found
994 return nullptr;
995}
996
997C4PlayerInfo *C4PlayerInfoList::GetPlayerInfoByID(int32_t id) const
998{
999 // must be a valid ID
1000 assert(id);
1001 // check all packets for a player
1002 for (int32_t i = 0; i < iClientCount; ++i)
1003 {
1004 int32_t j = 0; C4PlayerInfo *pInfo;
1005 while (pInfo = ppClients[i]->GetPlayerInfo(iIndex: j++))
1006 if (pInfo->GetID() == id) return pInfo;
1007 }
1008 // nothing found
1009 return nullptr;
1010}
1011
1012C4ClientPlayerInfos *C4PlayerInfoList::GetClientInfoByPlayerID(int32_t id) const
1013{
1014 // get client info that contains a specific player
1015 assert(id);
1016 for (int32_t i = 0; i < iClientCount; ++i)
1017 {
1018 int32_t j = 0; C4PlayerInfo *pInfo;
1019 while (pInfo = ppClients[i]->GetPlayerInfo(iIndex: j++))
1020 if (pInfo->GetID() == id) return ppClients[i];
1021 }
1022 // nothing found
1023 return nullptr;
1024}
1025
1026C4PlayerInfo *C4PlayerInfoList::GetPlayerInfoByID(int32_t id, int32_t *pidClient) const
1027{
1028 // must be a valid ID
1029 assert(id); assert(pidClient);
1030 // check all packets for a player
1031 for (int32_t i = 0; i < iClientCount; ++i)
1032 {
1033 int32_t j = 0; C4PlayerInfo *pInfo;
1034 while (pInfo = ppClients[i]->GetPlayerInfo(iIndex: j++))
1035 if (pInfo->GetID() == id)
1036 {
1037 *pidClient = ppClients[i]->GetClientID();
1038 return pInfo;
1039 }
1040 }
1041 // nothing found
1042 return nullptr;
1043}
1044
1045C4PlayerInfo *C4PlayerInfoList::GetPlayerInfoBySavegameID(int32_t id) const
1046{
1047 // must be a valid ID
1048 assert(id);
1049 // check all packets for a player
1050 for (int32_t i = 0; i < iClientCount; ++i)
1051 {
1052 int32_t j = 0; C4PlayerInfo *pInfo;
1053 while (pInfo = ppClients[i]->GetPlayerInfo(iIndex: j++))
1054 if (pInfo->GetAssociatedSavegamePlayerID() == id) return pInfo;
1055 }
1056 // nothing found
1057 return nullptr;
1058}
1059
1060C4PlayerInfo *C4PlayerInfoList::GetNextPlayerInfoByID(int32_t id) const
1061{
1062 // check all packets for players
1063 C4PlayerInfo *pSmallest = nullptr;
1064 for (int32_t i = 0; i < iClientCount; ++i)
1065 {
1066 int32_t j = 0; C4PlayerInfo *pInfo;
1067 while (pInfo = ppClients[i]->GetPlayerInfo(iIndex: j++))
1068 if (pInfo->GetID() > id)
1069 if (!pSmallest || pSmallest->GetID() > pInfo->GetID())
1070 pSmallest = pInfo;
1071 }
1072 // return best found
1073 return pSmallest;
1074}
1075
1076C4PlayerInfo *C4PlayerInfoList::GetActivePlayerInfoByName(const char *szName)
1077{
1078 // check all packets for matching players
1079 for (int32_t i = 0; i < iClientCount; ++i)
1080 {
1081 int32_t j = 0; C4PlayerInfo *pInfo;
1082 while (pInfo = ppClients[i]->GetPlayerInfo(iIndex: j++))
1083 if (!pInfo->IsRemoved())
1084 if (SEqualNoCase(szStr1: szName, szStr2: pInfo->GetName()))
1085 return pInfo;
1086 }
1087 // nothing found
1088 return nullptr;
1089}
1090
1091C4PlayerInfo *C4PlayerInfoList::FindSavegameResumePlayerInfo(const C4PlayerInfo *pMatchInfo, MatchingLevel mlMatchStart, MatchingLevel mlMatchEnd) const
1092{
1093 assert(pMatchInfo);
1094 // try different matching levels using the infamous for-case-paradigm
1095 for (int iMatchLvl = mlMatchStart; iMatchLvl <= mlMatchEnd; ++iMatchLvl)
1096 {
1097 for (int32_t i = 0; i < iClientCount; ++i)
1098 {
1099 int32_t j = 0; C4PlayerInfo *pInfo;
1100 while (pInfo = ppClients[i]->GetPlayerInfo(iIndex: j++))
1101 if (!Game.PlayerInfos.GetPlayerInfoByID(id: pInfo->GetID()) && !Game.PlayerInfos.GetPlayerInfoBySavegameID(id: pInfo->GetID())) // only unassigned player infos
1102 switch (iMatchLvl)
1103 {
1104 case PML_PlrFileName: // file name and player name must match
1105 if (!pMatchInfo->GetFilename() || !pInfo->GetFilename()) break;
1106 if (!SEqualNoCase(szStr1: GetFilename(path: pMatchInfo->GetFilename()), szStr2: GetFilename(path: pInfo->GetFilename()))) break;
1107 // nobreak: Check player name as well
1108 case PML_PlrName: // match player name
1109 if (SEqualNoCase(szStr1: pMatchInfo->GetName(), szStr2: pInfo->GetName()))
1110 return pInfo;
1111 break;
1112 case PML_PrefColor: // match player color
1113 if (pMatchInfo->GetOriginalColor() == pInfo->GetOriginalColor())
1114 return pInfo;
1115 break;
1116 case PML_Any: // match anything
1117 return pInfo;
1118 }
1119 }
1120 }
1121 // no match
1122 return nullptr;
1123}
1124
1125C4PlayerInfo *C4PlayerInfoList::FindUnassociatedRestoreInfo(const C4PlayerInfoList &rRestoreInfoList)
1126{
1127 // search given list for a player that's not associated locally
1128 C4ClientPlayerInfos *pRestoreClient; int32_t iClient = 0;
1129 while (pRestoreClient = rRestoreInfoList.GetIndexedInfo(iIndex: iClient++))
1130 {
1131 C4PlayerInfo *pRestoreInfo; int32_t iInfo = 0;
1132 while (pRestoreInfo = pRestoreClient->GetPlayerInfo(iIndex: iInfo++))
1133 if (pRestoreInfo->IsJoined())
1134 // match association either by savegame ID (before C4Game::InitPlayers) or real ID (after C4Game::InitPlayers)
1135 if (!GetPlayerInfoBySavegameID(id: pRestoreInfo->GetID()) && !GetPlayerInfoByID(id: pRestoreInfo->GetID()))
1136 return pRestoreInfo;
1137 }
1138 // no unassociated info found
1139 return nullptr;
1140}
1141
1142bool C4PlayerInfoList::HasSameTeamPlayers(int32_t iClient1, int32_t iClient2) const
1143{
1144 // compare all player teams of clients
1145 const C4ClientPlayerInfos *pCnfo1 = GetInfoByClientID(iClientID: iClient1);
1146 const C4ClientPlayerInfos *pCnfo2 = GetInfoByClientID(iClientID: iClient2);
1147 if (!pCnfo1 || !pCnfo2) return false;
1148 int32_t i = 0, j; const C4PlayerInfo *pNfo1, *pNfo2;
1149 while (pNfo1 = pCnfo1->GetPlayerInfo(iIndex: i++))
1150 {
1151 if (!pNfo1->IsUsingTeam()) continue;
1152 j = 0;
1153 while (pNfo2 = pCnfo2->GetPlayerInfo(iIndex: j++))
1154 {
1155 if (!pNfo2->IsUsingTeam()) continue;
1156 if (pNfo2->GetTeam() == pNfo1->GetTeam())
1157 // match found!
1158 return true;
1159 }
1160 }
1161 // no match
1162 return false;
1163}
1164
1165bool C4PlayerInfoList::Load(C4Group &hGroup, const char *szFromFile, C4LangStringTable *pLang)
1166{
1167 // clear previous
1168 Clear();
1169 // load file contents
1170 StdStrBuf Buf;
1171 if (!hGroup.LoadEntryString(szEntryName: szFromFile, Buf))
1172 // no file is OK; means no player infos
1173 return true;
1174 // replace strings
1175 if (pLang) pLang->ReplaceStrings(rBuf&: Buf);
1176 // (try to) compile
1177 if (!CompileFromBuf_LogWarn<StdCompilerINIRead>(
1178 TargetStruct: mkNamingAdapt(rValue&: *this, szName: "PlayerInfoList"),
1179 SrcBuf: Buf, szName: szFromFile))
1180 return false;
1181 // done, success
1182 return true;
1183}
1184
1185bool C4PlayerInfoList::Save(C4Group &hGroup, const char *szToFile)
1186{
1187 // remove previous entry from group
1188 hGroup.DeleteEntry(szFilename: szToFile);
1189 // anything to save?
1190 if (!iClientCount) return true;
1191 // save it
1192 try
1193 {
1194 // decompile
1195 const std::string buf{DecompileToBuf<StdCompilerINIWrite>(
1196 SrcStruct: mkNamingAdapt(rValue&: *this, szName: "PlayerInfoList"))};
1197 // save buffer to group
1198 StdStrBuf copy{buf.c_str(), buf.size()};
1199 hGroup.Add(szName: szToFile, pBuffer&: copy, fChild: false, fHoldBuffer: true);
1200 }
1201 catch (const StdCompiler::Exception &)
1202 {
1203 return false;
1204 }
1205 // done, success
1206 return true;
1207}
1208
1209bool C4PlayerInfoList::LoadFromGameText(const char *pSource)
1210{
1211 C4ClientPlayerInfos *pkPlrInfo = nullptr;
1212 // copied stuff from C4Game::LoadPlayerFilenames...
1213 // hacking some data out of the game text. Luckily, the format is different nowadays.
1214 const char *szPos;
1215 char szLinebuf[30 + _MAX_PATH + 1];
1216 if (szPos = SSearch(szString: pSource, szIndex: "[PlayerFiles]"))
1217 while (true)
1218 {
1219 szPos = SAdvanceSpace(szSPos: szPos);
1220 SCopyUntil(szSource: szPos, sTarget: szLinebuf, cUntil: 0x0D, iMaxL: 30 + _MAX_PATH);
1221 szPos += SLen(sptr: szLinebuf);
1222 if (SEqual2(szStr1: szLinebuf, szStr2: "Player") && (SCharPos(cTarget: '=', szInStr: szLinebuf) > -1))
1223 {
1224 const char *szPlayerFilename = szLinebuf + SCharPos(cTarget: '=', szInStr: szLinebuf) + 1;
1225 // szPlayerFilename points to the filename of a player
1226 C4PlayerInfo *pNewInfo = new C4PlayerInfo();
1227 // compose filename within scenario file
1228 char szPlrInSzenName[_MAX_PATH * 2];
1229 SCopy(szSource: Game.ScenarioFilename, sTarget: szPlrInSzenName);
1230 AppendBackslash(szFileName: szPlrInSzenName);
1231 SAppend(szSource: szPlayerFilename, szTarget: szPlrInSzenName);
1232 // load info from there
1233 if (!pNewInfo->LoadFromLocalFile(szFilename: szPlrInSzenName))
1234 {
1235 Log(id: C4ResStrTableKey::IDS_ERR_LOAD_PLRINFO, args&: szPlayerFilename);
1236 delete pNewInfo;
1237 }
1238 else
1239 {
1240 // loading success
1241 // ensure holding packet is generated
1242 if (!pkPlrInfo) pkPlrInfo = new C4ClientPlayerInfos();
1243 // determine player number
1244 int iJoinedNumber = C4P_Number_None;
1245 // store "Player1"
1246 StdStrBuf sPlrIndex; sPlrIndex.Copy(pnData: szLinebuf, iChars: SCharPos(cTarget: '=', szInStr: szLinebuf));
1247 // search for section "[Player1]" in game text
1248 const std::string plrSect{std::format(fmt: "[{}]", args: sPlrIndex.getData())};
1249 const char *szPlrSect = SSearch(szString: pSource, szIndex: plrSect.c_str());
1250 // get "Index=%d" from that section
1251 if (szPlrSect && (szPlrSect = SSearch(szString: szPlrSect, szIndex: "Index="))) sscanf(s: szPlrSect, format: "%d", &iJoinedNumber);
1252 // this info is already joined
1253 pNewInfo->SetJoined(iJoinedNumber);
1254 // add loaded info to list
1255 pkPlrInfo->AddInfo(pAddInfo: pNewInfo);
1256 }
1257 }
1258 else
1259 break;
1260 }
1261 // anything loaded?
1262 if (!pkPlrInfo) return false;
1263 int32_t iJoinedPlrCount = pkPlrInfo->GetPlayerCount();
1264 if (!iJoinedPlrCount) { delete pkPlrInfo; return false; }
1265 // Workaround: Because MaxPlayers may not be loaded yet, make sure it's large enough to allow all currently joined players
1266 if (Game.Parameters.MaxPlayers < iJoinedPlrCount) Game.Parameters.MaxPlayers = iJoinedPlrCount;
1267 // add it
1268 AddInfo(pNewClientInfo: pkPlrInfo);
1269 // success
1270 return true;
1271}
1272
1273void C4PlayerInfoList::InitLocal()
1274{
1275 // not in replay
1276 if (Game.C4S.Head.Replay) return;
1277 // no double init
1278 assert(!GetInfoCount());
1279 // no network
1280 assert(!Game.Network.isEnabled());
1281 // create player info for local player joins
1282 C4ClientPlayerInfos *pLocalInfo = new C4ClientPlayerInfos(Game.PlayerFilenames);
1283 // register local info immediately
1284 pLocalInfo = AddInfo(pNewClientInfo: pLocalInfo);
1285 // Script players in restore infos need to be associated with matching script players in main info list
1286 CreateRestoreInfosForJoinedScriptPlayers(rSavegamePlayers&: Game.RestorePlayerInfos);
1287 // and assign teams
1288 if (Game.Teams.IsMultiTeams() && pLocalInfo)
1289 AssignTeams(pNewClientInfo: pLocalInfo, fByHost: true);
1290}
1291
1292bool C4PlayerInfoList::LocalJoinUnjoinedPlayersInQueue()
1293{
1294 // local call only - in network, C4Network2Players joins players!
1295 assert(!Game.Network.isEnabled());
1296 // get local players
1297 C4ClientPlayerInfos **ppkLocal = GetInfoPtrByClientID(iClientID: Game.Control.ClientID()), *pkLocal;
1298 if (!ppkLocal) return false;
1299 pkLocal = *ppkLocal;
1300 // check all players
1301 int32_t i = 0; C4PlayerInfo *pInfo;
1302 while (pInfo = pkLocal->GetPlayerInfo(iIndex: i++))
1303 // not yet joined?
1304 if (!pInfo->HasJoinIssued())
1305 {
1306 // join will be marked when queue is executed (C4Player::Join)
1307 // but better mark join now already to prevent permanent sending overkill
1308 pInfo->SetJoinIssued();
1309 // join by filename if possible. Script players may not have a filename assigned though
1310 const char *szFilename = pInfo->GetFilename();
1311 if (!szFilename && (pInfo->GetType() != C4PT_Script))
1312 {
1313 // failure for user players
1314 const char *szPlrName = pInfo->GetName(); if (!szPlrName) szPlrName = "???";
1315 Log(id: C4ResStrTableKey::IDS_ERR_JOINQUEUEPLRS, args&: szPlrName);
1316 continue;
1317 }
1318 Game.Input.Add(eType: CID_JoinPlr,
1319 pCtrl: new C4ControlJoinPlayer(szFilename, Game.Control.ClientID(), pInfo->GetID()));
1320 }
1321 // done, success
1322 return true;
1323}
1324
1325void C4PlayerInfoList::CreateRestoreInfosForJoinedScriptPlayers(C4PlayerInfoList &rSavegamePlayers)
1326{
1327 // create matching script player joins for all script playeers in restore info
1328 // Just copy their infos to the first client
1329 int32_t i;
1330 C4ClientPlayerInfos *pHostInfo = GetIndexedInfo(iIndex: 0);
1331 for (i = 0; i < rSavegamePlayers.GetInfoCount(); ++i)
1332 {
1333 C4ClientPlayerInfos *pkInfo = rSavegamePlayers.GetIndexedInfo(iIndex: i);
1334 int32_t j = 0; C4PlayerInfo *pInfo;
1335 while (pInfo = pkInfo->GetPlayerInfo(iIndex: j++))
1336 if (pInfo->GetType() == C4PT_Script)
1337 {
1338 // safety
1339 C4PlayerInfo *pRejoinInfo;
1340 if (pRejoinInfo = GetPlayerInfoBySavegameID(id: pInfo->GetID()))
1341 {
1342 LogNTr(level: spdlog::level::warn, fmt: "User player {} takes over script player {}!", args: pRejoinInfo->GetName(), args: pInfo->GetName());
1343 continue;
1344 }
1345 if (!pHostInfo)
1346 {
1347 LogNTr(level: spdlog::level::err, message: "Error restoring savegame script players: No host player infos to add to!");
1348 continue;
1349 }
1350 // generate takeover info
1351 pRejoinInfo = new C4PlayerInfo(*pInfo);
1352 pRejoinInfo->SetAssociatedSavegamePlayer(pInfo->GetID());
1353 pHostInfo->AddInfo(pAddInfo: pRejoinInfo);
1354 }
1355 }
1356 // teams must recognize the change
1357 Game.Teams.RecheckPlayers();
1358}
1359
1360bool C4PlayerInfoList::RestoreSavegameInfos(C4PlayerInfoList &rSavegamePlayers)
1361{
1362 // any un-associated players?
1363 if (rSavegamePlayers.GetPlayerCount())
1364 {
1365 // for runtime network joins, this should never happen!
1366 assert(!Game.C4S.Head.NetworkRuntimeJoin);
1367
1368 // do savegame player association of real players
1369 // for non-lobby games do automatic association first
1370 int32_t iNumGrabbed = 0, i;
1371 if (!Game.Network.isEnabled() && Game.C4S.Head.SaveGame)
1372 {
1373 // do several passes: First passes using regular player matching; following passes matching anything but with a warning message
1374 for (int eMatchingLevel = 0; eMatchingLevel <= PML_Any; ++eMatchingLevel)
1375 for (int32_t i = 0; i < iClientCount; ++i)
1376 {
1377 C4ClientPlayerInfos *pkInfo = GetIndexedInfo(iIndex: i);
1378 int32_t j = 0, id; C4PlayerInfo *pInfo, *pSavegameInfo;
1379 while (pInfo = pkInfo->GetPlayerInfo(iIndex: j++))
1380 if (!(id = pInfo->GetAssociatedSavegamePlayerID()))
1381 if (pSavegameInfo = rSavegamePlayers.FindSavegameResumePlayerInfo(pMatchInfo: pInfo, mlMatchStart: static_cast<MatchingLevel>(eMatchingLevel), mlMatchEnd: static_cast<MatchingLevel>(eMatchingLevel)))
1382 {
1383 pInfo->SetAssociatedSavegamePlayer(pSavegameInfo->GetID());
1384 if (eMatchingLevel > PML_PlrName)
1385 {
1386 // this is a "wild" match: Warn the player (but not in replays)
1387 const std::string msg{LoadResStr(id: C4ResStrTableKey::IDS_MSG_PLAYERASSIGNMENT, args: pInfo->GetName(), args: pSavegameInfo->GetName())};
1388 LogNTr(message: msg);
1389 if (Game.pGUI && FullScreen.Active && !Game.C4S.Head.Replay)
1390 Game.pGUI->ShowMessageModal(szMessage: msg.c_str(), szCaption: LoadResStr(id: C4ResStrTableKey::IDS_MSG_FREESAVEGAMEPLRS), dwButtons: C4GUI::MessageDialog::btnOK, icoIcon: C4GUI::Ico_Notify, pbConfigDontShowAgainSetting: &Config.Startup.HideMsgPlrTakeOver);
1391 }
1392 }
1393 }
1394 }
1395 // association complete: evaluate it
1396 for (i = 0; i < iClientCount; ++i)
1397 {
1398 C4ClientPlayerInfos *pkInfo = GetIndexedInfo(iIndex: i);
1399 int32_t j = 0, id; C4PlayerInfo *pInfo, *pSavegameInfo;
1400 while (pInfo = pkInfo->GetPlayerInfo(iIndex: j++))
1401 if (id = pInfo->GetAssociatedSavegamePlayerID())
1402 {
1403 if (pSavegameInfo = rSavegamePlayers.GetPlayerInfoByID(id))
1404 {
1405 // pInfo continues for pSavegameInfo
1406 pInfo->SetSavegameResume(pSavegameInfo);
1407 ++iNumGrabbed;
1408 }
1409 else
1410 {
1411 // shouldn't happen
1412 assert(!"Invalid savegame association");
1413 }
1414 }
1415 else
1416 {
1417 // no association for this info: Joins as new player
1418 // in savegames, this is unusual. For regular script player restore, it's not
1419 if (Game.C4S.Head.SaveGame) Log(id: C4ResStrTableKey::IDS_PRC_RESUMENOPLRASSOCIATION, args: pInfo->GetName());
1420 }
1421 }
1422 // otherwise any remaining players
1423 int32_t iCountRemaining = rSavegamePlayers.GetPlayerCount() - iNumGrabbed;
1424 if (iCountRemaining)
1425 {
1426 // in replay mode, if there are no regular player joins, it must have been a runtime record
1427 // i.e., a record that was started during the game
1428 // in this case, the savegame player infos equal the real player infos to be used
1429 if (Game.Control.isReplay() && !GetInfoCount())
1430 {
1431 *this = rSavegamePlayers;
1432 }
1433 else
1434 {
1435 // in regular mode, these players must be removed
1436 Log(id: C4ResStrTableKey::IDS_PRC_RESUMEREMOVEPLRS, args&: iCountRemaining);
1437 // remove them directly from the game
1438 RemoveUnassociatedPlayers(rSavegamePlayers);
1439 }
1440 }
1441 }
1442 // now that players are restored, restore teams
1443 Game.Teams.RecheckPlayers();
1444 // done, success
1445 return true;
1446}
1447
1448bool C4PlayerInfoList::RecreatePlayerFiles()
1449{
1450 // Note that this method will be called on the main list for savegame resumes (even in network) or regular games with RecreateInfos,
1451 // and on RestorePlayerInfos for runtime network joins
1452 // check all player files that need to be recreated
1453 for (int32_t i = 0; i < iClientCount; ++i)
1454 {
1455 C4ClientPlayerInfos *pkInfo = ppClients[i];
1456 int32_t j = 0; C4PlayerInfo *pInfo;
1457 while (pInfo = pkInfo->GetPlayerInfo(iIndex: j++))
1458 if (pInfo->IsJoined())
1459 {
1460 // all players in replays and runtime joins; script players even in savegames need to be restored from the scenario goup
1461 if (Game.C4S.Head.Replay || Game.C4S.Head.NetworkRuntimeJoin || pInfo->GetType() == C4PT_Script)
1462 {
1463 // in this case, a filename must have been assigned while saving
1464 // and mark a file inside the scenario file
1465 // get filename of joined player - this should always be valid!
1466 const char *szCurrPlrFile;
1467 std::string filenameInRecord;
1468 if (Game.C4S.Head.Replay)
1469 {
1470 // replay of resumed savegame: RecreatePlayers saves used player files into the record group in this manner
1471 filenameInRecord = std::format(fmt: "Recreate-{}.c4p", args: pInfo->GetID());
1472 szCurrPlrFile = filenameInRecord.c_str();
1473 }
1474 else
1475 szCurrPlrFile = pInfo->GetFilename();
1476 const char *szPlrName = pInfo->GetName(); if (!szPlrName) szPlrName = "???";
1477 if (!szCurrPlrFile || !*szCurrPlrFile)
1478 {
1479 // that's okay for script players, because those may join w/o recreation files
1480 if (pInfo->GetType() != C4PT_Script)
1481 {
1482 Log(id: C4ResStrTableKey::IDS_ERR_LOAD_RECR_NOFILE, args&: szPlrName);
1483 }
1484 continue;
1485 }
1486 // join from temp file
1487 StdStrBuf szJoinPath;
1488 szJoinPath = Config.AtTempPath(szFilename: GetFilename(path: szCurrPlrFile));
1489 // extract player there
1490 if (!Game.ScenarioFile.FindEntry(szWildCard: GetFilename(path: szCurrPlrFile)) || !Game.ScenarioFile.Extract(szFiles: GetFilename(path: szCurrPlrFile), szExtractTo: szJoinPath.getData()))
1491 {
1492 // that's okay for script players, because those may join w/o recreation files
1493 if (pInfo->GetType() != C4PT_Script)
1494 {
1495 Log(id: C4ResStrTableKey::IDS_ERR_LOAD_RECR_NOEXTRACT, args&: szPlrName, args: GetFilename(path: szCurrPlrFile));
1496 }
1497 continue;
1498 }
1499 // set join source
1500 pInfo->SetFilename(szJoinPath.getData());
1501 pInfo->DiscardResource();
1502 // setting a temp file here will cause the player file to be deleted directly after recreation
1503 // if recreation fails (e.g. the game gets aborted due to invalid files), the info dtor will delete the file
1504 pInfo->SetTempFile();
1505 }
1506 else
1507 {
1508 // regular player in savegame being resumed in network or normal mode:
1509 // the filenames and/or ressources should have been assigned
1510 // a) either in lobby mode during player re-acquisition
1511 // b) or when players from rSavegamePlayers were taken over
1512 }
1513 }
1514 else if (!pInfo->HasJoinIssued())
1515 {
1516 // new players to be joined into the game:
1517 // regular control queue join can be done; no special handling needed
1518 }
1519 }
1520 // done, success
1521 return true;
1522}
1523
1524bool C4PlayerInfoList::RecreatePlayers()
1525{
1526 // check all player infos
1527 for (int32_t i = 0; i < iClientCount; ++i)
1528 {
1529 C4ClientPlayerInfos *pkInfo = ppClients[i];
1530 // skip clients without joined players
1531 if (!pkInfo->GetJoinedPlayerCount()) continue;
1532 // determine client ID and name
1533 // client IDs must be set correctly even in replays,
1534 // so client-removal packets are executed correctly
1535 int32_t idAtClient = pkInfo->GetClientID();
1536 const char *szAtClientName;
1537 if (Game.C4S.Head.Replay)
1538 // the client name can currently not really be retrieved in replays
1539 // but it's not used anyway
1540 szAtClientName = "Replay";
1541 else
1542 // local non-network non-replay games set local name
1543 if (!Game.Network.isEnabled())
1544 {
1545 assert(idAtClient == Game.Control.ClientID());
1546 szAtClientName = "Local";
1547 }
1548 else
1549 {
1550 // network non-replay games: find client and set name by it
1551 const C4Client *pClient = Game.Clients.getClientByID(iID: idAtClient);
1552 if (pClient)
1553 szAtClientName = pClient->getName();
1554 else
1555 {
1556 // this shouldn't happen - remove the player info
1557 Log(id: C4ResStrTableKey::IDS_PRC_RESUMENOCLIENT, args&: idAtClient, args: pkInfo->GetPlayerCount());
1558 continue;
1559 }
1560 }
1561 // rejoin all joined players of that client
1562 int32_t j = 0; C4PlayerInfo *pInfo;
1563 while (pInfo = pkInfo->GetPlayerInfo(iIndex: j++))
1564 if (pInfo->IsJoined())
1565 {
1566 // get filename to join from
1567 const char *szFilename = pInfo->GetLocalJoinFilename();
1568 // ensure ressource is loaded, if joining from ressource
1569 // this may display a waiting dialog and block the thread for a while
1570 C4Network2Res *pJoinRes = pInfo->GetRes();
1571 if (szFilename && pJoinRes && pJoinRes->isLoading())
1572 {
1573 const char *szName = pInfo->GetName();
1574 if (!Game.Network.RetrieveRes(Core: pJoinRes->getCore(), iTimeout: C4NetResRetrieveTimeout,
1575 szResName: LoadResStr(id: C4ResStrTableKey::IDS_NET_RES_PLRFILE, args&: szName).c_str()))
1576 szFilename = nullptr;
1577 }
1578 // file present?
1579 if (!szFilename || !*szFilename)
1580 {
1581 if (pInfo->GetType() == C4PT_User)
1582 {
1583 // for user players, this could happen only if the user cancelled the ressource
1584 const char *szPlrName = pInfo->GetName(); if (!szPlrName) szPlrName = "???";
1585 Log(id: C4ResStrTableKey::IDS_ERR_LOAD_RECR_NOFILEFROMNET, args&: szPlrName);
1586 continue;
1587 }
1588 else
1589 {
1590 // for script players: Recreation without filename OK
1591 szFilename = nullptr;
1592 }
1593 }
1594 // record file handling: Save to the record file in the manner it's expected by C4PlayerInfoList::RecreatePlayers
1595 if (Game.Control.isRecord() && szFilename)
1596 {
1597 const std::string filenameInRecord{std::format(fmt: "Recreate-{}.c4p", args: pInfo->GetID())};
1598 Game.Control.RecAddFile(szLocalFilename: szFilename, szAddAs: filenameInRecord.c_str());
1599 }
1600 // recreate join directly
1601 Game.Players.Join(szFilename, fScenarioInit: false, iAtClient: idAtClient, szAtClientName, pInfo);
1602 // delete temporary files immediately
1603 if (pInfo->IsTempFile()) pInfo->DeleteTempFile();
1604 }
1605 }
1606 // done!
1607 return true;
1608}
1609
1610bool C4PlayerInfoList::RemoveUnassociatedPlayers(C4PlayerInfoList &rSavegamePlayers)
1611{
1612 // check all joined infos
1613 C4ClientPlayerInfos *pClient; int iClient = 0;
1614 bool fSuccess = true;
1615 while (pClient = rSavegamePlayers.GetIndexedInfo(iIndex: iClient++))
1616 {
1617 C4PlayerInfo *pInfo; int iInfo = 0;
1618 while (pInfo = pClient->GetPlayerInfo(iIndex: iInfo++))
1619 {
1620 // remove players that were in the game but are not associated
1621 if (pInfo->IsJoined() && !GetPlayerInfoBySavegameID(id: pInfo->GetID()))
1622 {
1623 if (Game.Players.RemoveUnjoined(iPlayer: pInfo->GetInGameNumber()))
1624 {
1625 Log(id: C4ResStrTableKey::IDS_PRC_REMOVEPLR, args: pInfo->GetName());
1626 }
1627 else
1628 fSuccess = false;
1629 }
1630 pInfo->SetRemoved();
1631 }
1632 }
1633 return true;
1634}
1635
1636bool C4PlayerInfoList::SetAsRestoreInfos(C4PlayerInfoList &rFromPlayers, bool fSaveUserPlrs, bool fSaveScriptPlrs, bool fSetUserPlrRefToLocalGroup, bool fSetScriptPlrRefToLocalGroup)
1637{
1638 // copy everything
1639 *this = rFromPlayers;
1640 // then remove everything that's no longer joined and update the rest
1641 C4ClientPlayerInfos *pClient; int iClient = 0;
1642 while (pClient = GetIndexedInfo(iIndex: iClient++))
1643 {
1644 // update all players for this client
1645 C4PlayerInfo *pInfo; int iInfo = 0;
1646 while (pInfo = pClient->GetPlayerInfo(iIndex: iInfo++))
1647 {
1648 bool fKeepInfo = false;
1649 // remove players that are not in the game
1650 if (pInfo->IsJoined())
1651 {
1652 // pre-reset filename
1653 pInfo->SetFilename(nullptr);
1654 if (pInfo->GetType() == C4PT_User)
1655 {
1656 fKeepInfo = fSaveUserPlrs;
1657 if (fSetUserPlrRefToLocalGroup)
1658 {
1659 // in the game: Set filename for inside savegame file
1660 std::string newName;
1661 if (Game.Network.isEnabled())
1662 {
1663 C4Client *pGameClient = Game.Clients.getClientByID(iID: pClient->GetClientID());
1664 const char *szName = pGameClient ? pGameClient->getName() : "Unknown";
1665 newName = std::format(fmt: "{}-{}", args&: szName, args: GetFilename(path: pInfo->GetLocalJoinFilename()));
1666 }
1667 else
1668 newName = GetFilename(path: pInfo->GetFilename());
1669
1670 // O(n) is fast.
1671 // If not, blame whoever wrote Replace! ;)
1672 newName = ReplaceInString(subject&: newName, needle: "%", value: "%25");
1673 for (int ch = 128; ch < 256; ++ch)
1674 {
1675 const char *hexChars = "0123456789abcdef";
1676 char old[] = { static_cast<char>(ch), 0 };
1677 char safe[] = { '%', 'x', 'x', 0 };
1678 safe[1] = hexChars[ch / 16];
1679 safe[2] = hexChars[ch % 16];
1680 newName = ReplaceInString(subject&: newName, needle: old, value: safe);
1681 }
1682
1683 pInfo->SetFilename(newName.c_str());
1684 }
1685 }
1686 else if (pInfo->GetType() == C4PT_Script)
1687 {
1688 fKeepInfo = fSaveScriptPlrs;
1689 if (fSetScriptPlrRefToLocalGroup)
1690 {
1691 // just compose a unique filename for script player
1692 pInfo->SetFilename(std::format(fmt: "ScriptPlr-{}.c4p", args: pInfo->GetID()).c_str());
1693 }
1694 }
1695 }
1696 if (!fKeepInfo)
1697 {
1698 pClient->RemoveIndexedInfo(iAtIndex: --iInfo);
1699 }
1700 else
1701 {
1702 pInfo->DiscardResource();
1703 }
1704 }
1705 // remove empty clients
1706 if (!pClient->GetPlayerCount())
1707 {
1708 RemoveInfo(ppRemoveInfo: GetInfoPtrByClientID(iClientID: pClient->GetClientID()));
1709 delete pClient;
1710 --iClient;
1711 }
1712 }
1713 // done
1714 return true;
1715}
1716
1717void C4PlayerInfoList::ResetLeagueProjectedGain(bool fSetUpdated)
1718{
1719 C4ClientPlayerInfos *pClient; int iClient = 0;
1720 while (pClient = GetIndexedInfo(iIndex: iClient++))
1721 {
1722 C4PlayerInfo *pInfo; int iInfo = 0;
1723 while (pInfo = pClient->GetPlayerInfo(iIndex: iInfo++))
1724 if (pInfo->IsLeagueProjectedGainValid())
1725 {
1726 pInfo->ResetLeagueProjectedGain();
1727 if (fSetUpdated)
1728 pClient->SetUpdated();
1729 }
1730 }
1731}
1732
1733void C4PlayerInfoList::CompileFunc(StdCompiler *pComp)
1734{
1735 bool fCompiler = pComp->isCompiler();
1736 if (fCompiler) Clear();
1737 // skip compiling if there is nothing to compile (cosmentics)
1738 if (!fCompiler && pComp->hasNaming() && iLastPlayerID == 0 && iClientCount == 0)
1739 return;
1740 // header
1741 pComp->Value(rStruct: mkNamingAdapt(rValue&: iLastPlayerID, szName: "LastPlayerID", rDefault: 0));
1742 // client count
1743 int32_t iTemp = iClientCount;
1744 pComp->Value(rStruct: mkNamingCountAdapt<int32_t>(iCount&: iTemp, szName: "Client"));
1745 if (iTemp < 0 || iTemp > C4MaxClient)
1746 {
1747 pComp->excCorrupt(message: "client count out of range"); return;
1748 }
1749 // grow list
1750 if (fCompiler)
1751 {
1752 if (iTemp > iClientCapacity) GrowList(iByVal: iTemp - iClientCapacity);
1753 iClientCount = iTemp;
1754 std::fill_n(first: ppClients, n: iClientCount, value: nullptr);
1755 }
1756 // client packets
1757 pComp->Value(
1758 rStruct: mkNamingAdapt(
1759 rValue: mkArrayAdaptMapS(array: ppClients, size: iClientCount, map: mkPtrAdaptNoNull<C4ClientPlayerInfos>),
1760 szName: "Client"));
1761 // force compiler to specialize
1762 mkPtrAdaptNoNull<C4ClientPlayerInfos>(rpObj&: *ppClients);
1763}
1764
1765int32_t C4PlayerInfoList::GetStartupCount()
1766{
1767 // count all joined and to-be-joined
1768 int32_t iCnt = 0;
1769 for (int32_t i = 0; i < iClientCount; ++i)
1770 {
1771 int32_t j = 0; C4PlayerInfo *pInfo;
1772 while (pInfo = ppClients[i]->GetPlayerInfo(iIndex: j++))
1773 if (!pInfo->IsRemoved()) ++iCnt;
1774 }
1775 return iCnt;
1776}
1777
1778void C4PlayerInfoList::LoadResources()
1779{
1780 // load for all players
1781 int32_t i = iClientCount; C4ClientPlayerInfos **ppClient = ppClients;
1782 while (i--)(*ppClient++)->LoadResources();
1783}
1784
1785void C4PlayerInfoList::FixIDCounter()
1786{
1787 // make sure ID counter is same as largest info
1788 for (int32_t i = 0; i < iClientCount; ++i)
1789 {
1790 int32_t j = 0; C4PlayerInfo *pInfo;
1791 while (pInfo = ppClients[i]->GetPlayerInfo(iIndex: j++))
1792 {
1793 iLastPlayerID = std::max<int32_t>(a: pInfo->GetID(), b: iLastPlayerID);
1794 }
1795 }
1796}
1797
1798// Player info packets
1799
1800void C4PacketPlayerInfoUpdRequest::CompileFunc(StdCompiler *pComp)
1801{
1802 pComp->Value(rStruct&: Info);
1803}
1804