| 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 | |
| 35 | void 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 | |
| 57 | void 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 | |
| 70 | bool 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 | |
| 109 | bool C4PlayerInfo::SetAsScriptPlayer(const char *szName, uint32_t dwColor, uint32_t dwFlags, C4ID ) |
| 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 | |
| 124 | const 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 | |
| 132 | uint32_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 | |
| 145 | StdStrBuf 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 | |
| 167 | bool 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 | |
| 177 | void 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 | |
| 270 | void C4PlayerInfo::SetFilename(const char *szToFilename) |
| 271 | { |
| 272 | szFilename = szToFilename; |
| 273 | } |
| 274 | |
| 275 | void 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 | |
| 295 | void 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 | |
| 308 | bool 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 | |
| 319 | void 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 | |
| 327 | void 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 | |
| 335 | bool 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 | |
| 357 | C4ClientPlayerInfos::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 | |
| 399 | C4ClientPlayerInfos::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 | |
| 421 | C4ClientPlayerInfos &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 | |
| 445 | void 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 | |
| 458 | void 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 | |
| 484 | void 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 | |
| 492 | void 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 | |
| 502 | void 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 | |
| 519 | void 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 | |
| 533 | C4PlayerInfo *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 | |
| 541 | C4PlayerInfo *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 | |
| 555 | C4PlayerInfo *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 | |
| 568 | C4PlayerInfo *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 | |
| 584 | bool 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 | |
| 593 | int32_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 | |
| 601 | void 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 | |
| 635 | void 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 | |
| 644 | C4PlayerInfoList::C4PlayerInfoList() : iClientCount(0), iClientCapacity(0), ppClients(nullptr), iLastPlayerID(0) |
| 645 | { |
| 646 | // no need to alloc mem yet |
| 647 | } |
| 648 | |
| 649 | C4PlayerInfoList &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 | |
| 667 | void 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 | |
| 679 | void 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 | |
| 693 | bool 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 | |
| 704 | bool 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 | |
| 717 | bool 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 | |
| 735 | void 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 | |
| 771 | void 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 | |
| 781 | bool 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 | |
| 804 | int32_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 | |
| 810 | void 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 | |
| 819 | void 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 | |
| 834 | C4ClientPlayerInfos *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 | |
| 883 | C4ClientPlayerInfos **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 | |
| 891 | int32_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 | |
| 901 | int32_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 | |
| 916 | int32_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 | |
| 935 | int32_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 | |
| 956 | StdStrBuf 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 | |
| 983 | C4PlayerInfo *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 | |
| 997 | C4PlayerInfo *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 | |
| 1012 | C4ClientPlayerInfos *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 | |
| 1026 | C4PlayerInfo *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 | |
| 1045 | C4PlayerInfo *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 | |
| 1060 | C4PlayerInfo *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 | |
| 1076 | C4PlayerInfo *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 | |
| 1091 | C4PlayerInfo *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 | |
| 1125 | C4PlayerInfo *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 | |
| 1142 | bool 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 | |
| 1165 | bool 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 | |
| 1185 | bool 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 | |
| 1209 | bool 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 | |
| 1273 | void 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 | |
| 1292 | bool 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 | |
| 1325 | void 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 | |
| 1360 | bool 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 | |
| 1448 | bool 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 | |
| 1524 | bool 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 | |
| 1610 | bool 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 | |
| 1636 | bool 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 | |
| 1717 | void 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 | |
| 1733 | void 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 | |
| 1765 | int32_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 | |
| 1778 | void C4PlayerInfoList::LoadResources() |
| 1779 | { |
| 1780 | // load for all players |
| 1781 | int32_t i = iClientCount; C4ClientPlayerInfos **ppClient = ppClients; |
| 1782 | while (i--)(*ppClient++)->LoadResources(); |
| 1783 | } |
| 1784 | |
| 1785 | void 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 | |
| 1800 | void C4PacketPlayerInfoUpdRequest::CompileFunc(StdCompiler *pComp) |
| 1801 | { |
| 1802 | pComp->Value(rStruct&: Info); |
| 1803 | } |
| 1804 | |