| 1 | /* |
| 2 | * LegacyClonk |
| 3 | * |
| 4 | * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design) |
| 5 | * Copyright (c) 2016, The OpenClonk Team and contributors |
| 6 | * Copyright (c) 2017-2022, The LegacyClonk Team and contributors |
| 7 | * |
| 8 | * Distributed under the terms of the ISC license; see accompanying file |
| 9 | * "COPYING" for details. |
| 10 | * |
| 11 | * "Clonk" is a registered trademark of Matthes Bender, used with permission. |
| 12 | * See accompanying file "TRADEMARK" for details. |
| 13 | * |
| 14 | * To redistribute this file separately, substitute the full license texts |
| 15 | * for the above references. |
| 16 | */ |
| 17 | |
| 18 | /* Main class to run the game */ |
| 19 | |
| 20 | #include <C4Include.h> |
| 21 | #include <C4Game.h> |
| 22 | #include <C4Version.h> |
| 23 | #include <C4Network2Reference.h> |
| 24 | #include <C4FileMonitor.h> |
| 25 | |
| 26 | #include <C4GameSave.h> |
| 27 | #include <C4Record.h> |
| 28 | #include <C4Application.h> |
| 29 | #include <C4Object.h> |
| 30 | #include <C4ObjectInfo.h> |
| 31 | #include <C4Random.h> |
| 32 | #include <C4ObjectCom.h> |
| 33 | #include <C4SurfaceFile.h> |
| 34 | #include <C4FullScreen.h> |
| 35 | #include <C4Startup.h> |
| 36 | #include <C4Viewport.h> |
| 37 | #include <C4Command.h> |
| 38 | #include <C4Stat.h> |
| 39 | #include <C4PlayerInfo.h> |
| 40 | #include <C4LoaderScreen.h> |
| 41 | #include <C4Network2Dialogs.h> |
| 42 | #include <C4Console.h> |
| 43 | #include <C4Network2Stats.h> |
| 44 | #include <C4Log.h> |
| 45 | #include <C4Wrappers.h> |
| 46 | #include <C4Player.h> |
| 47 | #include <C4GameOverDlg.h> |
| 48 | #include <C4ObjectMenu.h> |
| 49 | #include <C4GameLobby.h> |
| 50 | #include <C4ChatDlg.h> |
| 51 | #include "C4KeyboardInput.h" |
| 52 | #include "C4Thread.h" |
| 53 | |
| 54 | #include <StdFile.h> |
| 55 | #include <StdGL.h> |
| 56 | |
| 57 | #include <format> |
| 58 | #include <iterator> |
| 59 | #include <sstream> |
| 60 | #include <utility> |
| 61 | |
| 62 | constexpr unsigned int defaultIngameGameTickDelay = 28; |
| 63 | |
| 64 | C4Game::C4Game() |
| 65 | : Input(Control.Input), KeyboardInput(C4KeyboardInput_Init()), fQuitWithError(false), fPreinited(false), |
| 66 | Teams(Parameters.Teams), |
| 67 | PlayerInfos(Parameters.PlayerInfos), |
| 68 | RestorePlayerInfos(Parameters.RestorePlayerInfos), |
| 69 | Clients(Parameters.Clients) |
| 70 | { |
| 71 | Default(); |
| 72 | } |
| 73 | |
| 74 | C4Game::~C4Game() |
| 75 | { |
| 76 | // make sure no startup gfx remain loaded |
| 77 | C4Startup::Unload(); |
| 78 | } |
| 79 | |
| 80 | bool C4Game::InitDefs() |
| 81 | { |
| 82 | int32_t iDefs = 0; |
| 83 | Log(id: C4ResStrTableKey::IDS_PRC_INITDEFS); |
| 84 | int iDefResCount = 0; |
| 85 | for ([[maybe_unused]] const auto &def : Parameters.GameRes.iterRes(eType: NRT_Definitions)) |
| 86 | ++iDefResCount; |
| 87 | int i = 0; |
| 88 | // Load specified defs |
| 89 | for (const auto &def : Parameters.GameRes.iterRes(eType: NRT_Definitions)) |
| 90 | { |
| 91 | int iMinProgress = 10 + (25 * i) / iDefResCount; |
| 92 | int iMaxProgress = 10 + (25 * (i + 1)) / iDefResCount; |
| 93 | ++i; |
| 94 | iDefs += Defs.Load(szSearch: def.getFile(), dwLoadWhat: C4D_Load_RX, szLanguage: Config.General.LanguageEx, pSoundSystem: &*Application.SoundSystem, fOverload: true, iMinProgress, iMaxProgress); |
| 95 | |
| 96 | // Def load failure |
| 97 | if (Defs.LoadFailure) return false; |
| 98 | } |
| 99 | |
| 100 | // Load for scenario file - ignore sys group here, because it has been loaded already |
| 101 | iDefs += Defs.Load(hGroup&: ScenarioFile, dwLoadWhat: C4D_Load_RX, szLanguage: Config.General.LanguageEx, pSoundSystem: &*Application.SoundSystem, fOverload: true, fSearchMessage: true, iMinProgress: 35, iMaxProgress: 40, fLoadSysGroups: false); |
| 102 | |
| 103 | // Absolutely no defs: we don't like that |
| 104 | if (!iDefs) { LogFatal(id: C4ResStrTableKey::IDS_PRC_NODEFS); return false; } |
| 105 | |
| 106 | // Check def engine version (should be done immediately on def load) |
| 107 | iDefs = Defs.CheckEngineVersion(C4XVER1, C4XVER2, C4XVER3, C4XVER4, C4XVERBUILD); |
| 108 | if (iDefs > 0) { Log(id: C4ResStrTableKey::IDS_PRC_DEFSINVC4X, args&: iDefs); } |
| 109 | |
| 110 | // sort before CheckRequireDef for better id-lookup performance |
| 111 | Defs.SortByID(); |
| 112 | |
| 113 | // Check for unmet requirements |
| 114 | Defs.CheckRequireDef(); |
| 115 | |
| 116 | // get default particles |
| 117 | Particles.SetDefParticles(); |
| 118 | |
| 119 | // Done |
| 120 | return true; |
| 121 | } |
| 122 | |
| 123 | bool C4Game::OpenScenario() |
| 124 | { |
| 125 | // Scenario from record stream |
| 126 | if (RecordStream.getSize()) |
| 127 | { |
| 128 | StdStrBuf RecordFile; |
| 129 | if (!C4Playback::StreamToRecord(szStream: RecordStream.getData(), pRecord: &RecordFile)) |
| 130 | { |
| 131 | LogFatalNTr(message: "[!] Could not process record stream data!" ); return false; |
| 132 | } |
| 133 | SCopy(szSource: RecordFile.getData(), sTarget: ScenarioFilename, _MAX_PATH); |
| 134 | } |
| 135 | |
| 136 | // Scenario filename check & log |
| 137 | if (!ScenarioFilename[0]) { LogFatal(id: C4ResStrTableKey::IDS_PRC_NOC4S); return false; } |
| 138 | Log(id: C4ResStrTableKey::IDS_PRC_LOADC4S, args: +ScenarioFilename); |
| 139 | |
| 140 | // get parent folder, if it's c4f |
| 141 | pParentGroup = GroupSet.RegisterParentFolders(szScenFilename: ScenarioFilename); |
| 142 | |
| 143 | // open scenario |
| 144 | if (pParentGroup) |
| 145 | { |
| 146 | // open from parent group |
| 147 | if (!ScenarioFile.OpenAsChild(pMother: pParentGroup, szEntryName: GetFilename(path: ScenarioFilename))) |
| 148 | { |
| 149 | LogNTr(fmt: "{}: {}" , args: LoadResStr(id: C4ResStrTableKey::IDS_PRC_FILENOTFOUND), args: +ScenarioFilename); return false; |
| 150 | } |
| 151 | } |
| 152 | else |
| 153 | // open directly |
| 154 | if (!ScenarioFile.Open(szGroupName: ScenarioFilename)) |
| 155 | { |
| 156 | LogNTr(fmt: "{}: {}" , args: LoadResStr(id: C4ResStrTableKey::IDS_PRC_FILENOTFOUND), args: +ScenarioFilename); return false; |
| 157 | } |
| 158 | |
| 159 | // add scenario to group |
| 160 | GroupSet.RegisterGroup(rGroup&: ScenarioFile, fOwnGrp: false, C4GSPrio_Scenario, C4GSCnt_Scenario); |
| 161 | |
| 162 | // Read scenario core |
| 163 | if (!C4S.Load(hGroup&: ScenarioFile)) |
| 164 | { |
| 165 | LogFatal(id: C4ResStrTableKey::IDS_PRC_FILEINVALID); return false; |
| 166 | } |
| 167 | |
| 168 | // Check minimum engine version |
| 169 | if (CompareVersion(iVer1: C4S.Head.C4XVer[0], iVer2: C4S.Head.C4XVer[1], iVer3: C4S.Head.C4XVer[2], iVer4: C4S.Head.C4XVer[3], iVerBuild: C4S.Head.C4XVer[4]) > 0) |
| 170 | { |
| 171 | LogFatal(id: C4ResStrTableKey::IDS_PRC_NOREQC4X, args&: C4S.Head.C4XVer[0], args&: C4S.Head.C4XVer[1], args&: C4S.Head.C4XVer[2], args&: C4S.Head.C4XVer[3], args&: C4S.Head.C4XVer[4]); |
| 172 | return false; |
| 173 | } |
| 174 | |
| 175 | // Add scenario origin to group set |
| 176 | if (C4S.Head.Origin.getLength() && !ItemIdentical(szFilename1: C4S.Head.Origin.getData(), szFilename2: ScenarioFilename)) |
| 177 | GroupSet.RegisterParentFolders(szScenFilename: C4S.Head.Origin.getData()); |
| 178 | |
| 179 | // Scenario definition preset |
| 180 | if (!FixedDefinitions) |
| 181 | { |
| 182 | const std::vector<std::string> &defs = C4S.Definitions.GetModules(); |
| 183 | if (!defs.empty()) DefinitionFilenames = defs; |
| 184 | |
| 185 | if (DefinitionFilenames.empty()) |
| 186 | { |
| 187 | Log(id: C4ResStrTableKey::IDS_PRC_LOCALONLY); |
| 188 | } |
| 189 | else |
| 190 | { |
| 191 | Log(id: C4ResStrTableKey::IDS_PRC_SCEOWNDEFS); |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | // add any custom definition path |
| 196 | if (*Config.General.DefinitionPath) |
| 197 | { |
| 198 | StdStrBuf sDefPath; sDefPath.Copy(pnData: Config.General.DefinitionPath); |
| 199 | char *szDefPath = sDefPath.GrabPointer(); TruncateBackslash(szFilename: szDefPath); sDefPath.Take(pnData: szDefPath); |
| 200 | if (DirectoryExists(szFileName: sDefPath.getData())) |
| 201 | { |
| 202 | std::transform(first: DefinitionFilenames.begin(), last: DefinitionFilenames.end(), result: std::inserter(x&: DefinitionFilenames, i: DefinitionFilenames.begin()), unary_op: [](const std::string &def) |
| 203 | { |
| 204 | return std::string{Config.General.DefinitionPath} + def; |
| 205 | }); |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | // Scan folder local definitions |
| 210 | std::vector<std::string> localDefs = FoldersWithLocalsDefs(path: ScenarioFilename); |
| 211 | |
| 212 | DefinitionFilenames.insert(position: DefinitionFilenames.end(), first: localDefs.begin(), last: localDefs.end()); |
| 213 | |
| 214 | // Check mission access |
| 215 | if (C4S.Head.MissionAccess[0]) |
| 216 | if (!SIsModule(szList: Config.General.MissionAccess, szString: C4S.Head.MissionAccess)) |
| 217 | { |
| 218 | LogFatal(id: C4ResStrTableKey::IDS_PRC_NOMISSIONACCESS); return false; |
| 219 | } |
| 220 | |
| 221 | // Game (runtime data) |
| 222 | GameText.Load(C4CFN_Game, hGroup&: ScenarioFile, C4CFN_Game); |
| 223 | |
| 224 | // SaveGame definition preset override (not needed with new scenarios that |
| 225 | // have def specs in scenario core, keep for downward compatibility) |
| 226 | if (C4S.Head.SaveGame) DefinitionFilenamesFromSaveGame(); |
| 227 | |
| 228 | // String tables |
| 229 | ScenarioLangStringTable.LoadEx(szName: "StringTbl" , hGroup&: ScenarioFile, C4CFN_ScriptStringTbl, szLanguage: Config.General.LanguageEx); |
| 230 | |
| 231 | // Load parameters (not as network client, because then team info has already been sent by host) |
| 232 | if (!Network.isEnabled() || Network.isHost()) |
| 233 | { |
| 234 | if (!Parameters.Load(hGroup&: ScenarioFile, pDefault: &C4S, szGameText: GameText.GetData(), pLang: &ScenarioLangStringTable, DefinitionFilenames)) |
| 235 | { |
| 236 | LogFatal(id: C4ResStrTableKey::IDS_ERR_LOAD_PARAMETERS); |
| 237 | return false; |
| 238 | } |
| 239 | |
| 240 | if (C4S.Head.SaveGame) |
| 241 | { |
| 242 | // make sure that at least all players from the savegame can join |
| 243 | const auto restoreCount = Parameters.RestorePlayerInfos.GetPlayerCount(); |
| 244 | if (Parameters.MaxPlayers < restoreCount) |
| 245 | { |
| 246 | Parameters.MaxPlayers = restoreCount; |
| 247 | } |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | // Title |
| 252 | Title.LoadEx(szName: LoadResStr(id: C4ResStrTableKey::IDS_CNS_TITLE), hGroup&: ScenarioFile, C4CFN_Title, szLanguage: Config.General.LanguageEx); |
| 253 | if (!Title.GetLanguageString(szLanguage: Config.General.LanguageEx, rTarget&: Parameters.ScenarioTitle)) |
| 254 | Parameters.ScenarioTitle.CopyValidated(szFromVal: C4S.Head.Title); |
| 255 | |
| 256 | // Load Strings (since kept objects aren't denumerated in sect-load, no problems should occur...) |
| 257 | if (ScenarioFile.FindEntry(C4CFN_Strings)) |
| 258 | if (!ScriptEngine.Strings.Load(ParentGroup&: ScenarioFile)) |
| 259 | { |
| 260 | LogFatal(id: C4ResStrTableKey::IDS_ERR_STRINGS); return false; |
| 261 | } |
| 262 | SetInitProgress(4); |
| 263 | |
| 264 | // Compile runtime data |
| 265 | if (!CompileRuntimeData(rGameData&: GameText)) |
| 266 | { |
| 267 | LogFatal(id: C4ResStrTableKey::IDS_ERR_LOAD_RUNTIMEDATA); return false; |
| 268 | } |
| 269 | |
| 270 | // If scenario is a directory: Watch for changes |
| 271 | if (!ScenarioFile.IsPacked()) |
| 272 | { |
| 273 | AddDirectoryForMonitoring(directory: ScenarioFile.GetFullName().getData()); |
| 274 | } |
| 275 | |
| 276 | PreloadStatus = PreloadLevel::Scenario; |
| 277 | |
| 278 | return true; |
| 279 | } |
| 280 | |
| 281 | void C4Game::CloseScenario() |
| 282 | { |
| 283 | // safe scenario file name |
| 284 | char szSzenarioFile[_MAX_PATH + 1]; |
| 285 | SCopy(szSource: ScenarioFile.GetFullName().getData(), sTarget: szSzenarioFile, _MAX_PATH); |
| 286 | // close scenario |
| 287 | ScenarioFile.Close(); |
| 288 | GroupSet.CloseFolders(); |
| 289 | pParentGroup = nullptr; |
| 290 | // remove if temporary |
| 291 | if (TempScenarioFile) |
| 292 | { |
| 293 | EraseItem(szItemName: szSzenarioFile); |
| 294 | TempScenarioFile = false; |
| 295 | } |
| 296 | // clear scenario section |
| 297 | // this removes any temp files, which may yet need to be used by any future features |
| 298 | // so better don't do this too early (like, in C4Game::Clear) |
| 299 | if (pScenarioSections) { delete pScenarioSections; pScenarioSections = pCurrentScenarioSection = nullptr; } |
| 300 | } |
| 301 | |
| 302 | bool C4Game::PreInit() |
| 303 | { |
| 304 | // System |
| 305 | if (!InitSystem()) |
| 306 | { |
| 307 | return false; |
| 308 | } |
| 309 | |
| 310 | // Startup message board |
| 311 | if (Application.isFullScreen) |
| 312 | { |
| 313 | C4Facet cgo; cgo.Set(nsfc: Application.DDraw->lpBack, nx: 0, ny: 0, nwdt: Config.Graphics.ResX, nhgt: Config.Graphics.ResY); |
| 314 | GraphicsSystem.MessageBoard.Init(cgo, fStartup: true); |
| 315 | } |
| 316 | |
| 317 | LogNTr(FANPROJECTTEXT); |
| 318 | LogNTr(TRADEMARKTEXT); |
| 319 | |
| 320 | // gfx resource file preinit (global files only) |
| 321 | Log(id: C4ResStrTableKey::IDS_PRC_GFXRES); |
| 322 | if (!GraphicsResource.Init()) |
| 323 | // Error was already logged |
| 324 | return false; |
| 325 | |
| 326 | // Graphics system (required for GUI) |
| 327 | if (!GraphicsSystem.Init()) |
| 328 | { |
| 329 | LogFatal(id: C4ResStrTableKey::IDS_ERR_NOGFXSYS); return false; |
| 330 | } |
| 331 | |
| 332 | // load GUI |
| 333 | if (!pGUI) |
| 334 | pGUI = new C4GUI::Screen(0, 0, Config.Graphics.ResX, Config.Graphics.ResY); |
| 335 | |
| 336 | fPreinited = true; |
| 337 | |
| 338 | return true; |
| 339 | } |
| 340 | |
| 341 | void C4Game::InitLogger() |
| 342 | { |
| 343 | Control.InitLogger(); |
| 344 | } |
| 345 | |
| 346 | bool C4Game::Init() |
| 347 | { |
| 348 | IsRunning = false; |
| 349 | |
| 350 | InitProgress = 0; LastInitProgress = 0; |
| 351 | SetInitProgress(0); |
| 352 | |
| 353 | Application.LogSystem.ClearRingbuffer(); |
| 354 | |
| 355 | fQuitWithError = false; |
| 356 | C4GameLobby::UserAbort = false; |
| 357 | |
| 358 | // Store a start time that identifies this game on this host |
| 359 | StartTime = static_cast<int32_t>(time(timer: nullptr)); |
| 360 | |
| 361 | // Get PlayerFilenames from Config, if ParseCommandLine did not fill some in |
| 362 | // Must be done here, because InitGame calls PlayerInfos.InitLocal |
| 363 | if (!*PlayerFilenames) |
| 364 | SCopy(szSource: Config.General.Participants, sTarget: PlayerFilenames, iMaxL: (std::min)(a: sizeof(PlayerFilenames), b: sizeof(Config.General.Participants)) - 1); |
| 365 | |
| 366 | // Join a game? |
| 367 | if (pJoinReference || *DirectJoinAddress) |
| 368 | { |
| 369 | if (!GraphicsSystem.pLoaderScreen) |
| 370 | { |
| 371 | // init extra; needed for loader screen |
| 372 | Log(id: C4ResStrTableKey::IDS_PRC_INITEXTRA); |
| 373 | Extra.Init(); |
| 374 | |
| 375 | // init loader |
| 376 | if (Application.isFullScreen && !GraphicsSystem.InitLoaderScreen(szLoaderSpec: C4S.Head.Loader)) |
| 377 | { |
| 378 | LogFatal(id: C4ResStrTableKey::IDS_PRC_ERRLOADER); return false; |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | SetInitProgress(5); |
| 383 | |
| 384 | // Initialize network |
| 385 | if (pJoinReference) |
| 386 | { |
| 387 | // By reference |
| 388 | bool fSuccess = InitNetworkFromReference(Reference: *pJoinReference); |
| 389 | delete pJoinReference; pJoinReference = nullptr; |
| 390 | if (!fSuccess) |
| 391 | return false; |
| 392 | } |
| 393 | else |
| 394 | { |
| 395 | // By address |
| 396 | if (!InitNetworkFromAddress(szAddress: DirectJoinAddress)) |
| 397 | return false; |
| 398 | } |
| 399 | |
| 400 | // check wether console mode is allowed |
| 401 | if (!Application.isFullScreen && !Parameters.AllowDebug) |
| 402 | { |
| 403 | LogFatal(id: C4ResStrTableKey::IDS_TEXT_JOININCONSOLEMODENOTALLOW); return false; |
| 404 | } |
| 405 | |
| 406 | // do lobby (if desired) |
| 407 | if (Network.isLobbyActive()) |
| 408 | if (!Network.DoLobby()) |
| 409 | return false; |
| 410 | } |
| 411 | |
| 412 | // Local game or host? |
| 413 | else |
| 414 | { |
| 415 | // check whether console mode is allowed |
| 416 | if (!Application.isFullScreen && (NetworkActive && Config.Network.LeagueServerSignUp)) |
| 417 | { |
| 418 | LogFatalNTr(message: "[!] League games in developer mode not allowed!" ); return false; |
| 419 | } |
| 420 | |
| 421 | // Open scenario |
| 422 | if (!OpenScenario()) |
| 423 | { |
| 424 | return false; |
| 425 | } |
| 426 | |
| 427 | // init extra; needed for loader screen |
| 428 | Log(id: C4ResStrTableKey::IDS_PRC_INITEXTRA); |
| 429 | Extra.Init(); |
| 430 | |
| 431 | // init loader |
| 432 | if (Application.isFullScreen && !GraphicsSystem.InitLoaderScreen(szLoaderSpec: C4S.Head.Loader)) |
| 433 | { |
| 434 | LogFatal(id: C4ResStrTableKey::IDS_PRC_ERRLOADER); return false; |
| 435 | } |
| 436 | |
| 437 | // Init network |
| 438 | if (!InitNetworkHost()) return false; |
| 439 | SetInitProgress(7); |
| 440 | } |
| 441 | |
| 442 | Application.SetGameTickDelay(defaultIngameGameTickDelay); |
| 443 | // now free all startup gfx to make room for game gfx |
| 444 | C4Startup::Unload(); |
| 445 | |
| 446 | // Init debugmode |
| 447 | DebugMode = !Application.isFullScreen; |
| 448 | if (Config.General.AlwaysDebug) |
| 449 | DebugMode = true; |
| 450 | if (!Parameters.AllowDebug) |
| 451 | DebugMode = false; |
| 452 | |
| 453 | Application.LogSystem.EnableDebugLog(enable: DebugMode); |
| 454 | |
| 455 | // Init game |
| 456 | if (!InitGame(hGroup&: ScenarioFile, section: nullptr, fLoadSky: true)) return false; |
| 457 | |
| 458 | // Network final init |
| 459 | if (Network.isEnabled()) |
| 460 | { |
| 461 | if (!Network.FinalInit()) |
| 462 | { |
| 463 | LogFatal(id: C4ResStrTableKey::IDS_ERR_NETWORKFINALINIT); |
| 464 | return false; |
| 465 | } |
| 466 | } |
| 467 | // non-net may have to synchronize now to keep in sync with replays |
| 468 | // also needs to synchronize to update transfer zones |
| 469 | else |
| 470 | { |
| 471 | // - would kill DebugRec-sync for runtime debugrec starts |
| 472 | C4DebugRecOff DBGRECOFF(!!C4S.Head.SaveGame); |
| 473 | SyncClearance(); |
| 474 | Synchronize(fSavePlayerFiles: false); |
| 475 | } |
| 476 | |
| 477 | // Init players |
| 478 | if (!InitPlayers()) return false; |
| 479 | SetInitProgress(98); |
| 480 | |
| 481 | // Final init |
| 482 | if (!InitGameFinal()) return false; |
| 483 | SetInitProgress(99); |
| 484 | |
| 485 | // Color palette |
| 486 | if (Application.isFullScreen) Application.DDraw->WipeSurface(sfcSurface: Application.DDraw->lpPrimary); |
| 487 | GraphicsSystem.SetPalette(); |
| 488 | GraphicsSystem.ApplyGamma(); |
| 489 | |
| 490 | // Message board and upper board |
| 491 | if (Application.isFullScreen) |
| 492 | { |
| 493 | InitFullscreenComponents(fRunning: true); |
| 494 | } |
| 495 | |
| 496 | // Default fullscreen menu, in case any old surfaces are left (extra safety) |
| 497 | FullScreen.CloseMenu(); |
| 498 | |
| 499 | // start statistics (always for now. Make this a config?) |
| 500 | pNetworkStatistics = new C4Network2Stats(); |
| 501 | |
| 502 | // clear loader screen |
| 503 | if (GraphicsSystem.pLoaderScreen) |
| 504 | { |
| 505 | delete GraphicsSystem.pLoaderScreen; |
| 506 | GraphicsSystem.pLoaderScreen = nullptr; |
| 507 | } |
| 508 | |
| 509 | // game running now! |
| 510 | IsRunning = true; |
| 511 | |
| 512 | // Start message |
| 513 | if (C4S.Head.NetworkGame) |
| 514 | { |
| 515 | Log(id: C4ResStrTableKey::IDS_PRC_JOIN); |
| 516 | } |
| 517 | else if (C4S.Head.SaveGame) |
| 518 | { |
| 519 | Log(id: C4ResStrTableKey::IDS_PRC_RESUME); |
| 520 | } |
| 521 | else |
| 522 | { |
| 523 | Log(id: C4ResStrTableKey::IDS_PRC_START); |
| 524 | } |
| 525 | |
| 526 | // set non-exclusive GUI |
| 527 | if (pGUI) |
| 528 | { |
| 529 | pGUI->SetExclusive(false); |
| 530 | } |
| 531 | |
| 532 | // after GUI is made non-exclusive, recheck the scoreboard |
| 533 | Scoreboard.DoDlgShow(iChange: 0, fUserToggle: false); |
| 534 | SetInitProgress(100); |
| 535 | |
| 536 | // and redraw background |
| 537 | GraphicsSystem.InvalidateBg(); |
| 538 | |
| 539 | return true; |
| 540 | } |
| 541 | |
| 542 | void C4Game::Clear() |
| 543 | { |
| 544 | // join the thread first as it will mess with the cleared state otherwise |
| 545 | if (PreloadThread.joinable()) |
| 546 | { |
| 547 | PreloadThread.join(); |
| 548 | } |
| 549 | |
| 550 | FileMonitor.reset(); |
| 551 | |
| 552 | if (Application.MusicSystem) |
| 553 | { |
| 554 | // fade out music |
| 555 | Application.MusicSystem->Stop(fadeoutMS: 2000); |
| 556 | } |
| 557 | // game no longer running |
| 558 | IsRunning = false; |
| 559 | PointersDenumerated = false; |
| 560 | |
| 561 | C4ST_SHOWSTAT |
| 562 | |
| 563 | // Evaluation |
| 564 | if (GameOver) |
| 565 | { |
| 566 | if (!Evaluated) Evaluate(); |
| 567 | } |
| 568 | |
| 569 | // stop statistics |
| 570 | delete pNetworkStatistics; pNetworkStatistics = nullptr; |
| 571 | C4AulProfiler::Abort(); |
| 572 | |
| 573 | // exit gui |
| 574 | delete pGUI; pGUI = nullptr; |
| 575 | |
| 576 | // next mission (shoud have been transferred to C4Application now if next mission was desired) |
| 577 | NextMission.Clear(); NextMissionText.Clear(); NextMissionDesc.Clear(); |
| 578 | |
| 579 | Network.Clear(); |
| 580 | Control.Clear(); |
| 581 | |
| 582 | // Clear |
| 583 | Scoreboard.Clear(); |
| 584 | MouseControl.Clear(); |
| 585 | Players.Clear(); |
| 586 | Parameters.Clear(); |
| 587 | RoundResults.Clear(); |
| 588 | C4S.Clear(); |
| 589 | Weather.Clear(); |
| 590 | GraphicsSystem.Clear(); |
| 591 | DeleteObjects(fDeleteInactive: true); |
| 592 | Defs.Clear(); |
| 593 | Landscape.Clear(); |
| 594 | PXS.Clear(); |
| 595 | delete pGlobalEffects; pGlobalEffects = nullptr; |
| 596 | Particles.Clear(); |
| 597 | Material.Clear(); |
| 598 | TextureMap.Clear(); // texture map *MUST* be cleared after the materials, because of the patterns! |
| 599 | GraphicsResource.Clear(); |
| 600 | Messages.Clear(); |
| 601 | MessageInput.Clear(); |
| 602 | Info.Clear(); |
| 603 | Title.Clear(); |
| 604 | Script.Clear(); |
| 605 | Names.Clear(); |
| 606 | GameText.Clear(); |
| 607 | RecordDumpFile.Clear(); |
| 608 | RecordStream.Clear(); |
| 609 | |
| 610 | PathFinder.Clear(); |
| 611 | TransferZones.Clear(); |
| 612 | #ifndef USE_CONSOLE |
| 613 | FontLoader.Clear(); |
| 614 | #endif |
| 615 | |
| 616 | ScriptEngine.Clear(); |
| 617 | MainSysLangStringTable.Clear(); |
| 618 | ScenarioLangStringTable.Clear(); |
| 619 | ScenarioSysLangStringTable.Clear(); |
| 620 | CloseScenario(); |
| 621 | GroupSet.Clear(); |
| 622 | KeyboardInput.Clear(); |
| 623 | |
| 624 | if (Application.MusicSystem) |
| 625 | { |
| 626 | SetMusicLevel(100); |
| 627 | } |
| 628 | |
| 629 | PlayList.Clear(); |
| 630 | |
| 631 | // global fullscreen class is not cleared, because it holds the carrier window |
| 632 | // but the menu must be cleared (maybe move Fullscreen.Menu somewhere else?) |
| 633 | FullScreen.CloseMenu(); |
| 634 | |
| 635 | // Message |
| 636 | // avoid double message by not printing it if no restbl is loaded |
| 637 | // this would log an "[Undefined]" only, anyway |
| 638 | // (could abort the whole clear-procedure here, btw?) |
| 639 | if (Application.ResStrTable) Log(id: C4ResStrTableKey::IDS_CNS_GAMECLOSED); |
| 640 | |
| 641 | // clear game starting parameters |
| 642 | DefinitionFilenames.clear(); |
| 643 | *DirectJoinAddress = *ScenarioFilename = *PlayerFilenames = 0; |
| 644 | |
| 645 | // join reference |
| 646 | delete pJoinReference; pJoinReference = nullptr; |
| 647 | |
| 648 | fPreinited = false; |
| 649 | Application.LogSystem.EnableDebugLog(enable: false); |
| 650 | } |
| 651 | |
| 652 | bool C4Game::GameOverCheck() |
| 653 | { |
| 654 | int32_t cnt; |
| 655 | bool fDoGameOver = false; |
| 656 | |
| 657 | // Only every 35 ticks |
| 658 | if (Tick35) return false; |
| 659 | |
| 660 | // do not GameOver in replay |
| 661 | if (Control.isReplay()) return false; |
| 662 | |
| 663 | // All players eliminated: game over |
| 664 | if (!Players.GetCountNotEliminated()) |
| 665 | fDoGameOver = true; |
| 666 | |
| 667 | // Cooperative game over (obsolete with new game goal objects, kept for |
| 668 | // downward compatibility with CreateObjects,ClearObjects,ClearMaterial settings) |
| 669 | C4ID c_id; |
| 670 | int32_t count, mat; |
| 671 | bool condition_valid, condition_true; |
| 672 | bool game_over_valid = false, game_over = true; |
| 673 | // CreateObjects |
| 674 | condition_valid = false; |
| 675 | condition_true = true; |
| 676 | for (cnt = 0; (c_id = C4S.Game.CreateObjects.GetID(index: cnt, ipCount: &count)); cnt++) |
| 677 | if (count > 0) |
| 678 | { |
| 679 | condition_valid = true; |
| 680 | // Count objects, fullsize only |
| 681 | C4ObjectLink *cLnk; |
| 682 | int32_t iCount = 0; |
| 683 | for (cLnk = Objects.First; cLnk; cLnk = cLnk->Next) |
| 684 | if (cLnk->Obj->Status) |
| 685 | if (cLnk->Obj->Def->id == c_id) |
| 686 | if (cLnk->Obj->GetCon() >= FullCon) |
| 687 | iCount++; |
| 688 | if (iCount < count) condition_true = false; |
| 689 | } |
| 690 | if (condition_valid) |
| 691 | { |
| 692 | game_over_valid = true; if (!condition_true) game_over = false; |
| 693 | } |
| 694 | // ClearObjects |
| 695 | condition_valid = false; |
| 696 | condition_true = true; |
| 697 | for (cnt = 0; (c_id = C4S.Game.ClearObjects.GetID(index: cnt, ipCount: &count)); cnt++) |
| 698 | { |
| 699 | condition_valid = true; |
| 700 | // Count objects, if category living, live only |
| 701 | C4ObjectLink *cLnk; |
| 702 | C4Def *cdef = C4Id2Def(id: c_id); |
| 703 | bool alive_only = false; |
| 704 | if (cdef && (cdef->Category & C4D_Living)) alive_only = true; |
| 705 | int32_t iCount = 0; |
| 706 | for (cLnk = Objects.First; cLnk; cLnk = cLnk->Next) |
| 707 | if (cLnk->Obj->Status) |
| 708 | if (cLnk->Obj->Def->id == c_id) |
| 709 | if (!alive_only || cLnk->Obj->GetAlive()) |
| 710 | iCount++; |
| 711 | if (iCount > count) condition_true = false; |
| 712 | } |
| 713 | if (condition_valid) |
| 714 | { |
| 715 | game_over_valid = true; if (!condition_true) game_over = false; |
| 716 | } |
| 717 | // ClearMaterial |
| 718 | condition_valid = false; |
| 719 | condition_true = true; |
| 720 | for (cnt = 0; cnt < C4MaxNameList; cnt++) |
| 721 | if (C4S.Game.ClearMaterial.Name[cnt][0]) |
| 722 | if (MatValid(mat: mat = Material.Get(szMaterial: C4S.Game.ClearMaterial.Name[cnt]))) |
| 723 | { |
| 724 | condition_valid = true; |
| 725 | if (Landscape.EffectiveMatCount[mat] > static_cast<uint32_t>(C4S.Game.ClearMaterial.Count[cnt])) |
| 726 | condition_true = false; |
| 727 | } |
| 728 | if (condition_valid) |
| 729 | { |
| 730 | game_over_valid = true; if (!condition_true) game_over = false; |
| 731 | } |
| 732 | |
| 733 | // Evaluate game over |
| 734 | if (game_over_valid) |
| 735 | if (game_over) |
| 736 | fDoGameOver = true; |
| 737 | |
| 738 | // Message |
| 739 | if (fDoGameOver) DoGameOver(); |
| 740 | |
| 741 | return GameOver; |
| 742 | } |
| 743 | |
| 744 | int32_t iLastControlSize = 0; |
| 745 | extern int32_t iPacketDelay; |
| 746 | |
| 747 | C4ST_NEW(ControlRcvStat, "C4Game::Execute ReceiveControl" ) |
| 748 | C4ST_NEW(ControlStat, "C4Game::Execute ExecuteControl" ) |
| 749 | C4ST_NEW(ExecObjectsStat, "C4Game::Execute ExecObjects" ) |
| 750 | C4ST_NEW(GEStats, "C4Game::Execute pGlobalEffects->Execute" ) |
| 751 | C4ST_NEW(PXSStat, "C4Game::Execute PXS.Execute" ) |
| 752 | C4ST_NEW(PartStat, "C4Game::Execute Particles.Execute" ) |
| 753 | C4ST_NEW(MassMoverStat, "C4Game::Execute MassMover.Execute" ) |
| 754 | C4ST_NEW(WeatherStat, "C4Game::Execute Weather.Execute" ) |
| 755 | C4ST_NEW(PlayersStat, "C4Game::Execute Players.Execute" ) |
| 756 | C4ST_NEW(LandscapeStat, "C4Game::Execute Landscape.Execute" ) |
| 757 | C4ST_NEW(MusicSystemStat, "C4Game::Execute MusicSystem.Execute" ) |
| 758 | C4ST_NEW(MessagesStat, "C4Game::Execute Messages.Execute" ) |
| 759 | C4ST_NEW(ScriptStat, "C4Game::Execute Script.Execute" ) |
| 760 | |
| 761 | #define EXEC_S(Expressions, Stat) \ |
| 762 | { C4ST_START(Stat) Expressions C4ST_STOP(Stat) } |
| 763 | |
| 764 | #ifdef DEBUGREC |
| 765 | #define EXEC_S_DR(Expressions, Stat, DebugRecName) { AddDbgRec(RCT_Block, DebugRecName, 6); EXEC_S(Expressions, Stat) } |
| 766 | #define EXEC_DR(Expressions, DebugRecName) { AddDbgRec(RCT_Block, DebugRecName, 6); Expressions } |
| 767 | #else |
| 768 | #define EXEC_S_DR(Expressions, Stat, DebugRecName) EXEC_S(Expressions, Stat) |
| 769 | #define EXEC_DR(Expressions, DebugRecName) Expressions |
| 770 | #endif |
| 771 | |
| 772 | bool C4Game::Execute() // Returns true if the game is over |
| 773 | { |
| 774 | // Let's go |
| 775 | GameGo = true; |
| 776 | |
| 777 | // Network |
| 778 | Network.Execute(); |
| 779 | |
| 780 | // Prepare control |
| 781 | bool fControl; |
| 782 | EXEC_S(fControl = Control.Prepare();, ControlStat) |
| 783 | if (!fControl) return false; // not ready yet: wait |
| 784 | |
| 785 | // Halt |
| 786 | if (HaltCount) return false; |
| 787 | |
| 788 | #ifdef DEBUGREC |
| 789 | Landscape.DoRelights(); |
| 790 | #endif |
| 791 | |
| 792 | // Execute the control |
| 793 | Control.Execute(); |
| 794 | if (!IsRunning) return false; |
| 795 | |
| 796 | // Ticks |
| 797 | EXEC_DR(Ticks();, "Ticks" ) |
| 798 | |
| 799 | #ifdef DEBUGREC |
| 800 | // debugrec |
| 801 | AddDbgRec(RCT_DbgFrame, &FrameCounter, sizeof(int32_t)); |
| 802 | #endif |
| 803 | |
| 804 | // Game |
| 805 | |
| 806 | EXEC_S(ExecObjects();, ExecObjectsStat) |
| 807 | if (pGlobalEffects) |
| 808 | EXEC_S_DR(pGlobalEffects->Execute(nullptr);, GEStats, "GEEx\0" ); |
| 809 | EXEC_S_DR(PXS.Execute();, PXSStat, "PXSEx" ) |
| 810 | EXEC_S_DR(Particles.GlobalParticles.Exec();, PartStat, "ParEx" ) |
| 811 | EXEC_S_DR(MassMover.Execute();, MassMoverStat, "MMvEx" ) |
| 812 | EXEC_S_DR(Weather.Execute();, WeatherStat, "WtrEx" ) |
| 813 | EXEC_S_DR(Landscape.Execute();, LandscapeStat, "LdsEx" ) |
| 814 | EXEC_S_DR(Players.Execute();, PlayersStat, "PlrEx" ) |
| 815 | // FIXME: C4Application::Execute should do this, but what about the stats? |
| 816 | EXEC_S_DR(Application.MusicSystem->Execute();, MusicSystemStat, "Music" ) |
| 817 | EXEC_S_DR(Messages.Execute();, MessagesStat, "MsgEx" ) |
| 818 | EXEC_S_DR(Script.Execute();, ScriptStat, "Scrpt" ) |
| 819 | |
| 820 | EXEC_DR(MouseControl.Execute();, "Input" ) |
| 821 | |
| 822 | EXEC_DR(UpdateRules(); |
| 823 | GameOverCheck();, "Misc\0" ) |
| 824 | |
| 825 | Control.DoSyncCheck(); |
| 826 | |
| 827 | // Evaluation; Game over dlg |
| 828 | if (GameOver) |
| 829 | { |
| 830 | if (!Evaluated) Evaluate(); |
| 831 | if (!GameOverDlgShown) ShowGameOverDlg(); |
| 832 | } |
| 833 | |
| 834 | // show stat each 1000 ticks |
| 835 | if (!(FrameCounter % 1000)) |
| 836 | { |
| 837 | C4ST_SHOWPARTSTAT |
| 838 | C4ST_RESETPART |
| 839 | } |
| 840 | |
| 841 | #ifdef DEBUGREC |
| 842 | AddDbgRec(RCT_Block, "eGame" , 6); |
| 843 | |
| 844 | Landscape.DoRelights(); |
| 845 | #endif |
| 846 | |
| 847 | return true; |
| 848 | } |
| 849 | |
| 850 | void C4Game::InitFullscreenComponents(bool fRunning) |
| 851 | { |
| 852 | if (!Application.DDraw) return; |
| 853 | |
| 854 | if (fRunning) |
| 855 | { |
| 856 | // running game: Message board upper board and viewports |
| 857 | C4Facet cgo2; |
| 858 | cgo2.Set(nsfc: Application.DDraw->lpBack, nx: 0, ny: 0, nwdt: Config.Graphics.ResX, nhgt: C4UpperBoard::Height()); |
| 859 | |
| 860 | C4Facet cgo; |
| 861 | cgo.Set(nsfc: Application.DDraw->lpBack, nx: 0, ny: Config.Graphics.ResY - GraphicsResource.FontRegular.GetLineHeight(), |
| 862 | nwdt: Config.Graphics.ResX, nhgt: GraphicsResource.FontRegular.GetLineHeight()); |
| 863 | GraphicsSystem.UpperBoard.Init(cgo&: cgo2, messageBoardCgo&: cgo); |
| 864 | GraphicsSystem.MessageBoard.Init(cgo, fStartup: false); |
| 865 | |
| 866 | GraphicsSystem.RecalculateViewports(); |
| 867 | } |
| 868 | else |
| 869 | { |
| 870 | // startup game: Just fullscreen message board |
| 871 | C4Facet cgo; cgo.Set(nsfc: Application.DDraw->lpBack, nx: 0, ny: 0, nwdt: Config.Graphics.ResX, nhgt: Config.Graphics.ResY); |
| 872 | GraphicsSystem.MessageBoard.Init(cgo, fStartup: true); |
| 873 | } |
| 874 | } |
| 875 | |
| 876 | bool C4Game::InitMaterialTexture() |
| 877 | { |
| 878 | // Clear old data |
| 879 | TextureMap.Clear(); |
| 880 | Material.Clear(); |
| 881 | |
| 882 | // Check for scenario local materials |
| 883 | bool fHaveScenMaterials = ScenarioFile.FindEntry(C4CFN_Material); |
| 884 | |
| 885 | // Load all materials |
| 886 | auto matRes = Parameters.GameRes.iterRes(eType: NRT_Material); |
| 887 | bool fFirst = true, fOverloadMaterials = true, fOverloadTextures = true; |
| 888 | long tex_count = 0, mat_count = 0; |
| 889 | while (fOverloadMaterials || fOverloadTextures) |
| 890 | { |
| 891 | // Are there any scenario local materials that need to be looked at firs? |
| 892 | C4Group Mats; |
| 893 | if (fHaveScenMaterials) |
| 894 | { |
| 895 | if (!Mats.OpenAsChild(pMother: &ScenarioFile, C4CFN_Material)) |
| 896 | { |
| 897 | LogFatal(id: C4ResStrTableKey::IDS_ERR_SCENARIOMATERIALS, args: Mats.GetError()); |
| 898 | return false; |
| 899 | } |
| 900 | // Once only |
| 901 | fHaveScenMaterials = false; |
| 902 | } |
| 903 | else |
| 904 | { |
| 905 | if (matRes == matRes.end()) break; |
| 906 | // Find next external material source |
| 907 | if (!Mats.Open(szGroupName: matRes->getFile())) |
| 908 | { |
| 909 | LogFatal(id: C4ResStrTableKey::IDS_ERR_EXTERNALMATERIALS, args: matRes->getFile(), args: Mats.GetError()); |
| 910 | return false; |
| 911 | } |
| 912 | ++matRes; |
| 913 | } |
| 914 | |
| 915 | // First material file? Load texture map. |
| 916 | bool fNewOverloadMaterials = false, fNewOverloadTextures = false; |
| 917 | if (fFirst) |
| 918 | { |
| 919 | long tme_count = TextureMap.LoadMap(hGroup&: Mats, C4CFN_TexMap, pOverloadMaterials: &fNewOverloadMaterials, pOverloadTextures: &fNewOverloadTextures); |
| 920 | Log(id: C4ResStrTableKey::IDS_PRC_TEXMAPENTRIES, args&: tme_count); |
| 921 | // Only once |
| 922 | fFirst = false; |
| 923 | } |
| 924 | else |
| 925 | { |
| 926 | // Check overload-flags only |
| 927 | if (!C4TextureMap::LoadFlags(hGroup&: Mats, C4CFN_TexMap, pOverloadMaterials: &fNewOverloadMaterials, pOverloadTextures: &fNewOverloadTextures)) |
| 928 | fOverloadMaterials = fOverloadTextures = false; |
| 929 | } |
| 930 | |
| 931 | // Load textures |
| 932 | if (fOverloadTextures) |
| 933 | { |
| 934 | int iTexs = TextureMap.LoadTextures(hGroup&: Mats); |
| 935 | // Automatically continue search if no texture was found |
| 936 | if (!iTexs) fNewOverloadTextures = true; |
| 937 | tex_count += iTexs; |
| 938 | } |
| 939 | |
| 940 | // Load materials |
| 941 | if (fOverloadMaterials) |
| 942 | { |
| 943 | int iMats = Material.Load(hGroup&: Mats); |
| 944 | // Automatically continue search if no material was found |
| 945 | if (!iMats) fNewOverloadMaterials = true; |
| 946 | mat_count += iMats; |
| 947 | } |
| 948 | |
| 949 | // Set flags |
| 950 | fOverloadTextures = fNewOverloadTextures; |
| 951 | fOverloadMaterials = fNewOverloadMaterials; |
| 952 | } |
| 953 | |
| 954 | // Logs |
| 955 | Log(id: C4ResStrTableKey::IDS_PRC_TEXTURES, args&: tex_count); |
| 956 | Log(id: C4ResStrTableKey::IDS_PRC_MATERIALS, args&: mat_count); |
| 957 | |
| 958 | // Load material enumeration |
| 959 | if (!Material.LoadEnumeration(hGroup&: ScenarioFile)) |
| 960 | { |
| 961 | LogFatal(id: C4ResStrTableKey::IDS_PRC_NOMATENUM); return false; |
| 962 | } |
| 963 | |
| 964 | // Initialize texture map |
| 965 | TextureMap.Init(); |
| 966 | |
| 967 | // Cross map mats (after texture init, because Material-Texture-combinations are used) |
| 968 | Material.CrossMapMaterials(); |
| 969 | |
| 970 | // Enumerate materials |
| 971 | if (!EnumerateMaterials()) return false; |
| 972 | |
| 973 | // get material script funcs |
| 974 | Material.UpdateScriptPointers(); |
| 975 | |
| 976 | return true; |
| 977 | } |
| 978 | |
| 979 | void C4Game::ClearObjectPtrs(C4Object *pObj) |
| 980 | { |
| 981 | // May not call Objects.ClearPointers() because that would |
| 982 | // remove pObj from primary list and pObj is to be kept |
| 983 | // until CheckObjectRemoval(). |
| 984 | C4Object *cObj; C4ObjectLink *clnk; |
| 985 | for (clnk = Objects.First; clnk && (cObj = clnk->Obj); clnk = clnk->Next) |
| 986 | cObj->ClearPointers(ptr: pObj); |
| 987 | // check in inactive objects as well |
| 988 | for (clnk = Objects.InactiveObjects.First; clnk && (cObj = clnk->Obj); clnk = clnk->Next) |
| 989 | cObj->ClearPointers(ptr: pObj); |
| 990 | Application.SoundSystem->ClearPointers(obj: pObj); |
| 991 | } |
| 992 | |
| 993 | void C4Game::ClearPointers(C4Object *pObj) |
| 994 | { |
| 995 | BackObjects.ClearPointers(pObj); |
| 996 | ForeObjects.ClearPointers(pObj); |
| 997 | Messages.ClearPointers(pObj); |
| 998 | ClearObjectPtrs(pObj); |
| 999 | Players.ClearPointers(pObj); |
| 1000 | GraphicsSystem.ClearPointers(pObj); |
| 1001 | MessageInput.ClearPointers(pObj); |
| 1002 | Console.ClearPointers(pObj); |
| 1003 | MouseControl.ClearPointers(pObj); |
| 1004 | TransferZones.ClearPointers(pObj); |
| 1005 | if (pGlobalEffects) |
| 1006 | pGlobalEffects->ClearPointers(pObj); |
| 1007 | } |
| 1008 | |
| 1009 | bool C4Game::TogglePause() |
| 1010 | { |
| 1011 | // pause toggling disabled during round evaluation |
| 1012 | if (C4GameOverDlg::IsShown()) return false; |
| 1013 | // otherwise, toggle |
| 1014 | if (IsPaused()) return Unpause(); else return Pause(); |
| 1015 | } |
| 1016 | |
| 1017 | bool C4Game::Pause() |
| 1018 | { |
| 1019 | // already paused? |
| 1020 | if (IsPaused()) return false; |
| 1021 | // pause by net? |
| 1022 | if (Network.isEnabled()) |
| 1023 | { |
| 1024 | // league? Vote... |
| 1025 | if (Parameters.isLeague() && !Evaluated) |
| 1026 | { |
| 1027 | Network.Vote(eType: VT_Pause, fApprove: true, iData: true); |
| 1028 | return false; |
| 1029 | } |
| 1030 | // host only |
| 1031 | if (!Network.isHost()) return true; |
| 1032 | Network.Pause(); |
| 1033 | } |
| 1034 | else |
| 1035 | { |
| 1036 | // pause game directly |
| 1037 | HaltCount = true; |
| 1038 | } |
| 1039 | Console.UpdateHaltCtrls(fHalt: IsPaused()); |
| 1040 | return true; |
| 1041 | } |
| 1042 | |
| 1043 | bool C4Game::Unpause() |
| 1044 | { |
| 1045 | // already paused? |
| 1046 | if (!IsPaused()) return false; |
| 1047 | // pause by net? |
| 1048 | if (Network.isEnabled()) |
| 1049 | { |
| 1050 | // league? Vote... |
| 1051 | if (Parameters.isLeague() && !Evaluated) |
| 1052 | { |
| 1053 | Network.Vote(eType: VT_Pause, fApprove: true, iData: false); |
| 1054 | return false; |
| 1055 | } |
| 1056 | // host only |
| 1057 | if (!Network.isHost()) return true; |
| 1058 | Network.Start(); |
| 1059 | } |
| 1060 | else |
| 1061 | { |
| 1062 | // unpause game directly |
| 1063 | HaltCount = false; |
| 1064 | } |
| 1065 | Console.UpdateHaltCtrls(fHalt: IsPaused()); |
| 1066 | return true; |
| 1067 | } |
| 1068 | |
| 1069 | bool C4Game::IsPaused() |
| 1070 | { |
| 1071 | // pause state defined either by network or by game halt count |
| 1072 | if (Network.isEnabled()) |
| 1073 | return !Network.isRunning(); |
| 1074 | return !!HaltCount; |
| 1075 | } |
| 1076 | |
| 1077 | C4Object *C4Game::NewObject(C4Def *pDef, C4Object *pCreator, |
| 1078 | int32_t iOwner, C4ObjectInfo *pInfo, |
| 1079 | int32_t iX, int32_t iY, int32_t iR, |
| 1080 | C4Fixed xdir, C4Fixed ydir, C4Fixed rdir, |
| 1081 | int32_t iCon, int32_t iController) |
| 1082 | { |
| 1083 | // Safety |
| 1084 | if (!pDef) return nullptr; |
| 1085 | #ifdef DEBUGREC |
| 1086 | C4RCCreateObj rc; |
| 1087 | rc.id = pDef->id; |
| 1088 | rc.oei = ObjectEnumerationIndex + 1; |
| 1089 | rc.x = iX; rc.y = iY; rc.ownr = iOwner; |
| 1090 | AddDbgRec(RCT_CrObj, &rc, sizeof(rc)); |
| 1091 | #endif |
| 1092 | // Create object |
| 1093 | auto obj = std::make_unique<C4Object>(); |
| 1094 | C4Object *const objPtr{obj.get()}; |
| 1095 | |
| 1096 | // Initialize object |
| 1097 | obj->Init(ndef: pDef, pCreator, owner: iOwner, info: pInfo, nx: iX, ny: iY, nr: iR, nxdir: xdir, nydir: ydir, nrdir: rdir, iController); |
| 1098 | // Enumerate object |
| 1099 | obj->Number = ++ObjectEnumerationIndex; |
| 1100 | // Add to object list |
| 1101 | if (Objects.Add(nObj: obj.get())) |
| 1102 | { |
| 1103 | obj.release(); |
| 1104 | } |
| 1105 | else |
| 1106 | { |
| 1107 | return nullptr; |
| 1108 | } |
| 1109 | |
| 1110 | // From now on, object is ready to be used in scripts! |
| 1111 | // Construction callback |
| 1112 | C4AulParSet pars(C4VObj(pObj: pCreator)); |
| 1113 | objPtr->Call(PSF_Construction, pPars: pars); |
| 1114 | // AssignRemoval called? (Con 0) |
| 1115 | if (!objPtr->Status) { return nullptr; } |
| 1116 | // Do initial con |
| 1117 | objPtr->DoCon(iChange: iCon, fInitial: true); |
| 1118 | // AssignRemoval called? (Con 0) |
| 1119 | if (!objPtr->Status) { return nullptr; } |
| 1120 | // Success |
| 1121 | return objPtr; |
| 1122 | } |
| 1123 | |
| 1124 | void C4Game::DeleteObjects(bool fDeleteInactive) |
| 1125 | { |
| 1126 | // del any objects |
| 1127 | Objects.DeleteObjects(); |
| 1128 | BackObjects.Clear(); |
| 1129 | ForeObjects.Clear(); |
| 1130 | if (fDeleteInactive) Objects.InactiveObjects.DeleteObjects(); |
| 1131 | // reset resort flag |
| 1132 | fResortAnyObject = false; |
| 1133 | } |
| 1134 | |
| 1135 | C4Object *C4Game::CreateObject(C4ID id, C4Object *pCreator, int32_t iOwner, |
| 1136 | int32_t x, int32_t y, int32_t r, |
| 1137 | C4Fixed xdir, C4Fixed ydir, C4Fixed rdir, int32_t iController) |
| 1138 | { |
| 1139 | C4Def *pDef; |
| 1140 | // Get pDef |
| 1141 | if (!(pDef = C4Id2Def(id))) return nullptr; |
| 1142 | // Create object |
| 1143 | return NewObject(pDef, pCreator, |
| 1144 | iOwner, pInfo: nullptr, |
| 1145 | iX: x, iY: y, iR: r, |
| 1146 | xdir, ydir, rdir, |
| 1147 | iCon: FullCon, iController); |
| 1148 | } |
| 1149 | |
| 1150 | C4Object *C4Game::CreateInfoObject(C4ObjectInfo *cinf, int32_t iOwner, |
| 1151 | int32_t tx, int32_t ty) |
| 1152 | { |
| 1153 | C4Def *def; |
| 1154 | // Valid check |
| 1155 | if (!cinf) return nullptr; |
| 1156 | // Get def |
| 1157 | if (!(def = C4Id2Def(id: cinf->id))) return nullptr; |
| 1158 | // Create object |
| 1159 | return NewObject(pDef: def, pCreator: nullptr, |
| 1160 | iOwner, pInfo: cinf, |
| 1161 | iX: tx, iY: ty, iR: 0, |
| 1162 | xdir: Fix0, ydir: Fix0, rdir: Fix0, |
| 1163 | iCon: FullCon, iController: NO_OWNER); |
| 1164 | } |
| 1165 | |
| 1166 | C4Object *C4Game::CreateObjectConstruction(C4ID id, |
| 1167 | C4Object *pCreator, |
| 1168 | int32_t iOwner, |
| 1169 | int32_t iX, int32_t iBY, |
| 1170 | int32_t iCon, |
| 1171 | bool fTerrain) |
| 1172 | { |
| 1173 | C4Def *pDef; |
| 1174 | C4Object *pObj; |
| 1175 | |
| 1176 | // Get def |
| 1177 | if (!(pDef = C4Id2Def(id))) return nullptr; |
| 1178 | |
| 1179 | int32_t dx, dy, dwdt, dhgt; |
| 1180 | dwdt = pDef->Shape.Wdt; dhgt = pDef->Shape.Hgt; |
| 1181 | dx = iX - dwdt / 2; dy = iBY - dhgt; |
| 1182 | |
| 1183 | // Terrain & Basement |
| 1184 | if (fTerrain) |
| 1185 | { |
| 1186 | // Clear site background (ignored for ultra-large structures) |
| 1187 | if (dwdt * dhgt < 12000) |
| 1188 | Landscape.DigFreeRect(tx: dx, ty: dy, wdt: dwdt, hgt: dhgt); |
| 1189 | // Raise Terrain |
| 1190 | Landscape.RaiseTerrain(tx: dx, ty: dy + dhgt, wdt: dwdt); |
| 1191 | // Basement |
| 1192 | if (pDef->Basement) |
| 1193 | { |
| 1194 | const int32_t BasementStrength = 8; |
| 1195 | // Border basement |
| 1196 | if (pDef->Basement > 1) |
| 1197 | { |
| 1198 | Landscape.DrawMaterialRect(mat: MGranite, tx: dx, ty: dy + dhgt, wdt: std::min<int32_t>(a: pDef->Basement, b: dwdt), hgt: BasementStrength); |
| 1199 | Landscape.DrawMaterialRect(mat: MGranite, tx: dx + dwdt - std::min<int32_t>(a: pDef->Basement, b: dwdt), ty: dy + dhgt, wdt: std::min<int32_t>(a: pDef->Basement, b: dwdt), hgt: BasementStrength); |
| 1200 | } |
| 1201 | // Normal basement |
| 1202 | else |
| 1203 | Landscape.DrawMaterialRect(mat: MGranite, tx: dx, ty: dy + dhgt, wdt: dwdt, hgt: BasementStrength); |
| 1204 | } |
| 1205 | } |
| 1206 | |
| 1207 | // Create object |
| 1208 | if (!(pObj = NewObject(pDef, |
| 1209 | pCreator, |
| 1210 | iOwner, pInfo: nullptr, |
| 1211 | iX, iY: iBY, iR: 0, |
| 1212 | xdir: Fix0, ydir: Fix0, rdir: Fix0, |
| 1213 | iCon, iController: pCreator ? pCreator->Controller : NO_OWNER))) return nullptr; |
| 1214 | |
| 1215 | return pObj; |
| 1216 | } |
| 1217 | |
| 1218 | void C4Game::BlastObjects(int32_t tx, int32_t ty, int32_t level, C4Object *inobj, int32_t iCausedBy, C4Object *pByObj) |
| 1219 | { |
| 1220 | C4Object *cObj; C4ObjectLink *clnk; |
| 1221 | |
| 1222 | // layer check: Blast in same layer only |
| 1223 | if (pByObj) pByObj = pByObj->pLayer; |
| 1224 | |
| 1225 | // Contained blast |
| 1226 | if (inobj) |
| 1227 | { |
| 1228 | inobj->Blast(iLevel: level, iCausedBy); |
| 1229 | for (clnk = Objects.First; clnk && (cObj = clnk->Obj); clnk = clnk->Next) |
| 1230 | if (cObj->Status) if (cObj->Contained == inobj) if (cObj->pLayer == pByObj) |
| 1231 | cObj->Blast(iLevel: level, iCausedBy); |
| 1232 | } |
| 1233 | |
| 1234 | // Uncontained blast local outside objects |
| 1235 | else |
| 1236 | { |
| 1237 | for (clnk = Objects.First; clnk && (cObj = clnk->Obj); clnk = clnk->Next) |
| 1238 | if (cObj->Status) if (!cObj->Contained) if (cObj->pLayer == pByObj) |
| 1239 | { |
| 1240 | // Direct hit (5 pixel range to all sides) |
| 1241 | if (Inside<int32_t>(ival: ty - (cObj->y + cObj->Shape.y), lbound: -5, rbound: cObj->Shape.Hgt - 1 + 10)) |
| 1242 | if (Inside<int32_t>(ival: tx - (cObj->x + cObj->Shape.x), lbound: -5, rbound: cObj->Shape.Wdt - 1 + 10)) |
| 1243 | cObj->Blast(iLevel: level, iCausedBy); |
| 1244 | // Shock wave hit (if in level range, living, object and vehicle only. No structures/StatickBack, as this would mess up castles, elevators, etc.!) |
| 1245 | if (cObj->Category & (C4D_Living | C4D_Object | C4D_Vehicle)) |
| 1246 | if (!cObj->Def->NoHorizontalMove) |
| 1247 | if (Abs(val: ty - cObj->y) <= level) |
| 1248 | if (Abs(val: tx - cObj->x) <= level) |
| 1249 | { |
| 1250 | // vehicles and floating objects only if grab+pushable (no throne, no tower entrances...) |
| 1251 | if (cObj->Def->Grab != 1) |
| 1252 | { |
| 1253 | if (cObj->Category & C4D_Vehicle) |
| 1254 | continue; |
| 1255 | if (cObj->Action.Act >= 0) |
| 1256 | if (cObj->Def->ActMap[cObj->Action.Act].Procedure == DFA_FLOAT) |
| 1257 | continue; |
| 1258 | } |
| 1259 | if (cObj->Category & C4D_Living) |
| 1260 | { |
| 1261 | // living takes additional dmg from blasts |
| 1262 | cObj->DoEnergy(iChange: -level / 2, fExact: false, C4FxCall_EngBlast, iCausedByPlr: iCausedBy); |
| 1263 | cObj->DoDamage(iLevel: level / 2, iCausedByPlr: iCausedBy, C4FxCall_DmgBlast); |
| 1264 | } |
| 1265 | |
| 1266 | // force argument evaluation order |
| 1267 | const auto p2 = itofix(x: -level + Abs(val: ty - cObj->y)) / BoundBy<int32_t>(bval: cObj->Mass / 10, lbound: 4, rbound: (cObj->Category & C4D_Living) ? 8 : 20); |
| 1268 | const auto p1 = itofix(x: Sign(val: cObj->x - tx + Rnd3()) * (level - Abs(val: tx - cObj->x))) / BoundBy<int32_t>(bval: cObj->Mass / 10, lbound: 4, rbound: (cObj->Category & C4D_Living) ? 8 : 20); |
| 1269 | cObj->Fling(txdir: p1, tydir: p2, fAddSpeed: true, byPlayer: iCausedBy); |
| 1270 | } |
| 1271 | } |
| 1272 | } |
| 1273 | } |
| 1274 | |
| 1275 | void C4Game::ShakeObjects(int32_t tx, int32_t ty, int32_t range, int32_t iCausedBy) |
| 1276 | { |
| 1277 | C4Object *cObj; C4ObjectLink *clnk; |
| 1278 | |
| 1279 | for (clnk = Objects.First; clnk && (cObj = clnk->Obj); clnk = clnk->Next) |
| 1280 | if (cObj->Status) if (!cObj->Contained) |
| 1281 | if (cObj->Category & C4D_Living) |
| 1282 | if (Abs(val: ty - cObj->y) <= range) |
| 1283 | if (Abs(val: tx - cObj->x) <= range) |
| 1284 | if (!Random(iRange: 3)) |
| 1285 | if (cObj->Action.t_attach) |
| 1286 | if (!MatVehicle(iMat: cObj->Shape.AttachMat)) |
| 1287 | { |
| 1288 | cObj->Fling(txdir: itofix(x: Rnd3()), tydir: Fix0, fAddSpeed: false, byPlayer: iCausedBy); |
| 1289 | } |
| 1290 | } |
| 1291 | |
| 1292 | C4Object *C4Game::OverlapObject(int32_t tx, int32_t ty, int32_t wdt, int32_t hgt, int32_t category) |
| 1293 | { |
| 1294 | C4Object *cObj; C4ObjectLink *clnk; |
| 1295 | C4Rect rect1, rect2; |
| 1296 | rect1.x = tx; rect1.y = ty; rect1.Wdt = wdt; rect1.Hgt = hgt; |
| 1297 | C4LArea Area(&Objects.Sectors, tx, ty, wdt, hgt); C4LSector *pSector; |
| 1298 | for (C4ObjectList *pObjs = Area.FirstObjectShapes(ppSct: &pSector); pSector; pObjs = Area.NextObjectShapes(pPrev: pObjs, ppSct: &pSector)) |
| 1299 | for (clnk = pObjs->First; clnk && (cObj = clnk->Obj); clnk = clnk->Next) |
| 1300 | if (cObj->Status) if (!cObj->Contained) |
| 1301 | if (cObj->Category & category & C4D_SortLimit) |
| 1302 | { |
| 1303 | rect2 = cObj->Shape; rect2.x += cObj->x; rect2.y += cObj->y; |
| 1304 | if (rect1.Overlap(rTarget&: rect2)) return cObj; |
| 1305 | } |
| 1306 | return nullptr; |
| 1307 | } |
| 1308 | |
| 1309 | C4Object *C4Game::FindObject(C4ID id, |
| 1310 | int32_t iX, int32_t iY, int32_t iWdt, int32_t iHgt, |
| 1311 | uint32_t ocf, |
| 1312 | const char *szAction, C4Object *pActionTarget, |
| 1313 | C4Object *pExclude, |
| 1314 | C4Object *pContainer, |
| 1315 | int32_t iOwner, |
| 1316 | C4Object *pFindNext) |
| 1317 | { |
| 1318 | C4Object *pClosest = nullptr; |
| 1319 | int32_t iClosest = 0, iDistance, iFartherThan = -1; |
| 1320 | C4Object *cObj; |
| 1321 | C4ObjectLink *cLnk; |
| 1322 | C4Def *pDef; |
| 1323 | C4Object *pFindNextCpy = pFindNext; |
| 1324 | |
| 1325 | // check the easy cases first |
| 1326 | if (id != C4ID_None) |
| 1327 | { |
| 1328 | if (!(pDef = C4Id2Def(id))) return nullptr; // no valid def |
| 1329 | if (!pDef->Count) return nullptr; // no instances at all |
| 1330 | } |
| 1331 | |
| 1332 | // Finding next closest: find closest but further away than last closest |
| 1333 | if (pFindNext && (iWdt == -1) && (iHgt == -1)) |
| 1334 | { |
| 1335 | iFartherThan = (pFindNext->x - iX) * (pFindNext->x - iX) + (pFindNext->y - iY) * (pFindNext->y - iY); |
| 1336 | pFindNext = nullptr; |
| 1337 | } |
| 1338 | |
| 1339 | bool bFindActIdle = SEqual(szStr1: szAction, szStr2: "Idle" ) || SEqual(szStr1: szAction, szStr2: "ActIdle" ); |
| 1340 | |
| 1341 | // Scan all objects |
| 1342 | for (cLnk = Objects.First; cLnk && (cObj = cLnk->Obj); cLnk = cLnk->Next) |
| 1343 | { |
| 1344 | // Not skipping to find next |
| 1345 | if (!pFindNext) |
| 1346 | // Status |
| 1347 | if (cObj->Status) |
| 1348 | // ID |
| 1349 | if ((id == C4ID_None) || (cObj->Def->id == id)) |
| 1350 | // OCF (match any specified) |
| 1351 | if (cObj->OCF & ocf) |
| 1352 | // Exclude |
| 1353 | if (cObj != pExclude) |
| 1354 | // Action |
| 1355 | if (!szAction || !szAction[0] || (bFindActIdle && cObj->Action.Act <= ActIdle) || ((cObj->Action.Act > ActIdle) && SEqual(szStr1: szAction, szStr2: cObj->Def->ActMap[cObj->Action.Act].Name))) |
| 1356 | // ActionTarget |
| 1357 | if (!pActionTarget || ((cObj->Action.Act > ActIdle) && ((cObj->Action.Target == pActionTarget) || (cObj->Action.Target2 == pActionTarget)))) |
| 1358 | // Container |
| 1359 | if (!pContainer || (cObj->Contained == pContainer) || ((reinterpret_cast<std::intptr_t>(pContainer) == NO_CONTAINER) && !cObj->Contained) || ((reinterpret_cast<std::intptr_t>(pContainer) == ANY_CONTAINER) && cObj->Contained)) |
| 1360 | // Owner |
| 1361 | if ((iOwner == ANY_OWNER) || (cObj->Owner == iOwner)) |
| 1362 | // Area |
| 1363 | { |
| 1364 | // Full range |
| 1365 | if ((iX == 0) && (iY == 0) && (iWdt == 0) && (iHgt == 0)) |
| 1366 | return cObj; |
| 1367 | // Point |
| 1368 | if ((iWdt == 0) && (iHgt == 0)) |
| 1369 | { |
| 1370 | if (Inside<int32_t>(ival: iX - (cObj->x + cObj->Shape.x), lbound: 0, rbound: cObj->Shape.Wdt - 1)) |
| 1371 | if (Inside<int32_t>(ival: iY - (cObj->y + cObj->Shape.y), lbound: 0, rbound: cObj->Shape.Hgt - 1)) |
| 1372 | return cObj; |
| 1373 | continue; |
| 1374 | } |
| 1375 | // Closest |
| 1376 | if ((iWdt == -1) && (iHgt == -1)) |
| 1377 | { |
| 1378 | iDistance = (cObj->x - iX) * (cObj->x - iX) + (cObj->y - iY) * (cObj->y - iY); |
| 1379 | // same distance? |
| 1380 | if ((iDistance == iFartherThan) && !pFindNextCpy) |
| 1381 | return cObj; |
| 1382 | // nearer than/first closest? |
| 1383 | if (!pClosest || (iDistance < iClosest)) |
| 1384 | if (iDistance > iFartherThan) |
| 1385 | { |
| 1386 | pClosest = cObj; iClosest = iDistance; |
| 1387 | } |
| 1388 | } |
| 1389 | // Range |
| 1390 | else if (Inside<int32_t>(ival: cObj->x - iX, lbound: 0, rbound: iWdt - 1) && Inside<int32_t>(ival: cObj->y - iY, lbound: 0, rbound: iHgt - 1)) |
| 1391 | return cObj; |
| 1392 | } |
| 1393 | |
| 1394 | // Find next mark reached |
| 1395 | if (cObj == pFindNextCpy) pFindNext = pFindNextCpy = nullptr; |
| 1396 | } |
| 1397 | |
| 1398 | return pClosest; |
| 1399 | } |
| 1400 | |
| 1401 | C4Object *C4Game::FindVisObject(int32_t tx, int32_t ty, int32_t iPlr, const C4Facet &fctViewport, |
| 1402 | int32_t iX, int32_t iY, int32_t iWdt, int32_t iHgt, |
| 1403 | uint32_t ocf, |
| 1404 | C4Object *pExclude, |
| 1405 | int32_t iOwner, |
| 1406 | C4Object *pFindNext) |
| 1407 | { |
| 1408 | // FIXME: Use C4FindObject here for optimization |
| 1409 | C4Object *cObj; C4ObjectLink *cLnk; C4ObjectList *pLst = &ForeObjects; |
| 1410 | |
| 1411 | // scan all object lists separately |
| 1412 | while (pLst) |
| 1413 | { |
| 1414 | // Scan all objects in list |
| 1415 | for (cLnk = Objects.First; cLnk && (cObj = cLnk->Obj); cLnk = cLnk->Next) |
| 1416 | { |
| 1417 | // Not skipping to find next |
| 1418 | if (!pFindNext) |
| 1419 | // Status |
| 1420 | if (cObj->Status == C4OS_NORMAL) |
| 1421 | // exclude fore/back-objects from main list |
| 1422 | if ((pLst != &Objects) || (!(cObj->Category & C4D_BackgroundOrForeground))) |
| 1423 | // exclude MouseIgnore-objects |
| 1424 | if (~cObj->Category & C4D_MouseIgnore) |
| 1425 | // OCF (match any specified) |
| 1426 | if (cObj->OCF & ocf) |
| 1427 | // Exclude |
| 1428 | if (cObj != pExclude) |
| 1429 | // Container |
| 1430 | if (!cObj->Contained) |
| 1431 | // Owner |
| 1432 | if ((iOwner == ANY_OWNER) || (cObj->Owner == iOwner)) |
| 1433 | // Visibility |
| 1434 | if (!cObj->Visibility || cObj->IsVisible(iForPlr: iPlr, fAsOverlay: false)) |
| 1435 | // Area |
| 1436 | { |
| 1437 | // Layer check: Layered objects are invisible to players whose cursor is in another layer |
| 1438 | if (ValidPlr(plr: iPlr)) |
| 1439 | { |
| 1440 | C4Object *pCursor = Players.Get(iPlayer: iPlr)->Cursor; |
| 1441 | if (!pCursor || (pCursor->pLayer != cObj->pLayer)) continue; |
| 1442 | } |
| 1443 | // Full range |
| 1444 | if ((iX == 0) && (iY == 0) && (iWdt == 0) && (iHgt == 0)) |
| 1445 | return cObj; |
| 1446 | // get object position |
| 1447 | int32_t iObjX, iObjY; cObj->GetViewPos(riX&: iObjX, riY&: iObjY, tx, ty, fctViewport); |
| 1448 | // Point search |
| 1449 | if ((iWdt == 0) && (iHgt == 0)) |
| 1450 | { |
| 1451 | if (Inside<int32_t>(ival: iX - (iObjX + cObj->Shape.x), lbound: 0, rbound: cObj->Shape.Wdt - 1)) |
| 1452 | if (Inside<int32_t>(ival: iY - (iObjY + cObj->Shape.y - cObj->addtop()), lbound: 0, rbound: cObj->Shape.Hgt + cObj->addtop() - 1)) |
| 1453 | return cObj; |
| 1454 | continue; |
| 1455 | } |
| 1456 | // Range |
| 1457 | if (Inside<int32_t>(ival: iObjX - iX, lbound: 0, rbound: iWdt - 1) && Inside<int32_t>(ival: iObjY - iY, lbound: 0, rbound: iHgt - 1)) |
| 1458 | { |
| 1459 | return cObj; |
| 1460 | } |
| 1461 | } |
| 1462 | |
| 1463 | // Find next mark reached |
| 1464 | if (cObj == pFindNext) pFindNext = nullptr; |
| 1465 | } |
| 1466 | // next list |
| 1467 | if (pLst == &ForeObjects) pLst = &Objects; |
| 1468 | else if (pLst == &Objects) pLst = &BackObjects; |
| 1469 | else pLst = nullptr; |
| 1470 | } |
| 1471 | |
| 1472 | // none found |
| 1473 | return nullptr; |
| 1474 | } |
| 1475 | |
| 1476 | int32_t C4Game::ObjectCount(C4ID id, |
| 1477 | int32_t x, int32_t y, int32_t wdt, int32_t hgt, |
| 1478 | uint32_t ocf, |
| 1479 | const char *szAction, C4Object *pActionTarget, |
| 1480 | C4Object *pExclude, |
| 1481 | C4Object *pContainer, |
| 1482 | int32_t iOwner) |
| 1483 | { |
| 1484 | int32_t iResult = 0; C4Def *pDef; |
| 1485 | // check the easy cases first |
| 1486 | if (id != C4ID_None) |
| 1487 | { |
| 1488 | if (!(pDef = C4Id2Def(id))) return 0; // no valid def |
| 1489 | if (!pDef->Count) return 0; // no instances at all |
| 1490 | if (!x && !y && !wdt && !hgt && ocf == OCF_All && !szAction && !pActionTarget && !pExclude && !pContainer && (iOwner == ANY_OWNER)) |
| 1491 | // plain id-search: return known count |
| 1492 | return pDef->Count; |
| 1493 | } |
| 1494 | C4Object *cObj; C4ObjectLink *clnk; |
| 1495 | bool bFindActIdle = SEqual(szStr1: szAction, szStr2: "Idle" ) || SEqual(szStr1: szAction, szStr2: "ActIdle" ); |
| 1496 | for (clnk = Objects.First; clnk && (cObj = clnk->Obj); clnk = clnk->Next) |
| 1497 | // Status |
| 1498 | if (cObj->Status) |
| 1499 | // ID |
| 1500 | if ((id == C4ID_None) || (cObj->Def->id == id)) |
| 1501 | // OCF |
| 1502 | if (cObj->OCF & ocf) |
| 1503 | // Exclude |
| 1504 | if (cObj != pExclude) |
| 1505 | // Action |
| 1506 | if (!szAction || !szAction[0] || (bFindActIdle && cObj->Action.Act <= ActIdle) || ((cObj->Action.Act > ActIdle) && SEqual(szStr1: szAction, szStr2: cObj->Def->ActMap[cObj->Action.Act].Name))) |
| 1507 | // ActionTarget |
| 1508 | if (!pActionTarget || ((cObj->Action.Act > ActIdle) && ((cObj->Action.Target == pActionTarget) || (cObj->Action.Target2 == pActionTarget)))) |
| 1509 | // Container |
| 1510 | if (!pContainer || (cObj->Contained == pContainer) || ((reinterpret_cast<std::intptr_t>(pContainer) == NO_CONTAINER) && !cObj->Contained) || ((reinterpret_cast<std::intptr_t>(pContainer) == ANY_CONTAINER) && cObj->Contained)) |
| 1511 | // Owner |
| 1512 | if ((iOwner == ANY_OWNER) || (cObj->Owner == iOwner)) |
| 1513 | // Area |
| 1514 | { |
| 1515 | // Full range |
| 1516 | if ((x == 0) && (y == 0) && (wdt == 0) && (hgt == 0)) |
| 1517 | { |
| 1518 | iResult++; continue; |
| 1519 | } |
| 1520 | // Point |
| 1521 | if ((wdt == 0) && (hgt == 0)) |
| 1522 | { |
| 1523 | if (Inside<int32_t>(ival: x - (cObj->x + cObj->Shape.x), lbound: 0, rbound: cObj->Shape.Wdt - 1)) |
| 1524 | if (Inside<int32_t>(ival: y - (cObj->y + cObj->Shape.y), lbound: 0, rbound: cObj->Shape.Hgt - 1)) |
| 1525 | { |
| 1526 | iResult++; continue; |
| 1527 | } |
| 1528 | continue; |
| 1529 | } |
| 1530 | // Range |
| 1531 | if (Inside<int32_t>(ival: cObj->x - x, lbound: 0, rbound: wdt - 1) && Inside<int32_t>(ival: cObj->y - y, lbound: 0, rbound: hgt - 1)) |
| 1532 | { |
| 1533 | iResult++; continue; |
| 1534 | } |
| 1535 | } |
| 1536 | |
| 1537 | return iResult; |
| 1538 | } |
| 1539 | |
| 1540 | // Deletes removal-assigned data from list. |
| 1541 | // Pointer clearance is done by AssignRemoval. |
| 1542 | |
| 1543 | void C4Game::ObjectRemovalCheck() // Every Tick255 by ExecObjects |
| 1544 | { |
| 1545 | C4Object *cObj; C4ObjectLink *clnk, *next; |
| 1546 | for (clnk = Objects.First; clnk && (cObj = clnk->Obj); clnk = next) |
| 1547 | { |
| 1548 | next = clnk->Next; |
| 1549 | if (!cObj->Status && (cObj->RemovalDelay == 0)) |
| 1550 | { |
| 1551 | Objects.Remove(pObj: cObj); |
| 1552 | delete cObj; |
| 1553 | } |
| 1554 | } |
| 1555 | } |
| 1556 | |
| 1557 | void C4Game::ExecObjects() // Every Tick1 by Execute |
| 1558 | { |
| 1559 | #ifdef DEBUGREC |
| 1560 | AddDbgRec(RCT_Block, "ObjEx" , 6); |
| 1561 | #endif |
| 1562 | |
| 1563 | // Execute objects - reverse order to ensure |
| 1564 | for (auto it = Objects.BeginLast(); it != std::default_sentinel; ++it) |
| 1565 | { |
| 1566 | if ((*it)->Status) |
| 1567 | // Execute object |
| 1568 | (*it)->Execute(); |
| 1569 | else |
| 1570 | // Status reset: process removal delay |
| 1571 | if ((*it)->RemovalDelay > 0) (*it)->RemovalDelay--; |
| 1572 | } |
| 1573 | |
| 1574 | #ifdef DEBUGREC |
| 1575 | AddDbgRec(RCT_Block, "ObjCC" , 6); |
| 1576 | #endif |
| 1577 | |
| 1578 | // Cross check objects |
| 1579 | Objects.CrossCheck(); |
| 1580 | |
| 1581 | #ifdef DEBUGREC |
| 1582 | AddDbgRec(RCT_Block, "ObjRs" , 6); |
| 1583 | #endif |
| 1584 | |
| 1585 | // Resort |
| 1586 | if (fResortAnyObject) |
| 1587 | { |
| 1588 | fResortAnyObject = false; |
| 1589 | Objects.ResortUnsorted(); |
| 1590 | } |
| 1591 | if (Objects.ResortProc) Objects.ExecuteResorts(); |
| 1592 | |
| 1593 | #ifdef DEBUGREC |
| 1594 | AddDbgRec(RCT_Block, "ObjRm" , 6); |
| 1595 | #endif |
| 1596 | |
| 1597 | // Removal |
| 1598 | if (!Tick255) ObjectRemovalCheck(); |
| 1599 | } |
| 1600 | |
| 1601 | bool C4Game::CreateViewport(int32_t iPlayer, bool fSilent) |
| 1602 | { |
| 1603 | return GraphicsSystem.CreateViewport(iPlayer, fSilent); |
| 1604 | } |
| 1605 | |
| 1606 | C4ID DefFileGetID(const char *szFilename) |
| 1607 | { |
| 1608 | C4Group hDef; |
| 1609 | C4DefCore DefCore; |
| 1610 | if (!hDef.Open(szGroupName: szFilename)) return C4ID_None; |
| 1611 | if (!DefCore.Load(hGroup&: hDef)) { hDef.Close(); return C4ID_None; } |
| 1612 | hDef.Close(); |
| 1613 | return DefCore.id; |
| 1614 | } |
| 1615 | |
| 1616 | bool C4Game::DropFile(const char *szFilename, int32_t iX, int32_t iY) |
| 1617 | { |
| 1618 | C4ID c_id; C4Def *cdef; |
| 1619 | // Drop def to create object |
| 1620 | if (SEqualNoCase(szStr1: GetExtension(fname: szFilename), szStr2: "c4d" )) |
| 1621 | { |
| 1622 | // Get id from file |
| 1623 | if (c_id = DefFileGetID(szFilename)) |
| 1624 | // Get loaded def or try to load def from file |
| 1625 | if ((cdef = C4Id2Def(id: c_id)) |
| 1626 | || (Defs.Load(szSearch: szFilename, dwLoadWhat: C4D_Load_RX, szLanguage: Config.General.LanguageEx, pSoundSystem: &*Application.SoundSystem) && (cdef = C4Id2Def(id: c_id)))) |
| 1627 | { |
| 1628 | return DropDef(id: c_id, iX, iY); |
| 1629 | } |
| 1630 | // Failure |
| 1631 | Console.Out(text: LoadResStr(id: C4ResStrTableKey::IDS_CNS_DROPNODEF, args: GetFilename(path: szFilename)).c_str()); |
| 1632 | return false; |
| 1633 | } |
| 1634 | return false; |
| 1635 | } |
| 1636 | |
| 1637 | bool C4Game::DropDef(C4ID id, int32_t iX, int32_t iY) |
| 1638 | { |
| 1639 | // def exists? |
| 1640 | if (C4Id2Def(id)) |
| 1641 | { |
| 1642 | Control.DoInput(eCtrlType: CID_EMDropDef, pPkt: new C4ControlEMDropDef(id, iX, iY), eDelivery: CDT_Decide); |
| 1643 | return true; |
| 1644 | } |
| 1645 | else |
| 1646 | { |
| 1647 | // Failure |
| 1648 | Console.Out(text: LoadResStr(id: C4ResStrTableKey::IDS_CNS_DROPNODEF, args: C4IdText(id)).c_str()); |
| 1649 | } |
| 1650 | return false; |
| 1651 | } |
| 1652 | |
| 1653 | bool C4Game::EnumerateMaterials() |
| 1654 | { |
| 1655 | // Check material number |
| 1656 | if (Material.Num > C4MaxMaterial) |
| 1657 | { |
| 1658 | LogFatal(id: C4ResStrTableKey::IDS_PRC_TOOMANYMATS); return false; |
| 1659 | } |
| 1660 | |
| 1661 | // Get hardcoded system material indices |
| 1662 | MVehic = Material.Get(szMaterial: "Vehicle" ); MCVehic = Mat2PixColDefault(mat: MVehic); |
| 1663 | MTunnel = Material.Get(szMaterial: "Tunnel" ); |
| 1664 | MWater = Material.Get(szMaterial: "Water" ); |
| 1665 | MSnow = Material.Get(szMaterial: "Snow" ); |
| 1666 | MGranite = Material.Get(szMaterial: "Granite" ); |
| 1667 | |
| 1668 | const std::string_view material{C4S.Landscape.Material}; |
| 1669 | |
| 1670 | if (const std::size_t pos = material.find(c: '-'); pos != std::string_view::npos) |
| 1671 | { |
| 1672 | if (CompareVersion(iVer1: C4S.Head.C4XVer[0], iVer2: C4S.Head.C4XVer[1], iVer3: C4S.Head.C4XVer[2], iVer4: C4S.Head.C4XVer[3], iVerBuild: C4S.Head.C4XVer[4], iRVer1: 4, iRVer2: 9, iRVer3: 10, iRVer4: 15, iRVerBuild: 359) == -1) |
| 1673 | { |
| 1674 | if (!C4S.Landscape.InEarth.IsClear() || !C4S.Animals.EarthNest.IsClear()) |
| 1675 | { |
| 1676 | DebugLog(level: spdlog::level::warn, fmt: "Scenario.txt: Material={} specifies a texture, which breaks InEarth and Nest before [359]. Version=4,9,10,15,359 or higher enables the fixed behavior." , args: +C4S.Landscape.Material); |
| 1677 | } |
| 1678 | |
| 1679 | MEarth = MNone; |
| 1680 | } |
| 1681 | else |
| 1682 | { |
| 1683 | MEarth = Material.Get(szMaterial: std::string{material.substr(pos: 0, n: pos)}.c_str()); |
| 1684 | } |
| 1685 | } |
| 1686 | else |
| 1687 | { |
| 1688 | MEarth = Material.Get(szMaterial: C4S.Landscape.Material); |
| 1689 | } |
| 1690 | |
| 1691 | if ((MVehic == MNone) || (MTunnel == MNone)) |
| 1692 | { |
| 1693 | LogFatal(id: C4ResStrTableKey::IDS_PRC_NOSYSMATS); return false; |
| 1694 | } |
| 1695 | // mapping to landscape palette will occur when landscape has been created |
| 1696 | // set the pal |
| 1697 | GraphicsSystem.SetPalette(); |
| 1698 | |
| 1699 | return true; |
| 1700 | } |
| 1701 | |
| 1702 | void C4Game::CastObjects(C4ID id, C4Object *pCreator, int32_t num, int32_t level, int32_t tx, int32_t ty, int32_t iOwner, int32_t iController) |
| 1703 | { |
| 1704 | int32_t cnt; |
| 1705 | for (cnt = 0; cnt < num; cnt++) |
| 1706 | { |
| 1707 | // force argument evaluation order |
| 1708 | const auto r4 = itofix(x: Random(iRange: 3) + 1); |
| 1709 | const auto r3 = FIXED10(x: Random(iRange: 2 * level + 1) - level); |
| 1710 | const auto r2 = FIXED10(x: Random(iRange: 2 * level + 1) - level); |
| 1711 | const auto r1 = Random(iRange: 360); |
| 1712 | CreateObject(id, pCreator, iOwner, x: tx, y: ty, r: r1, xdir: r2, ydir: r3, rdir: r4, iController); |
| 1713 | } |
| 1714 | } |
| 1715 | |
| 1716 | void C4Game::BlastCastObjects(C4ID id, C4Object *pCreator, int32_t num, int32_t tx, int32_t ty, int32_t iController) |
| 1717 | { |
| 1718 | int32_t cnt; |
| 1719 | for (cnt = 0; cnt < num; cnt++) |
| 1720 | { |
| 1721 | // force argument evaluation order |
| 1722 | const auto r4 = itofix(x: Random(iRange: 3) + 1); |
| 1723 | const auto r3 = FIXED10(x: Random(iRange: 61) - 40); |
| 1724 | const auto r2 = FIXED10(x: Random(iRange: 61) - 30); |
| 1725 | const auto r1 = Random(iRange: 360); |
| 1726 | CreateObject(id, pCreator, iOwner: NO_OWNER, x: tx, y: ty, r: r1, xdir: r2, ydir: r3, rdir: r4, iController); |
| 1727 | } |
| 1728 | } |
| 1729 | |
| 1730 | void C4Game::Sec1Timer() |
| 1731 | { |
| 1732 | // updates the game clock |
| 1733 | if (TimeGo) { Time++; TimeGo = false; } |
| 1734 | FPS = cFPS; cFPS = 0; |
| 1735 | } |
| 1736 | |
| 1737 | void C4Game::Default() |
| 1738 | { |
| 1739 | PointersDenumerated = false; |
| 1740 | IsRunning = false; |
| 1741 | FrameCounter = 0; |
| 1742 | GameOver = GameOverDlgShown = false; |
| 1743 | ScenarioFilename[0] = 0; |
| 1744 | PlayerFilenames[0] = 0; |
| 1745 | DefinitionFilenames.clear(); |
| 1746 | FixedDefinitions = false; |
| 1747 | DirectJoinAddress[0] = 0; |
| 1748 | pJoinReference = nullptr; |
| 1749 | Parameters.ScenarioTitle.Ref(pnData: "Loading..." ); |
| 1750 | HaltCount = 0; |
| 1751 | Evaluated = false; |
| 1752 | TimeGo = false; |
| 1753 | Time = 0; |
| 1754 | StartTime = 0; |
| 1755 | InitProgress = 0; LastInitProgress = 0; |
| 1756 | FPS = cFPS = 0; |
| 1757 | fScriptCreatedObjects = false; |
| 1758 | fLobby = fObserve = false; |
| 1759 | iLobbyTimeout = 0; |
| 1760 | iTick2 = iTick3 = iTick5 = iTick10 = iTick35 = iTick255 = iTick500 = iTick1000 = 0; |
| 1761 | ObjectEnumerationIndex = 0; |
| 1762 | FullSpeed = false; |
| 1763 | FrameSkip = 1; DoSkipFrame = false; |
| 1764 | PreloadStatus = PreloadLevel::None; |
| 1765 | Defs.Clear(); |
| 1766 | Material.Default(); |
| 1767 | Objects.Default(); |
| 1768 | BackObjects.Default(); |
| 1769 | ForeObjects.Default(); |
| 1770 | Players.Default(); |
| 1771 | Weather.Default(); |
| 1772 | Landscape.Default(); |
| 1773 | TextureMap.Default(); |
| 1774 | Rank.Default(); |
| 1775 | MassMover.Default(); |
| 1776 | PXS.Default(); |
| 1777 | GraphicsSystem.Default(); |
| 1778 | C4S.Default(); |
| 1779 | Messages.Clear(); |
| 1780 | MessageInput.Default(); |
| 1781 | Info.Default(); |
| 1782 | Title.Default(); |
| 1783 | Names.Default(); |
| 1784 | GameText.Default(); |
| 1785 | MainSysLangStringTable.Default(); |
| 1786 | ScenarioLangStringTable.Default(); |
| 1787 | ScenarioSysLangStringTable.Default(); |
| 1788 | Script.Default(); |
| 1789 | GraphicsResource.Default(); |
| 1790 | Control.Default(); |
| 1791 | MouseControl.Default(); |
| 1792 | PathFinder.Default(); |
| 1793 | TransferZones.Default(); |
| 1794 | GroupSet.Default(); |
| 1795 | pParentGroup = nullptr; |
| 1796 | pGUI = nullptr; |
| 1797 | pScenarioSections = pCurrentScenarioSection = nullptr; |
| 1798 | *CurrentScenarioSection = 0; |
| 1799 | pGlobalEffects = nullptr; |
| 1800 | fResortAnyObject = false; |
| 1801 | pNetworkStatistics = nullptr; |
| 1802 | IsMusicEnabled = false; |
| 1803 | iMusicLevel = 100; |
| 1804 | PlayList.Clear(); |
| 1805 | } |
| 1806 | |
| 1807 | void C4Game::Evaluate() |
| 1808 | { |
| 1809 | // League game? |
| 1810 | bool fLeague = Network.isEnabled() && Network.isHost() && Parameters.isLeague(); |
| 1811 | |
| 1812 | // Stop record |
| 1813 | StdStrBuf RecordName; uint8_t RecordSHA[StdSha1::DigestLength]; |
| 1814 | if (Control.isRecord()) |
| 1815 | Control.StopRecord(pRecordName: &RecordName, pRecordSHA1: fLeague ? RecordSHA : nullptr); |
| 1816 | |
| 1817 | // Send league result |
| 1818 | if (fLeague) |
| 1819 | Network.LeagueGameEvaluate(szRecordName: RecordName.getData(), pRecordSHA: RecordSHA); |
| 1820 | |
| 1821 | // Players |
| 1822 | // saving local players only, because remote players will probably not rejoin after evaluation anyway) |
| 1823 | Players.Evaluate(); |
| 1824 | Players.Save(fSaveLocalOnly: true); |
| 1825 | |
| 1826 | // Round results |
| 1827 | RoundResults.EvaluateGame(); |
| 1828 | |
| 1829 | // Set game flag |
| 1830 | Log(id: C4ResStrTableKey::IDS_PRC_EVALUATED); |
| 1831 | Evaluated = true; |
| 1832 | } |
| 1833 | |
| 1834 | void C4Game::DrawCursors(C4FacetEx &cgo, int32_t iPlayer) |
| 1835 | { |
| 1836 | const auto scale = Application.GetScale(); |
| 1837 | const auto inverseScale = 1 / scale; |
| 1838 | C4DrawTransform transform; |
| 1839 | transform.SetMoveScale(dx: 0.f, dy: 0.f, sx: inverseScale, sy: inverseScale); |
| 1840 | // Draw cursor mark arrow & cursor object name |
| 1841 | C4Object *cursor; |
| 1842 | C4Facet &fctCursor = GraphicsResource.fctCursor; |
| 1843 | for (C4Player *pPlr = Players.First; pPlr; pPlr = pPlr->Next) |
| 1844 | if (pPlr->Number == iPlayer || iPlayer == NO_OWNER) |
| 1845 | if (pPlr->CursorFlash || pPlr->SelectFlash) |
| 1846 | if (pPlr->Cursor) |
| 1847 | { |
| 1848 | cursor = pPlr->Cursor; |
| 1849 | if (Inside<int32_t>(ival: cursor->x - fctCursor.Wdt / 2 - cgo.TargetX, lbound: 1 - fctCursor.Wdt, rbound: cgo.Wdt) && Inside<int32_t>(ival: cursor->y - cursor->Def->Shape.Hgt / 2 - fctCursor.Hgt - cgo.TargetY, lbound: 1 - fctCursor.Hgt, rbound: cgo.Hgt)) |
| 1850 | { |
| 1851 | int32_t cox = cgo.X + cursor->x - cgo.TargetX; |
| 1852 | int32_t coy = cgo.Y + cursor->y - cgo.TargetY; |
| 1853 | int32_t cphase = (cursor->Contained ? 1 : 0); |
| 1854 | fctCursor.DrawT(sfcTarget: cgo.Surface, iX: static_cast<int>(cox * scale) - fctCursor.Wdt / 2, iY: static_cast<int>(coy * scale) - cursor->Def->Shape.Hgt / 2 - fctCursor.Hgt, iPhaseX: cphase, iPhaseY: 0, pTransform: &transform, noScalingCorrection: true); |
| 1855 | if (cursor->Info) |
| 1856 | { |
| 1857 | std::string text{cursor->GetName()}; |
| 1858 | int32_t texthgt = GraphicsResource.FontRegular.GetLineHeight(); |
| 1859 | if (cursor->Info->Rank > 0) |
| 1860 | { |
| 1861 | text = std::format(fmt: "{}|{}" , args: cursor->Info->sRankName.getData(), args: cursor->GetName()); |
| 1862 | texthgt += texthgt; |
| 1863 | } |
| 1864 | |
| 1865 | Application.DDraw->TextOut(szText: text.c_str(), rFont&: GraphicsResource.FontRegular, fZoom: 1.0, sfcDest: cgo.Surface, |
| 1866 | iTx: cox, |
| 1867 | iTy: coy - cursor->Def->Shape.Hgt / 2 - static_cast<int>(fctCursor.Hgt / scale) - 2 - texthgt, |
| 1868 | dwFCol: 0xffff0000, byForm: ACenter); |
| 1869 | } |
| 1870 | } |
| 1871 | } |
| 1872 | } |
| 1873 | |
| 1874 | void C4Game::Ticks() |
| 1875 | { |
| 1876 | // Frames |
| 1877 | FrameCounter++; GameGo = FullSpeed; |
| 1878 | // Ticks |
| 1879 | if (++iTick2 == 2) iTick2 = 0; |
| 1880 | if (++iTick3 == 3) iTick3 = 0; |
| 1881 | if (++iTick5 == 5) iTick5 = 0; |
| 1882 | if (++iTick10 == 10) iTick10 = 0; |
| 1883 | if (++iTick35 == 35) iTick35 = 0; |
| 1884 | if (++iTick255 == 255) iTick255 = 0; |
| 1885 | if (++iTick500 == 500) iTick500 = 0; |
| 1886 | if (++iTick1000 == 1000) iTick1000 = 0; |
| 1887 | // FPS / time |
| 1888 | cFPS++; TimeGo = true; |
| 1889 | // Frame skip |
| 1890 | if (FrameCounter % FrameSkip) DoSkipFrame = true; |
| 1891 | // Control |
| 1892 | Control.Ticks(); |
| 1893 | // Full speed |
| 1894 | if (GameGo) Application.NextTick(fYield: false); // short-circuit the timer |
| 1895 | // statistics |
| 1896 | if (pNetworkStatistics) pNetworkStatistics->ExecuteFrame(); |
| 1897 | } |
| 1898 | |
| 1899 | bool C4Game::Compile(const char *szSource) |
| 1900 | { |
| 1901 | if (!szSource) return true; |
| 1902 | // C4Game is not defaulted on compilation. |
| 1903 | // Loading of runtime data overrides only certain values. |
| 1904 | // Doesn't compile players; those will be done later |
| 1905 | CompileSettings Settings(false, false, true); |
| 1906 | if (!CompileFromBuf_LogWarn<StdCompilerINIRead>( |
| 1907 | TargetStruct: mkParAdapt(rObj&: *this, rPar: Settings), |
| 1908 | SrcBuf: StdStrBuf(szSource, false), |
| 1909 | C4CFN_Game)) |
| 1910 | return false; |
| 1911 | return true; |
| 1912 | } |
| 1913 | |
| 1914 | void C4Game::CompileFunc(StdCompiler *pComp, CompileSettings comp) |
| 1915 | { |
| 1916 | if (!comp.fScenarioSection && comp.fExact) |
| 1917 | { |
| 1918 | const auto name = pComp->Name(szName: "Game" ); |
| 1919 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Time, szName: "Time" , rDefault: 0)); |
| 1920 | pComp->Value(rStruct: mkNamingAdapt(rValue&: FrameCounter, szName: "Frame" , rDefault: 0)); |
| 1921 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Control.ControlTick, szName: "ControlTick" , rDefault: 0)); |
| 1922 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Control.SyncRate, szName: "SyncRate" , rDefault: C4SyncCheckRate)); |
| 1923 | pComp->Value(rStruct: mkNamingAdapt(rValue&: iTick2, szName: "Tick2" , rDefault: 0)); |
| 1924 | pComp->Value(rStruct: mkNamingAdapt(rValue&: iTick3, szName: "Tick3" , rDefault: 0)); |
| 1925 | pComp->Value(rStruct: mkNamingAdapt(rValue&: iTick5, szName: "Tick5" , rDefault: 0)); |
| 1926 | pComp->Value(rStruct: mkNamingAdapt(rValue&: iTick10, szName: "Tick10" , rDefault: 0)); |
| 1927 | pComp->Value(rStruct: mkNamingAdapt(rValue&: iTick35, szName: "Tick35" , rDefault: 0)); |
| 1928 | pComp->Value(rStruct: mkNamingAdapt(rValue&: iTick255, szName: "Tick255" , rDefault: 0)); |
| 1929 | pComp->Value(rStruct: mkNamingAdapt(rValue&: iTick500, szName: "Tick500" , rDefault: 0)); |
| 1930 | pComp->Value(rStruct: mkNamingAdapt(rValue&: iTick1000, szName: "Tick1000" , rDefault: 0)); |
| 1931 | pComp->Value(rStruct: mkNamingAdapt(rValue&: ObjectEnumerationIndex, szName: "ObjectEnumerationIndex" , rDefault: 0)); |
| 1932 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Rules, szName: "Rules" , rDefault: 0)); |
| 1933 | pComp->Value(rStruct: mkNamingAdapt(rValue&: PlayList, szName: "PlayList" , rDefault: "" )); |
| 1934 | pComp->Value(rStruct: mkNamingAdapt(mkStringAdaptMA(CurrentScenarioSection), szName: "CurrentScenarioSection" , rDefault: "" )); |
| 1935 | pComp->Value(rStruct: mkNamingAdapt(rValue&: fResortAnyObject, szName: "ResortAnyObj" , rDefault: false)); |
| 1936 | pComp->Value(rStruct: mkNamingAdapt(rValue&: IsMusicEnabled, szName: "MusicEnabled" , rDefault: false)); |
| 1937 | pComp->Value(rStruct: mkNamingAdapt(rValue&: iMusicLevel, szName: "MusicLevel" , rDefault: 100)); |
| 1938 | pComp->Value(rStruct: mkNamingAdapt(rValue&: NextMission, szName: "NextMission" , rDefault: StdStrBuf())); |
| 1939 | pComp->Value(rStruct: mkNamingAdapt(rValue&: NextMissionText, szName: "NextMissionText" , rDefault: StdStrBuf())); |
| 1940 | pComp->Value(rStruct: mkNamingAdapt(rValue&: NextMissionDesc, szName: "NextMissionDesc" , rDefault: StdStrBuf())); |
| 1941 | pComp->Value(rStruct: mkNamingAdapt(rValue: mkSTLMapAdapt(map&: MessageInput.Commands), szName: "MessageBoardCommands" , rDefault: std::unordered_map<std::string, C4MessageBoardCommand>{})); |
| 1942 | } |
| 1943 | |
| 1944 | pComp->Value(rStruct: mkNamingAdapt(rValue: mkInsertAdapt(rObj&: Script, rIns&: ScriptEngine), szName: "Script" )); |
| 1945 | |
| 1946 | if (comp.fExact) |
| 1947 | { |
| 1948 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Weather, szName: "Weather" )); |
| 1949 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Landscape, szName: "Landscape" )); |
| 1950 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Landscape.Sky, szName: "Sky" )); |
| 1951 | } |
| 1952 | |
| 1953 | pComp->Value(rStruct: mkNamingAdapt(rValue: mkNamingPtrAdapt(rpObj&: pGlobalEffects, szNaming: "GlobalEffects" ), szName: "Effects" )); |
| 1954 | |
| 1955 | // scoreboard compiles into main level [Scoreboard] |
| 1956 | if (!comp.fScenarioSection && comp.fExact) |
| 1957 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Scoreboard, szName: "Scoreboard" )); |
| 1958 | if (comp.fPlayers) |
| 1959 | { |
| 1960 | assert(pComp->isDecompiler()); |
| 1961 | // player parsing: Parse all players |
| 1962 | // This doesn't create any players, but just parses existing by their ID |
| 1963 | // Primary player ininitialization (also setting ID) is done by player info list |
| 1964 | // Won't work this way for binary mode! |
| 1965 | for (C4Player *pPlr = Players.First; pPlr; pPlr = pPlr->Next) |
| 1966 | pComp->Value(rStruct: mkNamingAdapt(rValue&: *pPlr, szName: std::format(fmt: "Player{}" , args&: pPlr->ID).c_str())); |
| 1967 | } |
| 1968 | } |
| 1969 | |
| 1970 | void SetClientPrefix(char *szFilename, const char *szClient); |
| 1971 | |
| 1972 | bool C4Game::Decompile(std::string &buf, bool fSaveSection, bool fSaveExact) |
| 1973 | { |
| 1974 | // Decompile (without players for scenario sections) |
| 1975 | buf = DecompileToBuf<StdCompilerINIWrite>(SrcStruct: mkParAdapt(rObj&: *this, rPar: CompileSettings(fSaveSection, !fSaveSection && fSaveExact, fSaveExact))); |
| 1976 | return true; |
| 1977 | } |
| 1978 | |
| 1979 | bool C4Game::Preload() |
| 1980 | { |
| 1981 | if (CanPreload()) |
| 1982 | { |
| 1983 | #ifndef USE_CONSOLE |
| 1984 | std::unique_ptr<CStdGLCtx> context{lpDDraw->CreateContext(Application.pWindow, &Application)}; |
| 1985 | if (!context) |
| 1986 | { |
| 1987 | return false; |
| 1988 | } |
| 1989 | |
| 1990 | context->Deselect(); |
| 1991 | pGL->GetMainCtx().Select(); |
| 1992 | PreloadThread = C4Thread::Create(options: {.Name: "PreloadThread" }, func: [](std::unique_ptr<CStdGLCtx> preloadContext) |
| 1993 | { |
| 1994 | preloadContext->Select(verbose: false, selectOnly: true); |
| 1995 | CStdLock lock{&Game.PreloadMutex}; |
| 1996 | Game.InitGameFirstPart() && Game.InitGameSecondPart(hGroup&: Game.ScenarioFile, section: nullptr, fLoadSky: true, preloading: true); |
| 1997 | preloadContext->Finish(); |
| 1998 | preloadContext->Deselect(secondary: true); |
| 1999 | }, |
| 2000 | args: std::move(context)); |
| 2001 | #else |
| 2002 | PreloadThread = C4Thread::Create({"PreloadThread" }, [] |
| 2003 | { |
| 2004 | CStdLock lock{&Game.PreloadMutex}; |
| 2005 | Game.InitGameFirstPart() && Game.InitGameSecondPart(Game.ScenarioFile, nullptr, true, true); |
| 2006 | }); |
| 2007 | #endif |
| 2008 | |
| 2009 | return true; |
| 2010 | } |
| 2011 | |
| 2012 | return false; |
| 2013 | } |
| 2014 | |
| 2015 | bool C4Game::CanPreload() const |
| 2016 | { |
| 2017 | return PreloadStatus <= PreloadLevel::Scenario && !PreloadThread.joinable(); |
| 2018 | } |
| 2019 | |
| 2020 | bool C4Game::CompileRuntimeData(C4ComponentHost &rGameData) |
| 2021 | { |
| 2022 | // Compile |
| 2023 | if (!Compile(szSource: rGameData.GetData())) return false; |
| 2024 | // Music System: Set play list |
| 2025 | Application.MusicSystem->SetPlayList(PlayList.getData()); |
| 2026 | // Success |
| 2027 | return true; |
| 2028 | } |
| 2029 | |
| 2030 | bool C4Game::SaveData(C4Group &hGroup, bool fSaveSection, bool fInitial, bool fSaveExact) |
| 2031 | { |
| 2032 | // Enumerate pointers & strings |
| 2033 | if (PointersDenumerated) |
| 2034 | { |
| 2035 | Players.EnumeratePointers(); |
| 2036 | ScriptEngine.Strings.EnumStrings(); |
| 2037 | if (pGlobalEffects) pGlobalEffects->EnumeratePointers(); |
| 2038 | } |
| 2039 | |
| 2040 | // Decompile |
| 2041 | std::string buf; |
| 2042 | if (!Decompile(buf, fSaveSection, fSaveExact)) |
| 2043 | return false; |
| 2044 | |
| 2045 | // Denumerate pointers, if game is in denumerated state |
| 2046 | if (PointersDenumerated) |
| 2047 | { |
| 2048 | ScriptEngine.DenumerateVariablePointers(); |
| 2049 | Players.DenumeratePointers(); |
| 2050 | if (pGlobalEffects) pGlobalEffects->DenumeratePointers(); |
| 2051 | } |
| 2052 | |
| 2053 | // Initial? |
| 2054 | if (fInitial && GameText.GetData()) |
| 2055 | { |
| 2056 | // HACK: Reinsert player sections, if any. |
| 2057 | const char *pPlayerSections = strstr(haystack: GameText.GetData(), needle: "[Player" ); |
| 2058 | if (pPlayerSections) |
| 2059 | { |
| 2060 | buf += "\r\n\r\n" ; |
| 2061 | buf += pPlayerSections; |
| 2062 | } |
| 2063 | } |
| 2064 | |
| 2065 | // Empty? All default; just remove from group then |
| 2066 | if (buf.empty()) |
| 2067 | { |
| 2068 | hGroup.Delete(C4CFN_Game); |
| 2069 | return true; |
| 2070 | } |
| 2071 | |
| 2072 | // Save |
| 2073 | StdStrBuf copy{buf.c_str(), buf.size()}; |
| 2074 | return hGroup.Add(C4CFN_Game, pBuffer&: copy, fChild: false, fHoldBuffer: true); |
| 2075 | } |
| 2076 | |
| 2077 | bool C4Game::SaveGameTitle(C4Group &hGroup) |
| 2078 | { |
| 2079 | // Game not running |
| 2080 | if (!FrameCounter) |
| 2081 | { |
| 2082 | char *bpBytes; size_t iSize; |
| 2083 | if (ScenarioFile.LoadEntry(C4CFN_ScenarioTitle, lpbpBuf: &bpBytes, ipSize: &iSize)) |
| 2084 | hGroup.Add(C4CFN_ScenarioTitle, pBuffer: bpBytes, iSize, fChild: false, fHoldBuffer: true); |
| 2085 | if (ScenarioFile.LoadEntry(C4CFN_ScenarioTitlePNG, lpbpBuf: &bpBytes, ipSize: &iSize)) |
| 2086 | hGroup.Add(C4CFN_ScenarioTitlePNG, pBuffer: bpBytes, iSize, fChild: false, fHoldBuffer: true); |
| 2087 | } |
| 2088 | |
| 2089 | // Fullscreen screenshot |
| 2090 | else if (Application.isFullScreen && Application.Active) |
| 2091 | { |
| 2092 | constexpr std::int32_t surfaceWidth{200}; |
| 2093 | constexpr std::int32_t surfaceHeight{150}; |
| 2094 | |
| 2095 | const auto surface = std::make_unique<C4Surface>(args: surfaceWidth, args: surfaceHeight); |
| 2096 | |
| 2097 | // Fullscreen |
| 2098 | Application.DDraw->Blit(sfcSource: Application.DDraw->lpBack, |
| 2099 | fx: 0.0f, fy: 0.0f, fwdt: float(Application.DDraw->lpBack->Wdt), fhgt: float(Application.DDraw->lpBack->Hgt), |
| 2100 | sfcTarget: surface.get(), tx: 0, ty: 0, twdt: surfaceWidth, thgt: surfaceHeight); |
| 2101 | |
| 2102 | if (!surface->SavePNG(szFilename: Config.AtTempPath(C4CFN_TempTitle), fSaveAlpha: false, fApplyGamma: !Config.Graphics.Shader, fSaveOverlayOnly: false)) |
| 2103 | { |
| 2104 | return false; |
| 2105 | } |
| 2106 | |
| 2107 | if (!hGroup.Move(szFile: Config.AtTempPath(C4CFN_TempTitle), C4CFN_ScenarioTitlePNG)) |
| 2108 | { |
| 2109 | return false; |
| 2110 | } |
| 2111 | } |
| 2112 | |
| 2113 | return true; |
| 2114 | } |
| 2115 | |
| 2116 | bool C4Game::DoKeyboardInput(C4KeyCode vk_code, C4KeyEventType eEventType, bool fAlt, bool fCtrl, bool fShift, bool fRepeated, class C4GUI::Dialog *pForDialog, bool fPlrCtrlOnly) |
| 2117 | { |
| 2118 | #ifdef USE_X11 |
| 2119 | static std::map<C4KeyCode, bool> PressedKeys; |
| 2120 | // Keyrepeats are send as down, down, ..., down, up, where all downs are not distinguishable from the first. |
| 2121 | if (eEventType == KEYEV_Down) |
| 2122 | { |
| 2123 | if (PressedKeys[vk_code]) fRepeated = true; |
| 2124 | else PressedKeys[vk_code] = true; |
| 2125 | } |
| 2126 | else if (eEventType == KEYEV_Up) |
| 2127 | { |
| 2128 | PressedKeys[vk_code] = false; |
| 2129 | } |
| 2130 | #endif |
| 2131 | // compose key |
| 2132 | C4KeyCodeEx Key(vk_code, C4KeyShiftState(fAlt * KEYS_Alt + fCtrl * KEYS_Control + fShift * KEYS_Shift), fRepeated); |
| 2133 | // compose keyboard scope |
| 2134 | uint32_t InScope = 0; |
| 2135 | if (fPlrCtrlOnly) |
| 2136 | InScope = KEYSCOPE_Control; |
| 2137 | else |
| 2138 | { |
| 2139 | if (IsRunning) InScope = KEYSCOPE_Generic; |
| 2140 | // if GUI has keyfocus, this overrides regular controls |
| 2141 | if (pGUI && (pGUI->HasKeyboardFocus() || pForDialog)) |
| 2142 | { |
| 2143 | InScope |= KEYSCOPE_Gui; |
| 2144 | // control to console mode dialog: Make current keyboard target the active dlg, |
| 2145 | // so it can process input |
| 2146 | if (pForDialog) pGUI->ActivateDialog(pDlg: pForDialog); |
| 2147 | // any keystroke in GUI resets tooltip times |
| 2148 | pGUI->KeyAny(); |
| 2149 | } |
| 2150 | else |
| 2151 | { |
| 2152 | if (Application.isFullScreen) |
| 2153 | { |
| 2154 | if (FullScreen.pMenu && FullScreen.pMenu->IsActive()) // fullscreen menu |
| 2155 | InScope |= KEYSCOPE_FullSMenu; |
| 2156 | else if (C4S.Head.Replay && C4S.Head.Film) // film view only |
| 2157 | InScope |= KEYSCOPE_FilmView; |
| 2158 | else if (GraphicsSystem.GetViewport(iPlayer: NO_OWNER)) // NO_OWNER-viewport-controls |
| 2159 | InScope |= KEYSCOPE_FreeView; |
| 2160 | else |
| 2161 | { |
| 2162 | // regular player viewport controls |
| 2163 | InScope |= KEYSCOPE_FullSView; |
| 2164 | // player controls disabled during round over dlg |
| 2165 | if (!C4GameOverDlg::IsShown()) InScope |= KEYSCOPE_Control; |
| 2166 | } |
| 2167 | } |
| 2168 | else |
| 2169 | // regular player viewport controls |
| 2170 | InScope |= KEYSCOPE_Control; |
| 2171 | } |
| 2172 | // fullscreen/console (in running game) |
| 2173 | if (IsRunning) |
| 2174 | { |
| 2175 | if (FullScreen.Active) InScope |= KEYSCOPE_Fullscreen; |
| 2176 | if (Console.Active) InScope |= KEYSCOPE_Console; |
| 2177 | } |
| 2178 | } |
| 2179 | // okay; do input |
| 2180 | if (KeyboardInput.DoInput(InKey: Key, InEvent: eEventType, InScope)) |
| 2181 | return true; |
| 2182 | |
| 2183 | // unprocessed key |
| 2184 | return false; |
| 2185 | } |
| 2186 | |
| 2187 | bool C4Game::CanQuickSave() |
| 2188 | { |
| 2189 | if (Network.isEnabled()) |
| 2190 | { |
| 2191 | // Network hosts only |
| 2192 | if (!Network.isHost()) |
| 2193 | { |
| 2194 | Log(id: C4ResStrTableKey::IDS_GAME_NOCLIENTSAVE); return false; |
| 2195 | } |
| 2196 | |
| 2197 | // Not for league games |
| 2198 | if (Parameters.isLeague() && !Control.isReplay()) |
| 2199 | { |
| 2200 | LogNTr(message: "[!] It's not allowed to save running league games" ); return false; |
| 2201 | } |
| 2202 | } |
| 2203 | |
| 2204 | return true; |
| 2205 | } |
| 2206 | |
| 2207 | bool C4Game::QuickSave(const char *strFilename, const char *strTitle, bool fForceSave) |
| 2208 | { |
| 2209 | // Check |
| 2210 | if (!fForceSave) if (!CanQuickSave()) return false; |
| 2211 | |
| 2212 | // Set working directory (should already be in exe path anyway - just to make sure...?) |
| 2213 | SetWorkingDirectory(Config.General.ExePath); |
| 2214 | |
| 2215 | // Create savegame folder |
| 2216 | if (!Config.General.CreateSaveFolder(strDirectory: Config.General.SaveGameFolder.getData(), strLanguageTitle: LoadResStr(id: C4ResStrTableKey::IDS_GAME_SAVEGAMESTITLE))) |
| 2217 | { |
| 2218 | Log(id: C4ResStrTableKey::IDS_GAME_FAILSAVEGAME); return false; |
| 2219 | } |
| 2220 | |
| 2221 | // Create savegame subfolder(s) |
| 2222 | char strSaveFolder[_MAX_PATH + 1]; |
| 2223 | for (int i = 0; i < SCharCount(DirectorySeparator, szInStr: strFilename); i++) |
| 2224 | { |
| 2225 | SCopy(szSource: Config.General.SaveGameFolder.getData(), sTarget: strSaveFolder); AppendBackslash(szFileName: strSaveFolder); |
| 2226 | SCopyUntil(szSource: strFilename, sTarget: strSaveFolder + SLen(sptr: strSaveFolder), DirectorySeparator, _MAX_PATH, iIndex: i); |
| 2227 | if (!Config.General.CreateSaveFolder(strDirectory: strSaveFolder, strLanguageTitle: strTitle)) |
| 2228 | { |
| 2229 | Log(id: C4ResStrTableKey::IDS_GAME_FAILSAVEGAME); return false; |
| 2230 | } |
| 2231 | } |
| 2232 | |
| 2233 | // Compose savegame filename |
| 2234 | const std::string savePath{std::format(fmt: "{}" DirSep "{}" , args: Config.General.SaveGameFolder.getData(), args&: strFilename)}; |
| 2235 | |
| 2236 | // Must not be the scenario file that is currently open |
| 2237 | if (ItemIdentical(szFilename1: ScenarioFilename, szFilename2: savePath.c_str())) |
| 2238 | { |
| 2239 | StartSoundEffect(name: "Error" ); |
| 2240 | GameMsgGlobal(szText: LoadResStr(id: C4ResStrTableKey::IDS_GAME_NOSAVEONCURR), iFCol: FRed); |
| 2241 | Log(id: C4ResStrTableKey::IDS_GAME_FAILSAVEGAME); |
| 2242 | return false; |
| 2243 | } |
| 2244 | |
| 2245 | // Wait message |
| 2246 | Log(id: C4ResStrTableKey::IDS_HOLD_SAVINGGAME); |
| 2247 | GraphicsSystem.MessageBoard.EnsureLastMessage(); |
| 2248 | |
| 2249 | // Save to target scenario file |
| 2250 | C4GameSave *pGameSave; |
| 2251 | pGameSave = new C4GameSaveSavegame(); |
| 2252 | if (!pGameSave->Save(szFilename: savePath.c_str())) |
| 2253 | { |
| 2254 | Log(id: C4ResStrTableKey::IDS_GAME_FAILSAVEGAME); delete pGameSave; return false; |
| 2255 | } |
| 2256 | delete pGameSave; |
| 2257 | |
| 2258 | // Success |
| 2259 | Log(id: C4ResStrTableKey::IDS_CNS_GAMESAVED); |
| 2260 | return true; |
| 2261 | } |
| 2262 | |
| 2263 | bool LandscapeFree(int32_t x, int32_t y) |
| 2264 | { |
| 2265 | if (!Inside<int32_t>(ival: x, lbound: 0, GBackWdt - 1) || !Inside<int32_t>(ival: y, lbound: 0, GBackHgt - 1)) return false; |
| 2266 | return !DensitySolid(dens: GBackDensity(x, y)); |
| 2267 | } |
| 2268 | |
| 2269 | void C4Game::ReloadFile(const char *const path) |
| 2270 | { |
| 2271 | if (Network.isEnabled()) return; |
| 2272 | |
| 2273 | const char *const relativePath{Config.AtExeRelativePath(szFilename: path)}; |
| 2274 | |
| 2275 | if (C4Def *const def{Defs.GetByPath(szPath: relativePath)}; def) |
| 2276 | { |
| 2277 | ReloadDef(id: def->id); |
| 2278 | } |
| 2279 | else |
| 2280 | { |
| 2281 | ScriptEngine.ReloadScript(szScript: relativePath, pDefs: &Defs); |
| 2282 | } |
| 2283 | } |
| 2284 | |
| 2285 | bool C4Game::ReloadDef(C4ID id, uint32_t reloadWhat) |
| 2286 | { |
| 2287 | bool fSucc; |
| 2288 | // not in network |
| 2289 | if (Network.isEnabled()) return false; |
| 2290 | // syncronize (close menus with dead surfaces, etc.) |
| 2291 | // no need to sync back player files, though |
| 2292 | Synchronize(fSavePlayerFiles: false); |
| 2293 | // reload def |
| 2294 | C4ObjectLink *clnk; |
| 2295 | C4Def *pDef = Defs.ID2Def(id); |
| 2296 | if (!pDef) return false; |
| 2297 | // Message |
| 2298 | LogNTr(fmt: "Reloading {} from {}" , args: C4IdText(id: pDef->id), args: GetFilename(path: pDef->Filename)); |
| 2299 | // Reload def |
| 2300 | if (Defs.Reload(pDef, dwLoadWhat: reloadWhat, szLanguage: Config.General.LanguageEx, pSoundSystem: &*Application.SoundSystem)) |
| 2301 | { |
| 2302 | // Success, update all concerned object faces |
| 2303 | // may have been done by graphics-update already - but not for objects using graphics of another def |
| 2304 | // better update everything :) |
| 2305 | for (clnk = Objects.First; clnk && clnk->Obj; clnk = clnk->Next) |
| 2306 | { |
| 2307 | if (clnk->Obj->id == id) |
| 2308 | clnk->Obj->UpdateFace(bUpdateShape: true); |
| 2309 | } |
| 2310 | fSucc = true; |
| 2311 | } |
| 2312 | else |
| 2313 | { |
| 2314 | // Failure, remove all objects of this type |
| 2315 | for (clnk = Objects.First; clnk && clnk->Obj; clnk = clnk->Next) |
| 2316 | if (clnk->Obj->id == id) |
| 2317 | clnk->Obj->AssignRemoval(); |
| 2318 | // safety: If a removed def is being profiled, profiling must stop |
| 2319 | C4AulProfiler::Abort(); |
| 2320 | // Kill def |
| 2321 | Defs.Remove(def: pDef); |
| 2322 | // Log |
| 2323 | LogNTr(message: "Reloading failure. All objects of this type removed." ); |
| 2324 | fSucc = false; |
| 2325 | } |
| 2326 | // update game messages |
| 2327 | Messages.UpdateDef(idUpdDef: id); |
| 2328 | // done |
| 2329 | return fSucc; |
| 2330 | } |
| 2331 | |
| 2332 | bool C4Game::ReloadParticle(const char *szName) |
| 2333 | { |
| 2334 | // not in network |
| 2335 | if (Network.isEnabled()) return false; |
| 2336 | // safety |
| 2337 | if (!szName) return false; |
| 2338 | // get particle def |
| 2339 | C4ParticleDef *pDef = Particles.GetDef(szName); |
| 2340 | if (!pDef) return false; |
| 2341 | // verbose |
| 2342 | LogNTr(fmt: "Reloading particle {} from {}" , args: pDef->Name.getData(), args: GetFilename(path: pDef->Filename.getData())); |
| 2343 | // reload it |
| 2344 | if (!pDef->Reload()) |
| 2345 | { |
| 2346 | // safer: remove all particles |
| 2347 | ParticleSystem.ClearParticles(); |
| 2348 | // clear def |
| 2349 | delete pDef; |
| 2350 | // log |
| 2351 | LogNTr(message: "Reloading failure. All particles removed." ); |
| 2352 | // failure |
| 2353 | return false; |
| 2354 | } |
| 2355 | // success |
| 2356 | return true; |
| 2357 | } |
| 2358 | |
| 2359 | bool C4Game::InitGame(C4Group &hGroup, C4ScenarioSection *section, bool fLoadSky) |
| 2360 | { |
| 2361 | const CStdLock lock{&PreloadMutex}; |
| 2362 | { |
| 2363 | if (!section) |
| 2364 | { |
| 2365 | RestartRestoreInfos.Clear(); |
| 2366 | |
| 2367 | C4PlayerInfo *info; |
| 2368 | for (int32_t i = 0; info = PlayerInfos.GetPlayerInfoByIndex(index: i); ++i) |
| 2369 | { |
| 2370 | if (!info->IsRemoved() && !info->IsInvisible()) |
| 2371 | { |
| 2372 | RestartRestoreInfos.Players.emplace(args: std::string{info->GetName()}, args: C4NetworkRestartInfos::Player{std::string{info->GetName()}, info->GetType(), info->GetColor(), info->GetTeam()}); |
| 2373 | } |
| 2374 | } |
| 2375 | |
| 2376 | // file monitor |
| 2377 | if (Config.Developer.AutoFileReload && !Application.isFullScreen && !FileMonitor) |
| 2378 | { |
| 2379 | try |
| 2380 | { |
| 2381 | FileMonitor = std::make_unique<C4FileMonitor>(args: std::bind(f: &C4Game::ReloadFile, args: this, args: std::placeholders::_1)); |
| 2382 | } |
| 2383 | catch (const std::runtime_error &e) |
| 2384 | { |
| 2385 | Log(id: C4ResStrTableKey::IDS_ERR_FILEMONITOR, args: e.what()); |
| 2386 | } |
| 2387 | } |
| 2388 | |
| 2389 | CStdLock lock{&PreloadMutex}; |
| 2390 | if (!InitGameFirstPart()) return false; |
| 2391 | |
| 2392 | // join local players for regular games |
| 2393 | // should be done before record/replay is initialized, so the players are stored in PlayerInfos.txt |
| 2394 | // for local savegame resumes, players are joined into PlayerInfos and later associated in InitPlayers |
| 2395 | if (!Network.isEnabled()) |
| 2396 | { |
| 2397 | PlayerInfos.InitLocal(); |
| 2398 | } |
| 2399 | |
| 2400 | // for replays, make sure teams are assigned correctly |
| 2401 | if (C4S.Head.Replay) |
| 2402 | { |
| 2403 | PlayerInfos.RecheckAutoGeneratedTeams(); // checks that all teams used in playerinfos exist |
| 2404 | Teams.RecheckPlayers(); // syncs player list of teams with teams set in PlayerInfos |
| 2405 | } |
| 2406 | |
| 2407 | for (const auto &def : Parameters.GameRes.iterRes(eType: NRT_Definitions)) |
| 2408 | { |
| 2409 | auto group = std::make_unique<C4Group>(); |
| 2410 | if (!group->Open(szGroupName: def.getFile())) |
| 2411 | { |
| 2412 | LogFatal(id: C4ResStrTableKey::IDS_ERR_LOAD_OPENRES, args: def.getFile(), args: group->GetError()); |
| 2413 | return false; |
| 2414 | } |
| 2415 | |
| 2416 | GroupSet.RegisterGroup(rGroup&: *group.release(), fOwnGrp: true, C4GSPrio_Definitions, C4GSCnt_DefinitionRoot, fCheckContent: true); |
| 2417 | } |
| 2418 | |
| 2419 | // Graphics and fonts (may reinit main font, too) |
| 2420 | // Call it here for overloads by C4GroupSet (definitions, Extra.c4g, scenario, folders etc.) |
| 2421 | Log(id: C4ResStrTableKey::IDS_PRC_GFXRES); |
| 2422 | if (!GraphicsResource.Init()) |
| 2423 | { |
| 2424 | return false; |
| 2425 | } |
| 2426 | |
| 2427 | SetInitProgress(10); |
| 2428 | } |
| 2429 | |
| 2430 | // determine startup player count |
| 2431 | if (!FrameCounter) Parameters.StartupPlayerCount = PlayerInfos.GetStartupCount(); |
| 2432 | |
| 2433 | // The Landscape is the last long chunk of loading time, so it's a good place to start the music fadeout |
| 2434 | if (!section) Application.MusicSystem->Stop(fadeoutMS: 2000); |
| 2435 | |
| 2436 | if (!InitGameSecondPart(hGroup, section, fLoadSky, preloading: false)) |
| 2437 | { |
| 2438 | return false; |
| 2439 | } |
| 2440 | } |
| 2441 | |
| 2442 | if (!section) |
| 2443 | { |
| 2444 | // set up control (inits Record/Replay) |
| 2445 | if (!InitControl()) |
| 2446 | { |
| 2447 | LogFatal(id: C4ResStrTableKey::IDS_ERR_INITCONTROL); |
| 2448 | return false; |
| 2449 | } |
| 2450 | } |
| 2451 | |
| 2452 | // Load round results |
| 2453 | if (!section) |
| 2454 | if (hGroup.FindEntry(C4CFN_RoundResults)) |
| 2455 | { |
| 2456 | if (!RoundResults.Load(hGroup, C4CFN_RoundResults)) |
| 2457 | { |
| 2458 | LogFatal(id: C4ResStrTableKey::IDS_ERR_ERRORLOADINGROUNDRESULTS); return false; |
| 2459 | } |
| 2460 | } |
| 2461 | else |
| 2462 | { |
| 2463 | RoundResults.Init(); |
| 2464 | } |
| 2465 | |
| 2466 | // Denumerate game data pointers |
| 2467 | if (!section) ScriptEngine.DenumerateVariablePointers(); |
| 2468 | if (!section && pGlobalEffects) pGlobalEffects->DenumeratePointers(); |
| 2469 | |
| 2470 | // Check object enumeration |
| 2471 | if (!CheckObjectEnumeration()) |
| 2472 | { |
| 2473 | LogFatal(id: C4ResStrTableKey::IDS_ERR_CHECKOBJECTENUMERATION); |
| 2474 | return false; |
| 2475 | } |
| 2476 | |
| 2477 | // Okay; everything in denumerated state from now on |
| 2478 | PointersDenumerated = true; |
| 2479 | |
| 2480 | for (const auto &def : Defs) |
| 2481 | { |
| 2482 | def->Script.Call(PSF_InitializeDef, pPars: {section ? C4VString(strString: section->GetName()) : C4VNull}); |
| 2483 | } |
| 2484 | |
| 2485 | // Environment |
| 2486 | if (!C4S.Head.NoInitialize && LandscapeLoaded) |
| 2487 | { |
| 2488 | Log(id: C4ResStrTableKey::IDS_PRC_ENVIRONMENT); |
| 2489 | InitVegetation(); |
| 2490 | InitInEarth(); |
| 2491 | InitAnimals(); |
| 2492 | InitEnvironment(); |
| 2493 | InitRules(); |
| 2494 | InitGoals(); |
| 2495 | Landscape.PostInitMap(); |
| 2496 | } |
| 2497 | SetInitProgress(94); |
| 2498 | |
| 2499 | // Weather |
| 2500 | if (LandscapeLoaded) Weather.Init(fScenario: !C4S.Head.SaveGame); |
| 2501 | SetInitProgress(95); |
| 2502 | |
| 2503 | // FoW-color |
| 2504 | FoWColor = C4S.Game.FoWColor; |
| 2505 | |
| 2506 | // goal objects exist, but no GOAL? create it |
| 2507 | if (!C4S.Head.SaveGame) |
| 2508 | if (Objects.ObjectsInt().ObjectCount(id: C4ID_None, dwCategory: C4D_Goal)) |
| 2509 | if (!Objects.FindInternal(id: C4Id(str: "GOAL" ))) |
| 2510 | CreateObject(id: C4Id(str: "GOAL" ), pCreator: nullptr); |
| 2511 | SetInitProgress(96); |
| 2512 | |
| 2513 | // close any gfx groups, because they are no longer needed (after sky is initialized) |
| 2514 | GraphicsResource.CloseFiles(); |
| 2515 | |
| 2516 | if (!section) |
| 2517 | { |
| 2518 | // Music |
| 2519 | Application.MusicSystem->PlayScenarioMusic(ScenarioFile); |
| 2520 | SetMusicLevel(iMusicLevel); |
| 2521 | SetInitProgress(97); |
| 2522 | } |
| 2523 | return true; |
| 2524 | } |
| 2525 | |
| 2526 | bool C4Game::InitGameFirstPart() |
| 2527 | { |
| 2528 | if (PreloadStatus >= PreloadLevel::Basic) |
| 2529 | { |
| 2530 | return true; |
| 2531 | } |
| 2532 | |
| 2533 | if (PreloadStatus == PreloadLevel::None && NetworkActive && !Network.isHost()) |
| 2534 | { |
| 2535 | // get scenario |
| 2536 | SetInitProgress(6); |
| 2537 | char scenario[_MAX_PATH + 1]; |
| 2538 | |
| 2539 | if (!Network.RetrieveScenario(szScenario: scenario)) |
| 2540 | { |
| 2541 | LogFatal(id: C4ResStrTableKey::IDS_ERR_RETRIEVESCENARIO); |
| 2542 | return false; |
| 2543 | } |
| 2544 | |
| 2545 | // open new scenario |
| 2546 | SCopy(szSource: scenario, sTarget: ScenarioFilename, _MAX_PATH); |
| 2547 | if (!OpenScenario()) return false; |
| 2548 | TempScenarioFile = true; |
| 2549 | |
| 2550 | // get everything else |
| 2551 | if (!Parameters.GameRes.RetrieveFiles()) |
| 2552 | { |
| 2553 | LogFatal(id: C4ResStrTableKey::IDS_ERR_RETRIEVEFILES); |
| 2554 | return false; |
| 2555 | } |
| 2556 | |
| 2557 | // Check network game data scenario type (safety) |
| 2558 | if (!C4S.Head.NetworkGame) |
| 2559 | { |
| 2560 | LogFatal(id: C4ResStrTableKey::IDS_NET_NONETGAME); return false; |
| 2561 | } |
| 2562 | |
| 2563 | SetInitProgress(7); |
| 2564 | } |
| 2565 | |
| 2566 | // system scripts |
| 2567 | if (!InitScriptEngine()) |
| 2568 | { |
| 2569 | LogFatal(id: C4ResStrTableKey::IDS_ERR_INITSCRIPTENGINE); |
| 2570 | return false; |
| 2571 | } |
| 2572 | |
| 2573 | SetInitProgress(8); |
| 2574 | |
| 2575 | // Scenario components; |
| 2576 | if (!LoadScenarioComponents()) |
| 2577 | { |
| 2578 | return false; |
| 2579 | } |
| 2580 | |
| 2581 | // Definitions |
| 2582 | if (!InitDefs()) |
| 2583 | { |
| 2584 | LogFatal(id: C4ResStrTableKey::IDS_ERR_INITDEFS); |
| 2585 | return false; |
| 2586 | } |
| 2587 | |
| 2588 | SetInitProgress(40); |
| 2589 | |
| 2590 | // Scenario scripts (and local System.c4g) |
| 2591 | // After defs to get overloading priority |
| 2592 | LoadScenarioScripts(); |
| 2593 | |
| 2594 | SetInitProgress(56); |
| 2595 | |
| 2596 | // Link scripts |
| 2597 | LinkScriptEngine(); |
| 2598 | |
| 2599 | SetInitProgress(57); |
| 2600 | |
| 2601 | // Materials |
| 2602 | if (!InitMaterialTexture()) |
| 2603 | { |
| 2604 | return false; |
| 2605 | } |
| 2606 | SetInitProgress(58); |
| 2607 | |
| 2608 | // Colorize defs by material |
| 2609 | Defs.ColorizeByMaterial(rMats&: Material, bGBM: GBM); |
| 2610 | SetInitProgress(60); |
| 2611 | |
| 2612 | PreloadStatus = PreloadLevel::Basic; |
| 2613 | |
| 2614 | return true; |
| 2615 | } |
| 2616 | |
| 2617 | bool C4Game::InitGameSecondPart(C4Group &hGroup, C4ScenarioSection *section, bool fLoadSky, bool preloading) |
| 2618 | { |
| 2619 | if (!section) |
| 2620 | { |
| 2621 | if (PreloadStatus >= PreloadLevel::LandscapeObjects || (C4S.Landscape.MapPlayerExtend && preloading)) |
| 2622 | { |
| 2623 | return true; |
| 2624 | } |
| 2625 | |
| 2626 | FixRandom(iSeed: Parameters.RandomSeed); |
| 2627 | } |
| 2628 | |
| 2629 | // Landscape |
| 2630 | Log(id: C4ResStrTableKey::IDS_PRC_LANDSCAPE); |
| 2631 | LandscapeLoaded = false; |
| 2632 | if (!Landscape.Init(hGroup, fOverloadCurrent: section, fLoadSky, rfLoaded&: LandscapeLoaded, fSavegame: !!C4S.Head.SaveGame)) |
| 2633 | { |
| 2634 | LogFatal(id: C4ResStrTableKey::IDS_ERR_GBACK); return false; |
| 2635 | } |
| 2636 | SetInitProgress(88); |
| 2637 | // the savegame flag is set if runtime data is present, in which case this is to be used |
| 2638 | // except for scenario sections |
| 2639 | if (LandscapeLoaded && (!C4S.Head.SaveGame || section)) |
| 2640 | Landscape.ScenarioInit(); |
| 2641 | SetInitProgress(89); |
| 2642 | // Init main object list |
| 2643 | Objects.Init(iWidth: Landscape.Width, iHeight: Landscape.Height); |
| 2644 | |
| 2645 | // Pathfinder |
| 2646 | if (!section) PathFinder.Init(fnPointFree: &LandscapeFree, pTransferZones: &TransferZones); |
| 2647 | SetInitProgress(90); |
| 2648 | |
| 2649 | // PXS |
| 2650 | if (hGroup.FindEntry(C4CFN_PXS)) |
| 2651 | { |
| 2652 | if (!PXS.Load(hGroup)) |
| 2653 | { |
| 2654 | LogFatal(id: C4ResStrTableKey::IDS_ERR_PXS); return false; |
| 2655 | } |
| 2656 | } |
| 2657 | else if (LandscapeLoaded) |
| 2658 | { |
| 2659 | PXS.Clear(); |
| 2660 | PXS.Default(); |
| 2661 | } |
| 2662 | SetInitProgress(91); |
| 2663 | |
| 2664 | // MassMover |
| 2665 | if (hGroup.FindEntry(C4CFN_MassMover)) |
| 2666 | { |
| 2667 | if (!MassMover.Load(hGroup)) |
| 2668 | { |
| 2669 | LogFatal(id: C4ResStrTableKey::IDS_ERR_MOVER); return false; |
| 2670 | } |
| 2671 | } |
| 2672 | else if (LandscapeLoaded) |
| 2673 | { |
| 2674 | // clear and set mass-mover to default values |
| 2675 | MassMover.Clear(); |
| 2676 | MassMover.Default(); |
| 2677 | } |
| 2678 | |
| 2679 | SetInitProgress(92); |
| 2680 | |
| 2681 | // definition value overloads |
| 2682 | if (!section) InitValueOverloads(); |
| 2683 | |
| 2684 | Objects.Clear(fClearInactive: !section); |
| 2685 | |
| 2686 | // Load objects |
| 2687 | int32_t iObjects = Objects.Load(hGroup, fKeepInactive: section); |
| 2688 | if (iObjects) { Log(id: C4ResStrTableKey::IDS_PRC_OBJECTSLOADED, args&: iObjects); } |
| 2689 | SetInitProgress(93); |
| 2690 | |
| 2691 | if (!section) |
| 2692 | { |
| 2693 | PreloadStatus = PreloadLevel::LandscapeObjects; |
| 2694 | } |
| 2695 | |
| 2696 | return true; |
| 2697 | } |
| 2698 | |
| 2699 | bool C4Game::InitGameFinal() |
| 2700 | { |
| 2701 | // Validate object owners & assign loaded info objects |
| 2702 | Objects.ValidateOwners(); |
| 2703 | Objects.AssignInfo(); |
| 2704 | Objects.AssignPlrViewRange(); // update FoW-repellers |
| 2705 | |
| 2706 | // Script constructor call |
| 2707 | int32_t iObjCount = Objects.ObjectCount(); |
| 2708 | if (!C4S.Head.SaveGame) Script.Call(PSF_Initialize); |
| 2709 | if (Objects.ObjectCount() != iObjCount) fScriptCreatedObjects = true; |
| 2710 | |
| 2711 | // Player final init |
| 2712 | C4Player *pPlr; |
| 2713 | for (pPlr = Players.First; pPlr; pPlr = pPlr->Next) |
| 2714 | pPlr->FinalInit(fInitialValue: !C4S.Head.SaveGame); |
| 2715 | |
| 2716 | // Create viewports |
| 2717 | for (pPlr = Players.First; pPlr; pPlr = pPlr->Next) |
| 2718 | if (pPlr->LocalControl) |
| 2719 | CreateViewport(iPlayer: pPlr->Number); |
| 2720 | // Check fullscreen viewports |
| 2721 | FullScreen.ViewportCheck(); |
| 2722 | // update halt state |
| 2723 | Console.UpdateHaltCtrls(fHalt: !!HaltCount); |
| 2724 | |
| 2725 | // Host: players without connected clients: remove via control queue |
| 2726 | if (Network.isEnabled() && Network.isHost()) |
| 2727 | for (int32_t cnt = 0; cnt < Players.GetCount(); cnt++) |
| 2728 | if (Players.GetByIndex(iIndex: cnt)->AtClient < 0) |
| 2729 | Players.Remove(pPlr: Players.GetByIndex(iIndex: cnt), fDisonnected: true, fNoCalls: false); |
| 2730 | |
| 2731 | // It should be safe now to reload stuff |
| 2732 | if (FileMonitor) |
| 2733 | { |
| 2734 | FileMonitor->StartMonitoring(); |
| 2735 | } |
| 2736 | return true; |
| 2737 | } |
| 2738 | |
| 2739 | bool C4Game::InitScriptEngine() |
| 2740 | { |
| 2741 | // engine functions |
| 2742 | InitFunctionMap(pEngine: &ScriptEngine); |
| 2743 | |
| 2744 | // system functions: check if system group is open |
| 2745 | if (!Application.OpenSystemGroup()) |
| 2746 | { |
| 2747 | LogFatal(id: C4ResStrTableKey::IDS_ERR_INVALIDSYSGRP); return false; |
| 2748 | } |
| 2749 | C4Group &File = Application.SystemGroup; |
| 2750 | |
| 2751 | // Load string table |
| 2752 | MainSysLangStringTable.LoadEx(szName: "StringTbl" , hGroup&: File, C4CFN_ScriptStringTbl, szLanguage: Config.General.LanguageEx); |
| 2753 | |
| 2754 | // get scripts |
| 2755 | char fn[_MAX_FNAME + 1] = { 0 }; |
| 2756 | File.ResetSearch(); |
| 2757 | while (File.FindNextEntry(C4CFN_ScriptFiles, sFileName: fn, iSize: nullptr, fChild: nullptr, fStartAtFilename: !!fn[0])) |
| 2758 | { |
| 2759 | // host will be destroyed by script engine, so drop the references |
| 2760 | C4ScriptHost *scr = new C4ScriptHost(); |
| 2761 | scr->Reg2List(pEngine: &ScriptEngine, pOwner: &ScriptEngine); |
| 2762 | scr->Load(szName: nullptr, hGroup&: File, szFilename: fn, szLanguage: Config.General.LanguageEx, pDef: nullptr, pLocalTable: &MainSysLangStringTable); |
| 2763 | } |
| 2764 | |
| 2765 | // load standard clonk names |
| 2766 | Names.Load(szName: LoadResStr(id: C4ResStrTableKey::IDS_CNS_NAMES), hGroup&: File, C4CFN_Names); |
| 2767 | |
| 2768 | return true; |
| 2769 | } |
| 2770 | |
| 2771 | void C4Game::LinkScriptEngine() |
| 2772 | { |
| 2773 | // Link script engine (resolve includes/appends, generate code) |
| 2774 | ScriptEngine.Link(rDefs: &Defs); |
| 2775 | |
| 2776 | // Set name list for globals |
| 2777 | ScriptEngine.GlobalNamed.SetNameList(&ScriptEngine.GlobalNamedNames); |
| 2778 | } |
| 2779 | |
| 2780 | bool C4Game::InitPlayers() |
| 2781 | { |
| 2782 | int32_t iPlrCnt = 0; |
| 2783 | |
| 2784 | if (C4S.Head.NetworkRuntimeJoin) |
| 2785 | { |
| 2786 | // Load players to restore from scenario |
| 2787 | C4PlayerInfoList LocalRestorePlayerInfos; |
| 2788 | LocalRestorePlayerInfos.Load(hGroup&: ScenarioFile, C4CFN_SavePlayerInfos, pLang: &ScenarioLangStringTable); |
| 2789 | // -- runtime join player restore |
| 2790 | // all restore functions will be executed on RestorePlayerInfos, because the main playerinfos may be more up-to-date |
| 2791 | // extract all players to temp store and update filenames to point there |
| 2792 | if (!LocalRestorePlayerInfos.RecreatePlayerFiles()) |
| 2793 | { |
| 2794 | LogFatal(id: C4ResStrTableKey::IDS_ERR_NOPLRFILERECR); return false; |
| 2795 | } |
| 2796 | // recreate the files |
| 2797 | if (!LocalRestorePlayerInfos.RecreatePlayers()) |
| 2798 | { |
| 2799 | LogFatal(id: C4ResStrTableKey::IDS_ERR_NOPLRNETRECR); return false; |
| 2800 | } |
| 2801 | } |
| 2802 | else if (RestorePlayerInfos.GetActivePlayerCount(fCountInvisible: true)) |
| 2803 | { |
| 2804 | // -- savegame player restore |
| 2805 | // for savegames or regular scenarios with restore infos, the player info list should have been loaded from the savegame |
| 2806 | // or got restored from game text in OpenScenario() |
| 2807 | // merge restore player info into main player info list now |
| 2808 | // -for single-host games, this will move all infos |
| 2809 | // -for network games, it will merge according to savegame association done in the lobby |
| 2810 | // for all savegames, script players get restored by adding one new script player for earch savegame script player to the host |
| 2811 | if (!PlayerInfos.RestoreSavegameInfos(rSavegamePlayers&: RestorePlayerInfos)) |
| 2812 | { |
| 2813 | LogFatal(id: C4ResStrTableKey::IDS_ERR_NOPLRSAVEINFORECR); return false; |
| 2814 | } |
| 2815 | RestorePlayerInfos.Clear(); |
| 2816 | // try to associate local filenames (non-net+replay) or ressources (net) with all player infos |
| 2817 | if (!PlayerInfos.RecreatePlayerFiles()) |
| 2818 | { |
| 2819 | LogFatal(id: C4ResStrTableKey::IDS_ERR_NOPLRFILERECR); return false; |
| 2820 | } |
| 2821 | // recreate players by joining all players whose joined-flag is already set |
| 2822 | if (!PlayerInfos.RecreatePlayers()) |
| 2823 | { |
| 2824 | LogFatal(id: C4ResStrTableKey::IDS_ERR_NOPLRSAVERECR); return false; |
| 2825 | } |
| 2826 | } |
| 2827 | |
| 2828 | // any regular non-net non-replay game: Do the normal control queue join |
| 2829 | // this includes additional player joins in savegames |
| 2830 | if (!Network.isEnabled() && !Control.NoInput()) |
| 2831 | if (!PlayerInfos.LocalJoinUnjoinedPlayersInQueue()) |
| 2832 | { |
| 2833 | // error joining local players - either join was done earlier somehow, |
| 2834 | // or the player count check will soon end this round |
| 2835 | } |
| 2836 | |
| 2837 | // non-replay player joins will be done by player info list when go tick is reached |
| 2838 | // this is handled by C4Network2Players and needs no further treatment here |
| 2839 | // set iPlrCnt for player count check in host/single games |
| 2840 | iPlrCnt = PlayerInfos.GetJoinIssuedPlayerCount(); |
| 2841 | |
| 2842 | // Check valid participating player numbers (host/single only) |
| 2843 | if (!Network.isEnabled() || (Network.isHost() && !fLobby)) |
| 2844 | { |
| 2845 | #ifndef USE_CONSOLE |
| 2846 | // No players in fullscreen |
| 2847 | if (iPlrCnt == 0) |
| 2848 | if (Application.isFullScreen && !Control.NoInput()) |
| 2849 | { |
| 2850 | LogFatal(id: C4ResStrTableKey::IDS_CNS_NOFULLSCREENPLRS); |
| 2851 | return false; |
| 2852 | } |
| 2853 | #endif |
| 2854 | // Too many players |
| 2855 | if (iPlrCnt > Parameters.MaxPlayers) |
| 2856 | { |
| 2857 | if (Application.isFullScreen) |
| 2858 | { |
| 2859 | LogFatal(id: C4ResStrTableKey::IDS_PRC_TOOMANYPLRS, args&: Parameters.MaxPlayers); |
| 2860 | return false; |
| 2861 | } |
| 2862 | else |
| 2863 | { |
| 2864 | Console.Message(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_PRC_TOOMANYPLRS, args&: Parameters.MaxPlayers).c_str()); |
| 2865 | } |
| 2866 | } |
| 2867 | } |
| 2868 | // Console and no real players: halt |
| 2869 | if (Console.Active) |
| 2870 | if (!fLobby) |
| 2871 | if (!(PlayerInfos.GetActivePlayerCount(fCountInvisible: false) - PlayerInfos.GetActiveScriptPlayerCount(fCountSavegameResumes: true, fCountInvisible: false))) |
| 2872 | ++HaltCount; |
| 2873 | return true; |
| 2874 | } |
| 2875 | |
| 2876 | bool C4Game::InitControl() |
| 2877 | { |
| 2878 | // Replay? |
| 2879 | if (C4S.Head.Replay) |
| 2880 | { |
| 2881 | // no joins |
| 2882 | PlayerFilenames[0] = 0; |
| 2883 | // start playback |
| 2884 | if (!Control.InitReplay(rGroup&: ScenarioFile)) |
| 2885 | return false; |
| 2886 | } |
| 2887 | else if (Network.isEnabled()) |
| 2888 | { |
| 2889 | // initialize |
| 2890 | if (!Control.InitNetwork(pLocal: Clients.getLocal())) |
| 2891 | return false; |
| 2892 | // league? |
| 2893 | if (Parameters.isLeague()) |
| 2894 | { |
| 2895 | // enforce league rules |
| 2896 | if (Network.isHost()) |
| 2897 | if (!Parameters.CheckLeagueRulesStart(fFixIt: true)) |
| 2898 | return false; |
| 2899 | } |
| 2900 | } |
| 2901 | // Otherwise: local game |
| 2902 | else |
| 2903 | { |
| 2904 | // init |
| 2905 | if (!Control.InitLocal(pLocal: Clients.getLocal())) |
| 2906 | return false; |
| 2907 | } |
| 2908 | |
| 2909 | // record? |
| 2910 | if (!C4S.Head.Replay && (Config.General.Record || Parameters.isLeague())) |
| 2911 | if (!Control.StartRecord(fInitial: true, fStreaming: Parameters.doStreaming())) |
| 2912 | { |
| 2913 | // Special: If this happens for a league host, the game must not start. |
| 2914 | if (Network.isEnabled() && Network.isHost() && Parameters.isLeague()) |
| 2915 | { |
| 2916 | LogFatal(id: C4ResStrTableKey::IDS_ERR_NORECORD); |
| 2917 | return false; |
| 2918 | } |
| 2919 | else |
| 2920 | { |
| 2921 | Log(id: C4ResStrTableKey::IDS_ERR_NORECORD); |
| 2922 | } |
| 2923 | } |
| 2924 | |
| 2925 | return true; |
| 2926 | } |
| 2927 | |
| 2928 | int32_t ListExpandValids(C4IDList &rlist, |
| 2929 | C4ID *idlist, int32_t maxidlist) |
| 2930 | { |
| 2931 | int32_t cnt, cnt2, ccount, cpos; |
| 2932 | for (cpos = 0, cnt = 0; rlist.GetID(index: cnt); cnt++) |
| 2933 | if (C4Id2Def(id: rlist.GetID(index: cnt, ipCount: &ccount))) |
| 2934 | for (cnt2 = 0; cnt2 < ccount; cnt2++) |
| 2935 | if (cpos < maxidlist) |
| 2936 | { |
| 2937 | idlist[cpos] = rlist.GetID(index: cnt); cpos++; |
| 2938 | } |
| 2939 | return cpos; |
| 2940 | } |
| 2941 | |
| 2942 | bool C4Game::PlaceInEarth(C4ID id) |
| 2943 | { |
| 2944 | int32_t cnt, tx, ty; |
| 2945 | for (cnt = 0; cnt < 35; cnt++) // cheap trys |
| 2946 | { |
| 2947 | tx = Random(GBackWdt); ty = Random(GBackHgt); |
| 2948 | if (GBackMat(x: tx, y: ty) == MEarth) |
| 2949 | if (CreateObject(id, pCreator: nullptr, iOwner: NO_OWNER, x: tx, y: ty, r: Random(iRange: 360))) |
| 2950 | return true; |
| 2951 | } |
| 2952 | return false; |
| 2953 | } |
| 2954 | |
| 2955 | C4Object *C4Game::PlaceVegetation(C4ID id, int32_t iX, int32_t iY, int32_t iWdt, int32_t iHgt, int32_t iGrowth) |
| 2956 | { |
| 2957 | int32_t cnt, iTx, iTy, iMaterial; |
| 2958 | |
| 2959 | // Get definition |
| 2960 | C4Def *pDef; |
| 2961 | if (!(pDef = C4Id2Def(id))) return nullptr; |
| 2962 | |
| 2963 | // No growth specified: full or random growth |
| 2964 | if (iGrowth <= 0) |
| 2965 | { |
| 2966 | iGrowth = FullCon; |
| 2967 | if (pDef->Growth) if (!Random(iRange: 3)) iGrowth = Random(iRange: FullCon) + 1; |
| 2968 | } |
| 2969 | |
| 2970 | // Place by placement type |
| 2971 | switch (pDef->Placement) |
| 2972 | { |
| 2973 | // Surface soil |
| 2974 | case C4D_Place_Surface: |
| 2975 | for (cnt = 0; cnt < 20; cnt++) |
| 2976 | { |
| 2977 | // Random hit within target area |
| 2978 | iTx = iX + Random(iRange: iWdt); iTy = iY + Random(iRange: iHgt); |
| 2979 | // Above IFT |
| 2980 | while ((iTy > 0) && GBackIFT(x: iTx, y: iTy)) iTy--; |
| 2981 | // Above semi solid |
| 2982 | if (!AboveSemiSolid(rx&: iTx, ry&: iTy) || !Inside<int32_t>(ival: iTy, lbound: 50, GBackHgt - 50)) |
| 2983 | continue; |
| 2984 | // Free above |
| 2985 | if (GBackSemiSolid(x: iTx, y: iTy - pDef->Shape.Hgt) || GBackSemiSolid(x: iTx, y: iTy - pDef->Shape.Hgt / 2)) |
| 2986 | continue; |
| 2987 | // Free upleft and upright |
| 2988 | if (GBackSemiSolid(x: iTx - pDef->Shape.Wdt / 2, y: iTy - pDef->Shape.Hgt * 2 / 3) || GBackSemiSolid(x: iTx + pDef->Shape.Wdt / 2, y: iTy - pDef->Shape.Hgt * 2 / 3)) |
| 2989 | continue; |
| 2990 | // Soil check |
| 2991 | iTy += 3; // two pix into ground |
| 2992 | iMaterial = GBackMat(x: iTx, y: iTy); |
| 2993 | if (iMaterial != MNone) if (Material.Map[iMaterial].Soil) |
| 2994 | { |
| 2995 | if (!pDef->Growth) iGrowth = FullCon; |
| 2996 | iTy += 5; |
| 2997 | return CreateObjectConstruction(id, pCreator: nullptr, iOwner: NO_OWNER, iX: iTx, iBY: iTy, iCon: iGrowth); |
| 2998 | } |
| 2999 | } |
| 3000 | break; |
| 3001 | |
| 3002 | // Underwater |
| 3003 | case C4D_Place_Liquid: |
| 3004 | // Random range |
| 3005 | iTx = iX + Random(iRange: iWdt); iTy = iY + Random(iRange: iHgt); |
| 3006 | // Find liquid |
| 3007 | if (!FindSurfaceLiquid(rx&: iTx, ry&: iTy, width: pDef->Shape.Wdt, height: pDef->Shape.Hgt)) |
| 3008 | if (!FindLiquid(rx&: iTx, ry&: iTy, width: pDef->Shape.Wdt, height: pDef->Shape.Hgt)) |
| 3009 | return nullptr; |
| 3010 | // Liquid bottom |
| 3011 | if (!SemiAboveSolid(rx&: iTx, ry&: iTy)) return nullptr; |
| 3012 | iTy += 3; |
| 3013 | // Create object |
| 3014 | return CreateObjectConstruction(id, pCreator: nullptr, iOwner: NO_OWNER, iX: iTx, iBY: iTy, iCon: iGrowth); |
| 3015 | } |
| 3016 | |
| 3017 | // Undefined placement type |
| 3018 | return nullptr; |
| 3019 | } |
| 3020 | |
| 3021 | C4Object *C4Game::PlaceAnimal(C4ID idAnimal) |
| 3022 | { |
| 3023 | C4Def *pDef = C4Id2Def(id: idAnimal); |
| 3024 | if (!pDef) return nullptr; |
| 3025 | int32_t iX, iY; |
| 3026 | // Placement |
| 3027 | switch (pDef->Placement) |
| 3028 | { |
| 3029 | // Running free |
| 3030 | case C4D_Place_Surface: |
| 3031 | iX = Random(GBackWdt); iY = Random(GBackHgt); |
| 3032 | if (!FindSolidGround(rx&: iX, ry&: iY, width: pDef->Shape.Wdt)) return nullptr; |
| 3033 | break; |
| 3034 | // In liquid |
| 3035 | case C4D_Place_Liquid: |
| 3036 | iX = Random(GBackWdt); iY = Random(GBackHgt); |
| 3037 | if (!FindSurfaceLiquid(rx&: iX, ry&: iY, width: pDef->Shape.Wdt, height: pDef->Shape.Hgt)) |
| 3038 | if (!FindLiquid(rx&: iX, ry&: iY, width: pDef->Shape.Wdt, height: pDef->Shape.Hgt)) |
| 3039 | return nullptr; |
| 3040 | iY += pDef->Shape.Hgt / 2; |
| 3041 | break; |
| 3042 | // Floating in air |
| 3043 | case C4D_Place_Air: |
| 3044 | iX = Random(GBackWdt); |
| 3045 | for (iY = 0; (iY < GBackHgt) && !GBackSemiSolid(x: iX, y: iY); iY++); |
| 3046 | if (iY <= 0) return nullptr; |
| 3047 | iY = Random(iRange: iY); |
| 3048 | break; |
| 3049 | default: |
| 3050 | return nullptr; |
| 3051 | } |
| 3052 | // Create object |
| 3053 | return CreateObject(id: idAnimal, pCreator: nullptr, iOwner: NO_OWNER, x: iX, y: iY); |
| 3054 | } |
| 3055 | |
| 3056 | void C4Game::InitInEarth() |
| 3057 | { |
| 3058 | const int32_t maxvid = 100; |
| 3059 | int32_t cnt, vidnum; |
| 3060 | C4ID vidlist[maxvid]; |
| 3061 | // Amount |
| 3062 | int32_t amt = (GBackWdt * GBackHgt / 5000) * C4S.Landscape.InEarthLevel.Evaluate() / 100; |
| 3063 | // List all valid IDs from C4S |
| 3064 | vidnum = ListExpandValids(rlist&: C4S.Landscape.InEarth, idlist: vidlist, maxidlist: maxvid); |
| 3065 | // Place |
| 3066 | if (vidnum > 0) |
| 3067 | for (cnt = 0; cnt < amt; cnt++) |
| 3068 | PlaceInEarth(id: vidlist[Random(iRange: vidnum)]); |
| 3069 | } |
| 3070 | |
| 3071 | void C4Game::InitVegetation() |
| 3072 | { |
| 3073 | const int32_t maxvid = 100; |
| 3074 | int32_t cnt, vidnum; |
| 3075 | C4ID vidlist[maxvid]; |
| 3076 | // Amount |
| 3077 | int32_t amt = (GBackWdt / 50) * C4S.Landscape.VegLevel.Evaluate() / 100; |
| 3078 | // Get percentage vidlist from C4S |
| 3079 | vidnum = ListExpandValids(rlist&: C4S.Landscape.Vegetation, idlist: vidlist, maxidlist: maxvid); |
| 3080 | // Place vegetation |
| 3081 | if (vidnum > 0) |
| 3082 | for (cnt = 0; cnt < amt; cnt++) |
| 3083 | PlaceVegetation(id: vidlist[Random(iRange: vidnum)], iX: 0, iY: 0, GBackWdt, GBackHgt, iGrowth: -1); |
| 3084 | } |
| 3085 | |
| 3086 | void C4Game::InitAnimals() |
| 3087 | { |
| 3088 | int32_t cnt, cnt2; |
| 3089 | C4ID idAnimal; int32_t iCount; |
| 3090 | // Place animals |
| 3091 | for (cnt = 0; (idAnimal = C4S.Animals.FreeLife.GetID(index: cnt, ipCount: &iCount)); cnt++) |
| 3092 | for (cnt2 = 0; cnt2 < iCount; cnt2++) |
| 3093 | PlaceAnimal(idAnimal); |
| 3094 | // Place nests |
| 3095 | for (cnt = 0; (idAnimal = C4S.Animals.EarthNest.GetID(index: cnt, ipCount: &iCount)); cnt++) |
| 3096 | for (cnt2 = 0; cnt2 < iCount; cnt2++) |
| 3097 | PlaceInEarth(id: idAnimal); |
| 3098 | } |
| 3099 | |
| 3100 | void C4Game::ParseCommandLine(const char *szCmdLine) |
| 3101 | { |
| 3102 | LogNTr(message: "Command line: " ); LogNTr(message: szCmdLine); |
| 3103 | |
| 3104 | // Definitions by registry config |
| 3105 | DefinitionFilenames.clear(); |
| 3106 | std::istringstream stream(Config.General.Definitions); |
| 3107 | for (std::string s; std::getline(in&: stream, str&: s, delim: ';');) |
| 3108 | { |
| 3109 | DefinitionFilenames.push_back(x: s); |
| 3110 | } |
| 3111 | *PlayerFilenames = 0; |
| 3112 | NetworkActive = false; |
| 3113 | |
| 3114 | // Scan additional parameters from command line |
| 3115 | char szParameter[_MAX_PATH + 1]; |
| 3116 | for (int32_t iPar = 0; SGetParameter(strCommandLine: szCmdLine, iParameter: iPar, strTarget: szParameter, _MAX_PATH); iPar++) |
| 3117 | { |
| 3118 | // Scenario file |
| 3119 | if (SEqualNoCase(szStr1: GetExtension(fname: szParameter), szStr2: "c4s" )) |
| 3120 | { |
| 3121 | SCopy(szSource: szParameter, sTarget: ScenarioFilename, _MAX_PATH); continue; |
| 3122 | } |
| 3123 | if (SEqualNoCase(szStr1: GetFilename(path: szParameter), szStr2: "scenario.txt" )) |
| 3124 | { |
| 3125 | SCopy(szSource: szParameter, sTarget: ScenarioFilename, _MAX_PATH); |
| 3126 | if (GetFilename(path: ScenarioFilename) != ScenarioFilename) *(GetFilename(path: ScenarioFilename) - 1) = 0; |
| 3127 | continue; |
| 3128 | } |
| 3129 | // Player file |
| 3130 | if (SEqualNoCase(szStr1: GetExtension(fname: szParameter), szStr2: "c4p" )) |
| 3131 | { |
| 3132 | SAddModule(szList: PlayerFilenames, szModule: szParameter); |
| 3133 | continue; |
| 3134 | } |
| 3135 | // Definition file |
| 3136 | if (SEqualNoCase(szStr1: GetExtension(fname: szParameter), szStr2: "c4d" )) |
| 3137 | { |
| 3138 | DefinitionFilenames.push_back(x: szParameter); |
| 3139 | continue; |
| 3140 | } |
| 3141 | // Update file |
| 3142 | if (SEqualNoCase(szStr1: GetExtension(fname: szParameter), szStr2: "c4u" )) |
| 3143 | { |
| 3144 | Application.IncomingUpdate.Copy(pnData: szParameter); |
| 3145 | continue; |
| 3146 | } |
| 3147 | // record stream |
| 3148 | if (SEqualNoCase(szStr1: GetExtension(fname: szParameter), szStr2: "c4r" )) |
| 3149 | { |
| 3150 | RecordStream.Copy(pnData: szParameter); |
| 3151 | } |
| 3152 | // Fair Crew |
| 3153 | if (SEqualNoCase(szStr1: szParameter, szStr2: "/ncrw" ) || SEqualNoCase(szStr1: szParameter, szStr2: "/faircrew" )) |
| 3154 | Config.General.FairCrew = true; |
| 3155 | // Trained Crew (Player Crew) |
| 3156 | if (SEqualNoCase(szStr1: szParameter, szStr2: "/ucrw" ) || SEqualNoCase(szStr1: szParameter, szStr2: "/trainedcrew" )) |
| 3157 | Config.General.FairCrew = false; |
| 3158 | // record dump |
| 3159 | if (SEqual2NoCase(szStr1: szParameter, szStr2: "/recdump:" )) |
| 3160 | RecordDumpFile.Copy(pnData: szParameter + 9); |
| 3161 | // record stream |
| 3162 | if (SEqual2NoCase(szStr1: szParameter, szStr2: "/stream:" )) |
| 3163 | RecordStream.Copy(pnData: szParameter + 8); |
| 3164 | // startup start screen |
| 3165 | if (SEqual2NoCase(szStr1: szParameter, szStr2: "/startup:" )) |
| 3166 | C4Startup::SetStartScreen(szParameter + 9); |
| 3167 | // Network |
| 3168 | if (SEqualNoCase(szStr1: szParameter, szStr2: "/network" )) |
| 3169 | NetworkActive = true; |
| 3170 | if (SEqualNoCase(szStr1: szParameter, szStr2: "/nonetwork" )) |
| 3171 | NetworkActive = false; |
| 3172 | // Signup |
| 3173 | if (SEqualNoCase(szStr1: szParameter, szStr2: "/signup" )) |
| 3174 | { |
| 3175 | NetworkActive = true; |
| 3176 | Config.Network.MasterServerSignUp = true; |
| 3177 | } |
| 3178 | if (SEqualNoCase(szStr1: szParameter, szStr2: "/nosignup" )) |
| 3179 | Config.Network.MasterServerSignUp = Config.Network.LeagueServerSignUp = false; |
| 3180 | // League |
| 3181 | if (SEqualNoCase(szStr1: szParameter, szStr2: "/league" )) |
| 3182 | { |
| 3183 | NetworkActive = true; |
| 3184 | Config.Network.MasterServerSignUp = Config.Network.LeagueServerSignUp = true; |
| 3185 | } |
| 3186 | if (SEqualNoCase(szStr1: szParameter, szStr2: "/noleague" )) |
| 3187 | Config.Network.LeagueServerSignUp = false; |
| 3188 | // Lobby |
| 3189 | if (SEqual2NoCase(szStr1: szParameter, szStr2: "/lobby" )) |
| 3190 | { |
| 3191 | NetworkActive = true; fLobby = true; |
| 3192 | // lobby timeout specified? (e.g. /lobby:120) |
| 3193 | if (szParameter[6] == ':') |
| 3194 | { |
| 3195 | iLobbyTimeout = atoi(nptr: szParameter + 7); |
| 3196 | if (iLobbyTimeout < 0) iLobbyTimeout = 0; |
| 3197 | } |
| 3198 | } |
| 3199 | // Observe |
| 3200 | if (SEqualNoCase(szStr1: szParameter, szStr2: "/observe" )) |
| 3201 | { |
| 3202 | NetworkActive = true; fObserve = true; |
| 3203 | } |
| 3204 | // Enable runtime join |
| 3205 | if (SEqualNoCase(szStr1: szParameter, szStr2: "/runtimejoin" )) |
| 3206 | Config.Network.NoRuntimeJoin = false; |
| 3207 | // Disable runtime join |
| 3208 | if (SEqualNoCase(szStr1: szParameter, szStr2: "/noruntimejoin" )) |
| 3209 | Config.Network.NoRuntimeJoin = true; |
| 3210 | // Check for update |
| 3211 | if (SEqualNoCase(szStr1: szParameter, szStr2: "/update" )) |
| 3212 | Application.CheckForUpdates = true; |
| 3213 | // Direct join |
| 3214 | if (SEqual2NoCase(szStr1: szParameter, szStr2: "/join:" )) |
| 3215 | { |
| 3216 | NetworkActive = true; |
| 3217 | SCopy(szSource: szParameter + 6, sTarget: DirectJoinAddress, _MAX_PATH); |
| 3218 | continue; |
| 3219 | } |
| 3220 | // Direct join by URL |
| 3221 | if (SEqual2NoCase(szStr1: szParameter, szStr2: "clonk:" )) |
| 3222 | { |
| 3223 | // Store address |
| 3224 | SCopy(szSource: szParameter + 6, sTarget: DirectJoinAddress, _MAX_PATH); |
| 3225 | SClearFrontBack(szString: DirectJoinAddress, cClear: '/'); |
| 3226 | // Special case: if the target address is "update" then this is used for update initiation by url |
| 3227 | if (SEqualNoCase(szStr1: DirectJoinAddress, szStr2: "update" )) |
| 3228 | { |
| 3229 | Application.CheckForUpdates = true; |
| 3230 | DirectJoinAddress[0] = 0; |
| 3231 | continue; |
| 3232 | } |
| 3233 | // Self-enable network |
| 3234 | NetworkActive = true; |
| 3235 | continue; |
| 3236 | } |
| 3237 | // port overrides |
| 3238 | if (SEqual2NoCase(szStr1: szParameter, szStr2: "/tcpport:" )) |
| 3239 | Config.Network.PortTCP = atoi(nptr: szParameter + 9); |
| 3240 | if (SEqual2NoCase(szStr1: szParameter, szStr2: "/udpport:" )) |
| 3241 | Config.Network.PortUDP = atoi(nptr: szParameter + 9); |
| 3242 | // network game password |
| 3243 | if (SEqual2NoCase(szStr1: szParameter, szStr2: "/pass:" )) |
| 3244 | Network.SetPassword(szParameter + 6); |
| 3245 | // network game comment |
| 3246 | if (SEqual2NoCase(szStr1: szParameter, szStr2: "/comment:" )) |
| 3247 | Config.Network.Comment.CopyValidated(szFromVal: szParameter + 9); |
| 3248 | #ifndef NDEBUG |
| 3249 | // debug configs |
| 3250 | if (SEqualNoCase(szParameter, "/host" )) |
| 3251 | { |
| 3252 | NetworkActive = true; |
| 3253 | fLobby = true; |
| 3254 | Config.Network.PortTCP = 11112; |
| 3255 | Config.Network.PortUDP = 11113; |
| 3256 | Config.Network.MasterServerSignUp = Config.Network.LeagueServerSignUp = false; |
| 3257 | } |
| 3258 | if (SEqual2NoCase(szParameter, "/client:" )) |
| 3259 | { |
| 3260 | NetworkActive = true; |
| 3261 | SCopy("localhost" , DirectJoinAddress, _MAX_PATH); |
| 3262 | fLobby = true; |
| 3263 | Config.Network.PortTCP = 11112 + 2 * (atoi(szParameter + 8) + 1); |
| 3264 | Config.Network.PortUDP = 11113 + 2 * (atoi(szParameter + 8) + 1); |
| 3265 | } |
| 3266 | #endif |
| 3267 | } |
| 3268 | |
| 3269 | // Check for fullscreen switch in command line |
| 3270 | if (SSearchNoCase(szString: szCmdLine, szIndex: "/console" )) |
| 3271 | Application.isFullScreen = false; |
| 3272 | |
| 3273 | // startup dialog required? |
| 3274 | Application.UseStartupDialog = Application.isFullScreen && !*DirectJoinAddress && !*ScenarioFilename && !RecordStream.getSize(); |
| 3275 | } |
| 3276 | |
| 3277 | bool C4Game::LoadScenarioComponents() |
| 3278 | { |
| 3279 | // Info |
| 3280 | Info.Load(szName: LoadResStr(id: C4ResStrTableKey::IDS_CNS_INFO), hGroup&: ScenarioFile, C4CFN_Info); |
| 3281 | // Overload clonk names from scenario file |
| 3282 | if (ScenarioFile.EntryCount(C4CFN_Names)) |
| 3283 | Names.Load(szName: LoadResStr(id: C4ResStrTableKey::IDS_CNS_NAMES), hGroup&: ScenarioFile, C4CFN_Names); |
| 3284 | // scenario sections |
| 3285 | char fn[_MAX_FNAME + 1] = { 0 }; |
| 3286 | ScenarioFile.ResetSearch(); *fn = 0; |
| 3287 | while (ScenarioFile.FindNextEntry(C4CFN_ScenarioSections, sFileName: fn, iSize: nullptr, fChild: nullptr, fStartAtFilename: !!*fn)) |
| 3288 | { |
| 3289 | // get section name |
| 3290 | char SctName[_MAX_FNAME + 1]; |
| 3291 | int32_t iWildcardPos = SCharPos(cTarget: '*', C4CFN_ScenarioSections); |
| 3292 | SCopy(szSource: fn + iWildcardPos, sTarget: SctName, _MAX_FNAME); |
| 3293 | RemoveExtension(szFileName: SctName); |
| 3294 | if (SLen(sptr: SctName) > C4MaxName || !*SctName) |
| 3295 | { |
| 3296 | DebugLog(message: "invalid section name" ); |
| 3297 | LogFatal(id: C4ResStrTableKey::IDS_ERR_SCENSECTION, args: +fn); return false; |
| 3298 | } |
| 3299 | // load this section into temp store |
| 3300 | C4ScenarioSection *pSection = new C4ScenarioSection(SctName); |
| 3301 | if (!pSection->ScenarioLoad(szFilename: fn)) |
| 3302 | { |
| 3303 | LogFatal(id: C4ResStrTableKey::IDS_ERR_SCENSECTION, args: +fn); return false; |
| 3304 | } |
| 3305 | } |
| 3306 | |
| 3307 | // Success |
| 3308 | return true; |
| 3309 | } |
| 3310 | |
| 3311 | void C4Game::LoadScenarioScripts() |
| 3312 | { |
| 3313 | // Script |
| 3314 | Script.Reg2List(pEngine: &ScriptEngine, pOwner: &ScriptEngine); |
| 3315 | Script.Load(szName: LoadResStr(id: C4ResStrTableKey::IDS_CNS_SCRIPT), hGroup&: ScenarioFile, C4CFN_Script, szLanguage: Config.General.LanguageEx, pDef: nullptr, pLocalTable: &ScenarioLangStringTable); |
| 3316 | // additional system scripts? |
| 3317 | C4Group SysGroup; |
| 3318 | char fn[_MAX_FNAME + 1] = { 0 }; |
| 3319 | if (SysGroup.OpenAsChild(pMother: &ScenarioFile, C4CFN_System)) |
| 3320 | { |
| 3321 | ScenarioSysLangStringTable.LoadEx(szName: "StringTbl" , hGroup&: SysGroup, C4CFN_ScriptStringTbl, szLanguage: Config.General.LanguageEx); |
| 3322 | // load all scripts in there |
| 3323 | SysGroup.ResetSearch(); |
| 3324 | while (SysGroup.FindNextEntry(C4CFN_ScriptFiles, sFileName: fn, iSize: nullptr, fChild: nullptr, fStartAtFilename: !!fn[0])) |
| 3325 | { |
| 3326 | // host will be destroyed by script engine, so drop the references |
| 3327 | C4ScriptHost *scr = new C4ScriptHost(); |
| 3328 | scr->Reg2List(pEngine: &ScriptEngine, pOwner: &ScriptEngine); |
| 3329 | scr->Load(szName: nullptr, hGroup&: SysGroup, szFilename: fn, szLanguage: Config.General.LanguageEx, pDef: nullptr, pLocalTable: &ScenarioSysLangStringTable); |
| 3330 | } |
| 3331 | // if it's a physical group: watch out for changes |
| 3332 | if (!SysGroup.IsPacked()) |
| 3333 | { |
| 3334 | AddDirectoryForMonitoring(directory: SysGroup.GetFullName().getData()); |
| 3335 | } |
| 3336 | SysGroup.Close(); |
| 3337 | } |
| 3338 | } |
| 3339 | |
| 3340 | bool C4Game::InitKeyboard() |
| 3341 | { |
| 3342 | // clear previous |
| 3343 | KeyboardInput.Clear(); |
| 3344 | |
| 3345 | // globals |
| 3346 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_F3), "MusicToggle" , C4KeyScope(KEYSCOPE_Generic | KEYSCOPE_Gui), new C4KeyCB <C4Game> (*this, &C4Game::ToggleMusic))); |
| 3347 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_F3, KEYS_Control), "SoundToggle" , C4KeyScope(KEYSCOPE_Generic | KEYSCOPE_Gui), new C4KeyCB <C4Game> (*this, &C4Game::ToggleSound))); |
| 3348 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_F9), "Screenshot" , C4KeyScope(KEYSCOPE_Fullscreen | KEYSCOPE_Gui), new C4KeyCBEx<C4GraphicsSystem, bool>(GraphicsSystem, false, &C4GraphicsSystem::SaveScreenshot))); |
| 3349 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_F9, KEYS_Control), "ScreenshotEx" , KEYSCOPE_Fullscreen, new C4KeyCBEx<C4GraphicsSystem, bool>(GraphicsSystem, true, &C4GraphicsSystem::SaveScreenshot))); |
| 3350 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(KEY_C, KEYS_Alt), "ToggleChat" , C4KeyScope(KEYSCOPE_Generic | KEYSCOPE_Gui), new C4KeyCB <C4Game> (*this, &C4Game::ToggleChat))); |
| 3351 | |
| 3352 | // main ingame |
| 3353 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_F1), "ToggleShowHelp" , KEYSCOPE_Generic, new C4KeyCB<C4GraphicsSystem>(GraphicsSystem, &C4GraphicsSystem::ToggleShowHelp))); |
| 3354 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_F4), "NetClientListDlgToggle" , KEYSCOPE_Generic, new C4KeyCB<C4Network2> (Network, &C4Network2::ToggleClientListDlg))); |
| 3355 | |
| 3356 | // messageboard |
| 3357 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_UP, KEYS_Shift), "MsgBoardScrollUp" , KEYSCOPE_Fullscreen, new C4KeyCB<C4MessageBoard>(GraphicsSystem.MessageBoard, &C4MessageBoard::ControlScrollUp))); |
| 3358 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_DOWN, KEYS_Shift), "MsgBoardScrollDown" , KEYSCOPE_Fullscreen, new C4KeyCB<C4MessageBoard>(GraphicsSystem.MessageBoard, &C4MessageBoard::ControlScrollDown))); |
| 3359 | |
| 3360 | // debug mode & debug displays |
| 3361 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_F5, KEYS_Control), "DbgModeToggle" , KEYSCOPE_Generic, new C4KeyCB<C4Game> (*this, &C4Game::ToggleDebugMode))); |
| 3362 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_F6, KEYS_Control), "DbgShowVtxToggle" , KEYSCOPE_Generic, new C4KeyCB<C4GraphicsSystem>(GraphicsSystem, &C4GraphicsSystem::ToggleShowVertices))); |
| 3363 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_F7, KEYS_Control), "DbgShowActionToggle" , KEYSCOPE_Generic, new C4KeyCB<C4GraphicsSystem>(GraphicsSystem, &C4GraphicsSystem::ToggleShowAction))); |
| 3364 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_F8, KEYS_Control), "DbgShowSolidMaskToggle" , KEYSCOPE_Generic, new C4KeyCB<C4GraphicsSystem>(GraphicsSystem, &C4GraphicsSystem::ToggleShowSolidMask))); |
| 3365 | |
| 3366 | // playback speed - improve... |
| 3367 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_ADD, KEYS_Shift), "GameSpeedUp" , KEYSCOPE_Generic, new C4KeyCB<C4Game>(*this, &C4Game::SpeedUp))); |
| 3368 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_SUBTRACT, KEYS_Shift), "GameSlowDown" , KEYSCOPE_Generic, new C4KeyCB<C4Game>(*this, &C4Game::SlowDown))); |
| 3369 | |
| 3370 | constexpr auto keyWithGamepadGuiControl = [](const auto keyboardKey, const C4KeyCode gamepadButton, const auto ...moreKeyboardKeys) -> C4CustomKey::CodeList |
| 3371 | { |
| 3372 | if (Config.Controls.GamepadGuiControl) |
| 3373 | { |
| 3374 | return {C4KeyCodeEx(keyboardKey), static_cast<C4KeyCode>(moreKeyboardKeys)..., C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: gamepadButton))}; |
| 3375 | } |
| 3376 | else |
| 3377 | { |
| 3378 | return {C4KeyCodeEx(keyboardKey), static_cast<C4KeyCode>(moreKeyboardKeys)...}; |
| 3379 | } |
| 3380 | }; |
| 3381 | |
| 3382 | // fullscreen menu |
| 3383 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(keyWithGamepadGuiControl(K_LEFT, KEY_JOY_Left), "FullscreenMenuLeft" , KEYSCOPE_FullSMenu, new C4KeyCBEx<C4FullScreen, uint8_t>(FullScreen, COM_MenuLeft, &C4FullScreen::MenuKeyControl))); |
| 3384 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(keyWithGamepadGuiControl(K_RIGHT, KEY_JOY_Right), "FullscreenMenuRight" , KEYSCOPE_FullSMenu, new C4KeyCBEx<C4FullScreen, uint8_t>(FullScreen, COM_MenuRight, &C4FullScreen::MenuKeyControl))); |
| 3385 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(keyWithGamepadGuiControl(K_UP, KEY_JOY_Up), "FullscreenMenuUp" , KEYSCOPE_FullSMenu, new C4KeyCBEx<C4FullScreen, uint8_t>(FullScreen, COM_MenuUp, &C4FullScreen::MenuKeyControl))); |
| 3386 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(keyWithGamepadGuiControl(K_DOWN, KEY_JOY_Down), "FullscreenMenuDown" , KEYSCOPE_FullSMenu, new C4KeyCBEx<C4FullScreen, uint8_t>(FullScreen, COM_MenuDown, &C4FullScreen::MenuKeyControl))); |
| 3387 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(keyWithGamepadGuiControl(K_SPACE, KEY_JOY_AnyLowButton, K_RETURN), "FullscreenMenuOK" , KEYSCOPE_FullSMenu, new C4KeyCBEx<C4FullScreen, uint8_t>(FullScreen, COM_MenuEnter, &C4FullScreen::MenuKeyControl))); // name used by PlrControlKeyName |
| 3388 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(keyWithGamepadGuiControl(K_ESCAPE, KEY_JOY_AnyHighButton), "FullscreenMenuCancel" , KEYSCOPE_FullSMenu, new C4KeyCBEx<C4FullScreen, uint8_t>(FullScreen, COM_MenuClose, &C4FullScreen::MenuKeyControl))); // name used by PlrControlKeyName |
| 3389 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(keyWithGamepadGuiControl(K_SPACE, KEY_JOY_AnyButton), "FullscreenMenuOpen" , KEYSCOPE_FreeView, new C4KeyCB <C4FullScreen> (FullScreen, &C4FullScreen::ActivateMenuMain))); // name used by C4MainMenu! |
| 3390 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_RIGHT), "FilmNextPlayer" , KEYSCOPE_FilmView, new C4KeyCB <C4GraphicsSystem> (GraphicsSystem, &C4GraphicsSystem::ViewportNextPlayer))); |
| 3391 | |
| 3392 | // chat |
| 3393 | |
| 3394 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey({C4KeyCodeEx(K_RETURN), C4KeyCodeEx(K_F2)}, "ChatOpen" , KEYSCOPE_Generic, new C4KeyCBEx<C4MessageInput, C4ChatInputDialog::Mode>(MessageInput, C4ChatInputDialog::All, &C4MessageInput::KeyStartTypeIn))); |
| 3395 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_RETURN, KEYS_Shift), "ChatOpen2Allies" , KEYSCOPE_Generic, new C4KeyCBEx<C4MessageInput, C4ChatInputDialog::Mode>(MessageInput, C4ChatInputDialog::Allies, &C4MessageInput::KeyStartTypeIn))); |
| 3396 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey{C4KeyCodeEx{K_RETURN, KEYS_Alt}, "ChatOpen2Say" , KEYSCOPE_Generic, new C4KeyCBEx<C4MessageInput, C4ChatInputDialog::Mode>{MessageInput, C4ChatInputDialog::Say, &C4MessageInput::KeyStartTypeIn}}); |
| 3397 | |
| 3398 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_LEFT), "FreeViewScrollLeft" , KEYSCOPE_FreeView, new C4KeyCBEx<C4GraphicsSystem, C4Vec2D>(GraphicsSystem, C4Vec2D(-5, 0), &C4GraphicsSystem::FreeScroll))); |
| 3399 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_RIGHT), "FreeViewScrollRight" , KEYSCOPE_FreeView, new C4KeyCBEx<C4GraphicsSystem, C4Vec2D>(GraphicsSystem, C4Vec2D(+5, 0), &C4GraphicsSystem::FreeScroll))); |
| 3400 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_UP), "FreeViewScrollUp" , KEYSCOPE_FreeView, new C4KeyCBEx<C4GraphicsSystem, C4Vec2D>(GraphicsSystem, C4Vec2D(0, -5), &C4GraphicsSystem::FreeScroll))); |
| 3401 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_DOWN), "FreeViewScrollDown" , KEYSCOPE_FreeView, new C4KeyCBEx<C4GraphicsSystem, C4Vec2D>(GraphicsSystem, C4Vec2D(0, +5), &C4GraphicsSystem::FreeScroll))); |
| 3402 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_TAB), "ScoreboardToggle" , KEYSCOPE_Generic, new C4KeyCB <C4Scoreboard> (Scoreboard, &C4Scoreboard::KeyUserShow))); |
| 3403 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_ESCAPE), "GameAbort" , KEYSCOPE_Fullscreen, new C4KeyCB <C4FullScreen> (FullScreen, &C4FullScreen::ShowAbortDlg))); |
| 3404 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_PAUSE), "FullscreenPauseToggle" , KEYSCOPE_Fullscreen, new C4KeyCB <C4Game> (Game, &C4Game::TogglePause))); |
| 3405 | |
| 3406 | // console keys |
| 3407 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_PAUSE), "ConsolePauseToggle" , KEYSCOPE_Console, new C4KeyCB <C4Console> (Console, &C4Console::TogglePause))); |
| 3408 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_SPACE), "EditCursorModeToggle" , KEYSCOPE_Console, new C4KeyCB <C4EditCursor> (Console.EditCursor, &C4EditCursor::ToggleMode))); |
| 3409 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_ADD), "ToolsDlgGradeUp" , KEYSCOPE_Console, new C4KeyCBEx<C4ToolsDlg, int32_t>(Console.ToolsDlg, +5, &C4ToolsDlg::ChangeGrade))); |
| 3410 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_SUBTRACT), "ToolsDlgGradeDown" , KEYSCOPE_Console, new C4KeyCBEx<C4ToolsDlg, int32_t>(Console.ToolsDlg, -5, &C4ToolsDlg::ChangeGrade))); |
| 3411 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(KEY_M, KEYS_Control), "ToolsDlgPopMaterial" , KEYSCOPE_Console, new C4KeyCB <C4ToolsDlg> (Console.ToolsDlg, &C4ToolsDlg::PopMaterial))); |
| 3412 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(KEY_T, KEYS_Control), "ToolsDlgPopTextures" , KEYSCOPE_Console, new C4KeyCB <C4ToolsDlg> (Console.ToolsDlg, &C4ToolsDlg::PopTextures))); |
| 3413 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(KEY_I, KEYS_Control), "ToolsDlgIFTToggle" , KEYSCOPE_Console, new C4KeyCB <C4ToolsDlg> (Console.ToolsDlg, &C4ToolsDlg::ToggleIFT))); |
| 3414 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(KEY_W, KEYS_Control), "ToolsDlgToolToggle" , KEYSCOPE_Console, new C4KeyCB <C4ToolsDlg> (Console.ToolsDlg, &C4ToolsDlg::ToggleTool))); |
| 3415 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(K_DELETE), "EditCursorDelete" , KEYSCOPE_Console, new C4KeyCB <C4EditCursor> (Console.EditCursor, &C4EditCursor::Delete))); |
| 3416 | |
| 3417 | // no default keys assigned |
| 3418 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(KEY_Default), "ChartToggle" , C4KeyScope(KEYSCOPE_Generic | KEYSCOPE_Gui), new C4KeyCB <C4Game> (*this, &C4Game::ToggleChart))); |
| 3419 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(KEY_Default), "NetObsNextPlayer" , KEYSCOPE_FreeView, new C4KeyCB <C4GraphicsSystem> (GraphicsSystem, &C4GraphicsSystem::ViewportNextPlayer))); |
| 3420 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(KEY_Default), "CtrlRateDown" , KEYSCOPE_Generic, new C4KeyCBEx<C4GameControl, int32_t>(Control, -1, &C4GameControl::KeyAdjustControlRate))); |
| 3421 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(KEY_Default), "CtrlRateUp" , KEYSCOPE_Generic, new C4KeyCBEx<C4GameControl, int32_t>(Control, +1, &C4GameControl::KeyAdjustControlRate))); |
| 3422 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(KEY_Default), "NetAllowJoinToggle" , KEYSCOPE_Generic, new C4KeyCB <C4Network2> (Network, &C4Network2::ToggleAllowJoin))); |
| 3423 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey(C4KeyCodeEx(KEY_Default), "NetStatsToggle" , KEYSCOPE_Generic, new C4KeyCB <C4GraphicsSystem> (GraphicsSystem, &C4GraphicsSystem::ToggleShowNetStatus))); |
| 3424 | |
| 3425 | // Map player keyboard controls |
| 3426 | int32_t iKdbSet, iCtrl; |
| 3427 | std::string plrCtrlName; |
| 3428 | for (iKdbSet = C4P_Control_Keyboard1; iKdbSet <= C4P_Control_Keyboard4; iKdbSet++) |
| 3429 | for (iCtrl = 0; iCtrl < C4MaxKey; iCtrl++) |
| 3430 | { |
| 3431 | plrCtrlName = std::format(fmt: "Kbd{}Key{}" , args: iKdbSet - C4P_Control_Keyboard1 + 1, args: iCtrl + 1); |
| 3432 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey( |
| 3433 | C4KeyCodeEx(Config.Controls.Keyboard[iKdbSet][iCtrl]), |
| 3434 | plrCtrlName.c_str(), KEYSCOPE_Control, |
| 3435 | new C4KeyCBExPassKey<C4Game, C4KeySetCtrl>(*this, C4KeySetCtrl(iKdbSet, iCtrl), &C4Game::LocalControlKey, &C4Game::LocalControlKeyUp), |
| 3436 | C4CustomKey::PRIO_PlrControl)); |
| 3437 | } |
| 3438 | |
| 3439 | // Map player gamepad controls |
| 3440 | int32_t iGamepad; |
| 3441 | for (iGamepad = C4P_Control_GamePad1; iGamepad < C4P_Control_GamePad1 + C4ConfigMaxGamepads; iGamepad++) |
| 3442 | { |
| 3443 | C4ConfigGamepad &cfg = Config.Gamepads[iGamepad - C4P_Control_GamePad1]; |
| 3444 | for (iCtrl = 0; iCtrl < C4MaxKey; iCtrl++) |
| 3445 | { |
| 3446 | if (cfg.Button[iCtrl] == -1) continue; |
| 3447 | plrCtrlName = std::format(fmt: "Joy{}Btn{}" , args: iGamepad - C4P_Control_GamePad1 + 1, args: iCtrl + 1); |
| 3448 | KeyboardInput.RegisterKey(pRegKey: new C4CustomKey( |
| 3449 | C4KeyCodeEx(cfg.Button[iCtrl]), |
| 3450 | plrCtrlName.c_str(), KEYSCOPE_Control, |
| 3451 | new C4KeyCBExPassKey<C4Game, C4KeySetCtrl>(*this, C4KeySetCtrl(iGamepad, iCtrl), &C4Game::LocalControlKey, &C4Game::LocalControlKeyUp), |
| 3452 | C4CustomKey::PRIO_PlrControl)); |
| 3453 | } |
| 3454 | } |
| 3455 | |
| 3456 | // load any custom keysboard overloads |
| 3457 | KeyboardInput.LoadCustomConfig(); |
| 3458 | |
| 3459 | // done, success |
| 3460 | return true; |
| 3461 | } |
| 3462 | |
| 3463 | bool C4Game::InitSystem() |
| 3464 | { |
| 3465 | // Timer flags |
| 3466 | GameGo = false; |
| 3467 | // set gamma |
| 3468 | GraphicsSystem.SetGamma(dwClr1: Config.Graphics.Gamma1, dwClr2: Config.Graphics.Gamma2, dwClr3: Config.Graphics.Gamma3, C4GRI_USER); |
| 3469 | // open graphics group now for font-init |
| 3470 | if (!GraphicsResource.RegisterGlobalGraphics()) return false; |
| 3471 | // load font list |
| 3472 | #ifndef USE_CONSOLE |
| 3473 | if (!FontLoader.LoadDefs(hGroup&: Application.SystemGroup, rCfg&: Config)) |
| 3474 | { |
| 3475 | LogFatal(id: C4ResStrTableKey::IDS_ERR_FONTDEFS); return false; |
| 3476 | } |
| 3477 | #endif |
| 3478 | // init extra root group |
| 3479 | // this loads font definitions in this group as well |
| 3480 | // the function may return false, if no extra group is present - that is OK |
| 3481 | if (Extra.InitGroup()) |
| 3482 | // add any Graphics.c4g-files in Extra.c4g-root |
| 3483 | GraphicsResource.RegisterMainGroups(); |
| 3484 | // init main system font |
| 3485 | // This is preliminary, because it's not unlikely to be overloaded after scenario opening and Extra.c4g-initialization. |
| 3486 | // But postponing initialization until then would mean a black screen for quite some time of the initialization progress. |
| 3487 | // Peter wouldn't like this... |
| 3488 | #ifndef USE_CONSOLE |
| 3489 | if (!FontLoader.InitFont(rFont&: GraphicsResource.FontRegular, szFontName: Config.General.RXFontName, eType: C4FontLoader::C4FT_Main, iSize: Config.General.RXFontSize, pGfxGroups: &GraphicsResource.Files)) |
| 3490 | return false; |
| 3491 | #endif |
| 3492 | // init message input (default commands) |
| 3493 | MessageInput.Init(); |
| 3494 | // init keyboard input (default keys, plus overloads) |
| 3495 | if (!InitKeyboard()) |
| 3496 | { |
| 3497 | LogFatal(id: C4ResStrTableKey::IDS_ERR_NOKEYBOARD); return false; |
| 3498 | } |
| 3499 | // Rank system |
| 3500 | Rank.Init(szRegister: Config.GetSubkeyPath(strSubkey: "ClonkRanks" ), szDefRanks: LoadResStr(id: C4ResStrTableKey::IDS_GAME_DEFRANKS), iRankBase: 1000); |
| 3501 | // done, success |
| 3502 | return true; |
| 3503 | } |
| 3504 | |
| 3505 | C4Player *C4Game::JoinPlayer(const char *szFilename, int32_t iAtClient, const char *szAtClientName, C4PlayerInfo *pInfo) |
| 3506 | { |
| 3507 | assert(pInfo); |
| 3508 | C4Player *pPlr; |
| 3509 | // Join |
| 3510 | if (!(pPlr = Players.Join(szFilename, fScenarioInit: true, iAtClient, szAtClientName, pInfo))) return nullptr; |
| 3511 | // Player final init |
| 3512 | pPlr->FinalInit(fInitialValue: true); |
| 3513 | // Create player viewport |
| 3514 | if (pPlr->LocalControl) CreateViewport(iPlayer: pPlr->Number); |
| 3515 | // Check fullscreen viewports |
| 3516 | FullScreen.ViewportCheck(); |
| 3517 | // Update menus |
| 3518 | Console.UpdateMenus(); |
| 3519 | // Append player name to list of session player names (no duplicates) |
| 3520 | if (!SIsModule(szList: PlayerNames.getData(), szString: pPlr->GetName())) |
| 3521 | { |
| 3522 | if (PlayerNames) PlayerNames += ";" ; |
| 3523 | PlayerNames += pPlr->GetName(); |
| 3524 | } |
| 3525 | // Success |
| 3526 | return pPlr; |
| 3527 | } |
| 3528 | |
| 3529 | void C4Game::FixRandom(int32_t iSeed) |
| 3530 | { |
| 3531 | FixedRandom(dwSeed: iSeed); |
| 3532 | Randomize3(); |
| 3533 | } |
| 3534 | |
| 3535 | bool C4Game::LocalControlKey(C4KeyCodeEx key, C4KeySetCtrl Ctrl) |
| 3536 | { |
| 3537 | // keyboard callback: Perform local player control |
| 3538 | C4Player *pPlr; |
| 3539 | if (pPlr = Players.GetLocalByKbdSet(iKbdSet: Ctrl.iKeySet)) |
| 3540 | { |
| 3541 | // Swallow a event generated from Keyrepeat for AutoStopControl |
| 3542 | if (pPlr->ControlStyle) |
| 3543 | { |
| 3544 | if (key.IsRepeated()) |
| 3545 | return true; |
| 3546 | } |
| 3547 | LocalPlayerControl(iPlayer: pPlr->Number, iCom: Control2Com(iControl: Ctrl.iCtrl, fUp: false)); |
| 3548 | return true; |
| 3549 | } |
| 3550 | // not processed - must return false here, so unused keyboard control sets do not block used ones |
| 3551 | return false; |
| 3552 | } |
| 3553 | |
| 3554 | bool C4Game::LocalControlKeyUp(C4KeyCodeEx key, C4KeySetCtrl Ctrl) |
| 3555 | { |
| 3556 | // Direct callback for released key in AutoStopControl-mode (ignore repeated) |
| 3557 | if (key.IsRepeated()) |
| 3558 | return true; |
| 3559 | C4Player *pPlr; |
| 3560 | if ((pPlr = Players.GetLocalByKbdSet(iKbdSet: Ctrl.iKeySet)) && pPlr->ControlStyle) |
| 3561 | { |
| 3562 | int iCom = Control2Com(iControl: Ctrl.iCtrl, fUp: true); |
| 3563 | if (iCom != COM_None) LocalPlayerControl(iPlayer: pPlr->Number, iCom); |
| 3564 | return true; |
| 3565 | } |
| 3566 | // not processed - must return false here, so unused keyboard control sets do not block used ones |
| 3567 | return false; |
| 3568 | } |
| 3569 | |
| 3570 | void C4Game::LocalPlayerControl(int32_t iPlayer, int32_t iCom) |
| 3571 | { |
| 3572 | C4Player *pPlr = Players.Get(iPlayer); if (!pPlr) return; |
| 3573 | int32_t iData = 0; |
| 3574 | // Menu button com |
| 3575 | if (iCom == COM_PlayerMenu) |
| 3576 | { |
| 3577 | // Player menu open: close |
| 3578 | if (pPlr->Menu.IsActive()) |
| 3579 | pPlr->Menu.Close(fOK: false); |
| 3580 | // Menu closed: open main menu |
| 3581 | else |
| 3582 | pPlr->ActivateMenuMain(); |
| 3583 | return; |
| 3584 | } |
| 3585 | // Local player menu active: convert menu com and control local |
| 3586 | if (pPlr->Menu.ConvertCom(rCom&: iCom, rData&: iData, fAsyncConversion: true)) |
| 3587 | { |
| 3588 | pPlr->Menu.Control(byCom: iCom, iData); |
| 3589 | return; |
| 3590 | } |
| 3591 | // Pre-queue asynchronous menu conversions |
| 3592 | if (pPlr->Cursor && pPlr->Cursor->Menu) |
| 3593 | pPlr->Cursor->Menu->ConvertCom(rCom&: iCom, rData&: iData, fAsyncConversion: true); |
| 3594 | // Not for eliminated (checked again in DirectCom, but make sure no control is generated for eliminated players!) |
| 3595 | if (pPlr->Eliminated) return; |
| 3596 | // Player control: add to control queue |
| 3597 | Input.Add(eType: CID_PlrControl, pCtrl: new C4ControlPlayerControl(iPlayer, iCom, iData)); |
| 3598 | } |
| 3599 | |
| 3600 | bool C4Game::DefinitionFilenamesFromSaveGame() |
| 3601 | { |
| 3602 | std::string source(GameText.GetData()); |
| 3603 | |
| 3604 | // Use loaded game text component |
| 3605 | if (source.size()) |
| 3606 | { |
| 3607 | // Search def file name section |
| 3608 | size_t pos = source.find(s: "[DefinitionFiles]" ); |
| 3609 | if (pos != std::string::npos) |
| 3610 | { |
| 3611 | DefinitionFilenames.clear(); |
| 3612 | std::istringstream stream(source.substr(pos: pos)); |
| 3613 | std::string line; |
| 3614 | bool found = false; |
| 3615 | while (std::getline(is&: stream, str&: line)) |
| 3616 | { |
| 3617 | size_t p = line.find(s: "Definition" ); |
| 3618 | if (p == 0 && (p = line.find(c: '=', pos: p) != std::string::npos)) |
| 3619 | { |
| 3620 | found = true; |
| 3621 | DefinitionFilenames.push_back(x: line.substr(pos: p)); |
| 3622 | } |
| 3623 | else if (found) |
| 3624 | { |
| 3625 | break; |
| 3626 | } |
| 3627 | } |
| 3628 | return found; |
| 3629 | } |
| 3630 | } |
| 3631 | return false; |
| 3632 | } |
| 3633 | |
| 3634 | bool C4Game::DoGameOver() |
| 3635 | { |
| 3636 | // Duplication safety |
| 3637 | if (GameOver) return false; |
| 3638 | // Flag, log, call |
| 3639 | GameOver = true; |
| 3640 | Log(id: C4ResStrTableKey::IDS_PRC_GAMEOVER); |
| 3641 | Script.GRBroadcast(PSF_OnGameOver); |
| 3642 | // Flag all surviving players as winners |
| 3643 | for (C4Player *pPlayer = Players.First; pPlayer; pPlayer = pPlayer->Next) |
| 3644 | if (!pPlayer->Eliminated) |
| 3645 | pPlayer->EvaluateLeague(fDisconnected: false, fWon: true); |
| 3646 | return true; |
| 3647 | } |
| 3648 | |
| 3649 | void C4Game::ShowGameOverDlg() |
| 3650 | { |
| 3651 | // safety |
| 3652 | if (GameOverDlgShown) return; |
| 3653 | // flag, show |
| 3654 | GameOverDlgShown = true; |
| 3655 | #ifdef USE_CONSOLE |
| 3656 | // wait for streaming to finish |
| 3657 | if (Network.isEnabled() && Network.isStreaming()) |
| 3658 | { |
| 3659 | Network.Logger->info("Sending {} bytes pending for stream..." , Network.getPendingStreamData()); |
| 3660 | while (Network.isStreaming()) |
| 3661 | if (!Application.HandleMessage(100, false)) |
| 3662 | break; |
| 3663 | } |
| 3664 | // console engine quits here directly |
| 3665 | Application.QuitGame(); |
| 3666 | #else |
| 3667 | if (pGUI && Application.isFullScreen) |
| 3668 | { |
| 3669 | C4GameOverDlg *pDlg = new C4GameOverDlg(); |
| 3670 | pDlg->SetDelOnClose(); |
| 3671 | if (!pDlg->Show(pOnScreen: pGUI, fCB: true)) { delete pDlg; Application.QuitGame(); } |
| 3672 | } |
| 3673 | #endif |
| 3674 | } |
| 3675 | |
| 3676 | void C4Game::SyncClearance() |
| 3677 | { |
| 3678 | PXS.SyncClearance(); |
| 3679 | Objects.SyncClearance(); |
| 3680 | } |
| 3681 | |
| 3682 | void C4Game::Synchronize(bool fSavePlayerFiles) |
| 3683 | { |
| 3684 | // Log |
| 3685 | spdlog::debug(fmt: "Network: Synchronization (Frame {}) [PlrSave: {}]" , args&: FrameCounter, args&: fSavePlayerFiles); |
| 3686 | // callback to control (to start record) |
| 3687 | Control.OnGameSynchronizing(); |
| 3688 | // Fix random |
| 3689 | FixRandom(iSeed: Parameters.RandomSeed); |
| 3690 | // Synchronize members |
| 3691 | Defs.Synchronize(); |
| 3692 | Landscape.Synchronize(); |
| 3693 | MassMover.Synchronize(); |
| 3694 | PXS.Synchronize(); |
| 3695 | Objects.Synchronize(); |
| 3696 | // synchronize local player files if desired |
| 3697 | // this will reset any InActionTimes! |
| 3698 | // (not in replay mode) |
| 3699 | if (fSavePlayerFiles && !Control.isReplay()) Players.SynchronizeLocalFiles(); |
| 3700 | // callback to network |
| 3701 | if (Network.isEnabled()) Network.OnGameSynchronized(); |
| 3702 | // TransferZone synchronization: Must do this after dynamic creation to avoid synchronization loss |
| 3703 | // if UpdateTransferZone-callbacks do sync-relevant changes |
| 3704 | TransferZones.Synchronize(); |
| 3705 | } |
| 3706 | |
| 3707 | C4Object *C4Game::FindBase(int32_t iPlayer, int32_t iIndex) |
| 3708 | { |
| 3709 | C4Object *cObj; C4ObjectLink *clnk; |
| 3710 | for (clnk = Objects.First; clnk && (cObj = clnk->Obj); clnk = clnk->Next) |
| 3711 | // Status |
| 3712 | if (cObj->Status) |
| 3713 | // Base |
| 3714 | if (cObj->Base == iPlayer) |
| 3715 | // Index |
| 3716 | if (iIndex == 0) return cObj; |
| 3717 | else iIndex--; |
| 3718 | // Not found |
| 3719 | return nullptr; |
| 3720 | } |
| 3721 | |
| 3722 | C4Object *C4Game::FindFriendlyBase(int32_t iPlayer, int32_t iIndex) |
| 3723 | { |
| 3724 | C4Object *cObj; C4ObjectLink *clnk; |
| 3725 | for (clnk = Objects.First; clnk && (cObj = clnk->Obj); clnk = clnk->Next) |
| 3726 | // Status |
| 3727 | if (cObj->Status) |
| 3728 | // Base |
| 3729 | if (ValidPlr(plr: cObj->Base)) |
| 3730 | // friendly Base |
| 3731 | if (!Hostile(plr1: cObj->Base, plr2: iPlayer)) |
| 3732 | // Index |
| 3733 | if (iIndex == 0) return cObj; |
| 3734 | else iIndex--; |
| 3735 | // Not found |
| 3736 | return nullptr; |
| 3737 | } |
| 3738 | |
| 3739 | C4Object *C4Game::FindObjectByCommand(int32_t iCommand, C4Object *pTarget, C4Value iTx, int32_t iTy, C4Object *pTarget2, C4Object *pFindNext) |
| 3740 | { |
| 3741 | C4Object *cObj; C4ObjectLink *clnk; |
| 3742 | for (clnk = Objects.First; clnk && (cObj = clnk->Obj); clnk = clnk->Next) |
| 3743 | { |
| 3744 | // find next |
| 3745 | if (pFindNext) { if (cObj == pFindNext) pFindNext = nullptr; continue; } |
| 3746 | // Status |
| 3747 | if (cObj->Status) |
| 3748 | // Check commands |
| 3749 | for (C4Command *pCommand = cObj->Command; pCommand; pCommand = pCommand->Next) |
| 3750 | // Command |
| 3751 | if (pCommand->Command == iCommand) |
| 3752 | // Target |
| 3753 | if (!pTarget || (pCommand->Target == pTarget)) |
| 3754 | // Position |
| 3755 | if ((!iTx && !iTy) || ((pCommand->Tx == iTx) && (pCommand->Ty == iTy))) |
| 3756 | // Target2 |
| 3757 | if (!pTarget2 || (pCommand->Target2 == pTarget2)) |
| 3758 | // Found |
| 3759 | return cObj; |
| 3760 | } |
| 3761 | // Not found |
| 3762 | return nullptr; |
| 3763 | } |
| 3764 | |
| 3765 | bool C4Game::InitNetworkFromAddress(const char *szAddress) |
| 3766 | { |
| 3767 | // Query reference |
| 3768 | C4Network2RefClient RefClient; |
| 3769 | if (!RefClient.Init() || |
| 3770 | !RefClient.SetServer(serverAddress: szAddress, defaultPort: Config.Network.PortRefServer) || |
| 3771 | !RefClient.QueryReferences()) |
| 3772 | { |
| 3773 | LogFatal(id: C4ResStrTableKey::IDS_NET_REFQUERY_FAILED, args: RefClient.GetError()); return false; |
| 3774 | } |
| 3775 | // We have to wait for the answer |
| 3776 | const std::string message{LoadResStr(id: C4ResStrTableKey::IDS_NET_REFQUERY_QUERYMSG, args&: szAddress)}; |
| 3777 | LogNTr(message); |
| 3778 | // Set up wait dialog |
| 3779 | C4GUI::MessageDialog *pDlg = nullptr; |
| 3780 | if (pGUI && !Console.Active) |
| 3781 | { |
| 3782 | // create & show |
| 3783 | pDlg = new C4GUI::MessageDialog(message.c_str(), LoadResStr(id: C4ResStrTableKey::IDS_NET_REFQUERY_QUERYTITLE), |
| 3784 | C4GUI::MessageDialog::btnAbort, C4GUI::Ico_NetWait, C4GUI::MessageDialog::dsMedium); |
| 3785 | if (!pDlg || !pDlg->Show(pOnScreen: pGUI, fCB: true)) return false; |
| 3786 | } |
| 3787 | // Wait for response |
| 3788 | while (RefClient.isBusy()) |
| 3789 | { |
| 3790 | // Execute GUI |
| 3791 | if (Application.HandleMessage(iTimeout: 100) == HR_Failure || |
| 3792 | (pDlg && pDlg->IsAborted())) |
| 3793 | { |
| 3794 | if (pGUI) delete pDlg; |
| 3795 | return false; |
| 3796 | } |
| 3797 | // Check if reference is received |
| 3798 | if (!RefClient.Execute(iMaxTime: 0)) |
| 3799 | break; |
| 3800 | } |
| 3801 | // Close dialog |
| 3802 | if (pGUI) delete pDlg; |
| 3803 | // Error? |
| 3804 | if (!RefClient.isSuccess()) |
| 3805 | { |
| 3806 | LogFatal(id: C4ResStrTableKey::IDS_NET_REFQUERY_FAILED, args: RefClient.GetError()); return false; |
| 3807 | } |
| 3808 | // Get references |
| 3809 | C4Network2Reference **ppRefs = nullptr; int32_t iRefCount; |
| 3810 | if (!RefClient.GetReferences(rpReferences&: ppRefs, rRefCount&: iRefCount) || iRefCount <= 0) |
| 3811 | { |
| 3812 | LogFatal(id: C4ResStrTableKey::IDS_NET_REFQUERY_FAILED, args: LoadResStr(id: C4ResStrTableKey::IDS_NET_REFQUERY_NOREF)); return false; |
| 3813 | } |
| 3814 | // Connect to first reference |
| 3815 | bool fSuccess = InitNetworkFromReference(Reference: *ppRefs[0]); |
| 3816 | // Remove references |
| 3817 | for (int i = 0; i < iRefCount; i++) |
| 3818 | delete ppRefs[i]; |
| 3819 | delete[] ppRefs; |
| 3820 | return fSuccess; |
| 3821 | } |
| 3822 | |
| 3823 | bool C4Game::InitNetworkFromReference(const C4Network2Reference &Reference) |
| 3824 | { |
| 3825 | // Find host data |
| 3826 | C4Client *pHostData = Reference.Parameters.Clients.getClientByID(iID: C4ClientIDHost); |
| 3827 | if (!pHostData) { LogFatal(id: C4ResStrTableKey::IDS_NET_INVALIDREF); return false; } |
| 3828 | // Save scenario title |
| 3829 | Parameters.ScenarioTitle.CopyValidated(szFromVal: Reference.getTitle()); |
| 3830 | // Log |
| 3831 | Log(id: C4ResStrTableKey::IDS_NET_JOINGAMEBY, args: pHostData->getName()); |
| 3832 | // Init clients |
| 3833 | Clients.Init(); |
| 3834 | // Connect |
| 3835 | if (Network.InitClient(Ref: Reference, fObserver: false) != C4Network2::IR_Success) |
| 3836 | { |
| 3837 | LogFatal(id: C4ResStrTableKey::IDS_NET_NOHOSTCON, args: pHostData->getName()); |
| 3838 | return false; |
| 3839 | } |
| 3840 | // init control |
| 3841 | if (!Control.InitNetwork(pLocal: Clients.getLocal())) return false; |
| 3842 | // init local player info list |
| 3843 | Network.Players.Init(); |
| 3844 | return true; |
| 3845 | } |
| 3846 | |
| 3847 | bool C4Game::InitNetworkHost() |
| 3848 | { |
| 3849 | // Network not active? |
| 3850 | if (!NetworkActive) |
| 3851 | { |
| 3852 | // Clear client list |
| 3853 | if (!C4S.Head.Replay) |
| 3854 | Clients.Init(); |
| 3855 | return true; |
| 3856 | } |
| 3857 | // network not active? |
| 3858 | if (C4S.Head.NetworkGame) |
| 3859 | { |
| 3860 | LogFatal(id: C4ResStrTableKey::IDS_NET_NODIRECTSTART); Clients.Init(); |
| 3861 | } |
| 3862 | // replay? |
| 3863 | if (C4S.Head.Replay) |
| 3864 | { |
| 3865 | LogFatal(id: C4ResStrTableKey::IDS_PRC_NONETREPLAY); return true; |
| 3866 | } |
| 3867 | // clear client list |
| 3868 | Clients.Init(); |
| 3869 | // init network as host |
| 3870 | if (!Network.InitHost(fLobby)) return false; |
| 3871 | // init control |
| 3872 | if (!Control.InitNetwork(pLocal: Clients.getLocal())) return false; |
| 3873 | // init local player info list |
| 3874 | Network.Players.Init(); |
| 3875 | // allow connect |
| 3876 | Network.AllowJoin(fAllow: true); |
| 3877 | // do lobby (if desired) |
| 3878 | if (fLobby) |
| 3879 | { |
| 3880 | if (!Network.DoLobby()) return false; |
| 3881 | } |
| 3882 | else |
| 3883 | { |
| 3884 | // otherwise: start manually |
| 3885 | if (!Network.Start()) return false; |
| 3886 | } |
| 3887 | // ok |
| 3888 | return true; |
| 3889 | } |
| 3890 | |
| 3891 | bool C4Game::CheckObjectEnumeration() |
| 3892 | { |
| 3893 | // Check valid & maximum number & duplicate numbers |
| 3894 | int32_t iMax = 0; |
| 3895 | C4Object *cObj; C4ObjectLink *clnk; |
| 3896 | C4Object *cObj2; C4ObjectLink *clnk2; |
| 3897 | clnk = Objects.First; if (!clnk) clnk = Objects.InactiveObjects.First; |
| 3898 | while (clnk) |
| 3899 | { |
| 3900 | // Invalid number |
| 3901 | cObj = clnk->Obj; |
| 3902 | if (cObj->Number < 1) |
| 3903 | { |
| 3904 | LogNTr(level: spdlog::level::err, fmt: "Invalid object enumeration number ({}) of object {} (x={})" , args&: cObj->Number, args: C4IdText(id: cObj->id), args&: cObj->x); |
| 3905 | return false; |
| 3906 | } |
| 3907 | // Max |
| 3908 | if (cObj->Number > iMax) iMax = cObj->Number; |
| 3909 | // Duplicate |
| 3910 | for (clnk2 = Objects.First; clnk2 && (cObj2 = clnk2->Obj); clnk2 = clnk2->Next) |
| 3911 | if (cObj2 != cObj) |
| 3912 | if (cObj->Number == cObj2->Number) |
| 3913 | { |
| 3914 | LogNTr(level: spdlog::level::err, fmt: "Duplicate object enumeration number {} ({} and {})" , args&: cObj2->Number, args: cObj->GetName(), args: cObj2->GetName()); |
| 3915 | return false; |
| 3916 | } |
| 3917 | for (clnk2 = Objects.InactiveObjects.First; clnk2 && (cObj2 = clnk2->Obj); clnk2 = clnk2->Next) |
| 3918 | if (cObj2 != cObj) |
| 3919 | if (cObj->Number == cObj2->Number) |
| 3920 | { |
| 3921 | LogNTr(level: spdlog::level::err, fmt: "Duplicate object enumeration number {} ({} and {}(i))" , args&: cObj2->Number, args: cObj->GetName(), args: cObj2->GetName()); |
| 3922 | return false; |
| 3923 | } |
| 3924 | // next |
| 3925 | if (!clnk->Next) |
| 3926 | if (clnk == Objects.Last) clnk = Objects.InactiveObjects.First; else clnk = nullptr; |
| 3927 | else |
| 3928 | clnk = clnk->Next; |
| 3929 | } |
| 3930 | // Adjust enumeration index |
| 3931 | if (iMax > ObjectEnumerationIndex) ObjectEnumerationIndex = iMax; |
| 3932 | // Done |
| 3933 | return true; |
| 3934 | } |
| 3935 | |
| 3936 | std::vector<std::string> C4Game::FoldersWithLocalsDefs(std::string path) |
| 3937 | { |
| 3938 | std::vector<std::string> defs; |
| 3939 | |
| 3940 | // Get relative path |
| 3941 | path = Config.AtExeRelativePath(szFilename: path.c_str()); |
| 3942 | |
| 3943 | // Scan path for folder names |
| 3944 | std::string folderName; |
| 3945 | C4Group group; |
| 3946 | for (size_t pos = path.find(DirectorySeparator); pos != std::string::npos; pos = path.find(DirectorySeparator, pos: pos + 1)) |
| 3947 | { |
| 3948 | // Get folder name |
| 3949 | folderName = path.substr(pos: 0, n: pos); |
| 3950 | // Open folder |
| 3951 | if (SEqualNoCase(szStr1: GetExtension(fname: folderName.c_str()), szStr2: "c4f" )) |
| 3952 | { |
| 3953 | if (group.Open(szGroupName: folderName.c_str())) |
| 3954 | { |
| 3955 | // Check for contained defs |
| 3956 | // do not, however, add them to the group set: |
| 3957 | // parent folders are added by OpenScenario already! |
| 3958 | int32_t contents; |
| 3959 | if ((contents = GroupSet.CheckGroupContents(rGroup&: group, C4GSCnt_Definitions))) |
| 3960 | { |
| 3961 | defs.push_back(x: folderName); |
| 3962 | } |
| 3963 | // Close folder |
| 3964 | group.Close(); |
| 3965 | } |
| 3966 | } |
| 3967 | } |
| 3968 | |
| 3969 | return defs; |
| 3970 | } |
| 3971 | |
| 3972 | void C4Game::InitValueOverloads() |
| 3973 | { |
| 3974 | C4ID idOvrl; C4Def *pDef; |
| 3975 | // set new values |
| 3976 | for (int32_t cnt = 0; idOvrl = C4S.Game.Realism.ValueOverloads.GetID(index: cnt); cnt++) |
| 3977 | if (pDef = Defs.ID2Def(id: idOvrl)) |
| 3978 | pDef->Value = C4S.Game.Realism.ValueOverloads.GetIDCount(id: idOvrl); |
| 3979 | } |
| 3980 | |
| 3981 | void C4Game::InitEnvironment() |
| 3982 | { |
| 3983 | // Place environment objects |
| 3984 | int32_t cnt, cnt2; |
| 3985 | C4ID idType; int32_t iCount; |
| 3986 | for (cnt = 0; (idType = C4S.Environment.Objects.GetID(index: cnt, ipCount: &iCount)); cnt++) |
| 3987 | for (cnt2 = 0; cnt2 < iCount; cnt2++) |
| 3988 | CreateObject(id: idType, pCreator: nullptr); |
| 3989 | } |
| 3990 | |
| 3991 | void C4Game::InitRules() |
| 3992 | { |
| 3993 | // Place rule objects |
| 3994 | int32_t cnt, cnt2; |
| 3995 | C4ID idType; int32_t iCount; |
| 3996 | for (cnt = 0; (idType = Parameters.Rules.GetID(index: cnt, ipCount: &iCount)); cnt++) |
| 3997 | for (cnt2 = 0; cnt2 < std::max<int32_t>(a: iCount, b: 1); cnt2++) |
| 3998 | CreateObject(id: idType, pCreator: nullptr); |
| 3999 | // Update rule flags |
| 4000 | UpdateRules(); |
| 4001 | } |
| 4002 | |
| 4003 | void C4Game::InitGoals() |
| 4004 | { |
| 4005 | // Place goal objects |
| 4006 | int32_t cnt, cnt2; |
| 4007 | C4ID idType; int32_t iCount; |
| 4008 | for (cnt = 0; (idType = Parameters.Goals.GetID(index: cnt, ipCount: &iCount)); cnt++) |
| 4009 | for (cnt2 = 0; cnt2 < iCount; cnt2++) |
| 4010 | CreateObject(id: idType, pCreator: nullptr); |
| 4011 | } |
| 4012 | |
| 4013 | void C4Game::UpdateRules() |
| 4014 | { |
| 4015 | if (Tick255 && FrameCounter > 1) return; |
| 4016 | Rules = 0; |
| 4017 | if (ObjectCount(id: C4ID_Energy)) Rules |= C4RULE_StructuresNeedEnergy; |
| 4018 | if (ObjectCount(id: C4ID_CnMaterial)) Rules |= C4RULE_ConstructionNeedsMaterial; |
| 4019 | if (ObjectCount(id: C4ID_FlagRemvbl)) Rules |= C4RULE_FlagRemoveable; |
| 4020 | if (ObjectCount(id: C4Id(str: "STSN" ))) Rules |= C4RULE_StructuresSnowIn; |
| 4021 | if (ObjectCount(id: C4ID_TeamHomebase)) Rules |= C4RULE_TeamHombase; |
| 4022 | } |
| 4023 | |
| 4024 | void C4Game::SetInitProgress(float fToProgress) |
| 4025 | { |
| 4026 | // set new progress |
| 4027 | InitProgress = int32_t(fToProgress); |
| 4028 | // if progress is more than one percent, display it |
| 4029 | if (InitProgress > LastInitProgress) |
| 4030 | { |
| 4031 | LastInitProgress = InitProgress; |
| 4032 | if (Application.IsMainThread()) |
| 4033 | { |
| 4034 | GraphicsSystem.MessageBoard.LogNotify(); |
| 4035 | } |
| 4036 | |
| 4037 | if (Application.pWindow) |
| 4038 | { |
| 4039 | Application.pWindow->SetProgress(InitProgress); |
| 4040 | } |
| 4041 | } |
| 4042 | } |
| 4043 | |
| 4044 | void C4Game::OnResolutionChanged() |
| 4045 | { |
| 4046 | // update anything that's dependent on screen resolution |
| 4047 | if (pGUI) |
| 4048 | pGUI->SetBounds(C4Rect(0, 0, Config.Graphics.ResX, Config.Graphics.ResY)); |
| 4049 | if (FullScreen.Active) |
| 4050 | InitFullscreenComponents(fRunning: !!IsRunning); |
| 4051 | // note that this may fail if the gfx groups are closed already (runtime resolution change) |
| 4052 | // doesn't matter; old gfx are kept in this case |
| 4053 | if (GraphicsResource.IsInitialized()) |
| 4054 | { |
| 4055 | GraphicsResource.ReloadResolutionDependentFiles(); |
| 4056 | } |
| 4057 | } |
| 4058 | |
| 4059 | bool C4Game::LoadScenarioSection(const char *szSection, uint32_t dwFlags) |
| 4060 | { |
| 4061 | // note on scenario section saving: |
| 4062 | // if a scenario section overwrites a value that had used the default values in the main scenario section, |
| 4063 | // returning to the main section with an unsaved landscape (and thus an unsaved scenario core), |
| 4064 | // would leave those values in the altered state of the previous section |
| 4065 | // scenario designers should regard this and always define any values, that are defined in subsections as well |
| 4066 | C4Group hGroup, *pGrp; |
| 4067 | |
| 4068 | // if current section was the loaded section (maybe main, but need not for resumed savegames) |
| 4069 | if (!pCurrentScenarioSection) |
| 4070 | { |
| 4071 | pCurrentScenarioSection = new C4ScenarioSection(CurrentScenarioSection); |
| 4072 | if (!*CurrentScenarioSection) SCopy(szSource: C4ScenSect_Main, sTarget: CurrentScenarioSection, iMaxL: C4MaxName); |
| 4073 | } |
| 4074 | |
| 4075 | // find section to load |
| 4076 | C4ScenarioSection *pLoadSect = pScenarioSections; |
| 4077 | while (pLoadSect) if (SEqualNoCase(szStr1: pLoadSect->GetName(), szStr2: szSection)) break; else pLoadSect = pLoadSect->pNext; |
| 4078 | if (!pLoadSect) |
| 4079 | { |
| 4080 | DebugLog(level: spdlog::level::err, fmt: "LoadScenarioSection: scenario section {} not found!" , args&: szSection); |
| 4081 | return false; |
| 4082 | } |
| 4083 | |
| 4084 | // save current section state |
| 4085 | if (pLoadSect != pCurrentScenarioSection && dwFlags & (C4S_SAVE_LANDSCAPE | C4S_SAVE_OBJECTS)) |
| 4086 | { |
| 4087 | // ensure that the section file does point to temp store |
| 4088 | if (!pCurrentScenarioSection->EnsureTempStore(fExtractLandscape: !(dwFlags & C4S_SAVE_LANDSCAPE), fExtractObjects: !(dwFlags & C4S_SAVE_OBJECTS))) |
| 4089 | { |
| 4090 | DebugLog(level: spdlog::level::err, fmt: "LoadScenarioSection({}): could not extract section files of current section {}" , args&: szSection, args: pCurrentScenarioSection->GetName()); |
| 4091 | return false; |
| 4092 | } |
| 4093 | // open current group |
| 4094 | if (!(pGrp = pCurrentScenarioSection->GetGroupfile(rGrp&: hGroup))) |
| 4095 | { |
| 4096 | DebugLog(level: spdlog::level::err, message: "LoadScenarioSection: error opening current group file" ); |
| 4097 | return false; |
| 4098 | } |
| 4099 | // store landscape, if desired (w/o material enumeration - that's assumed to stay consistent during the game) |
| 4100 | if (dwFlags & C4S_SAVE_LANDSCAPE) |
| 4101 | { |
| 4102 | // storing the landscape implies storing the scenario core |
| 4103 | // otherwise, the ExactLandscape-flag would be lost |
| 4104 | // maybe imply exact landscapes by the existance of Landscape.png-files? |
| 4105 | C4Scenario rC4S = C4S; |
| 4106 | rC4S.SetExactLandscape(); |
| 4107 | if (!rC4S.Save(hGroup&: *pGrp, fSaveSection: true)) |
| 4108 | { |
| 4109 | DebugLog(level: spdlog::level::err, message: "LoadScenarioSection: Error saving C4S" ); |
| 4110 | return false; |
| 4111 | } |
| 4112 | // landscape |
| 4113 | { |
| 4114 | C4DebugRecOff DBGRECOFF; |
| 4115 | Objects.RemoveSolidMasks(); |
| 4116 | if (!Landscape.Save(hGroup&: *pGrp)) |
| 4117 | { |
| 4118 | DebugLog(level: spdlog::level::err, message: "LoadScenarioSection: Error saving Landscape" ); |
| 4119 | return false; |
| 4120 | } |
| 4121 | Objects.PutSolidMasks(); |
| 4122 | } |
| 4123 | // PXS |
| 4124 | if (!PXS.Save(hGroup&: *pGrp)) |
| 4125 | { |
| 4126 | DebugLog(level: spdlog::level::err, message: "LoadScenarioSection: Error saving PXS" ); |
| 4127 | return false; |
| 4128 | } |
| 4129 | // MassMover (create copy, may not modify running data) |
| 4130 | C4MassMoverSet MassMoverSet; |
| 4131 | MassMoverSet.Copy(rSet&: MassMover); |
| 4132 | if (!MassMoverSet.Save(hGroup&: *pGrp)) |
| 4133 | { |
| 4134 | DebugLog(level: spdlog::level::err, message: "LoadScenarioSection: Error saving MassMover" ); |
| 4135 | return false; |
| 4136 | } |
| 4137 | } |
| 4138 | // store objects |
| 4139 | if (dwFlags & C4S_SAVE_OBJECTS) |
| 4140 | { |
| 4141 | // strings; those will have to be merged when reloaded |
| 4142 | if (!ScriptEngine.Strings.Save(ParentGroup&: *pGrp)) |
| 4143 | { |
| 4144 | DebugLog(level: spdlog::level::err, message: "LoadScenarioSection: Error saving strings" ); |
| 4145 | return false; |
| 4146 | } |
| 4147 | // objects: do not save info objects or inactive objects |
| 4148 | if (!Objects.Save(hGroup&: *pGrp, fSaveGame: false, fSaveInactive: false)) |
| 4149 | { |
| 4150 | DebugLog(level: spdlog::level::err, message: "LoadScenarioSection: Error saving objects" ); |
| 4151 | return false; |
| 4152 | } |
| 4153 | } |
| 4154 | // close current group |
| 4155 | if (hGroup.IsOpen()) hGroup.Close(); |
| 4156 | // mark modified |
| 4157 | pCurrentScenarioSection->fModified = true; |
| 4158 | } |
| 4159 | // open section group |
| 4160 | if (!(pGrp = pLoadSect->GetGroupfile(rGrp&: hGroup))) |
| 4161 | { |
| 4162 | DebugLog(level: spdlog::level::err, message: "LoadScenarioSection: error opening group file" ); |
| 4163 | return false; |
| 4164 | } |
| 4165 | // remove all objects (except inactive) |
| 4166 | // do correct removal calls, because this will stop fire sounds, etc. |
| 4167 | C4ObjectLink *clnk; |
| 4168 | for (clnk = Objects.First; clnk; clnk = clnk->Next) clnk->Obj->AssignRemoval(); |
| 4169 | for (clnk = Objects.First; clnk; clnk = clnk->Next) |
| 4170 | if (clnk->Obj->Status) |
| 4171 | { |
| 4172 | DebugLog(level: spdlog::level::warn, fmt: "LoadScenarioSection: Object {} created in destruction process!" , args: static_cast<int>(clnk->Obj->Number)); |
| 4173 | ClearPointers(pObj: clnk->Obj); |
| 4174 | // clnk->Obj->AssignRemoval(); - this could create additional objects in endless recursion... |
| 4175 | } |
| 4176 | DeleteObjects(fDeleteInactive: false); |
| 4177 | // remove global effects |
| 4178 | if (pGlobalEffects) if (~dwFlags | C4S_KEEP_EFFECTS) |
| 4179 | { |
| 4180 | pGlobalEffects->ClearAll(pObj: nullptr, C4FxCall_RemoveClear); |
| 4181 | // scenario section call might have been done from a global effect |
| 4182 | // rely on dead effect removal for actually removing the effects; do not clear the array here! |
| 4183 | } |
| 4184 | // del particles as well |
| 4185 | Particles.ClearParticles(); |
| 4186 | // clear transfer zones |
| 4187 | TransferZones.Clear(); |
| 4188 | // backup old sky |
| 4189 | char szOldSky[C4MaxDefString + 1]; |
| 4190 | SCopy(szSource: C4S.Landscape.SkyDef, sTarget: szOldSky, iMaxL: C4MaxDefString); |
| 4191 | // overload scenario values (fails if no scenario core is present; that's OK) |
| 4192 | C4S.Load(hGroup&: *pGrp, fLoadSection: true); |
| 4193 | // determine whether a new sky has to be loaded |
| 4194 | bool fLoadNewSky = !SEqualNoCase(szStr1: szOldSky, szStr2: C4S.Landscape.SkyDef) || pGrp->FindEntry(C4CFN_Sky ".*" ); |
| 4195 | // re-init game in new section |
| 4196 | if (!InitGame(hGroup&: *pGrp, section: pLoadSect, fLoadSky: fLoadNewSky)) |
| 4197 | { |
| 4198 | DebugLog(level: spdlog::level::err, message: "LoadScenarioSection: Error reiniting game" ); |
| 4199 | return false; |
| 4200 | } |
| 4201 | // set new current section |
| 4202 | pCurrentScenarioSection = pLoadSect; |
| 4203 | SCopy(szSource: pCurrentScenarioSection->GetName(), sTarget: CurrentScenarioSection); |
| 4204 | // resize viewports |
| 4205 | GraphicsSystem.RecalculateViewports(); |
| 4206 | |
| 4207 | for (auto plr = Players.First; plr; plr = plr->Next) |
| 4208 | { |
| 4209 | plr->ApplyForcedControl(); |
| 4210 | } |
| 4211 | |
| 4212 | // done, success |
| 4213 | return true; |
| 4214 | } |
| 4215 | |
| 4216 | bool C4Game::ToggleDebugMode() |
| 4217 | { |
| 4218 | // debug mode not allowed |
| 4219 | if (!Parameters.AllowDebug && !DebugMode) { GraphicsSystem.FlashMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_DEBUGMODENOTALLOWED)); return false; } |
| 4220 | Toggle(v&: DebugMode); |
| 4221 | if (!DebugMode) GraphicsSystem.DeactivateDebugOutput(); |
| 4222 | GraphicsSystem.FlashMessageOnOff(strWhat: LoadResStr(id: C4ResStrTableKey::IDS_CTL_DEBUGMODE), fOn: DebugMode); |
| 4223 | return true; |
| 4224 | } |
| 4225 | |
| 4226 | bool C4Game::ToggleChart() |
| 4227 | { |
| 4228 | C4ChartDialog::Toggle(); |
| 4229 | return true; |
| 4230 | } |
| 4231 | |
| 4232 | void C4Game::Abort(bool fApproved) |
| 4233 | { |
| 4234 | // league needs approval |
| 4235 | if (Network.isEnabled() && Parameters.isLeague() && !fApproved) |
| 4236 | { |
| 4237 | if (Control.isCtrlHost() && !GameOver) |
| 4238 | { |
| 4239 | Network.Vote(eType: VT_Cancel); |
| 4240 | return; |
| 4241 | } |
| 4242 | if (!Control.isCtrlHost() && !GameOver && Players.GetLocalByIndex(iIndex: 0)) |
| 4243 | { |
| 4244 | Network.Vote(eType: VT_Kick, fApprove: true, iData: Control.ClientID()); |
| 4245 | return; |
| 4246 | } |
| 4247 | } |
| 4248 | // hard-abort: eval league and quit |
| 4249 | // manually evaluate league |
| 4250 | Players.RemoveLocal(fDisonnected: true, fNoCalls: true); |
| 4251 | Players.RemoveAtRemoteClient(fDisonnected: true, fNoCalls: true); |
| 4252 | // normal quit |
| 4253 | Application.QuitGame(); |
| 4254 | } |
| 4255 | |
| 4256 | bool C4Game::DrawTextSpecImage(C4FacetExSurface &fctTarget, const char *szSpec, uint32_t dwClr) |
| 4257 | { |
| 4258 | // safety |
| 4259 | if (!szSpec) return false; |
| 4260 | // regular ID? -> Draw def |
| 4261 | if (LooksLikeID(str: szSpec)) |
| 4262 | { |
| 4263 | C4Def *pDef = C4Id2Def(id: C4Id(str: szSpec)); |
| 4264 | if (!pDef) return false; |
| 4265 | pDef->Picture2Facet(cgo&: fctTarget, color: dwClr); |
| 4266 | return true; |
| 4267 | } |
| 4268 | // C4ID:Index? |
| 4269 | if (SLen(sptr: szSpec) > 5 && szSpec[4] == ':') |
| 4270 | { |
| 4271 | char idbuf[5]; SCopy(szSource: szSpec, sTarget: idbuf, iMaxL: 4); |
| 4272 | if (LooksLikeID(str: idbuf)) |
| 4273 | { |
| 4274 | int iIndex = -1; |
| 4275 | if (sscanf(s: szSpec + 5, format: "%d" , &iIndex) == 1) if (iIndex >= 0) |
| 4276 | { |
| 4277 | C4Def *pDef = C4Id2Def(id: C4Id(str: idbuf)); |
| 4278 | if (!pDef) return false; |
| 4279 | pDef->Picture2Facet(cgo&: fctTarget, color: dwClr, xPhase: iIndex); |
| 4280 | return true; |
| 4281 | } |
| 4282 | } |
| 4283 | } |
| 4284 | // portrait spec? |
| 4285 | if (SEqual2(szStr1: szSpec, szStr2: "Portrait:" )) |
| 4286 | { |
| 4287 | szSpec += 9; |
| 4288 | C4ID idPortrait; |
| 4289 | const char *szPortraitName = C4Portrait::EvaluatePortraitString(szPortrait: szSpec, rIDOut&: idPortrait, idDefaultID: C4ID_None, pdwClrOut: &dwClr); |
| 4290 | if (idPortrait == C4ID_None) return false; |
| 4291 | C4Def *pPortraitDef = Defs.ID2Def(id: idPortrait); |
| 4292 | if (!pPortraitDef || !pPortraitDef->Portraits) return false; |
| 4293 | C4DefGraphics *pDefPortraitGfx = pPortraitDef->Portraits->Get(szGrpName: szPortraitName); |
| 4294 | if (!pDefPortraitGfx) return false; |
| 4295 | C4PortraitGraphics *pPortraitGfx = pDefPortraitGfx->IsPortrait(); |
| 4296 | if (!pPortraitGfx) return false; |
| 4297 | C4Surface *sfcPortrait = pPortraitGfx->GetBitmap(dwClr); |
| 4298 | if (!sfcPortrait) return false; |
| 4299 | fctTarget.Set(nsfc: sfcPortrait, nx: 0, ny: 0, nwdt: sfcPortrait->Wdt, nhgt: sfcPortrait->Hgt); |
| 4300 | return true; |
| 4301 | } |
| 4302 | else if (SEqual2(szStr1: szSpec, szStr2: "Ico:Locked" )) |
| 4303 | { |
| 4304 | static_cast<C4Facet &>(fctTarget) = C4GUI::Icon::GetIconFacet(icoIconIndex: C4GUI::Ico_Ex_LockedFrontal); |
| 4305 | return true; |
| 4306 | } |
| 4307 | else if (SEqual2(szStr1: szSpec, szStr2: "Ico:League" )) |
| 4308 | { |
| 4309 | static_cast<C4Facet &>(fctTarget) = C4GUI::Icon::GetIconFacet(icoIconIndex: C4GUI::Ico_Ex_League); |
| 4310 | return true; |
| 4311 | } |
| 4312 | else if (SEqual2(szStr1: szSpec, szStr2: "Ico:GameRunning" )) |
| 4313 | { |
| 4314 | static_cast<C4Facet &>(fctTarget) = C4GUI::Icon::GetIconFacet(icoIconIndex: C4GUI::Ico_GameRunning); |
| 4315 | return true; |
| 4316 | } |
| 4317 | else if (SEqual2(szStr1: szSpec, szStr2: "Ico:Lobby" )) |
| 4318 | { |
| 4319 | static_cast<C4Facet &>(fctTarget) = C4GUI::Icon::GetIconFacet(icoIconIndex: C4GUI::Ico_Lobby); |
| 4320 | return true; |
| 4321 | } |
| 4322 | else if (SEqual2(szStr1: szSpec, szStr2: "Ico:RuntimeJoin" )) |
| 4323 | { |
| 4324 | static_cast<C4Facet &>(fctTarget) = C4GUI::Icon::GetIconFacet(icoIconIndex: C4GUI::Ico_RuntimeJoin); |
| 4325 | return true; |
| 4326 | } |
| 4327 | else if (SEqual2(szStr1: szSpec, szStr2: "Ico:FairCrew" )) |
| 4328 | { |
| 4329 | static_cast<C4Facet &>(fctTarget) = C4GUI::Icon::GetIconFacet(icoIconIndex: C4GUI::Ico_Ex_FairCrew); |
| 4330 | return true; |
| 4331 | } |
| 4332 | else if (SEqual2(szStr1: szSpec, szStr2: "Ico:Settlement" )) |
| 4333 | { |
| 4334 | static_cast<C4Facet &>(fctTarget) = GraphicsResource.fctScore; |
| 4335 | return true; |
| 4336 | } |
| 4337 | // unknown spec |
| 4338 | return false; |
| 4339 | } |
| 4340 | |
| 4341 | bool C4Game::SpeedUp() |
| 4342 | { |
| 4343 | // As these functions work stepwise, there's the old maximum speed of 50. |
| 4344 | // Use /fast to set to even higher speeds. |
| 4345 | FrameSkip = BoundBy<int32_t>(bval: FrameSkip + 1, lbound: 1, rbound: 50); |
| 4346 | FullSpeed = true; |
| 4347 | GraphicsSystem.FlashMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_SPEED, args&: FrameSkip).c_str()); |
| 4348 | return true; |
| 4349 | } |
| 4350 | |
| 4351 | bool C4Game::SlowDown() |
| 4352 | { |
| 4353 | FrameSkip = BoundBy<int32_t>(bval: FrameSkip - 1, lbound: 1, rbound: 50); |
| 4354 | if (FrameSkip == 1) |
| 4355 | FullSpeed = false; |
| 4356 | GraphicsSystem.FlashMessage(szMessage: LoadResStr(id: C4ResStrTableKey::IDS_MSG_SPEED, args&: FrameSkip).c_str()); |
| 4357 | return true; |
| 4358 | } |
| 4359 | |
| 4360 | void C4Game::SetMusicLevel(int32_t iToLvl) |
| 4361 | { |
| 4362 | // change game music volume; multiplied by config volume for real volume |
| 4363 | iMusicLevel = BoundBy<int32_t>(bval: iToLvl, lbound: 0, rbound: 100); |
| 4364 | Application.MusicSystem->UpdateVolume(); |
| 4365 | } |
| 4366 | |
| 4367 | bool C4Game::ToggleMusic() |
| 4368 | { |
| 4369 | Application.MusicSystem->ToggleOnOff(changeConfig: !IsRunning); |
| 4370 | return true; |
| 4371 | } |
| 4372 | |
| 4373 | bool C4Game::ToggleSound() |
| 4374 | { |
| 4375 | Application.SoundSystem->ToggleOnOff(); |
| 4376 | return true; |
| 4377 | } |
| 4378 | |
| 4379 | void C4Game::AddDirectoryForMonitoring(const char *const directory) |
| 4380 | { |
| 4381 | if (FileMonitor) |
| 4382 | { |
| 4383 | FileMonitor->AddDirectory(path: directory); |
| 4384 | } |
| 4385 | } |
| 4386 | |
| 4387 | bool C4Game::ToggleChat() |
| 4388 | { |
| 4389 | return C4ChatDlg::ToggleChat(); |
| 4390 | } |
| 4391 | |