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
62constexpr unsigned int defaultIngameGameTickDelay = 28;
63
64C4Game::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
74C4Game::~C4Game()
75{
76 // make sure no startup gfx remain loaded
77 C4Startup::Unload();
78}
79
80bool 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
123bool 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
281void 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
302bool 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
341void C4Game::InitLogger()
342{
343 Control.InitLogger();
344}
345
346bool 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
542void 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
652bool 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
744int32_t iLastControlSize = 0;
745extern int32_t iPacketDelay;
746
747C4ST_NEW(ControlRcvStat, "C4Game::Execute ReceiveControl")
748C4ST_NEW(ControlStat, "C4Game::Execute ExecuteControl")
749C4ST_NEW(ExecObjectsStat, "C4Game::Execute ExecObjects")
750C4ST_NEW(GEStats, "C4Game::Execute pGlobalEffects->Execute")
751C4ST_NEW(PXSStat, "C4Game::Execute PXS.Execute")
752C4ST_NEW(PartStat, "C4Game::Execute Particles.Execute")
753C4ST_NEW(MassMoverStat, "C4Game::Execute MassMover.Execute")
754C4ST_NEW(WeatherStat, "C4Game::Execute Weather.Execute")
755C4ST_NEW(PlayersStat, "C4Game::Execute Players.Execute")
756C4ST_NEW(LandscapeStat, "C4Game::Execute Landscape.Execute")
757C4ST_NEW(MusicSystemStat, "C4Game::Execute MusicSystem.Execute")
758C4ST_NEW(MessagesStat, "C4Game::Execute Messages.Execute")
759C4ST_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
772bool 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
850void 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
876bool 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
979void 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
993void 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
1009bool 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
1017bool 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
1043bool 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
1069bool 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
1077C4Object *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
1124void 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
1135C4Object *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
1150C4Object *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
1166C4Object *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
1218void 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
1275void 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
1292C4Object *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
1309C4Object *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
1401C4Object *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
1476int32_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
1543void 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
1557void 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
1601bool C4Game::CreateViewport(int32_t iPlayer, bool fSilent)
1602{
1603 return GraphicsSystem.CreateViewport(iPlayer, fSilent);
1604}
1605
1606C4ID 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
1616bool 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
1637bool 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
1653bool 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
1702void 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
1716void 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
1730void C4Game::Sec1Timer()
1731{
1732 // updates the game clock
1733 if (TimeGo) { Time++; TimeGo = false; }
1734 FPS = cFPS; cFPS = 0;
1735}
1736
1737void 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
1807void 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
1834void 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
1874void 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
1899bool 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
1914void 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
1970void SetClientPrefix(char *szFilename, const char *szClient);
1971
1972bool 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
1979bool 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
2015bool C4Game::CanPreload() const
2016{
2017 return PreloadStatus <= PreloadLevel::Scenario && !PreloadThread.joinable();
2018}
2019
2020bool 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
2030bool 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
2077bool 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
2116bool 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
2187bool 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
2207bool 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
2263bool 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
2269void 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
2285bool 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
2332bool 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
2359bool 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
2526bool 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
2617bool 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
2699bool 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
2739bool 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
2771void 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
2780bool 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
2876bool 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
2928int32_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
2942bool 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
2955C4Object *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
3021C4Object *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
3056void 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
3071void 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
3086void 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
3100void 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
3277bool 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
3311void 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
3340bool 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
3463bool 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
3505C4Player *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
3529void C4Game::FixRandom(int32_t iSeed)
3530{
3531 FixedRandom(dwSeed: iSeed);
3532 Randomize3();
3533}
3534
3535bool 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
3554bool 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
3570void 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
3600bool 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
3634bool 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
3649void 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
3676void C4Game::SyncClearance()
3677{
3678 PXS.SyncClearance();
3679 Objects.SyncClearance();
3680}
3681
3682void 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
3707C4Object *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
3722C4Object *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
3739C4Object *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
3765bool 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
3823bool 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
3847bool 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
3891bool 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
3936std::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
3972void 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
3981void 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
3991void 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
4003void 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
4013void 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
4024void 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
4044void 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
4059bool 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
4216bool 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
4226bool C4Game::ToggleChart()
4227{
4228 C4ChartDialog::Toggle();
4229 return true;
4230}
4231
4232void 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
4256bool 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
4341bool 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
4351bool 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
4360void 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
4367bool C4Game::ToggleMusic()
4368{
4369 Application.MusicSystem->ToggleOnOff(changeConfig: !IsRunning);
4370 return true;
4371}
4372
4373bool C4Game::ToggleSound()
4374{
4375 Application.SoundSystem->ToggleOnOff();
4376 return true;
4377}
4378
4379void C4Game::AddDirectoryForMonitoring(const char *const directory)
4380{
4381 if (FileMonitor)
4382 {
4383 FileMonitor->AddDirectory(path: directory);
4384 }
4385}
4386
4387bool C4Game::ToggleChat()
4388{
4389 return C4ChatDlg::ToggleChat();
4390}
4391