| 1 | /* |
| 2 | * LegacyClonk |
| 3 | * |
| 4 | * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design) |
| 5 | * Copyright (c) 2017-2022, The LegacyClonk Team and contributors |
| 6 | * |
| 7 | * Distributed under the terms of the ISC license; see accompanying file |
| 8 | * "COPYING" for details. |
| 9 | * |
| 10 | * "Clonk" is a registered trademark of Matthes Bender, used with permission. |
| 11 | * See accompanying file "TRADEMARK" for details. |
| 12 | * |
| 13 | * To redistribute this file separately, substitute the full license texts |
| 14 | * for the above references. |
| 15 | */ |
| 16 | |
| 17 | /* Player data at runtime */ |
| 18 | |
| 19 | #include <C4Include.h> |
| 20 | #include <C4Player.h> |
| 21 | |
| 22 | #include <C4Application.h> |
| 23 | #include <C4Object.h> |
| 24 | #include <C4ObjectInfo.h> |
| 25 | #include <C4Command.h> |
| 26 | #include <C4Network2Stats.h> |
| 27 | #include <C4MessageInput.h> |
| 28 | #include <C4GamePadCon.h> |
| 29 | #include <C4Wrappers.h> |
| 30 | #include <C4Random.h> |
| 31 | #include <C4Log.h> |
| 32 | #include <C4FullScreen.h> |
| 33 | #include <C4GameOverDlg.h> |
| 34 | #include <C4ObjectMenu.h> |
| 35 | |
| 36 | static constexpr std::int32_t C4FOW_Def_View_RangeX{500}; |
| 37 | |
| 38 | C4Player::C4Player() : C4PlayerInfoCore() |
| 39 | { |
| 40 | Default(); |
| 41 | } |
| 42 | |
| 43 | C4Player::~C4Player() |
| 44 | { |
| 45 | Clear(); |
| 46 | } |
| 47 | |
| 48 | bool C4Player::ObjectInCrew(C4Object *tobj) |
| 49 | { |
| 50 | C4Object *cobj; C4ObjectLink *clnk; |
| 51 | if (!tobj) return false; |
| 52 | for (clnk = Crew.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next) |
| 53 | if (cobj == tobj) return true; |
| 54 | return false; |
| 55 | } |
| 56 | |
| 57 | void C4Player::ClearPointers(C4Object *pObj, bool fDeath) |
| 58 | { |
| 59 | // Game |
| 60 | if (Captain == pObj) Captain = nullptr; |
| 61 | // Crew |
| 62 | while (Crew.Remove(pObj)); |
| 63 | // Cursor |
| 64 | if (Cursor == pObj) |
| 65 | { |
| 66 | // object is to be deleted; do NOT do script calls (like in Cursor->UnSelect(true)) |
| 67 | Cursor = nullptr; AdjustCursorCommand(); // also selects and eventually does a script call! |
| 68 | } |
| 69 | // View-Cursor |
| 70 | if (ViewCursor == pObj) ViewCursor = nullptr; |
| 71 | // View |
| 72 | if (ViewTarget == pObj) ViewTarget = nullptr; |
| 73 | // FoW |
| 74 | // (do not clear locals!) |
| 75 | // no clear when death to do normal decay |
| 76 | if (!fDeath) |
| 77 | while (FoWViewObjs.Remove(pObj)); |
| 78 | // Menu |
| 79 | Menu.ClearPointers(pObj); |
| 80 | // messageboard-queries |
| 81 | RemoveMessageBoardQuery(pForObj: pObj); |
| 82 | } |
| 83 | |
| 84 | void C4Player::UpdateValue() |
| 85 | { |
| 86 | int32_t lval = ValueGain, lobj = ObjectsOwned; |
| 87 | Value = 0; ObjectsOwned = 0; |
| 88 | |
| 89 | // Points |
| 90 | Value += Points; |
| 91 | |
| 92 | // Wealth |
| 93 | Value += Wealth; |
| 94 | |
| 95 | // Asset all owned objects |
| 96 | C4Object *cobj; C4ObjectLink *clnk; |
| 97 | for (clnk = Game.Objects.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next) |
| 98 | if (cobj->Owner == Number && cobj->Status) |
| 99 | { |
| 100 | ObjectsOwned++; |
| 101 | Value += cobj->GetValue(pInBase: nullptr, iForPlayer: Number); |
| 102 | } |
| 103 | |
| 104 | // Value gain (always positive) |
| 105 | ValueGain = Value - InitialValue; |
| 106 | |
| 107 | // Update |
| 108 | if ((ValueGain != lval) || (ObjectsOwned != lobj)) ViewValue = C4ViewDelay; |
| 109 | } |
| 110 | |
| 111 | bool C4Player::ScenarioAndTeamInit(int32_t idTeam) |
| 112 | { |
| 113 | C4PlayerInfo *pInfo = GetInfo(); |
| 114 | if (!pInfo) return false; |
| 115 | C4Team *pTeam; |
| 116 | if (idTeam == TEAMID_New) |
| 117 | { |
| 118 | // creation of a new team only if allowed by scenario |
| 119 | if (!Game.Teams.IsAutoGenerateTeams()) |
| 120 | pTeam = nullptr; |
| 121 | else |
| 122 | { |
| 123 | if (pTeam = Game.Teams.GetGenerateTeamByID(iID: idTeam)) idTeam = pTeam->GetID(); |
| 124 | } |
| 125 | } |
| 126 | else |
| 127 | { |
| 128 | // uage of an existing team |
| 129 | pTeam = Game.Teams.GetTeamByID(iID: idTeam); |
| 130 | } |
| 131 | C4Team *pPrevTeam = Game.Teams.GetTeamByID(iID: Team); |
| 132 | // check if join to team is possible; e.g. not too many players |
| 133 | if (pPrevTeam != pTeam && idTeam) |
| 134 | { |
| 135 | if (!Game.Teams.IsJoin2TeamAllowed(idTeam)) |
| 136 | { |
| 137 | pTeam = nullptr; |
| 138 | } |
| 139 | } |
| 140 | if (!pTeam && idTeam) |
| 141 | { |
| 142 | OnTeamSelectionFailed(); |
| 143 | return false; |
| 144 | } |
| 145 | // team selection OK; execute it! |
| 146 | if (pPrevTeam) pPrevTeam->RemovePlayerByID(iID: pInfo->GetID()); |
| 147 | if (pTeam) pTeam->AddPlayer(rInfo&: *pInfo, fAdjustPlayer: true); |
| 148 | if (!ScenarioInit()) return false; |
| 149 | if (Game.Rules & C4RULE_TeamHombase) SyncHomebaseMaterialFromTeam(); |
| 150 | if (!FinalInit(fInitialValue: false)) return false; |
| 151 | return true; |
| 152 | } |
| 153 | |
| 154 | void C4Player::Execute() |
| 155 | { |
| 156 | if (!Status) return; |
| 157 | |
| 158 | // Open/refresh team menu if desired |
| 159 | if (Status == PS_TeamSelection) |
| 160 | { |
| 161 | int32_t idSelectedTeam; |
| 162 | if (idSelectedTeam = Game.Teams.GetForcedTeamSelection(idForPlayer: ID)) |
| 163 | { |
| 164 | // There's only one team left to join? Join there immediately. |
| 165 | if (Menu.IsActive() && Menu.GetIdentification() == C4MN_TeamSelection) Menu.TryClose(fOK: false, fControl: false); |
| 166 | if (LocalControl && !Game.Control.isReplay()) |
| 167 | { |
| 168 | // team selection done through queue because TeamSelection-status may not be in sync (may be TeamSelectionPending!) |
| 169 | DoTeamSelection(idTeam: idSelectedTeam); |
| 170 | } |
| 171 | } |
| 172 | else if (!Menu.IsActive()) ActivateMenuTeamSelection(fFromMain: false); |
| 173 | else |
| 174 | { |
| 175 | // during team selection: Update view to selected team, if it has a position assigned |
| 176 | C4MenuItem *pSelectedTeamItem; |
| 177 | if (pSelectedTeamItem = Menu.GetSelectedItem()) |
| 178 | { |
| 179 | int32_t idSelectedTeam = int32_t(pSelectedTeamItem->GetC4ID()); |
| 180 | if (idSelectedTeam) |
| 181 | { |
| 182 | C4Team *pSelectedTeam; |
| 183 | if (pSelectedTeam = Game.Teams.GetTeamByID(iID: idSelectedTeam)) |
| 184 | { |
| 185 | int32_t iPlrStartIndex = pSelectedTeam->GetPlrStartIndex(); |
| 186 | if (iPlrStartIndex && Inside<int32_t>(ival: iPlrStartIndex, lbound: 1, rbound: C4S_MaxPlayer)) |
| 187 | { |
| 188 | if (Game.C4S.PlrStart[iPlrStartIndex - 1].Position[0] > -1) |
| 189 | { |
| 190 | // player has selected a team that has a valid start position assigned |
| 191 | // set view to this position! |
| 192 | ViewX = Game.C4S.PlrStart[iPlrStartIndex - 1].Position[0] * Game.Landscape.MapZoom; |
| 193 | ViewY = Game.C4S.PlrStart[iPlrStartIndex - 1].Position[1] * Game.Landscape.MapZoom; |
| 194 | } |
| 195 | } |
| 196 | } |
| 197 | } |
| 198 | } |
| 199 | } |
| 200 | } |
| 201 | else if (Menu.IsActive() && Menu.GetIdentification() == C4MN_TeamSelection) |
| 202 | { |
| 203 | Menu.TryClose(fOK: false, fControl: false); |
| 204 | } |
| 205 | |
| 206 | // Tick1 |
| 207 | UpdateCounts(); |
| 208 | UpdateView(); |
| 209 | ExecuteControl(); |
| 210 | Menu.Execute(); |
| 211 | if (Cursor) |
| 212 | Cursor->AutoContextMenu(iMenuSelect: -1); |
| 213 | |
| 214 | // decay of dead viewtargets |
| 215 | C4ObjectLink *pLnkNext = FoWViewObjs.First, *pLnk; |
| 216 | while (pLnk = pLnkNext) |
| 217 | { |
| 218 | pLnkNext = pLnk->Next; |
| 219 | C4Object *pDeadClonk = pLnk->Obj; |
| 220 | if (!pDeadClonk->GetAlive() && (pDeadClonk->Category & C4D_Living) && pDeadClonk->Status) |
| 221 | { |
| 222 | pDeadClonk->PlrViewRange -= 10; |
| 223 | if (pDeadClonk->PlrViewRange <= 0) |
| 224 | FoWViewObjs.Remove(pObj: pDeadClonk); |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | // Tick35 |
| 229 | if (!Tick35 && Status == PS_Normal) |
| 230 | { |
| 231 | ExecHomeBaseProduction(); |
| 232 | UpdateValue(); |
| 233 | CheckElimination(); |
| 234 | if (pMsgBoardQuery && LocalControl) ExecMsgBoardQueries(); |
| 235 | } |
| 236 | |
| 237 | // Delays |
| 238 | if (MessageStatus > 0) MessageStatus--; |
| 239 | if (RetireDelay > 0) RetireDelay--; |
| 240 | if (ViewWealth > 0) ViewWealth--; |
| 241 | if (ViewValue > 0) ViewValue--; |
| 242 | if (CursorFlash > 0) CursorFlash--; |
| 243 | if (SelectFlash > 0) SelectFlash--; |
| 244 | } |
| 245 | |
| 246 | bool C4Player::Init(int32_t iNumber, int32_t iAtClient, const char *szAtClientName, |
| 247 | const char *szFilename, bool fScenarioInit, class C4PlayerInfo *pInfo) |
| 248 | { |
| 249 | // safety |
| 250 | if (!pInfo) |
| 251 | { |
| 252 | spdlog::error(fmt: "Init player {} failed: No info!" , args&: szFilename); |
| 253 | assert(false); |
| 254 | return false; |
| 255 | } |
| 256 | // Status init |
| 257 | Status = PS_Normal; |
| 258 | if (szFilename) SCopy(szSource: szFilename, sTarget: Filename); else *Filename = '\0'; |
| 259 | Number = iNumber; |
| 260 | ID = pInfo->GetID(); |
| 261 | Team = pInfo->GetTeam(); |
| 262 | NoEliminationCheck = pInfo->IsNoEliminationCheck(); |
| 263 | |
| 264 | // At client |
| 265 | AtClient = iAtClient; SCopy(szSource: szAtClientName, sTarget: AtClientName, iMaxL: C4MaxTitle); |
| 266 | |
| 267 | if (szFilename) |
| 268 | { |
| 269 | // Load core & crew info list |
| 270 | // do not load portraits for remote players |
| 271 | // this will prevent portraits from being shown for "remotely controlled"-Clonks of other players |
| 272 | bool fLoadPortraits = (AtClient == C4ClientIDUnknown) || SEqualNoCase(szStr1: AtClientName, szStr2: Game.Clients.getLocalName()); |
| 273 | // fLoadPortraits = true |
| 274 | if (!Load(szFilename, fSavegame: !fScenarioInit, fLoadPortraits)) return false; |
| 275 | } |
| 276 | else |
| 277 | { |
| 278 | // no core file present: Keep defaults |
| 279 | // This can happen for script players only |
| 280 | assert(pInfo->GetType() == C4PT_Script); |
| 281 | } |
| 282 | |
| 283 | // Take player name from player info; forcing overloads by the league or because of doubled player names |
| 284 | Name.Copy(pnData: pInfo->GetName()); |
| 285 | |
| 286 | // view pos init: Start at center pos |
| 287 | ViewX = GBackWdt / 2; ViewY = GBackHgt / 2; |
| 288 | |
| 289 | // Scenario init |
| 290 | if (fScenarioInit) |
| 291 | { |
| 292 | // mark player join in player info list |
| 293 | // for non-scenarioinit, player should already be marked as joined |
| 294 | pInfo->SetJoined(iNumber); |
| 295 | |
| 296 | // Number might have changed: Recheck list sorting before scenarioinit, which will do script calls |
| 297 | Game.Players.RecheckPlayerSort(pForPlayer: this); |
| 298 | |
| 299 | // check for a postponed scenario init, if no team is specified (post-lobby-join in network, or simply non-network) |
| 300 | C4Team *pTeam = nullptr; |
| 301 | if (Team) |
| 302 | { |
| 303 | if (Game.Teams.IsAutoGenerateTeams()) |
| 304 | pTeam = Game.Teams.GetGenerateTeamByID(iID: Team); |
| 305 | else |
| 306 | pTeam = Game.Teams.GetTeamByID(iID: Team); |
| 307 | } |
| 308 | if (!pTeam && Game.Teams.IsRuntimeJoinTeamChoice()) |
| 309 | { |
| 310 | if (pInfo->GetType() == C4PT_Script) |
| 311 | { |
| 312 | // script player without team: This can usually not happen, because RecheckPlayerInfoTeams should have been executed |
| 313 | // just leave this player without the team |
| 314 | assert(false); |
| 315 | } |
| 316 | else |
| 317 | { |
| 318 | // postponed init: Chose team first |
| 319 | Status = PS_TeamSelection; |
| 320 | } |
| 321 | } |
| 322 | |
| 323 | // Init control method before scenario init, because script callbacks may need to know it! |
| 324 | InitControl(); |
| 325 | |
| 326 | // Special: Script players may skip scenario initialization altogether, and just desire a single callback to all objects |
| 327 | // of a given ID |
| 328 | if (!pInfo->IsScenarioInitDesired()) |
| 329 | { |
| 330 | // only initialization that's done anyway is team hostility |
| 331 | if (Team) SetTeamHostility(); |
| 332 | // ...and home base material from team |
| 333 | if (Game.Rules & C4RULE_TeamHombase) SyncHomebaseMaterialFromTeam(); |
| 334 | // callback definition passed? |
| 335 | C4ID idCallback = pInfo->GetScriptPlayerExtraID(); |
| 336 | C4Def *pDefCallback; |
| 337 | if (idCallback && (pDefCallback = C4Id2Def(id: idCallback))) |
| 338 | { |
| 339 | pDefCallback->Script.Call(PSF_InitializeScriptPlayer, pPars: {C4VInt(iVal: Number), C4VInt(iVal: Team)}); |
| 340 | } |
| 341 | } |
| 342 | else |
| 343 | { |
| 344 | // player preinit: In case a team needs to be chosen first, no InitializePlayer-broadcast is done |
| 345 | // this callback shall give scripters a chance to do stuff like starting an intro or enabling FoW, which might need to be done |
| 346 | Game.Script.GRBroadcast(PSF_PreInitializePlayer, pPars: {C4VInt(iVal: Number)}); |
| 347 | // direct init |
| 348 | if (Status != PS_TeamSelection) if (!ScenarioInit()) return false; |
| 349 | // ...and home base material from team |
| 350 | if (Game.Rules & C4RULE_TeamHombase) SyncHomebaseMaterialFromTeam(); |
| 351 | } |
| 352 | } |
| 353 | |
| 354 | // Load runtime data |
| 355 | else |
| 356 | { |
| 357 | assert(pInfo->IsJoined()); |
| 358 | // (compile using DefaultRuntimeData) - also check if compilation returned sane results, i.e. ID assigned |
| 359 | if (!LoadRuntimeData(hGroup&: Game.ScenarioFile) || !ID) |
| 360 | { |
| 361 | // for script players in non-savegames, this is OK - it means they get restored using default values |
| 362 | // this happens when the users saves a scenario using the "Save scenario"-option while a script player |
| 363 | // was joined |
| 364 | if (!Game.C4S.Head.SaveGame && pInfo->GetType() == C4PT_Script) |
| 365 | { |
| 366 | Number = pInfo->GetInGameNumber(); |
| 367 | ColorDw = pInfo->GetColor(); |
| 368 | ID = pInfo->GetID(); |
| 369 | Team = pInfo->GetTeam(); |
| 370 | } |
| 371 | else |
| 372 | return false; |
| 373 | } |
| 374 | // Reset values default-overriden by old runtime data load (safety?) |
| 375 | if (Number == C4P_Number_None) Number = iNumber; |
| 376 | if (szFilename) SCopy(szSource: szFilename, sTarget: Filename); else *Filename = '\0'; |
| 377 | // NET2: Direct joins always send accurate client IDs and names in params |
| 378 | // do not overwrite them with savegame data, because players might as well |
| 379 | // change clients |
| 380 | // (only call should be savegame recreation by C4PlayerInfoList::RecreatePlayers) |
| 381 | AtClient = iAtClient; |
| 382 | SCopy(szSource: szAtClientName, sTarget: AtClientName, iMaxL: C4MaxTitle); |
| 383 | // Number might have changed: Recheck list sorting |
| 384 | Game.Players.RecheckPlayerSort(pForPlayer: this); |
| 385 | // Init control after loading runtime data, because control init will overwrite some of the values |
| 386 | InitControl(); |
| 387 | } |
| 388 | |
| 389 | // store game joining time |
| 390 | GameJoinTime = Game.Time; |
| 391 | |
| 392 | // Init FoW-viewobjects: NO_OWNER-FoW-repellers might need to be added |
| 393 | for (C4ObjectLink *pLnk = Game.Objects.First; pLnk; pLnk = pLnk->Next) |
| 394 | { |
| 395 | C4Object *pObj = pLnk->Obj; |
| 396 | if (pObj->PlrViewRange && pObj->Owner == NO_OWNER) |
| 397 | pObj->PlrFoWActualize(); |
| 398 | } |
| 399 | |
| 400 | // init graphs |
| 401 | if (Game.pNetworkStatistics) CreateGraphs(); |
| 402 | |
| 403 | return true; |
| 404 | } |
| 405 | |
| 406 | bool C4Player::Save() |
| 407 | { |
| 408 | C4Group hGroup; |
| 409 | // Regular player saving need not be done for script players |
| 410 | if (GetType() == C4PT_Script) return false; |
| 411 | // Remote players need not be saved if they cannot resume |
| 412 | if (!LocalControl) |
| 413 | { |
| 414 | // This is the case in league |
| 415 | if (Game.Parameters.isLeague()) return false; |
| 416 | // And also if max player count is set to zero to prevent runtime joins |
| 417 | if (Game.Parameters.MaxPlayers <= 0) return false; |
| 418 | } |
| 419 | // Log |
| 420 | Log(id: C4ResStrTableKey::IDS_PRC_SAVEPLR, args: Config.AtExeRelativePath(szFilename: Filename)); |
| 421 | Game.GraphicsSystem.MessageBoard.EnsureLastMessage(); |
| 422 | // copy player to save somewhere else |
| 423 | char szPath[_MAX_PATH + 1]; |
| 424 | SCopy(szSource: Config.AtTempPath(C4CFN_TempPlayer), sTarget: szPath, _MAX_PATH); |
| 425 | MakeTempFilename(szFileName: szPath); |
| 426 | // For local players, we save over the old player file, as there might |
| 427 | // be all kinds of non-essential stuff in it. For non-local players, we |
| 428 | // just re-create it every time (it's temporary anyway). |
| 429 | if (LocalControl) |
| 430 | { |
| 431 | // But make sure to copy it first so full hard (flgr stupid) disks |
| 432 | // won't corrupt any player files... |
| 433 | C4Group_CopyItem(szSource: Filename, szTarget: szPath); |
| 434 | } |
| 435 | else |
| 436 | { |
| 437 | // For non-local players, we can actually use the loaded definition |
| 438 | // list to strip out all non-existent definitions. This is only valid |
| 439 | // because we know the file to be temporary. |
| 440 | CrewInfoList.Strip(rDefs&: Game.Defs); |
| 441 | } |
| 442 | // Open group |
| 443 | if (!hGroup.Open(szGroupName: szPath, fCreate: true)) |
| 444 | return false; |
| 445 | // Save |
| 446 | if (!Save(hGroup, fSavegame: false, fStoreTiny: !LocalControl)) |
| 447 | { |
| 448 | hGroup.Close(); return false; |
| 449 | } |
| 450 | // Close group |
| 451 | if (!hGroup.Close()) return false; |
| 452 | // resource |
| 453 | C4Network2Res::Ref pRes = Game.Network.ResList.getRefRes(szFile: Filename), |
| 454 | pDRes = nullptr; |
| 455 | bool fOfficial = pRes && Game.Control.isCtrlHost(); |
| 456 | if (pRes) pDRes = pRes->Derive(); |
| 457 | // move back |
| 458 | if (ItemExists(szItemName: Filename)) EraseItem(szItemName: Filename); |
| 459 | if (!C4Group_MoveItem(szSource: szPath, szTarget: Filename)) return false; |
| 460 | // finish update |
| 461 | if (pDRes && fOfficial) pDRes->FinishDerive(); |
| 462 | // Success |
| 463 | return true; |
| 464 | } |
| 465 | |
| 466 | bool C4Player::Save(C4Group &hGroup, bool fSavegame, bool fStoreTiny) |
| 467 | { |
| 468 | // Save core |
| 469 | if (!C4PlayerInfoCore::Save(hGroup)) |
| 470 | return false; |
| 471 | // Save crew |
| 472 | if (!CrewInfoList.Save(hGroup, fSavegame, fStoreTiny, pDefs: &Game.Defs)) |
| 473 | { |
| 474 | hGroup.Close(); return false; |
| 475 | } |
| 476 | // Sort |
| 477 | hGroup.Sort(C4FLS_Player); |
| 478 | return true; |
| 479 | } |
| 480 | |
| 481 | void C4Player::PlaceReadyCrew(int32_t tx1, int32_t tx2, int32_t ty, C4Object *FirstBase) |
| 482 | { |
| 483 | int32_t cnt, crewnum, ctx, cty; |
| 484 | C4Object *nobj; |
| 485 | C4ObjectInfo *pInfo; |
| 486 | C4Def *pDef; |
| 487 | |
| 488 | // Set name source |
| 489 | const char *cpNames = Game.Names.GetData(); |
| 490 | |
| 491 | // Old specification |
| 492 | if (Game.C4S.PlrStart[PlrStartIndex].ReadyCrew.IsClear()) |
| 493 | { |
| 494 | // Target number of ready crew |
| 495 | crewnum = Game.C4S.PlrStart[PlrStartIndex].Crew.Evaluate(); |
| 496 | // Place crew |
| 497 | for (cnt = 0; cnt < crewnum; cnt++) |
| 498 | { |
| 499 | // Set standard crew |
| 500 | C4ID idStdCrew = Game.C4S.PlrStart[PlrStartIndex].NativeCrew; |
| 501 | // Select member from home crew, add new if necessary |
| 502 | while (!(pInfo = CrewInfoList.GetIdle(c_id: idStdCrew, rDefs&: Game.Defs))) |
| 503 | if (!CrewInfoList.New(n_id: idStdCrew, pDefs: &Game.Defs, cpNames)) |
| 504 | break; |
| 505 | // Crew placement location |
| 506 | if (!pInfo || !(pDef = C4Id2Def(id: pInfo->id))) continue; |
| 507 | ctx = tx1 + Random(iRange: tx2 - tx1); cty = ty; |
| 508 | if (!Game.C4S.PlrStart[PlrStartIndex].EnforcePosition) |
| 509 | FindSolidGround(rx&: ctx, ry&: cty, width: pDef->Shape.Wdt * 3); |
| 510 | // Create object |
| 511 | if (nobj = Game.CreateInfoObject(cinf: pInfo, owner: Number, tx: ctx, ty: cty)) |
| 512 | { |
| 513 | // Add object to crew |
| 514 | Crew.Add(nObj: nobj, eSort: C4ObjectList::stMain); |
| 515 | // add visibility range |
| 516 | nobj->SetPlrViewRange(C4FOW_Def_View_RangeX); |
| 517 | // If base is present, enter base |
| 518 | if (FirstBase) { nobj->Enter(pTarget: FirstBase); nobj->SetCommand(iCommand: C4CMD_Exit); } |
| 519 | // OnJoinCrew callback |
| 520 | #ifndef DEBUGREC_RECRUITMENT |
| 521 | C4DebugRecOff DBGRECOFF; |
| 522 | #endif |
| 523 | nobj->Call(PSF_OnJoinCrew, pPars: {C4VInt(iVal: Number)}); |
| 524 | } |
| 525 | } |
| 526 | } |
| 527 | |
| 528 | // New specification |
| 529 | else |
| 530 | { |
| 531 | // Place crew |
| 532 | int32_t id, iCount; |
| 533 | for (cnt = 0; id = Game.C4S.PlrStart[PlrStartIndex].ReadyCrew.GetID(index: cnt, ipCount: &iCount); cnt++) |
| 534 | { |
| 535 | // Minimum one clonk if empty id |
| 536 | iCount = std::max<int32_t>(a: iCount, b: 1); |
| 537 | |
| 538 | for (int32_t cnt2 = 0; cnt2 < iCount; cnt2++) |
| 539 | { |
| 540 | // Select member from home crew, add new if necessary |
| 541 | while (!(pInfo = CrewInfoList.GetIdle(c_id: id, rDefs&: Game.Defs))) |
| 542 | if (!CrewInfoList.New(n_id: id, pDefs: &Game.Defs, cpNames)) |
| 543 | break; |
| 544 | // Safety |
| 545 | if (!pInfo || !(pDef = C4Id2Def(id: pInfo->id))) continue; |
| 546 | // Crew placement location |
| 547 | ctx = tx1 + Random(iRange: tx2 - tx1); cty = ty; |
| 548 | if (!Game.C4S.PlrStart[PlrStartIndex].EnforcePosition) |
| 549 | FindSolidGround(rx&: ctx, ry&: cty, width: pDef->Shape.Wdt * 3); |
| 550 | // Create object |
| 551 | if (nobj = Game.CreateInfoObject(cinf: pInfo, owner: Number, tx: ctx, ty: cty)) |
| 552 | { |
| 553 | // Add object to crew |
| 554 | Crew.Add(nObj: nobj, eSort: C4ObjectList::stMain); |
| 555 | // add visibility range |
| 556 | nobj->SetPlrViewRange(C4FOW_Def_View_RangeX); |
| 557 | // If base is present, enter base |
| 558 | if (FirstBase) { nobj->Enter(pTarget: FirstBase); nobj->SetCommand(iCommand: C4CMD_Exit); } |
| 559 | // OnJoinCrew callback |
| 560 | { |
| 561 | #ifndef DEBUGREC_RECRUITMENT |
| 562 | C4DebugRecOff DbgRecOff; |
| 563 | #endif |
| 564 | nobj->Call(PSF_OnJoinCrew, pPars: {C4VInt(iVal: Number)}); |
| 565 | } |
| 566 | } |
| 567 | } |
| 568 | } |
| 569 | } |
| 570 | } |
| 571 | |
| 572 | C4Object *CreateLine(C4ID linetype, int32_t owner, C4Object *fobj, C4Object *tobj); |
| 573 | |
| 574 | bool CreatePowerConnection(C4Object *fbase, C4Object *tbase) |
| 575 | { |
| 576 | if (CreateLine(linetype: C4ID_PowerLine, owner: fbase->Owner, fobj: fbase, tobj: tbase)) return true; |
| 577 | return false; |
| 578 | } |
| 579 | |
| 580 | void C4Player::PlaceReadyBase(int32_t &tx, int32_t &ty, C4Object **pFirstBase) |
| 581 | { |
| 582 | int32_t cnt, cnt2, ctx, cty; |
| 583 | C4Def *def; |
| 584 | C4ID cid; |
| 585 | C4Object *cbase, *fpower = nullptr; |
| 586 | // Create ready base structures |
| 587 | for (cnt = 0; (cid = Game.C4S.PlrStart[PlrStartIndex].ReadyBase.GetID(index: cnt)); cnt++) |
| 588 | { |
| 589 | if (def = C4Id2Def(id: cid)) |
| 590 | for (cnt2 = 0; cnt2 < Game.C4S.PlrStart[PlrStartIndex].ReadyBase.GetCount(index: cnt); cnt2++) |
| 591 | { |
| 592 | ctx = tx; cty = ty; |
| 593 | if (Game.C4S.PlrStart[PlrStartIndex].EnforcePosition |
| 594 | || FindConSiteSpot(rx&: ctx, ry&: cty, wdt: def->Shape.Wdt, hgt: def->Shape.Hgt, category: def->Category, hrange: 20)) |
| 595 | if (cbase = Game.CreateObjectConstruction(type: cid, pCreator: nullptr, owner: Number, ctx, bty: cty, con: FullCon, terrain: true)) |
| 596 | { |
| 597 | // FirstBase |
| 598 | if (!(*pFirstBase)) if (cbase->Def->CanBeBase) |
| 599 | { |
| 600 | *pFirstBase = cbase; tx = (*pFirstBase)->x; ty = (*pFirstBase)->y; |
| 601 | } |
| 602 | // First power plant |
| 603 | if (cbase->Def->LineConnect & C4D_Power_Generator) |
| 604 | if (!fpower) fpower = cbase; |
| 605 | } |
| 606 | } |
| 607 | } |
| 608 | |
| 609 | // Power connections |
| 610 | C4ObjectLink *clnk; C4Object *cobj; |
| 611 | if (Game.Rules & C4RULE_StructuresNeedEnergy) |
| 612 | if (fpower) |
| 613 | for (clnk = Game.Objects.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next) |
| 614 | if (cobj->Owner == Number) |
| 615 | if (cobj->Def->LineConnect & C4D_Power_Consumer) |
| 616 | CreatePowerConnection(fbase: fpower, tbase: cobj); |
| 617 | } |
| 618 | |
| 619 | void C4Player::PlaceReadyVehic(int32_t tx1, int32_t tx2, int32_t ty, C4Object *FirstBase) |
| 620 | { |
| 621 | int32_t cnt, cnt2, ctx, cty; |
| 622 | C4Def *def; C4ID cid; C4Object *cobj; |
| 623 | for (cnt = 0; (cid = Game.C4S.PlrStart[PlrStartIndex].ReadyVehic.GetID(index: cnt)); cnt++) |
| 624 | { |
| 625 | if (def = C4Id2Def(id: cid)) |
| 626 | for (cnt2 = 0; cnt2 < Game.C4S.PlrStart[PlrStartIndex].ReadyVehic.GetCount(index: cnt); cnt2++) |
| 627 | { |
| 628 | ctx = tx1 + Random(iRange: tx2 - tx1); cty = ty; |
| 629 | if (!Game.C4S.PlrStart[PlrStartIndex].EnforcePosition) |
| 630 | FindLevelGround(rx&: ctx, ry&: cty, width: def->Shape.Wdt, hrange: 6); |
| 631 | if (cobj = Game.CreateObject(type: cid, pCreator: nullptr, owner: Number, x: ctx, y: cty)) |
| 632 | { |
| 633 | if (FirstBase) // First base overrides target location |
| 634 | { |
| 635 | cobj->Enter(pTarget: FirstBase); cobj->SetCommand(iCommand: C4CMD_Exit); |
| 636 | } |
| 637 | } |
| 638 | } |
| 639 | } |
| 640 | } |
| 641 | |
| 642 | void C4Player::PlaceReadyMaterial(int32_t tx1, int32_t tx2, int32_t ty, C4Object *FirstBase) |
| 643 | { |
| 644 | int32_t cnt, cnt2, ctx, cty; |
| 645 | C4Def *def; C4ID cid; |
| 646 | |
| 647 | // In base |
| 648 | if (FirstBase) |
| 649 | { |
| 650 | FirstBase->CreateContentsByList(idlist&: Game.C4S.PlrStart[PlrStartIndex].ReadyMaterial); |
| 651 | } |
| 652 | |
| 653 | // Outside |
| 654 | else |
| 655 | { |
| 656 | for (cnt = 0; (cid = Game.C4S.PlrStart[PlrStartIndex].ReadyMaterial.GetID(index: cnt)); cnt++) |
| 657 | { |
| 658 | if (def = C4Id2Def(id: cid)) |
| 659 | for (cnt2 = 0; cnt2 < Game.C4S.PlrStart[PlrStartIndex].ReadyMaterial.GetCount(index: cnt); cnt2++) |
| 660 | { |
| 661 | ctx = tx1 + Random(iRange: tx2 - tx1); cty = ty; |
| 662 | if (!Game.C4S.PlrStart[PlrStartIndex].EnforcePosition) |
| 663 | FindSolidGround(rx&: ctx, ry&: cty, width: def->Shape.Wdt); |
| 664 | Game.CreateObject(type: cid, pCreator: nullptr, owner: Number, x: ctx, y: cty); |
| 665 | } |
| 666 | } |
| 667 | } |
| 668 | } |
| 669 | |
| 670 | bool C4Player::ScenarioInit() |
| 671 | { |
| 672 | int32_t ptx, pty; |
| 673 | |
| 674 | // player start index by team, if specified. Otherwise by player number |
| 675 | PlrStartIndex = Number % C4S_MaxPlayer; |
| 676 | C4Team *pTeam; int32_t i; |
| 677 | if (Team && (pTeam = Game.Teams.GetTeamByID(iID: Team))) if (i = pTeam->GetPlrStartIndex()) PlrStartIndex = i - 1; |
| 678 | |
| 679 | // Set color |
| 680 | int32_t iColor = BoundBy<int32_t>(bval: PrefColor, lbound: 0, rbound: C4MaxColor - 1); |
| 681 | while (Game.Players.ColorTaken(iColor)) |
| 682 | { |
| 683 | ++iColor %= C4MaxColor; if (iColor == PrefColor) break; |
| 684 | } |
| 685 | Color = iColor; |
| 686 | |
| 687 | C4PlayerInfo *pInfo = GetInfo(); |
| 688 | if (!pInfo) { assert(false); spdlog::error(fmt: "Internal error: ScenarioInit for ghost player {}" , args: GetName()); return false; } |
| 689 | |
| 690 | // set color by player info class |
| 691 | // re-setting, because runtime team choice may have altered color |
| 692 | ColorDw = pInfo->GetColor(); |
| 693 | |
| 694 | // any team selection is over now |
| 695 | Status = PS_Normal; |
| 696 | |
| 697 | // Wealth, home base materials, abilities |
| 698 | Wealth = Game.C4S.PlrStart[PlrStartIndex].Wealth.Evaluate(); |
| 699 | HomeBaseMaterial = Game.C4S.PlrStart[PlrStartIndex].HomeBaseMaterial; |
| 700 | HomeBaseMaterial.ConsolidateValids(rDefs&: Game.Defs); |
| 701 | HomeBaseProduction = Game.C4S.PlrStart[PlrStartIndex].HomeBaseProduction; |
| 702 | HomeBaseProduction.ConsolidateValids(rDefs&: Game.Defs); |
| 703 | Knowledge = Game.C4S.PlrStart[PlrStartIndex].BuildKnowledge; |
| 704 | Knowledge.ConsolidateValids(rDefs&: Game.Defs); |
| 705 | Magic = Game.C4S.PlrStart[PlrStartIndex].Magic; |
| 706 | Magic.ConsolidateValids(rDefs&: Game.Defs); |
| 707 | if (Magic.IsClear()) Magic.Load(defs&: Game.Defs, category: C4D_Magic); // All magic default if empty |
| 708 | Magic.SortByValue(rDefs&: Game.Defs); |
| 709 | |
| 710 | // Starting position |
| 711 | ptx = Game.C4S.PlrStart[PlrStartIndex].Position[0]; |
| 712 | pty = Game.C4S.PlrStart[PlrStartIndex].Position[1]; |
| 713 | |
| 714 | // Zoomed position |
| 715 | if (ptx > -1) ptx = BoundBy<int32_t>(bval: ptx * Game.C4S.Landscape.MapZoom.Evaluate(), lbound: 0, GBackWdt - 1); |
| 716 | if (pty > -1) pty = BoundBy<int32_t>(bval: pty * Game.C4S.Landscape.MapZoom.Evaluate(), lbound: 0, GBackHgt - 1); |
| 717 | |
| 718 | // Standard position (PrefPosition) |
| 719 | if (ptx < 0) |
| 720 | if (Game.Parameters.StartupPlayerCount >= 2) |
| 721 | { |
| 722 | int32_t iMaxPos = Game.Parameters.StartupPlayerCount; |
| 723 | // Map preferred position to available positions |
| 724 | int32_t iStartPos = BoundBy(bval: PrefPosition * iMaxPos / C4P_MaxPosition, lbound: 0, rbound: iMaxPos - 1); |
| 725 | int32_t iPosition = iStartPos; |
| 726 | // Distribute according to availability |
| 727 | while (Game.Players.PositionTaken(iPosition)) |
| 728 | { |
| 729 | ++iPosition %= iMaxPos; if (iPosition == iStartPos) break; |
| 730 | } |
| 731 | Position = iPosition; |
| 732 | // Set x position |
| 733 | ptx = BoundBy(bval: 16 + Position * (GBackWdt - 32) / (iMaxPos - 1), lbound: 0, GBackWdt - 16); |
| 734 | } |
| 735 | |
| 736 | // All-random position |
| 737 | if (ptx < 0) ptx = 16 + Random(GBackWdt - 32); |
| 738 | if (pty < 0) pty = 16 + Random(GBackHgt - 32); |
| 739 | |
| 740 | // Place to solid ground |
| 741 | if (!Game.C4S.PlrStart[PlrStartIndex].EnforcePosition) |
| 742 | { |
| 743 | // Use nearest above-ground... |
| 744 | FindSolidGround(rx&: ptx, ry&: pty, width: 30); |
| 745 | // Might have hit a small lake, or similar: Seach a real site spot from here |
| 746 | FindConSiteSpot(rx&: ptx, ry&: pty, wdt: 30, hgt: 50, category: C4D_Structure, hrange: 400); |
| 747 | } |
| 748 | |
| 749 | // Place Readies |
| 750 | C4Object *FirstBase = nullptr; |
| 751 | PlaceReadyBase(tx&: ptx, ty&: pty, pFirstBase: &FirstBase); |
| 752 | PlaceReadyMaterial(tx1: ptx - 10, tx2: ptx + 10, ty: pty, FirstBase); |
| 753 | PlaceReadyVehic(tx1: ptx - 30, tx2: ptx + 30, ty: pty, FirstBase); |
| 754 | PlaceReadyCrew(tx1: ptx - 30, tx2: ptx + 30, ty: pty, FirstBase); |
| 755 | |
| 756 | // set initial hostility by team info |
| 757 | if (Team) SetTeamHostility(); |
| 758 | |
| 759 | // Mouse control: init fog of war, if not specified otherwise by script |
| 760 | if (MouseControl && !bForceFogOfWar) |
| 761 | if (!fFogOfWarInitialized) |
| 762 | { |
| 763 | fFogOfWar = fFogOfWarInitialized = true; |
| 764 | // reset view objects |
| 765 | Game.Objects.AssignPlrViewRange(); |
| 766 | } |
| 767 | |
| 768 | // Scenario script initialization |
| 769 | Game.Script.GRBroadcast(PSF_InitializePlayer, pPars: {C4VInt(iVal: Number), |
| 770 | C4VInt(iVal: ptx), |
| 771 | C4VInt(iVal: pty), |
| 772 | C4VObj(pObj: FirstBase), |
| 773 | C4VInt(iVal: Team), |
| 774 | C4VID(idVal: GetInfo()->GetScriptPlayerExtraID())}); |
| 775 | return true; |
| 776 | } |
| 777 | |
| 778 | bool C4Player::FinalInit(bool fInitialValue) |
| 779 | { |
| 780 | if (!Status) return true; |
| 781 | |
| 782 | // Init player's mouse control |
| 783 | if (LocalControl) |
| 784 | if (MouseControl) |
| 785 | Game.MouseControl.Init(iPlayer: Number); |
| 786 | |
| 787 | // Set initial value |
| 788 | if (fInitialValue) |
| 789 | { |
| 790 | UpdateValue(); InitialValue = Value; |
| 791 | } |
| 792 | |
| 793 | // Cursor |
| 794 | if (!Cursor) AdjustCursorCommand(); |
| 795 | |
| 796 | // Assign Captain |
| 797 | if (Game.Objects.Find(id: C4Id(str: "KILC" ))) |
| 798 | if (!Captain) Captain = GetHiRankActiveCrew(fSelectedOnly: false); |
| 799 | |
| 800 | // Update counts, pointers, views, value |
| 801 | UpdateValue(); |
| 802 | Execute(); |
| 803 | |
| 804 | // Restore FoW after savegame |
| 805 | if (fFogOfWar && !fFogOfWarInitialized) |
| 806 | { |
| 807 | fFogOfWarInitialized = true; |
| 808 | // reset view objects |
| 809 | Game.Objects.AssignPlrViewRange(); |
| 810 | } |
| 811 | |
| 812 | return true; |
| 813 | } |
| 814 | |
| 815 | void C4Player::SetFoW(bool fEnable) |
| 816 | { |
| 817 | // enable FoW |
| 818 | if (fEnable && !fFogOfWarInitialized) |
| 819 | Game.Objects.AssignPlrViewRange(); |
| 820 | // set flag |
| 821 | fFogOfWar = fFogOfWarInitialized = fEnable; |
| 822 | // forced (not activated by mouse) |
| 823 | bForceFogOfWar = true; |
| 824 | } |
| 825 | |
| 826 | C4Object *C4Player::Buy(C4ID id, bool fShowErrors, int32_t iForPlr, C4Object *pBuyObj) |
| 827 | { |
| 828 | int32_t iAvailable; C4Def *pDef; C4Object *pThing; |
| 829 | // Base owner eliminated |
| 830 | if (Eliminated) |
| 831 | { |
| 832 | if (!fShowErrors) return nullptr; |
| 833 | StartSoundEffect(name: "Error" , loop: false, volume: 100, obj: pBuyObj); |
| 834 | GameMsgPlayer(szText: LoadResStr(id: C4ResStrTableKey::IDS_PLR_ELIMINATED, args: GetName()).c_str(), iPlayer: Number); return nullptr; |
| 835 | } |
| 836 | // Get def (base owner's homebase material) |
| 837 | iAvailable = HomeBaseMaterial.GetIDCount(id); |
| 838 | if (!(pDef = C4Id2Def(id))) return nullptr; |
| 839 | // Object not available |
| 840 | if (iAvailable <= 0) return nullptr; |
| 841 | // get value |
| 842 | int32_t iValue = pDef->GetValue(pInBase: pBuyObj, iBuyPlayer: Number); |
| 843 | // Not enough wealth (base owner's wealth) |
| 844 | if (iValue > Wealth) |
| 845 | { |
| 846 | if (!fShowErrors) return nullptr; |
| 847 | GameMsgPlayer(szText: LoadResStr(id: C4ResStrTableKey::IDS_PLR_NOWEALTH), iPlayer: Number); |
| 848 | StartSoundEffect(name: "Error" , loop: false, volume: 100, obj: pBuyObj); return nullptr; |
| 849 | } |
| 850 | // Decrease homebase material count |
| 851 | HomeBaseMaterial.DecreaseIDCount(id, removeEmpty: false); |
| 852 | if (Game.Rules & C4RULE_TeamHombase) SyncHomebaseMaterialToTeam(); |
| 853 | // Reduce wealth |
| 854 | DoWealth(change: -iValue); |
| 855 | // Create object (for player) |
| 856 | if (!(pThing = Game.CreateObject(type: id, pCreator: pBuyObj, owner: iForPlr))) return nullptr; |
| 857 | // Make crew member |
| 858 | if (pDef->CrewMember) if (ValidPlr(plr: iForPlr)) |
| 859 | Game.Players.Get(iPlayer: iForPlr)->MakeCrewMember(pObj: pThing); |
| 860 | // success |
| 861 | pThing->Call(PSF_Purchase, pPars: {C4VInt(iVal: Number), C4VObj(pObj: pBuyObj)}); |
| 862 | if (!pThing->Status) return nullptr; |
| 863 | return pThing; |
| 864 | } |
| 865 | |
| 866 | bool C4Player::Sell2Home(C4Object *pObj) |
| 867 | { |
| 868 | C4Object *cObj; |
| 869 | if (!CanSell(obj: pObj)) return false; |
| 870 | // Sell contents first |
| 871 | while (cObj = pObj->Contents.GetObject()) |
| 872 | { |
| 873 | if (pObj->Contained) cObj->Enter(pTarget: pObj->Contained); else cObj->Exit(iX: cObj->x, iY: cObj->y); |
| 874 | Sell2Home(pObj: cObj); |
| 875 | } |
| 876 | // Do transaction |
| 877 | DoWealth(change: +pObj->GetValue(pInBase: pObj->Contained, iForPlayer: Number)); |
| 878 | // script handling of SellTo |
| 879 | C4ID id = pObj->Def->id; |
| 880 | C4Def *pSellDef = pObj->Def; |
| 881 | if (C4AulScriptFunc *f = pObj->Def->Script.SFn_SellTo) |
| 882 | { |
| 883 | id = f->Exec(pObj, pPars: {C4VInt(iVal: Number)}).getC4ID(); |
| 884 | pSellDef = C4Id2Def(id); |
| 885 | } |
| 886 | // Add to homebase material |
| 887 | if (pSellDef) |
| 888 | { |
| 889 | HomeBaseMaterial.IncreaseIDCount(id, addNewID: !!pSellDef->Rebuyable); |
| 890 | if (Game.Rules & C4RULE_TeamHombase) SyncHomebaseMaterialToTeam(); |
| 891 | } |
| 892 | // Remove object, eject any crew members |
| 893 | if (pObj->Contained) pObj->Exit(); |
| 894 | pObj->Call(PSF_Sale, pPars: {C4VInt(iVal: Number)}); |
| 895 | pObj->AssignRemoval(fExitContents: true); |
| 896 | // Done |
| 897 | return true; |
| 898 | } |
| 899 | |
| 900 | bool C4Player::CanSell(C4Object *const obj) const |
| 901 | { |
| 902 | return !Eliminated && obj && obj->Status && !(obj->OCF & OCF_CrewMember); |
| 903 | } |
| 904 | |
| 905 | bool C4Player::DoWealth(int32_t iChange) |
| 906 | { |
| 907 | Wealth = BoundBy<int32_t>(bval: Wealth + iChange, lbound: 0, rbound: 10000); |
| 908 | if (LocalControl) |
| 909 | { |
| 910 | if (iChange > 0) StartSoundEffect(name: "Cash" ); |
| 911 | if (iChange < 0) StartSoundEffect(name: "UnCash" ); |
| 912 | } |
| 913 | ViewWealth = C4ViewDelay; |
| 914 | return true; |
| 915 | } |
| 916 | |
| 917 | void C4Player::SetViewMode(int32_t iMode, C4Object *pTarget) |
| 918 | { |
| 919 | // safe back |
| 920 | ViewMode = iMode; ViewTarget = pTarget; |
| 921 | } |
| 922 | |
| 923 | void C4Player::ResetCursorView() |
| 924 | { |
| 925 | // reset view to cursor if any cursor exists |
| 926 | if (!ViewCursor && !Cursor) return; |
| 927 | SetViewMode(iMode: C4PVM_Cursor); |
| 928 | } |
| 929 | |
| 930 | void C4Player::Evaluate() |
| 931 | { |
| 932 | // do not evaluate twice |
| 933 | if (Evaluated) return; |
| 934 | |
| 935 | const int32_t SuccessBonus = 100; |
| 936 | |
| 937 | // Set last round |
| 938 | LastRound.Title = Game.Parameters.ScenarioTitle; |
| 939 | time(timer: reinterpret_cast<time_t *>(&LastRound.Date)); |
| 940 | LastRound.Duration = Game.Time; |
| 941 | LastRound.Won = !Eliminated; |
| 942 | // Melee: personal value gain score ...check Game.Objects(C4D_Goal) |
| 943 | if (Game.C4S.Game.IsMelee()) LastRound.Score = std::max<int32_t>(a: ValueGain, b: 0); |
| 944 | // Cooperative: shared score |
| 945 | else LastRound.Score = (std::max)(a: Game.Players.AverageValueGain(), b: 0); |
| 946 | LastRound.Level = 0; // unknown... |
| 947 | LastRound.Bonus = SuccessBonus * LastRound.Won; |
| 948 | LastRound.FinalScore = LastRound.Score + LastRound.Bonus; |
| 949 | LastRound.TotalScore = Score + LastRound.FinalScore; |
| 950 | |
| 951 | // Update player |
| 952 | Rounds++; |
| 953 | if (LastRound.Won) RoundsWon++; else RoundsLost++; |
| 954 | Score = LastRound.TotalScore; |
| 955 | TotalPlayingTime += Game.Time - GameJoinTime; |
| 956 | |
| 957 | // Crew |
| 958 | CrewInfoList.Evaluate(); |
| 959 | |
| 960 | // league |
| 961 | if (Game.Parameters.isLeague()) |
| 962 | EvaluateLeague(fDisconnected: false, fWon: Game.GameOver && !Eliminated); |
| 963 | |
| 964 | // Player is now evaluated |
| 965 | Evaluated = true; |
| 966 | |
| 967 | // round results |
| 968 | Game.RoundResults.EvaluatePlayer(pPlr: this); |
| 969 | } |
| 970 | |
| 971 | void C4Player::Surrender() |
| 972 | { |
| 973 | if (Surrendered) return; |
| 974 | Surrendered = true; |
| 975 | Eliminated = true; |
| 976 | RetireDelay = C4RetireDelay; |
| 977 | StartSoundEffect(name: "Eliminated" ); |
| 978 | Log(id: C4ResStrTableKey::IDS_PRC_PLRSURRENDERED, args: GetName()); |
| 979 | } |
| 980 | |
| 981 | bool C4Player::SetHostility(int32_t iOpponent, int32_t iHostility, bool fSilent) |
| 982 | { |
| 983 | // Check opponent valid |
| 984 | if (!ValidPlr(plr: iOpponent) || (iOpponent == Number)) return false; |
| 985 | // Set hostility |
| 986 | Hostility.SetIDCount(id: iOpponent + 1, count: iHostility, addNewID: true); |
| 987 | // no announce in first frame, or if specified |
| 988 | if (!Game.FrameCounter || fSilent) return true; |
| 989 | // Announce |
| 990 | StartSoundEffect(name: "Trumpet" ); |
| 991 | if (iHostility) |
| 992 | { |
| 993 | Log(id: C4ResStrTableKey::IDS_PLR_HOSTILITY, args: GetName(), args: Game.Players.Get(iPlayer: iOpponent)->GetName()); |
| 994 | } |
| 995 | else |
| 996 | { |
| 997 | Log(id: C4ResStrTableKey::IDS_PLR_NOHOSTILITY, args: GetName(), args: Game.Players.Get(iPlayer: iOpponent)->GetName()); |
| 998 | } |
| 999 | // Success |
| 1000 | return true; |
| 1001 | } |
| 1002 | |
| 1003 | C4Object *C4Player::GetHiRankActiveCrew(bool fSelectOnly) |
| 1004 | { |
| 1005 | C4ObjectLink *clnk; |
| 1006 | C4Object *cobj, *hirank = nullptr; |
| 1007 | int32_t iHighestRank = -2, iRank; |
| 1008 | for (clnk = Crew.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next) |
| 1009 | if (!cobj->CrewDisabled) |
| 1010 | if (!fSelectOnly || cobj->Select) |
| 1011 | { |
| 1012 | if (cobj->Info) iRank = cobj->Info->Rank; else iRank = -1; |
| 1013 | if (!hirank || (iRank > iHighestRank)) |
| 1014 | { |
| 1015 | hirank = cobj; |
| 1016 | iHighestRank = iRank; |
| 1017 | } |
| 1018 | } |
| 1019 | return hirank; |
| 1020 | } |
| 1021 | |
| 1022 | void C4Player::SetTeamHostility() |
| 1023 | { |
| 1024 | // team only |
| 1025 | if (!Team) return; |
| 1026 | // set hostilities |
| 1027 | for (C4Player *pPlr = Game.Players.First; pPlr; pPlr = pPlr->Next) |
| 1028 | if (pPlr != this) |
| 1029 | { |
| 1030 | bool fHostile = (pPlr->Team != Team); |
| 1031 | SetHostility(iOpponent: pPlr->Number, iHostility: fHostile, fSilent: true); |
| 1032 | pPlr->SetHostility(iOpponent: Number, iHostility: fHostile, fSilent: true); |
| 1033 | } |
| 1034 | } |
| 1035 | |
| 1036 | void C4Player::Clear() |
| 1037 | { |
| 1038 | ClearGraphs(); |
| 1039 | Crew.Clear(); |
| 1040 | CrewInfoList.Clear(); |
| 1041 | Menu.Clear(); |
| 1042 | BigIcon.Clear(); |
| 1043 | fFogOfWar = false; bForceFogOfWar = false; |
| 1044 | FoWViewObjs.Clear(); |
| 1045 | fFogOfWarInitialized = false; |
| 1046 | while (pMsgBoardQuery) |
| 1047 | { |
| 1048 | C4MessageBoardQuery *pNext = pMsgBoardQuery->pNext; |
| 1049 | delete pMsgBoardQuery; |
| 1050 | pMsgBoardQuery = pNext; |
| 1051 | } |
| 1052 | delete pGamepad; |
| 1053 | pGamepad = nullptr; |
| 1054 | Status = 0; |
| 1055 | } |
| 1056 | |
| 1057 | void C4Player::Default() |
| 1058 | { |
| 1059 | Filename[0] = 0; |
| 1060 | Status = 0; |
| 1061 | Number = C4P_Number_None; |
| 1062 | ID = 0; |
| 1063 | Team = 0; |
| 1064 | DefaultRuntimeData(); |
| 1065 | Menu.Default(); |
| 1066 | Crew.Default(); |
| 1067 | CrewInfoList.Default(); |
| 1068 | LocalControl = false; |
| 1069 | BigIcon.Default(); |
| 1070 | Next = nullptr; |
| 1071 | fFogOfWar = false; fFogOfWarInitialized = false; |
| 1072 | bForceFogOfWar = false; |
| 1073 | FoWViewObjs.Default(); |
| 1074 | LeagueEvaluated = false; |
| 1075 | GameJoinTime = 0; // overwritten in Init |
| 1076 | pstatControls = pstatActions = nullptr; |
| 1077 | ControlCount = ActionCount = 0; |
| 1078 | AutoContextMenu = ControlStyle = 0; |
| 1079 | LastControlType = PCID_None; |
| 1080 | LastControlID = 0; |
| 1081 | PressedComs = 0; |
| 1082 | pMsgBoardQuery = nullptr; |
| 1083 | pGamepad = nullptr; |
| 1084 | NoEliminationCheck = false; |
| 1085 | Evaluated = false; |
| 1086 | ColorDw = 0; |
| 1087 | } |
| 1088 | |
| 1089 | bool C4Player::Load(const char *szFilename, bool fSavegame, bool fLoadPortraits) |
| 1090 | { |
| 1091 | C4Group hGroup; |
| 1092 | // Open group |
| 1093 | if (!hGroup.Open(szGroupName: szFilename)) return false; |
| 1094 | // Load core |
| 1095 | if (!C4PlayerInfoCore::Load(hGroup)) |
| 1096 | { |
| 1097 | hGroup.Close(); return false; |
| 1098 | } |
| 1099 | // Load BigIcon |
| 1100 | if (hGroup.FindEntry(C4CFN_BigIcon)) BigIcon.Load(hGroup, C4CFN_BigIcon); |
| 1101 | // Load crew info list |
| 1102 | CrewInfoList.Load(hGroup, fLoadPortraits); |
| 1103 | // Close group |
| 1104 | hGroup.Close(); |
| 1105 | // Success |
| 1106 | return true; |
| 1107 | } |
| 1108 | |
| 1109 | bool C4Player::Strip(const char *szFilename, bool fAggressive) |
| 1110 | { |
| 1111 | // Opem group |
| 1112 | C4Group Grp; |
| 1113 | if (!Grp.Open(szGroupName: szFilename)) |
| 1114 | return false; |
| 1115 | // Which type of stripping? |
| 1116 | if (!fAggressive) |
| 1117 | { |
| 1118 | // remove portrais |
| 1119 | Grp.Delete(C4CFN_Portraits, fRecursive: true); |
| 1120 | // remove bigicon, if the file size is too large |
| 1121 | size_t iBigIconSize = 0; |
| 1122 | if (Grp.FindEntry(C4CFN_BigIcon, sFileName: nullptr, iSize: &iBigIconSize)) |
| 1123 | if (iBigIconSize > C4NetResMaxBigicon * 1024) |
| 1124 | Grp.Delete(C4CFN_BigIcon); |
| 1125 | Grp.Close(); |
| 1126 | } |
| 1127 | else |
| 1128 | { |
| 1129 | // Load info core and crew info list |
| 1130 | C4PlayerInfoCore PlrInfoCore; |
| 1131 | C4ObjectInfoList CrewInfoList; |
| 1132 | if (!PlrInfoCore.Load(hGroup&: Grp) || !CrewInfoList.Load(hGroup&: Grp, fLoadPortraits: false)) |
| 1133 | return false; |
| 1134 | // Strip crew info list (remove object infos that are invalid for this scenario) |
| 1135 | CrewInfoList.Strip(rDefs&: Game.Defs); |
| 1136 | // Create a new group that receives the bare essentials |
| 1137 | Grp.Close(); |
| 1138 | if (!EraseItem(szItemName: szFilename) || |
| 1139 | !Grp.Open(szGroupName: szFilename, fCreate: true)) |
| 1140 | return false; |
| 1141 | // Save info core & crew info list to newly-created file |
| 1142 | if (!PlrInfoCore.Save(hGroup&: Grp) || !CrewInfoList.Save(hGroup&: Grp, fSavegame: true, fStoreTiny: true, pDefs: &Game.Defs)) |
| 1143 | return false; |
| 1144 | Grp.Close(); |
| 1145 | } |
| 1146 | return true; |
| 1147 | } |
| 1148 | |
| 1149 | void C4Player::DrawHostility(C4Facet &cgo, int32_t iIndex) |
| 1150 | { |
| 1151 | C4Player *pPlr; |
| 1152 | if (pPlr = Game.Players.GetByIndex(iIndex)) |
| 1153 | { |
| 1154 | // Portrait |
| 1155 | if (Config.Graphics.ShowPortraits && pPlr->BigIcon.Surface) |
| 1156 | pPlr->BigIcon.Draw(cgo); |
| 1157 | // Standard player image |
| 1158 | else |
| 1159 | Game.GraphicsResource.fctCrewClr.DrawClr(cgo, fAspect: true, dwClr: pPlr->ColorDw); |
| 1160 | // Other player and hostile |
| 1161 | if (pPlr != this) |
| 1162 | if (Hostility.GetIDCount(id: pPlr->Number + 1)) |
| 1163 | Game.GraphicsResource.fctMenu.GetPhase(iPhaseX: 7).Draw(cgo); |
| 1164 | } |
| 1165 | } |
| 1166 | |
| 1167 | bool C4Player::MakeCrewMember(C4Object *pObj, bool fForceInfo, bool fDoCalls) |
| 1168 | { |
| 1169 | C4ObjectInfo *cInf = nullptr; |
| 1170 | if (!pObj || !pObj->Def->CrewMember || !pObj->Status) return false; |
| 1171 | |
| 1172 | // only if info is not yet assigned |
| 1173 | if (!pObj->Info && fForceInfo) |
| 1174 | { |
| 1175 | // Find crew info by name |
| 1176 | if (pObj->nInfo) |
| 1177 | cInf = CrewInfoList.GetIdle(szByName: pObj->nInfo.getData()); |
| 1178 | |
| 1179 | // Set name source |
| 1180 | const char *cpNames = nullptr; |
| 1181 | if (pObj->Def->pClonkNames) cpNames = pObj->Def->pClonkNames->GetData(); |
| 1182 | if (!cpNames) cpNames = Game.Names.GetData(); |
| 1183 | |
| 1184 | // Find crew info by id |
| 1185 | if (!cInf) |
| 1186 | while (!(cInf = CrewInfoList.GetIdle(c_id: pObj->id, rDefs&: Game.Defs))) |
| 1187 | if (!CrewInfoList.New(n_id: pObj->id, pDefs: &Game.Defs, cpNames)) |
| 1188 | return false; |
| 1189 | |
| 1190 | // Set object info |
| 1191 | pObj->Info = cInf; |
| 1192 | } |
| 1193 | |
| 1194 | // Add to crew |
| 1195 | if (!Crew.GetLink(pObj)) |
| 1196 | Crew.Add(nObj: pObj, eSort: C4ObjectList::stMain); |
| 1197 | |
| 1198 | // add plr view |
| 1199 | if (!pObj->PlrViewRange) pObj->SetPlrViewRange(C4FOW_Def_View_RangeX); else pObj->PlrFoWActualize(); |
| 1200 | |
| 1201 | // controlled by the player |
| 1202 | pObj->Controller = Number; |
| 1203 | |
| 1204 | // OnJoinCrew callback |
| 1205 | if (fDoCalls) |
| 1206 | { |
| 1207 | pObj->Call(PSF_OnJoinCrew, pPars: {C4VInt(iVal: Number)}); |
| 1208 | } |
| 1209 | |
| 1210 | return true; |
| 1211 | } |
| 1212 | |
| 1213 | void C4Player::ExecuteControl() |
| 1214 | { |
| 1215 | // LastCom |
| 1216 | if (LastCom != COM_None) |
| 1217 | { |
| 1218 | // Advance delay counter |
| 1219 | LastComDelay++; |
| 1220 | // Check for COM_Single com (after delay) |
| 1221 | if (LastComDelay > C4DoubleClick) |
| 1222 | { |
| 1223 | // Pass additional COM_Single com (unless it already was a single com in the first place) |
| 1224 | if (!(LastCom & COM_Single)) |
| 1225 | DirectCom(byCom: LastCom | COM_Single, iData: 0); // Currently, com data is not stored for single coms... |
| 1226 | LastCom = COM_None; |
| 1227 | LastComDelay = 0; |
| 1228 | } |
| 1229 | } |
| 1230 | |
| 1231 | // LastComDownDouble |
| 1232 | if (LastComDownDouble > 0) LastComDownDouble--; |
| 1233 | } |
| 1234 | |
| 1235 | void C4Player::AdjustCursorCommand() |
| 1236 | { |
| 1237 | // Reset view |
| 1238 | ResetCursorView(); |
| 1239 | // Set cursor to hirank Select clonk |
| 1240 | C4Object *pHiRank = nullptr; |
| 1241 | // Find hirank Select |
| 1242 | pHiRank = GetHiRankActiveCrew(fSelectOnly: true); |
| 1243 | // If none, check non-Selects as well |
| 1244 | if (!pHiRank) |
| 1245 | pHiRank = GetHiRankActiveCrew(fSelectOnly: false); |
| 1246 | // The cursor is on someone else: set the cursor to the hirank |
| 1247 | C4Object *pPrev = Cursor; |
| 1248 | if (Cursor != pHiRank) |
| 1249 | { |
| 1250 | Cursor = pHiRank; |
| 1251 | UpdateView(); |
| 1252 | } |
| 1253 | // UnSelect previous cursor |
| 1254 | if (pPrev && pPrev != Cursor) pPrev->UnSelect(fCursor: true); |
| 1255 | // We have a cursor: do select it |
| 1256 | if (Cursor) Cursor->DoSelect(); |
| 1257 | // Updates |
| 1258 | CursorFlash = 30; |
| 1259 | } |
| 1260 | |
| 1261 | void C4Player::CursorRight() |
| 1262 | { |
| 1263 | C4ObjectLink *cLnk; |
| 1264 | // Get next crew member |
| 1265 | if (cLnk = Crew.GetLink(pObj: Cursor)) |
| 1266 | for (cLnk = cLnk->Next; cLnk; cLnk = cLnk->Next) |
| 1267 | if (cLnk->Obj->Status && !cLnk->Obj->CrewDisabled) break; |
| 1268 | if (!cLnk) |
| 1269 | for (cLnk = Crew.First; cLnk; cLnk = cLnk->Next) |
| 1270 | if (cLnk->Obj->Status && !cLnk->Obj->CrewDisabled) break; |
| 1271 | if (cLnk) SetCursor(pObj: cLnk->Obj, fSelectFlash: false, fSelectArrow: true); |
| 1272 | // Updates |
| 1273 | CursorFlash = 30; |
| 1274 | CursorSelection = 1; |
| 1275 | UpdateView(); |
| 1276 | } |
| 1277 | |
| 1278 | void C4Player::CursorLeft() |
| 1279 | { |
| 1280 | C4ObjectLink *cLnk; |
| 1281 | // Get prev crew member |
| 1282 | if (cLnk = Crew.GetLink(pObj: Cursor)) |
| 1283 | for (cLnk = cLnk->Prev; cLnk; cLnk = cLnk->Prev) |
| 1284 | if (cLnk->Obj->Status && !cLnk->Obj->CrewDisabled) break; |
| 1285 | if (!cLnk) |
| 1286 | for (cLnk = Crew.Last; cLnk; cLnk = cLnk->Prev) |
| 1287 | if (cLnk->Obj->Status && !cLnk->Obj->CrewDisabled) break; |
| 1288 | if (cLnk) SetCursor(pObj: cLnk->Obj, fSelectFlash: false, fSelectArrow: true); |
| 1289 | // Updates |
| 1290 | CursorFlash = 30; |
| 1291 | CursorSelection = 1; |
| 1292 | UpdateView(); |
| 1293 | } |
| 1294 | |
| 1295 | void C4Player::UnselectCrew() |
| 1296 | { |
| 1297 | C4Object *cObj; C4ObjectLink *cLnk; bool fCursorDeselected = false; |
| 1298 | for (cLnk = Crew.First; cLnk && (cObj = cLnk->Obj); cLnk = cLnk->Next) |
| 1299 | { |
| 1300 | if (Cursor == cObj) fCursorDeselected = true; |
| 1301 | if (cObj->Status) |
| 1302 | cObj->UnSelect(); |
| 1303 | } |
| 1304 | // if cursor is outside crew (done by some scenarios), unselect that one, too! (script callback) |
| 1305 | if (Cursor && !fCursorDeselected) Cursor->UnSelect(); |
| 1306 | } |
| 1307 | |
| 1308 | void C4Player::SelectSingleByCursor() |
| 1309 | { |
| 1310 | // Unselect crew |
| 1311 | UnselectCrew(); |
| 1312 | // Select cursor |
| 1313 | if (Cursor) Cursor->DoSelect(); |
| 1314 | // Updates |
| 1315 | SelectFlash = 30; |
| 1316 | AdjustCursorCommand(); |
| 1317 | } |
| 1318 | |
| 1319 | void C4Player::CursorToggle() |
| 1320 | { |
| 1321 | C4ObjectLink *clnk; |
| 1322 | // Selection mode: toggle cursor select |
| 1323 | if (CursorSelection) |
| 1324 | { |
| 1325 | if (Cursor) |
| 1326 | if (Cursor->Select) Cursor->UnSelect(); else Cursor->DoSelect(); |
| 1327 | CursorToggled = 1; |
| 1328 | } |
| 1329 | // Pure toggle: toggle all Select |
| 1330 | else |
| 1331 | { |
| 1332 | for (clnk = Crew.First; clnk; clnk = clnk->Next) |
| 1333 | if (!clnk->Obj->CrewDisabled) |
| 1334 | if (clnk->Obj->Select) clnk->Obj->UnSelect(); else clnk->Obj->DoSelect(); |
| 1335 | AdjustCursorCommand(); |
| 1336 | } |
| 1337 | // Updates |
| 1338 | SelectFlash = 30; |
| 1339 | } |
| 1340 | |
| 1341 | void C4Player::SelectAllCrew() |
| 1342 | { |
| 1343 | C4ObjectLink *clnk; |
| 1344 | // Select all crew |
| 1345 | for (clnk = Crew.First; clnk; clnk = clnk->Next) |
| 1346 | clnk->Obj->DoSelect(); |
| 1347 | // Updates |
| 1348 | AdjustCursorCommand(); |
| 1349 | CursorSelection = CursorToggled = 0; |
| 1350 | SelectFlash = 30; |
| 1351 | // Game display |
| 1352 | if (LocalControl) StartSoundEffect(name: "Ding" ); |
| 1353 | } |
| 1354 | |
| 1355 | void C4Player::UpdateSelectionToggleStatus() |
| 1356 | { |
| 1357 | if (CursorSelection) |
| 1358 | // Select toggled: cursor to hirank |
| 1359 | if (CursorToggled) |
| 1360 | AdjustCursorCommand(); |
| 1361 | // Cursor select only: single control |
| 1362 | else |
| 1363 | SelectSingleByCursor(); |
| 1364 | CursorSelection = 0; |
| 1365 | CursorToggled = 0; |
| 1366 | } |
| 1367 | |
| 1368 | bool C4Player::ObjectCom(uint8_t byCom, int32_t iData) // By DirectCom |
| 1369 | { |
| 1370 | if (Eliminated) return false; |
| 1371 | #ifdef DEBUGREC_OBJCOM |
| 1372 | C4RCObjectCom rc = { byCom, iData, Number }; |
| 1373 | AddDbgRec(RCT_PlrCom, &rc, sizeof(C4RCObjectCom)); |
| 1374 | #endif |
| 1375 | // Hide startup |
| 1376 | ShowStartup = false; |
| 1377 | // If regular com, update cursor & selection status |
| 1378 | if (!(byCom & COM_Single) && !(byCom & COM_Double) && (byCom < COM_ReleaseFirst || byCom > COM_ReleaseLast)) |
| 1379 | UpdateSelectionToggleStatus(); |
| 1380 | // Apply direct com to cursor object |
| 1381 | if (Cursor) |
| 1382 | { |
| 1383 | // update controller |
| 1384 | Cursor->Controller = Number; |
| 1385 | // send com |
| 1386 | Cursor->DirectCom(byCom, iData); |
| 1387 | } |
| 1388 | // Done |
| 1389 | return true; |
| 1390 | } |
| 1391 | |
| 1392 | void C4Player::ClearPressedComsSynced() |
| 1393 | { |
| 1394 | Game.Input.Add(eType: CID_PlrControl, pCtrl: new C4ControlPlayerControl(Number, COM_ClearPressedComs, 0)); |
| 1395 | } |
| 1396 | |
| 1397 | bool C4Player::ObjectCommand(int32_t iCommand, C4Object *pTarget, int32_t iX, int32_t iY, C4Object *pTarget2, int32_t iData, int32_t iMode) |
| 1398 | { |
| 1399 | // Eliminated |
| 1400 | if (Eliminated) return false; |
| 1401 | // Hide startup |
| 1402 | if (ShowStartup) ShowStartup = false; |
| 1403 | // Update selection & toggle status |
| 1404 | UpdateSelectionToggleStatus(); |
| 1405 | // Apply to all selected crew members (in cursor range) except pTarget. |
| 1406 | // Set, Add, Append mode flags may be combined and have a priority order. |
| 1407 | C4ObjectLink *cLnk; C4Object *cObj; bool fCursorProcessed = false; |
| 1408 | for (cLnk = Crew.First; cLnk && (cObj = cLnk->Obj); cLnk = cLnk->Next) |
| 1409 | { |
| 1410 | if (cObj == Cursor) fCursorProcessed = true; |
| 1411 | if (cObj->Status) if (cObj->Select) |
| 1412 | if (!(iMode & C4P_Command_Range) || (Cursor && Inside<int32_t>(ival: cObj->x - Cursor->x, lbound: -15, rbound: +15) && Inside<int32_t>(ival: cObj->y - Cursor->y, lbound: -15, rbound: +15))) |
| 1413 | if (cObj != pTarget) |
| 1414 | { |
| 1415 | // Put command with unspecified put object |
| 1416 | if ((iCommand == C4CMD_Put) && !pTarget2) |
| 1417 | { |
| 1418 | // Special: the put command is only applied by this function if the clonk actually |
| 1419 | // has something to put. Also, the put count is adjusted to that the clonk will not try to put |
| 1420 | // more items than he actually has. This workaround is needed so the put command can be used |
| 1421 | // to tell all selected clonks to put when in a container, simulating the old all-throw behavior. |
| 1422 | if (cObj->Contents.ObjectCount(id: iData)) |
| 1423 | ObjectCommand2Obj(cObj, iCommand, pTarget, iX: std::min<int32_t>(a: iX, b: cObj->Contents.ObjectCount(id: iData)), iY, pTarget2, iData, iMode); |
| 1424 | } |
| 1425 | // Other command |
| 1426 | else |
| 1427 | ObjectCommand2Obj(cObj, iCommand, pTarget, iX, iY, pTarget2, iData, iMode); |
| 1428 | // don't issue multiple Construct-commands - store previous Clonk as target for next command object |
| 1429 | // note that if three Clonks get the command, and the middle one gets deleted (the Clonk, not the command), the third will start to build anyway |
| 1430 | // that is very unlikely, though...could be catched in ClearPointers, if need be? |
| 1431 | // also, if one Clonk of the chain aborts his command (controlled elsewhere, for instance), the following ones will fail their commands |
| 1432 | // It's not a perfect solution, after all. But certainly better than placing tons of construction sites in the first place |
| 1433 | if (iCommand == C4CMD_Construct) pTarget = cObj; |
| 1434 | } |
| 1435 | } |
| 1436 | // Always apply to cursor, even if it's not in the crew |
| 1437 | if (Cursor && !fCursorProcessed) |
| 1438 | if (Cursor->Status && Cursor != pTarget) |
| 1439 | ObjectCommand2Obj(cObj: Cursor, iCommand, pTarget, iX, iY, pTarget2, iData, iMode); |
| 1440 | |
| 1441 | // Success |
| 1442 | return true; |
| 1443 | } |
| 1444 | |
| 1445 | void C4Player::ObjectCommand2Obj(C4Object *cObj, int32_t iCommand, C4Object *pTarget, int32_t iX, int32_t iY, C4Object *pTarget2, int32_t iData, int32_t iMode) |
| 1446 | { |
| 1447 | // forward to object |
| 1448 | if (iMode & C4P_Command_Append) cObj->AddCommand(iCommand, pTarget, iTx: iX, iTy: iY, iUpdateInterval: 0, pTarget2, fInitEvaluation: true, iData, fAppend: true, iRetries: 0, szText: nullptr, iBaseMode: C4CMD_Mode_Base); // append: by Shift-click and for dragging of multiple objects (all independent; thus C4CMD_Mode_Base) |
| 1449 | else if (iMode & C4P_Command_Add) cObj->AddCommand(iCommand, pTarget, iTx: iX, iTy: iY, iUpdateInterval: 0, pTarget2, fInitEvaluation: true, iData, fAppend: false, iRetries: 0, szText: nullptr, iBaseMode: C4CMD_Mode_Base); // append: by context menu and keyboard throw command (all independent; thus C4CMD_Mode_Base) |
| 1450 | else if (iMode & C4P_Command_Set) cObj->SetCommand(iCommand, pTarget, iTx: iX, iTy: iY, pTarget2, fControl: true, iData); |
| 1451 | } |
| 1452 | |
| 1453 | void C4Player::DirectCom(uint8_t byCom, int32_t iData) // By InCom or ExecuteControl |
| 1454 | { |
| 1455 | switch (byCom) |
| 1456 | { |
| 1457 | case COM_CursorLeft: |
| 1458 | case COM_CursorLeft_D: |
| 1459 | case COM_CursorRight: |
| 1460 | case COM_CursorRight_D: |
| 1461 | case COM_CursorToggle: |
| 1462 | case COM_CursorToggle_D: |
| 1463 | if (!Eliminated && Cursor) |
| 1464 | { |
| 1465 | Cursor->Controller = Number; |
| 1466 | if (Cursor->CallControl(pPlr: this, byCom)) |
| 1467 | { |
| 1468 | if (!(byCom & COM_Double)) |
| 1469 | { |
| 1470 | UpdateSelectionToggleStatus(); |
| 1471 | } |
| 1472 | return; |
| 1473 | } |
| 1474 | } |
| 1475 | break; |
| 1476 | default: |
| 1477 | break; |
| 1478 | } |
| 1479 | switch (byCom) |
| 1480 | { |
| 1481 | case COM_CursorLeft: case COM_CursorLeft_D: CursorLeft(); break; |
| 1482 | case COM_CursorRight: case COM_CursorRight_D: CursorRight(); break; |
| 1483 | case COM_CursorToggle: CursorToggle(); break; |
| 1484 | case COM_CursorToggle_D: SelectAllCrew(); break; |
| 1485 | |
| 1486 | default: ObjectCom(byCom, iData); break; |
| 1487 | } |
| 1488 | } |
| 1489 | |
| 1490 | void C4Player::InCom(uint8_t byCom, int32_t iData) |
| 1491 | { |
| 1492 | #ifdef DEBUGREC_OBJCOM |
| 1493 | C4RCObjectCom rc = { byCom, iData, Number }; |
| 1494 | AddDbgRec(RCT_PlrInCom, &rc, sizeof(C4RCObjectCom)); |
| 1495 | #endif |
| 1496 | if (byCom == COM_ClearPressedComs) |
| 1497 | { |
| 1498 | PressedComs = 0; |
| 1499 | LastCom = COM_None; |
| 1500 | return; |
| 1501 | } |
| 1502 | // Cursor object menu active: convert regular com to menu com |
| 1503 | if (Cursor) if (Cursor->Menu) |
| 1504 | { |
| 1505 | int32_t iCom = byCom; |
| 1506 | Cursor->Menu->ConvertCom(rCom&: iCom, rData&: iData, fAsyncConversion: false); |
| 1507 | byCom = iCom; |
| 1508 | } |
| 1509 | // Menu control: no single/double processing |
| 1510 | if (Inside(ival: byCom, lbound: COM_MenuFirst, rbound: COM_MenuLast)) |
| 1511 | { |
| 1512 | DirectCom(byCom, iData); return; |
| 1513 | } |
| 1514 | // Ignore KeyRelease for Single/Double |
| 1515 | if (!Inside(ival: byCom, lbound: COM_ReleaseFirst, rbound: COM_ReleaseLast)) |
| 1516 | { |
| 1517 | // Reset view |
| 1518 | ResetCursorView(); |
| 1519 | // Update state |
| 1520 | if (Inside<int>(ival: byCom, lbound: COM_ReleaseFirst - 16, rbound: COM_ReleaseLast - 16)) |
| 1521 | PressedComs |= 1 << byCom; |
| 1522 | // Check LastCom buffer for prior COM_Single |
| 1523 | if (LastCom != COM_None) |
| 1524 | if (LastCom != byCom) |
| 1525 | { |
| 1526 | DirectCom(byCom: LastCom | COM_Single, iData); |
| 1527 | // AutoStopControl uses a single COM_Down instead of DOM_Down_D for drop |
| 1528 | // So a COM_Down_S does what a COM_Down_D normally does, if generated by another key |
| 1529 | // instead of a timeout |
| 1530 | if (ControlStyle && LastCom == COM_Down) LastComDownDouble = C4DoubleClick; |
| 1531 | } |
| 1532 | // Check LastCom buffer for COM_Double |
| 1533 | if (LastCom == byCom) byCom |= COM_Double; |
| 1534 | // LastCom/Del process |
| 1535 | // this is set before issuing the DirectCom, so DirectCom-scripts may delete it |
| 1536 | LastCom = byCom; LastComDelay = 0; |
| 1537 | } |
| 1538 | else |
| 1539 | { |
| 1540 | // Update state |
| 1541 | if (Inside(ival: byCom, lbound: COM_ReleaseFirst, rbound: COM_ReleaseLast)) |
| 1542 | { |
| 1543 | if (!(PressedComs & (1 << (byCom - 16)))) |
| 1544 | { |
| 1545 | return; |
| 1546 | } |
| 1547 | PressedComs &= ~(1 << (byCom - 16)); |
| 1548 | } |
| 1549 | } |
| 1550 | // Pass regular/COM_Double byCom to player |
| 1551 | DirectCom(byCom, iData); |
| 1552 | // LastComDownDouble process |
| 1553 | if (byCom == COM_Down_D) LastComDownDouble = C4DoubleClick; |
| 1554 | } |
| 1555 | |
| 1556 | void C4Player::CompileFunc(StdCompiler *pComp) |
| 1557 | { |
| 1558 | assert(ID); |
| 1559 | |
| 1560 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Status, szName: "Status" , rDefault: 0)); |
| 1561 | pComp->Value(rStruct: mkNamingAdapt(rValue&: AtClient, szName: "AtClient" , rDefault: C4ClientIDUnknown)); |
| 1562 | pComp->Value(rStruct: mkNamingAdapt(toC4CStr(AtClientName), szName: "AtClientName" , rDefault: "Local" )); |
| 1563 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Number, szName: "Index" , rDefault: C4P_Number_None)); |
| 1564 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ID, szName: "ID" , rDefault: 0)); |
| 1565 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Eliminated, szName: "Eliminated" , rDefault: 0)); |
| 1566 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Surrendered, szName: "Surrendered" , rDefault: 0)); |
| 1567 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Evaluated, szName: "Evaluated" , rDefault: false)); |
| 1568 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Color, szName: "Color" , rDefault: -1)); |
| 1569 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ColorDw, szName: "ColorDw" , rDefault: 0u)); |
| 1570 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Control, szName: "Control" , rDefault: 0)); |
| 1571 | pComp->Value(rStruct: mkNamingAdapt(rValue&: MouseControl, szName: "MouseControl" , rDefault: 0)); |
| 1572 | pComp->Value(rStruct: mkNamingAdapt(rValue&: AutoContextMenu, szName: "AutoContextMenu" , rDefault: 0)); |
| 1573 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ControlStyle, szName: "AutoStopControl" , rDefault: 0)); |
| 1574 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Position, szName: "Position" , rDefault: 0)); |
| 1575 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ViewMode, szName: "ViewMode" , rDefault: C4PVM_Cursor)); |
| 1576 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ViewX, szName: "ViewX" , rDefault: 0)); |
| 1577 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ViewY, szName: "ViewY" , rDefault: 0)); |
| 1578 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ViewWealth, szName: "ViewWealth" , rDefault: 0)); |
| 1579 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ViewValue, szName: "ViewValue" , rDefault: 0)); |
| 1580 | pComp->Value(rStruct: mkNamingAdapt(rValue&: fFogOfWar, szName: "FogOfWar" , rDefault: false)); |
| 1581 | pComp->Value(rStruct: mkNamingAdapt(rValue&: bForceFogOfWar, szName: "ForceFogOfWar" , rDefault: false)); |
| 1582 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ShowStartup, szName: "ShowStartup" , rDefault: false)); |
| 1583 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ShowControl, szName: "ShowControl" , rDefault: 0)); |
| 1584 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ShowControlPos, szName: "ShowControlPos" , rDefault: 0)); |
| 1585 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Wealth, szName: "Wealth" , rDefault: 0)); |
| 1586 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Points, szName: "Points" , rDefault: 0)); |
| 1587 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Value, szName: "Value" , rDefault: 0)); |
| 1588 | pComp->Value(rStruct: mkNamingAdapt(rValue&: InitialValue, szName: "InitialValue" , rDefault: 0)); |
| 1589 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ValueGain, szName: "ValueGain" , rDefault: 0)); |
| 1590 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ObjectsOwned, szName: "ObjectsOwned" , rDefault: 0)); |
| 1591 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Hostility, szName: "Hostile" )); |
| 1592 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ProductionDelay, szName: "ProductionDelay" , rDefault: 0)); |
| 1593 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ProductionUnit, szName: "ProductionUnit" , rDefault: 0)); |
| 1594 | pComp->Value(rStruct: mkNamingAdapt(rValue&: SelectCount, szName: "SelectCount" , rDefault: 0)); |
| 1595 | pComp->Value(rStruct: mkNamingAdapt(rValue&: SelectFlash, szName: "SelectFlash" , rDefault: 0)); |
| 1596 | pComp->Value(rStruct: mkNamingAdapt(rValue&: CursorFlash, szName: "CursorFlash" , rDefault: 0)); |
| 1597 | pComp->Value(rStruct: mkNamingAdapt(rValue&: reinterpret_cast<int32_t &>(Cursor), szName: "Cursor" , rDefault: 0)); |
| 1598 | pComp->Value(rStruct: mkNamingAdapt(rValue&: reinterpret_cast<int32_t &>(ViewCursor), szName: "ViewCursor" , rDefault: 0)); |
| 1599 | pComp->Value(rStruct: mkNamingAdapt(rValue&: reinterpret_cast<int32_t &>(Captain), szName: "Captain" , rDefault: 0)); |
| 1600 | pComp->Value(rStruct: mkNamingAdapt(rValue&: LastCom, szName: "LastCom" , rDefault: 0)); |
| 1601 | pComp->Value(rStruct: mkNamingAdapt(rValue&: LastComDelay, szName: "LastComDel" , rDefault: 0)); |
| 1602 | pComp->Value(rStruct: mkNamingAdapt(rValue&: PressedComs, szName: "PressedComs" , rDefault: 0)); |
| 1603 | pComp->Value(rStruct: mkNamingAdapt(rValue&: LastComDownDouble, szName: "LastComDownDouble" , rDefault: 0)); |
| 1604 | pComp->Value(rStruct: mkNamingAdapt(rValue&: CursorSelection, szName: "CursorSelection" , rDefault: 0)); |
| 1605 | pComp->Value(rStruct: mkNamingAdapt(rValue&: CursorToggled, szName: "CursorToggled" , rDefault: 0)); |
| 1606 | pComp->Value(rStruct: mkNamingAdapt(rValue&: MessageStatus, szName: "MessageStatus" , rDefault: 0)); |
| 1607 | pComp->Value(rStruct: mkNamingAdapt(toC4CStr(MessageBuf), szName: "MessageBuf" , rDefault: "" )); |
| 1608 | pComp->Value(rStruct: mkNamingAdapt(rValue&: HomeBaseMaterial, szName: "HomeBaseMaterial" )); |
| 1609 | pComp->Value(rStruct: mkNamingAdapt(rValue&: HomeBaseProduction, szName: "HomeBaseProduction" )); |
| 1610 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Knowledge, szName: "Knowledge" )); |
| 1611 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Magic, szName: "Magic" )); |
| 1612 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Crew, szName: "Crew" )); |
| 1613 | pComp->Value(rStruct: mkNamingAdapt(rValue&: CrewInfoList.iNumCreated, szName: "CrewCreated" , rDefault: 0)); |
| 1614 | pComp->Value(rStruct: mkNamingPtrAdapt(rpObj&: pMsgBoardQuery, szNaming: "MsgBoardQueries" )); |
| 1615 | } |
| 1616 | |
| 1617 | bool C4Player::LoadRuntimeData(C4Group &hGroup) |
| 1618 | { |
| 1619 | const char *pSource; |
| 1620 | // Use loaded game text component |
| 1621 | if (!(pSource = Game.GameText.GetData())) return false; |
| 1622 | // safety: Do nothing if playeer section is not even present (could kill initialized values) |
| 1623 | if (!SSearch(szString: pSource, szIndex: std::format(fmt: "[Player{}]" , args&: ID).c_str())) return false; |
| 1624 | // Compile (Search player section - runtime data is stored by unique player ID) |
| 1625 | assert(ID); |
| 1626 | if (!CompileFromBuf_LogWarn<StdCompilerINIRead>( |
| 1627 | TargetStruct: mkNamingAdapt(rValue&: *this, szName: std::format(fmt: "Player{}" , args&: ID).c_str()), |
| 1628 | SrcBuf: StdStrBuf::MakeRef(str: pSource), |
| 1629 | szName: Game.GameText.GetFilePath())) |
| 1630 | return false; |
| 1631 | // Denumerate pointers |
| 1632 | DenumeratePointers(); |
| 1633 | // Success |
| 1634 | return true; |
| 1635 | } |
| 1636 | |
| 1637 | void C4Player::ExecHomeBaseProduction() |
| 1638 | { |
| 1639 | const int32_t MaxHomeBaseProduction = 25; |
| 1640 | ProductionDelay++; |
| 1641 | if (ProductionDelay >= 60) // Minute Production Unit |
| 1642 | { |
| 1643 | // team home base: Only execute production on first player of team |
| 1644 | C4Team *pTeam; |
| 1645 | if (Game.Rules & C4RULE_TeamHombase) |
| 1646 | if (pTeam = Game.Teams.GetTeamByID(iID: Team)) |
| 1647 | if (pTeam->GetFirstActivePlayerID() != ID) |
| 1648 | // other players will be synced by first player |
| 1649 | return; |
| 1650 | // do production |
| 1651 | bool fAnyChange = false; |
| 1652 | ProductionDelay = 0; ProductionUnit++; |
| 1653 | for (int32_t cnt = 0; HomeBaseProduction.GetID(index: cnt); cnt++) |
| 1654 | if (HomeBaseProduction.GetCount(index: cnt) > 0) |
| 1655 | if (ProductionUnit % BoundBy<int32_t>(bval: 11 - HomeBaseProduction.GetCount(index: cnt), lbound: 1, rbound: 10) == 0) |
| 1656 | if (HomeBaseMaterial.GetIDCount(id: HomeBaseProduction.GetID(index: cnt)) < MaxHomeBaseProduction) |
| 1657 | { |
| 1658 | HomeBaseMaterial.IncreaseIDCount(id: HomeBaseProduction.GetID(index: cnt)); |
| 1659 | fAnyChange = true; |
| 1660 | } |
| 1661 | // All team members get same production if rule is active |
| 1662 | if (fAnyChange) if (Game.Rules & C4RULE_TeamHombase) |
| 1663 | SyncHomebaseMaterialToTeam(); |
| 1664 | } |
| 1665 | } |
| 1666 | |
| 1667 | void C4Player::UpdateCounts() |
| 1668 | { |
| 1669 | int32_t nclkcnt, nselcnt; |
| 1670 | C4Object *cobj; C4ObjectLink *clnk; |
| 1671 | nclkcnt = nselcnt = 0; |
| 1672 | for (clnk = Crew.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next) |
| 1673 | { |
| 1674 | nclkcnt++; if (cobj->Select) nselcnt++; |
| 1675 | } |
| 1676 | if (CrewCnt != nclkcnt) CrewCnt = nclkcnt; |
| 1677 | if (SelectCount != nselcnt) SelectCount = nselcnt; |
| 1678 | } |
| 1679 | |
| 1680 | void C4Player::CheckElimination() |
| 1681 | { |
| 1682 | // Standard elimination: no crew |
| 1683 | if (CrewCnt <= 0) |
| 1684 | // Already eliminated safety |
| 1685 | if (!Eliminated) |
| 1686 | // No automatic elimination desired? |
| 1687 | if (!NoEliminationCheck) |
| 1688 | // Do elimination! |
| 1689 | Eliminate(); |
| 1690 | } |
| 1691 | |
| 1692 | void C4Player::UpdateView() |
| 1693 | { |
| 1694 | // view target/cursor |
| 1695 | switch (ViewMode) |
| 1696 | { |
| 1697 | case C4PVM_Cursor: |
| 1698 | { |
| 1699 | C4Object *pViewObj; |
| 1700 | if (!(pViewObj = ViewCursor)) pViewObj = Cursor; |
| 1701 | if (pViewObj) |
| 1702 | { |
| 1703 | ViewX = pViewObj->x; ViewY = pViewObj->y; |
| 1704 | } |
| 1705 | break; |
| 1706 | } |
| 1707 | case C4PVM_Target: |
| 1708 | if (ViewTarget) |
| 1709 | { |
| 1710 | ViewX = ViewTarget->x; ViewY = ViewTarget->y; |
| 1711 | } |
| 1712 | break; |
| 1713 | case C4PVM_Scrolling: |
| 1714 | break; |
| 1715 | } |
| 1716 | } |
| 1717 | |
| 1718 | void C4Player::DefaultRuntimeData() |
| 1719 | { |
| 1720 | Status = 0; |
| 1721 | Eliminated = 0; |
| 1722 | Surrendered = 0; |
| 1723 | AtClient = C4ClientIDUnknown; |
| 1724 | SCopy(szSource: "Local" , sTarget: AtClientName); |
| 1725 | Color = -1; |
| 1726 | Control = C4P_Control_None; |
| 1727 | MouseControl = false; |
| 1728 | Position = -1; |
| 1729 | PlrStartIndex = 0; |
| 1730 | RetireDelay = 0; |
| 1731 | ViewMode = C4PVM_Cursor; |
| 1732 | ViewX = ViewY = 0; |
| 1733 | ViewTarget = nullptr; |
| 1734 | CursorSelection = CursorToggled = 0; |
| 1735 | ShowStartup = true; |
| 1736 | CrewCnt = 0; |
| 1737 | ViewWealth = ViewValue = 0; |
| 1738 | ShowControl = ShowControlPos = 0; |
| 1739 | Wealth = 0; |
| 1740 | Points = 0; |
| 1741 | Value = InitialValue = ValueGain = 0; |
| 1742 | ObjectsOwned = 0; |
| 1743 | Captain = nullptr; |
| 1744 | ProductionDelay = ProductionUnit = 0; |
| 1745 | Cursor = ViewCursor = nullptr; |
| 1746 | SelectCount = 0; |
| 1747 | SelectFlash = CursorFlash = 30; |
| 1748 | LastCom = 0; |
| 1749 | LastComDelay = 0; |
| 1750 | LastComDownDouble = 0; |
| 1751 | CursorSelection = CursorToggled = 0; |
| 1752 | MessageStatus = 0; |
| 1753 | MessageBuf[0] = 0; |
| 1754 | Hostility.Clear(); |
| 1755 | HomeBaseMaterial.Clear(); |
| 1756 | HomeBaseProduction.Clear(); |
| 1757 | Knowledge.Clear(); |
| 1758 | Magic.Clear(); |
| 1759 | FlashCom = 0; |
| 1760 | } |
| 1761 | |
| 1762 | bool C4Player::(bool fFromMain) |
| 1763 | { |
| 1764 | // Menu symbol/init |
| 1765 | bool fSwitch = !(Status == PS_TeamSelection); |
| 1766 | Menu.InitRefSym(fctSymbol: C4GUI::Icon::GetIconFacet(icoIconIndex: C4GUI::Ico_Team), szEmpty: LoadResStr(id: C4ResStrTableKey::IDS_MSG_SELTEAM), iPlayer: Number, iExtra: C4MN_Extra_None, iExtraData: 0, iId: fSwitch ? C4MN_TeamSwitch : C4MN_TeamSelection); |
| 1767 | Menu.SetAlignment(fSwitch ? C4MN_Align_Left | C4MN_Align_Bottom : 0); |
| 1768 | Menu.Refill(); |
| 1769 | // Go back to options menu on close |
| 1770 | if (fFromMain) Menu.SetCloseCommand("ActivateMenu:Main" ); |
| 1771 | return true; |
| 1772 | } |
| 1773 | |
| 1774 | void C4Player::DoTeamSelection(int32_t idTeam) |
| 1775 | { |
| 1776 | // stop team selection. This might close the menu forever if the control gets lost |
| 1777 | // let's hope it doesn't! |
| 1778 | Status = PS_TeamSelectionPending; |
| 1779 | Game.Control.DoInput(eCtrlType: CID_InitScenarioPlayer, pPkt: new C4ControlInitScenarioPlayer(Number, idTeam), eDelivery: CDT_Queue); |
| 1780 | } |
| 1781 | |
| 1782 | void C4Player::EnumeratePointers() |
| 1783 | { |
| 1784 | EnumerateObjectPtrs(args&: Cursor, args&: ViewCursor, args&: Captain); |
| 1785 | for (C4MessageBoardQuery *pCheck = pMsgBoardQuery; pCheck; pCheck = pCheck->pNext) |
| 1786 | pCheck->pCallbackObj.Enumerate(); |
| 1787 | } |
| 1788 | |
| 1789 | void C4Player::DenumeratePointers() |
| 1790 | { |
| 1791 | DenumerateObjectPtrs(args&: Cursor, args&: ViewCursor, args&: Captain); |
| 1792 | // Crew |
| 1793 | Crew.DenumerateRead(); |
| 1794 | // messageboard-queries |
| 1795 | for (C4MessageBoardQuery *pCheck = pMsgBoardQuery; pCheck; pCheck = pCheck->pNext) |
| 1796 | pCheck->pCallbackObj.Denumerate(); |
| 1797 | } |
| 1798 | |
| 1799 | void C4Player::RemoveCrewObjects() |
| 1800 | { |
| 1801 | C4Object *pCrew; |
| 1802 | |
| 1803 | // Remove all crew objects |
| 1804 | while (pCrew = Crew.GetObject()) pCrew->AssignRemoval(fExitContents: true); |
| 1805 | } |
| 1806 | |
| 1807 | void C4Player::NotifyOwnedObjects() |
| 1808 | { |
| 1809 | C4Object *cobj; C4ObjectLink *clnk; |
| 1810 | |
| 1811 | // notify objects in all object lists |
| 1812 | for (C4ObjectList *pList = &Game.Objects; pList; pList = ((pList == &Game.Objects) ? &Game.Objects.InactiveObjects : nullptr)) |
| 1813 | for (clnk = pList->First; clnk && (cobj = clnk->Obj); clnk = clnk->Next) |
| 1814 | if (cobj->Status) |
| 1815 | if (cobj->Owner == Number) |
| 1816 | { |
| 1817 | C4AulFunc *pFn = cobj->Def->Script.GetFuncRecursive(PSF_OnOwnerRemoved); |
| 1818 | // PSF_OnOwnerRemoved has an internal fallback function |
| 1819 | assert(pFn); |
| 1820 | if (pFn) pFn->Exec(pObj: cobj); |
| 1821 | } |
| 1822 | } |
| 1823 | |
| 1824 | bool C4Player::DoPoints(int32_t iChange) |
| 1825 | { |
| 1826 | Points = BoundBy<int32_t>(bval: Points + iChange, lbound: -100000, rbound: 100000); |
| 1827 | ViewValue = C4ViewDelay; |
| 1828 | return true; |
| 1829 | } |
| 1830 | |
| 1831 | void C4Player::SetCursor(C4Object *pObj, bool fSelectFlash, bool fSelectArrow) |
| 1832 | { |
| 1833 | // check disabled |
| 1834 | if (pObj) if (pObj->CrewDisabled) return; |
| 1835 | bool fChanged = pObj != Cursor; |
| 1836 | C4Object *pPrev = Cursor; |
| 1837 | // Set cursor |
| 1838 | Cursor = pObj; |
| 1839 | // unselect previous |
| 1840 | if (pPrev && fChanged) pPrev->UnSelect(fCursor: true); |
| 1841 | // Select object |
| 1842 | if (Cursor) Cursor->DoSelect(fCursor: true); |
| 1843 | // View flash |
| 1844 | if (fSelectArrow) CursorFlash = 30; |
| 1845 | if (fSelectFlash) SelectFlash = 30; |
| 1846 | } |
| 1847 | |
| 1848 | void C4Player::SelectCrew(C4ObjectList &rList) |
| 1849 | { |
| 1850 | // Unselect |
| 1851 | UnselectCrew(); |
| 1852 | // Select (does not check whether objects are in crew) |
| 1853 | C4ObjectLink *clnk; |
| 1854 | for (clnk = rList.First; clnk; clnk = clnk->Next) |
| 1855 | if (clnk->Obj->Status) |
| 1856 | clnk->Obj->DoSelect(); |
| 1857 | // Updates |
| 1858 | AdjustCursorCommand(); |
| 1859 | CursorSelection = CursorToggled = 0; |
| 1860 | SelectFlash = 30; |
| 1861 | } |
| 1862 | |
| 1863 | void C4Player::ScrollView(int32_t iX, int32_t iY, int32_t ViewWdt, int32_t ViewHgt) |
| 1864 | { |
| 1865 | SetViewMode(iMode: C4PVM_Scrolling); |
| 1866 | int32_t ViewportScrollBorder = Application.isFullScreen ? C4ViewportScrollBorder : 0; |
| 1867 | ViewX = BoundBy<int32_t>(bval: ViewX + iX, lbound: ViewWdt / 2 - ViewportScrollBorder, GBackWdt + ViewportScrollBorder - ViewWdt / 2); |
| 1868 | ViewY = BoundBy<int32_t>(bval: ViewY + iY, lbound: ViewHgt / 2 - ViewportScrollBorder, GBackHgt + ViewportScrollBorder - ViewHgt / 2); |
| 1869 | } |
| 1870 | |
| 1871 | void C4Player::InitControl() |
| 1872 | { |
| 1873 | // Check local control |
| 1874 | LocalControl = false; |
| 1875 | if (AtClient == Game.Control.ClientID()) |
| 1876 | if (!GetInfo() || GetInfo()->GetType() == C4PT_User) |
| 1877 | LocalControl = true; |
| 1878 | // Set control |
| 1879 | Control = C4P_Control_None; |
| 1880 | // Preferred control |
| 1881 | int32_t iControl = PrefControl; |
| 1882 | // gamepad control safety |
| 1883 | if (Inside<int32_t>(ival: iControl, lbound: C4P_Control_GamePad1, rbound: C4P_Control_GamePadMax) && !Config.General.GamepadEnabled) |
| 1884 | { |
| 1885 | iControl = C4P_Control_Keyboard1; |
| 1886 | } |
| 1887 | // Choose next while control taken |
| 1888 | if (Game.Players.ControlTaken(iControl)) |
| 1889 | { |
| 1890 | // Preferred control taken, search for available keyboard control |
| 1891 | for (iControl = C4P_Control_Keyboard1; iControl <= C4P_Control_Keyboard4; iControl++) |
| 1892 | if (!Game.Players.ControlTaken(iControl)) // Available control found |
| 1893 | break; |
| 1894 | // No available control found |
| 1895 | if (iControl > C4P_Control_Keyboard4) |
| 1896 | iControl = C4P_Control_None; |
| 1897 | } |
| 1898 | // Set control |
| 1899 | Control = iControl; |
| 1900 | ApplyForcedControl(); |
| 1901 | // init gamepad |
| 1902 | delete pGamepad; pGamepad = nullptr; |
| 1903 | if (Inside<int32_t>(ival: Control, lbound: C4P_Control_GamePad1, rbound: C4P_Control_GamePadMax)) |
| 1904 | { |
| 1905 | pGamepad = new C4GamePadOpener(Control - C4P_Control_GamePad1); |
| 1906 | } |
| 1907 | // Mouse |
| 1908 | if (PrefMouse && !Game.Control.isReplay()) |
| 1909 | if (!Game.C4S.Head.DisableMouse) |
| 1910 | if (Inside<int32_t>(ival: Control, lbound: C4P_Control_Keyboard1, rbound: C4P_Control_GamePadMax)) |
| 1911 | if (!Game.Players.MouseControlTaken()) |
| 1912 | MouseControl = true; |
| 1913 | // no controls issued yet |
| 1914 | ControlCount = ActionCount = 0; |
| 1915 | LastControlType = PCID_None; |
| 1916 | LastControlID = 0; |
| 1917 | PressedComs = 0; |
| 1918 | } |
| 1919 | |
| 1920 | void C4Player::FoW2Map(CClrModAddMap &rMap, int iOffX, int iOffY) |
| 1921 | { |
| 1922 | // No fog of war |
| 1923 | if (!fFogOfWar) return; |
| 1924 | // Add view for all FoW-repellers - keep track of FoW-generators, which should be avaluated finally |
| 1925 | // so they override repellers |
| 1926 | bool fAnyGenerators = false; |
| 1927 | C4Object *cobj; C4ObjectLink *clnk; |
| 1928 | for (clnk = FoWViewObjs.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next) |
| 1929 | if (!cobj->Contained || cobj->Contained->Def->ClosedContainer != 1) |
| 1930 | if (cobj->PlrViewRange > 0) |
| 1931 | rMap.ReduceModulation(cx: cobj->x + iOffX, cy: cobj->y + iOffY, iRadius1: cobj->PlrViewRange * 2 / 3, iRadius2: cobj->PlrViewRange); |
| 1932 | else |
| 1933 | fAnyGenerators = true; |
| 1934 | // Add view for target view object |
| 1935 | if (ViewMode == C4PVM_Target) |
| 1936 | if (ViewTarget) |
| 1937 | if (!ViewTarget->Contained || ViewTarget->Contained->Def->ClosedContainer != 1) |
| 1938 | { |
| 1939 | int iRange = ViewTarget->PlrViewRange; |
| 1940 | if (!iRange && Cursor) iRange = Cursor->PlrViewRange; |
| 1941 | if (!iRange) iRange = C4FOW_Def_View_RangeX; |
| 1942 | rMap.ReduceModulation(cx: ViewTarget->x + iOffX, cy: ViewTarget->y + iOffY, iRadius1: iRange * 2 / 3, iRadius2: iRange); |
| 1943 | } |
| 1944 | // apply generators |
| 1945 | // do this check, be cause in 99% of all normal scenarios, there will be no FoW-generators |
| 1946 | if (fAnyGenerators) FoWGenerators2Map(rMap, iOffX, iOffY); |
| 1947 | } |
| 1948 | |
| 1949 | void C4Player::FoWGenerators2Map(CClrModAddMap &rMap, int iOffX, int iOffY) |
| 1950 | { |
| 1951 | // add fog to any generator pos (view range |
| 1952 | C4Object *cobj; C4ObjectLink *clnk; |
| 1953 | for (clnk = FoWViewObjs.First; clnk && (cobj = clnk->Obj); clnk = clnk->Next) |
| 1954 | if (!cobj->Contained || cobj->Contained->Def->ClosedContainer != 1) |
| 1955 | if (cobj->PlrViewRange < 0) |
| 1956 | rMap.AddModulation(cx: cobj->x + iOffX, cy: cobj->y + iOffY, iRadius1: -cobj->PlrViewRange, iRadius2: -cobj->PlrViewRange + 200, byTransparency: cobj->ColorMod >> 24); |
| 1957 | } |
| 1958 | |
| 1959 | bool C4Player::FoWIsVisible(int32_t x, int32_t y) |
| 1960 | { |
| 1961 | // check repellers and generators and ViewTarget |
| 1962 | bool fSeen = false; |
| 1963 | C4Object *cobj = nullptr; C4ObjectLink *clnk; |
| 1964 | clnk = FoWViewObjs.First; |
| 1965 | int32_t iRange; |
| 1966 | for (;;) |
| 1967 | { |
| 1968 | if (clnk) |
| 1969 | { |
| 1970 | cobj = clnk->Obj; |
| 1971 | clnk = clnk->Next; |
| 1972 | iRange = cobj->PlrViewRange; |
| 1973 | } |
| 1974 | else if (ViewMode != C4PVM_Target || !ViewTarget || ViewTarget == cobj) |
| 1975 | break; |
| 1976 | else |
| 1977 | { |
| 1978 | cobj = ViewTarget; |
| 1979 | iRange = cobj->PlrViewRange; |
| 1980 | if (!iRange && Cursor) iRange = Cursor->PlrViewRange; |
| 1981 | if (!iRange) iRange = C4FOW_Def_View_RangeX; |
| 1982 | } |
| 1983 | if (!cobj->Contained || cobj->Contained->Def->ClosedContainer != 1) |
| 1984 | if (Distance(iX1: cobj->x, iY1: cobj->y, iX2: x, iY2: y) < Abs(val: iRange)) |
| 1985 | if (iRange < 0) |
| 1986 | { |
| 1987 | if (!(cobj->ColorMod & 0xff000000)) // faded generators generate darkness only; no FoW blocking |
| 1988 | return false; // shadowed by FoW-generator |
| 1989 | } |
| 1990 | else |
| 1991 | fSeen = true; // made visible by FoW-repeller |
| 1992 | } |
| 1993 | return fSeen; |
| 1994 | } |
| 1995 | |
| 1996 | void C4Player::SelectCrew(C4Object *pObj, bool fSelect) |
| 1997 | { |
| 1998 | // Not a valid crew member |
| 1999 | if (!pObj || !Crew.GetLink(pObj)) return; |
| 2000 | // Select/unselect |
| 2001 | if (fSelect) pObj->DoSelect(); |
| 2002 | else pObj->UnSelect(); |
| 2003 | // Updates |
| 2004 | SelectFlash = 30; |
| 2005 | CursorSelection = CursorToggled = 0; |
| 2006 | AdjustCursorCommand(); |
| 2007 | } |
| 2008 | |
| 2009 | void C4Player::() |
| 2010 | { |
| 2011 | // cancel all player menus |
| 2012 | Menu.Close(fOK: false); |
| 2013 | } |
| 2014 | |
| 2015 | void C4Player::Eliminate() |
| 2016 | { |
| 2017 | if (Eliminated) return; |
| 2018 | Eliminated = true; |
| 2019 | RetireDelay = C4RetireDelay; |
| 2020 | StartSoundEffect(name: "Eliminated" ); |
| 2021 | Log(id: C4ResStrTableKey::IDS_PRC_PLRELIMINATED, args: GetName()); |
| 2022 | |
| 2023 | // Early client deactivation check |
| 2024 | if (Game.Control.isCtrlHost() && AtClient > C4ClientIDHost) |
| 2025 | { |
| 2026 | // Check: Any player left at this client? |
| 2027 | C4Player *pPlr = nullptr; |
| 2028 | for (int i = 0; pPlr = Game.Players.GetAtClient(iClient: AtClient, iIndex: i); i++) |
| 2029 | if (!pPlr->Eliminated) |
| 2030 | break; |
| 2031 | // If not, deactivate the client |
| 2032 | if (!pPlr) |
| 2033 | Game.Control.DoInput(eCtrlType: CID_ClientUpdate, |
| 2034 | pPkt: new C4ControlClientUpdate(AtClient, CUT_Activate, false), |
| 2035 | eDelivery: CDT_Sync); |
| 2036 | } |
| 2037 | } |
| 2038 | |
| 2039 | int32_t C4Player::ActiveCrewCount() |
| 2040 | { |
| 2041 | // get number of objects in crew that is not disabled |
| 2042 | int32_t iNum = 0; |
| 2043 | C4Object *cObj; |
| 2044 | for (C4ObjectLink *cLnk = Crew.First; cLnk; cLnk = cLnk->Next) |
| 2045 | if (cObj = cLnk->Obj) |
| 2046 | if (!cObj->CrewDisabled) |
| 2047 | ++iNum; |
| 2048 | // return it |
| 2049 | return iNum; |
| 2050 | } |
| 2051 | |
| 2052 | int32_t C4Player::GetSelectedCrewCount() |
| 2053 | { |
| 2054 | int32_t iNum = 0; |
| 2055 | C4Object *cObj; |
| 2056 | for (C4ObjectLink *cLnk = Crew.First; cLnk; cLnk = cLnk->Next) |
| 2057 | if (cObj = cLnk->Obj) |
| 2058 | if (!cObj->CrewDisabled) |
| 2059 | if (cObj->Select) |
| 2060 | ++iNum; |
| 2061 | // return it |
| 2062 | return iNum; |
| 2063 | } |
| 2064 | |
| 2065 | void C4Player::EvaluateLeague(bool fDisconnected, bool fWon) |
| 2066 | { |
| 2067 | // already evaluated? |
| 2068 | if (LeagueEvaluated) return; LeagueEvaluated = true; |
| 2069 | // set fate |
| 2070 | C4PlayerInfo *pInfo = GetInfo(); |
| 2071 | if (pInfo) |
| 2072 | { |
| 2073 | if (fDisconnected) |
| 2074 | pInfo->SetDisconnected(); |
| 2075 | if (fWon) |
| 2076 | pInfo->SetWinner(); |
| 2077 | } |
| 2078 | } |
| 2079 | |
| 2080 | bool C4Player::LocalSync() |
| 2081 | { |
| 2082 | // local sync not necessary for script players |
| 2083 | if (GetType() == C4PT_Script) return true; |
| 2084 | // evaluate total playing time |
| 2085 | TotalPlayingTime += Game.Time - GameJoinTime; |
| 2086 | GameJoinTime = Game.Time; |
| 2087 | // evaluate total playing time of all the crew |
| 2088 | for (C4ObjectInfo *pInf = CrewInfoList.GetFirst(); pInf; pInf = pInf->Next) |
| 2089 | if (pInf->InAction) |
| 2090 | { |
| 2091 | pInf->TotalPlayingTime += (Game.Time - pInf->InActionTime); |
| 2092 | pInf->InActionTime = Game.Time; |
| 2093 | } |
| 2094 | // save player |
| 2095 | if (!Save()) |
| 2096 | return false; |
| 2097 | |
| 2098 | // done, success |
| 2099 | return true; |
| 2100 | } |
| 2101 | |
| 2102 | C4PlayerInfo *C4Player::GetInfo() |
| 2103 | { |
| 2104 | return Game.PlayerInfos.GetPlayerInfoByID(id: ID); |
| 2105 | } |
| 2106 | |
| 2107 | bool C4Player::SetObjectCrewStatus(C4Object *pCrew, bool fNewStatus) |
| 2108 | { |
| 2109 | // either add... |
| 2110 | if (fNewStatus) |
| 2111 | { |
| 2112 | // is in crew already? |
| 2113 | if (Crew.IsContained(pObj: pCrew)) return true; |
| 2114 | return MakeCrewMember(pObj: pCrew, fForceInfo: false); |
| 2115 | } |
| 2116 | else |
| 2117 | { |
| 2118 | // already outside? |
| 2119 | if (!Crew.IsContained(pObj: pCrew)) return true; |
| 2120 | // ...or remove |
| 2121 | Crew.Remove(pObj: pCrew); |
| 2122 | // make sure it's not selected any more |
| 2123 | if (pCrew->Select) pCrew->UnSelect(); |
| 2124 | // remove info, if assigned to this player |
| 2125 | // theoretically, info objects could remain when the player is deleted |
| 2126 | // but then they would be reassigned to the player crew when loaded in a savegame |
| 2127 | // by the crew-assignment code kept for backwards compatibility with pre-4.95.2-savegames |
| 2128 | if (pCrew->Info && CrewInfoList.IsElement(pInfo: pCrew->Info)) |
| 2129 | { |
| 2130 | pCrew->Info->Retire(); |
| 2131 | pCrew->Info = nullptr; |
| 2132 | } |
| 2133 | } |
| 2134 | // done, success |
| 2135 | return true; |
| 2136 | } |
| 2137 | |
| 2138 | void C4Player::CreateGraphs() |
| 2139 | { |
| 2140 | // del prev |
| 2141 | ClearGraphs(); |
| 2142 | // create graphs |
| 2143 | if (Game.pNetworkStatistics) |
| 2144 | { |
| 2145 | uint32_t dwGraphClr = ColorDw; |
| 2146 | C4PlayerInfo *pInfo; |
| 2147 | if (ID && (pInfo = Game.PlayerInfos.GetPlayerInfoByID(id: ID))) |
| 2148 | { |
| 2149 | // set color by player info class |
| 2150 | dwGraphClr = pInfo->GetColor(); |
| 2151 | } |
| 2152 | C4GUI::MakeColorReadableOnBlack(rdwClr&: dwGraphClr); dwGraphClr &= 0xffffff; |
| 2153 | pstatControls = new C4TableGraph(C4TableGraph::DefaultBlockLength * 20, Game.pNetworkStatistics->ControlCounter); |
| 2154 | pstatControls->SetColorDw(dwGraphClr); |
| 2155 | pstatControls->SetTitle(GetName()); |
| 2156 | pstatActions = new C4TableGraph(C4TableGraph::DefaultBlockLength * 20, Game.pNetworkStatistics->ControlCounter); |
| 2157 | pstatActions->SetColorDw(dwGraphClr); |
| 2158 | pstatActions->SetTitle(GetName()); |
| 2159 | // register into |
| 2160 | Game.pNetworkStatistics->statControls.AddGraph(pAdd: pstatControls); |
| 2161 | Game.pNetworkStatistics->statActions.AddGraph(pAdd: pstatActions); |
| 2162 | } |
| 2163 | } |
| 2164 | |
| 2165 | void C4Player::ClearGraphs() |
| 2166 | { |
| 2167 | // del all assigned graphs |
| 2168 | if (pstatControls && Game.pNetworkStatistics) |
| 2169 | { |
| 2170 | Game.pNetworkStatistics->statControls.RemoveGraph(pRemove: pstatControls); |
| 2171 | } |
| 2172 | delete pstatControls; |
| 2173 | pstatControls = nullptr; |
| 2174 | if (pstatActions && Game.pNetworkStatistics) |
| 2175 | { |
| 2176 | Game.pNetworkStatistics->statActions.RemoveGraph(pRemove: pstatActions); |
| 2177 | } |
| 2178 | delete pstatActions; |
| 2179 | pstatActions = nullptr; |
| 2180 | } |
| 2181 | |
| 2182 | void C4Player::CountControl(ControlType eType, int32_t iID, int32_t iCntAdd) |
| 2183 | { |
| 2184 | // count it |
| 2185 | ControlCount += iCntAdd; |
| 2186 | // catch doubles |
| 2187 | if (eType == LastControlType && iID == LastControlID) return; |
| 2188 | // no double: count as action |
| 2189 | LastControlType = eType; |
| 2190 | LastControlID = iID; |
| 2191 | ActionCount += iCntAdd; |
| 2192 | // and give experience |
| 2193 | if (Cursor && Cursor->Info) |
| 2194 | { |
| 2195 | if (Cursor->Info) |
| 2196 | { |
| 2197 | Cursor->Info->ControlCount++; if ((Cursor->Info->ControlCount % 5) == 0) Cursor->DoExperience(change: +1); |
| 2198 | } |
| 2199 | } |
| 2200 | } |
| 2201 | |
| 2202 | void C4Player::ExecMsgBoardQueries() |
| 2203 | { |
| 2204 | // query now possible? |
| 2205 | if (!C4GUI::IsGUIValid()) return; |
| 2206 | // already active? |
| 2207 | if (Game.MessageInput.IsTypeIn()) return; |
| 2208 | // find an un-evaluated query |
| 2209 | C4MessageBoardQuery *pCheck = pMsgBoardQuery; |
| 2210 | while (pCheck) if (!pCheck->fAnswered) break; else pCheck = pCheck->pNext; |
| 2211 | if (!pCheck) return; |
| 2212 | // open it |
| 2213 | Game.MessageInput.StartTypeIn(fObjInput: true, pObj: pCheck->pCallbackObj, fUpperCase: pCheck->fIsUppercase, mode: C4ChatInputDialog::All, iPlr: Number, rsInputQuery: pCheck->sInputQuery); |
| 2214 | } |
| 2215 | |
| 2216 | void C4Player::CallMessageBoard(C4Object *pForObj, const StdStrBuf &sQueryString, bool fIsUppercase) |
| 2217 | { |
| 2218 | // remove any previous query for the same object |
| 2219 | RemoveMessageBoardQuery(pForObj); |
| 2220 | // sort new query to end of list |
| 2221 | C4MessageBoardQuery **ppTarget = &pMsgBoardQuery; |
| 2222 | while (*ppTarget) ppTarget = &((*ppTarget)->pNext); |
| 2223 | *ppTarget = new C4MessageBoardQuery(pForObj, sQueryString, fIsUppercase); |
| 2224 | } |
| 2225 | |
| 2226 | bool C4Player::RemoveMessageBoardQuery(C4Object *pForObj) |
| 2227 | { |
| 2228 | // get matching query |
| 2229 | C4MessageBoardQuery **ppCheck = &pMsgBoardQuery, *pFound; |
| 2230 | while (*ppCheck) if ((*ppCheck)->pCallbackObj == pForObj) break; else ppCheck = &((*ppCheck)->pNext); |
| 2231 | pFound = *ppCheck; |
| 2232 | if (!pFound) return false; |
| 2233 | // remove it |
| 2234 | *ppCheck = (*ppCheck)->pNext; |
| 2235 | delete pFound; |
| 2236 | return true; |
| 2237 | } |
| 2238 | |
| 2239 | bool C4Player::MarkMessageBoardQueryAnswered(C4Object *pForObj) |
| 2240 | { |
| 2241 | // get matching query |
| 2242 | C4MessageBoardQuery *pCheck = pMsgBoardQuery; |
| 2243 | while (pCheck) if (pCheck->pCallbackObj == pForObj && !pCheck->fAnswered) break; else pCheck = pCheck->pNext; |
| 2244 | if (!pCheck) return false; |
| 2245 | // mark it |
| 2246 | pCheck->fAnswered = true; |
| 2247 | return true; |
| 2248 | } |
| 2249 | |
| 2250 | bool C4Player::HasMessageBoardQuery() |
| 2251 | { |
| 2252 | // return whether any object has a messageboard-query |
| 2253 | return !!pMsgBoardQuery; |
| 2254 | } |
| 2255 | |
| 2256 | void C4Player::OnTeamSelectionFailed() |
| 2257 | { |
| 2258 | // looks like a selected team was not available: Go back to team selection if this is not a mislead call |
| 2259 | if (Status == PS_TeamSelectionPending) |
| 2260 | Status = PS_TeamSelection; |
| 2261 | } |
| 2262 | |
| 2263 | void C4Player::SetPlayerColor(uint32_t dwNewClr) |
| 2264 | { |
| 2265 | // no change? |
| 2266 | if (dwNewClr == ColorDw) return; |
| 2267 | // reflect change in all active, player-owned objects |
| 2268 | // this can never catch everything (thinking of overlays, etc.); scenarios that allow team changes should take care of the rest |
| 2269 | uint32_t dwOldClr = ColorDw; |
| 2270 | ColorDw = dwNewClr; |
| 2271 | C4Object *pObj; |
| 2272 | for (C4ObjectLink *pLnk = Game.Objects.First; pLnk; pLnk = pLnk->Next) |
| 2273 | if (pObj = pLnk->Obj) |
| 2274 | if (pObj->Status) |
| 2275 | if (pObj->Owner == Number) |
| 2276 | { |
| 2277 | if ((pObj->Color & 0xffffff) == (dwOldClr & 0xffffff)) |
| 2278 | pObj->Color = (pObj->Color & 0xff000000u) | (dwNewClr & 0xffffff); |
| 2279 | } |
| 2280 | } |
| 2281 | |
| 2282 | C4PlayerType C4Player::GetType() const |
| 2283 | { |
| 2284 | // type by info |
| 2285 | C4PlayerInfo *pInfo = Game.PlayerInfos.GetPlayerInfoByID(id: ID); |
| 2286 | if (pInfo) return pInfo->GetType(); else { assert(false); return C4PT_User; } |
| 2287 | } |
| 2288 | |
| 2289 | bool C4Player::IsInvisible() const |
| 2290 | { |
| 2291 | // invisible by info |
| 2292 | C4PlayerInfo *pInfo = Game.PlayerInfos.GetPlayerInfoByID(id: ID); |
| 2293 | if (pInfo) return pInfo->IsInvisible(); else { assert(false); return false; } |
| 2294 | } |
| 2295 | |
| 2296 | void C4Player::ToggleMouseControl() |
| 2297 | { |
| 2298 | // Activate mouse control if it's available |
| 2299 | if (!MouseControl && !Game.Players.MouseControlTaken()) |
| 2300 | { |
| 2301 | Game.MouseControl.Init(iPlayer: Number); |
| 2302 | MouseControl = true; |
| 2303 | // init fog of war |
| 2304 | if (!fFogOfWar && !bForceFogOfWar) |
| 2305 | { |
| 2306 | fFogOfWar = fFogOfWarInitialized = true; |
| 2307 | Game.Objects.AssignPlrViewRange(); |
| 2308 | } |
| 2309 | } |
| 2310 | // Deactivate mouse control |
| 2311 | else if (MouseControl) |
| 2312 | { |
| 2313 | Game.MouseControl.Clear(); |
| 2314 | Game.MouseControl.Default(); |
| 2315 | MouseControl = 0; |
| 2316 | // Scrolling isn't possible any more |
| 2317 | if (ViewMode == C4PVM_Scrolling) |
| 2318 | SetViewMode(iMode: C4PVM_Cursor); |
| 2319 | if (!bForceFogOfWar) |
| 2320 | { |
| 2321 | // delete fog of war |
| 2322 | fFogOfWarInitialized = fFogOfWar = false; |
| 2323 | } |
| 2324 | } |
| 2325 | } |
| 2326 | |
| 2327 | bool C4Player::ActivateMenuMain() |
| 2328 | { |
| 2329 | // Not during game over dialog |
| 2330 | if (C4GameOverDlg::IsShown()) return false; |
| 2331 | // Open menu |
| 2332 | return !!Menu.ActivateMain(iPlayer: Number); |
| 2333 | } |
| 2334 | |
| 2335 | void C4Player::SyncHomebaseMaterialToTeam() |
| 2336 | { |
| 2337 | // Copy own home base material to all team members |
| 2338 | C4Team *pTeam; |
| 2339 | if (Team && ((pTeam = Game.Teams.GetTeamByID(iID: Team)))) |
| 2340 | { |
| 2341 | int32_t iTeamSize = pTeam->GetPlayerCount(); |
| 2342 | for (int32_t i = 0; i < iTeamSize; ++i) |
| 2343 | { |
| 2344 | int32_t iTeammate = pTeam->GetIndexedPlayer(iIndex: i); |
| 2345 | C4Player *pTeammate; |
| 2346 | if (iTeammate != ID && ((pTeammate = Game.Players.GetByInfoID(iInfoID: iTeammate)))) |
| 2347 | { |
| 2348 | pTeammate->HomeBaseMaterial = HomeBaseMaterial; |
| 2349 | } |
| 2350 | } |
| 2351 | } |
| 2352 | } |
| 2353 | |
| 2354 | void C4Player::SyncHomebaseMaterialFromTeam() |
| 2355 | { |
| 2356 | // Copy own home base material from team's first player (unless it's ourselves) |
| 2357 | C4Team *pTeam; |
| 2358 | if (Team && ((pTeam = Game.Teams.GetTeamByID(iID: Team)))) |
| 2359 | { |
| 2360 | int32_t iTeamCaptain = pTeam->GetIndexedPlayer(iIndex: 0); |
| 2361 | C4Player *pTeamCaptain; |
| 2362 | if (iTeamCaptain != ID && ((pTeamCaptain = Game.Players.GetByInfoID(iInfoID: iTeamCaptain)))) |
| 2363 | { |
| 2364 | HomeBaseMaterial = pTeamCaptain->HomeBaseMaterial; |
| 2365 | } |
| 2366 | } |
| 2367 | } |
| 2368 | |
| 2369 | void C4Player::ApplyForcedControl() |
| 2370 | { |
| 2371 | const auto oldControl = ControlStyle; |
| 2372 | |
| 2373 | ControlStyle = ((Game.C4S.Head.ForcedControlStyle > -1) ? Game.C4S.Head.ForcedControlStyle : PrefControlStyle); |
| 2374 | AutoContextMenu = ((Game.C4S.Head.ForcedAutoContextMenu > -1) ? Game.C4S.Head.ForcedAutoContextMenu : PrefAutoContextMenu); |
| 2375 | |
| 2376 | if (oldControl != ControlStyle) |
| 2377 | { |
| 2378 | LastCom = COM_None; |
| 2379 | PressedComs = 0; |
| 2380 | if (ControlStyle) // AutoStopControl |
| 2381 | { |
| 2382 | for (auto objLink = Game.Objects.InactiveObjects.First; objLink; objLink = objLink->Next) |
| 2383 | { |
| 2384 | const auto obj = objLink->Obj; |
| 2385 | if (obj->Def->CrewMember && obj->Owner == Number) |
| 2386 | { |
| 2387 | obj->Action.ComDir = COMD_None; |
| 2388 | } |
| 2389 | } |
| 2390 | } |
| 2391 | } |
| 2392 | } |
| 2393 | |