1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16
17/* Main class to initialize configuration and execute the game */
18
19#include <C4Include.h>
20#include <C4Application.h>
21#include <C4Version.h>
22#ifdef _WIN32
23#include <StdRegistry.h>
24#include <C4UpdateDlg.h>
25#endif
26
27#include <C4FileClasses.h>
28#include <C4FullScreen.h>
29#include <C4Language.h>
30#include <C4Console.h>
31#include <C4Startup.h>
32#include <C4Log.h>
33#include <C4GamePadCon.h>
34#include <C4GameLobby.h>
35#include "C4Toast.h"
36
37#ifdef _WIN32
38#include "StdRegistry.h" // For DDraw emulation warning
39#endif
40
41#include <cassert>
42#include <stdexcept>
43
44constexpr unsigned int defaultGameTickDelay = 16;
45
46C4Sec1TimerCallbackBase::C4Sec1TimerCallbackBase() : pNext(nullptr), iRefs(2)
47{
48 // register into engine callback stack
49 Application.AddSec1Timer(callback: this);
50}
51
52C4Application::C4Application() :
53 isFullScreen(true), UseStartupDialog(true), launchEditor(false), restartAtEnd(false),
54 DDraw(nullptr), AppState(C4AS_None),
55 iLastGameTick(0), iGameTickDelay(defaultGameTickDelay), iExtraGameTickDelay(0), pGamePadControl(nullptr),
56 CheckForUpdates(false) {}
57
58C4Application::~C4Application()
59{
60 // clear gamepad
61 delete pGamePadControl;
62 // flush loggers
63 spdlog::apply_all(fun: [](const auto &logger) { logger->flush(); });
64 // Launch editor
65 if (launchEditor)
66 {
67#ifdef _WIN32
68 char strCommandLine[_MAX_PATH + 1]; SCopy(Config.AtExePath(C4CFN_Editor), strCommandLine);
69 STARTUPINFOA StartupInfo{};
70 StartupInfo.cb = sizeof(StartupInfo);
71 PROCESS_INFORMATION ProcessInfo{};
72 CreateProcessA(nullptr, strCommandLine, nullptr, nullptr, TRUE, 0, nullptr, nullptr, &StartupInfo, &ProcessInfo);
73#endif
74 }
75}
76
77void C4Application::DoInit()
78{
79 assert(AppState == C4AS_None);
80 // Config overwrite by parameter
81 StdStrBuf sConfigFilename;
82 bool verbose{false};
83 char szParameter[_MAX_PATH + 1];
84 for (int32_t iPar = 0; SGetParameter(strCommandLine: GetCommandLine(), iParameter: iPar, strTarget: szParameter, _MAX_PATH); iPar++)
85 {
86 if (SEqual2NoCase(szStr1: szParameter, szStr2: "/config:"))
87 {
88 sConfigFilename.Copy(pnData: szParameter + 8);
89 }
90 else if (!verbose && SEqualNoCase(szStr1: szParameter, szStr2: "/verbose"))
91 {
92 verbose = true;
93 }
94 }
95 // Config check
96 Config.Init();
97 Config.Load(forceWorkingDirectory: true, szConfigFile: sConfigFilename.getData());
98 Config.Save();
99 // sometimes, the configuration can become corrupted due to loading errors or w/e
100 // check this and reset defaults if necessary
101 if (Config.IsCorrupted())
102 {
103 if (sConfigFilename)
104 {
105 // custom config corrupted: Fail
106 throw StartupException{"Warning: Custom configuration corrupted - program abort!"};
107 }
108 else
109 {
110 // default config corrupted: Restore default
111 spdlog::warn(msg: "Configuration corrupted - restoring default!");
112 Config.Default();
113 Config.Save();
114 Config.Load();
115 }
116 }
117
118 // Init C4Group
119 C4Group_SetMaker(szMaker: Config.General.Name);
120 C4Group_SetProcessCallback(fnCallback: &ProcessCallback);
121 C4Group_SetTempPath(szPath: Config.General.TempPath);
122 C4Group_SetSortList(ppSortList: C4CFN_FLS);
123
124 // Open log
125 LogSystem.OpenLog(verbose);
126
127 // init system group
128 if (!SystemGroup.Open(C4CFN_System))
129 {
130 // Error opening system group - no LogFatal, because it needs language table.
131 // This will *not* use the FatalErrors stack, but this will cause the game
132 // to instantly halt, anyway.
133 throw StartupException{"Error opening system group file (System.c4g)!"};
134 }
135
136 // Language override by parameter
137 const char *pLanguage;
138 if (pLanguage = SSearchNoCase(szString: GetCommandLine(), szIndex: "/Language:"))
139 SCopyUntil(szSource: pLanguage, sTarget: Config.General.LanguageEx, cUntil: ' ', iMaxL: CFG_MaxString);
140
141 // Init external language packs
142 Languages.Init();
143 // Load language string table
144 if (!Languages.LoadLanguage(strLanguages: Config.General.LanguageEx))
145 // No language table was loaded - bad luck...
146 if (!ResStrTable)
147 spdlog::warn(msg: "No language string table loaded!");
148
149 // Init game loggers
150 Game.InitLogger();
151
152 // Parse command line
153 Game.ParseCommandLine(szCmdLine: GetCommandLine());
154
155#ifdef _WIN32
156 C4ThreadPool::Global = std::make_shared<C4ThreadPool>();
157#else
158 C4ThreadPool::Global = std::make_shared<C4ThreadPool>(args&: Config.General.ThreadPoolThreadCount, args&: Config.General.ThreadPoolThreadCount);
159#endif
160
161 // Initialize curl
162 CurlSystem.emplace();
163
164#ifdef _WIN32
165 // Windows: handle incoming updates directly, even before starting up the gui
166 // because updates will be applied in the console anyway.
167 if (Application.IncomingUpdate)
168 if (C4UpdateDlg::ApplyUpdate(Application.IncomingUpdate.getData(), false, nullptr))
169 return;
170#endif
171
172 // activate
173 Active = true;
174
175 // Init carrier window
176 if (isFullScreen)
177 {
178 if (!FullScreen.Init(app: this))
179 {
180 Clear(); return;
181 }
182 pWindow = &FullScreen;
183 pWindow->SetSize(cx: static_cast<int32_t>(Config.Graphics.ResX * GetScale()), cy: static_cast<int32_t>(Config.Graphics.ResY * GetScale()));
184 SetDisplayMode(Config.Graphics.UseDisplayMode);
185
186#ifdef _WIN32
187 if (Config.Graphics.UseDisplayMode == DisplayMode::Window)
188 {
189 if (Config.Graphics.Maximized) pWindow->Maximize();
190 else pWindow->SetPosition(Config.Graphics.PositionX, Config.Graphics.PositionY);
191 }
192#endif
193 }
194 else
195 {
196 if (!Console.Init(app: this))
197 {
198 Clear(); return;
199 }
200
201 pWindow = &Console;
202 }
203
204 // init timers (needs window)
205 if (!InitTimer())
206 {
207 LogFatal(id: C4ResStrTableKey::IDS_ERR_TIMER);
208 Clear(); throw StartupException{LogSystem.GetFatalErrorString()};
209 }
210
211 // Engine header message
212 spdlog::info(C4ENGINEINFOLONG);
213 spdlog::info(msg: "Version: " C4VERSION " " C4_OS);
214
215 // Initialize OpenGL
216 DDraw = DDrawInit(pApp: this, logSystem&: LogSystem, Engine: Config.Graphics.Engine);
217 if (!DDraw) { LogFatal(id: C4ResStrTableKey::IDS_ERR_DDRAW); Clear(); throw StartupException{LogSystem.GetFatalErrorString()}; }
218
219#if defined(_WIN32) && !defined(USE_CONSOLE)
220 // Register clonk file classes - notice: this will only work if we have administrator rights
221 char szModule[_MAX_PATH + 1]; GetModuleFileNameA(nullptr, szModule, _MAX_PATH);
222 SetC4FileClasses(szModule);
223#endif
224
225 // Initialize gamepad
226 if (!pGamePadControl && Config.General.GamepadEnabled)
227 pGamePadControl = new C4GamePadControl();
228
229 AppState = C4AS_PreInit;
230}
231
232bool C4Application::PreInit()
233{
234 SetGameTickDelay(defaultGameTickDelay);
235
236 if (!Game.PreInit()) return false;
237
238 // startup dialog: Only use if no next mission has been provided
239 bool fDoUseStartupDialog = UseStartupDialog && !*Game.ScenarioFilename;
240
241 // init loader: default spec
242 if (fDoUseStartupDialog)
243 {
244 if (!Game.GraphicsSystem.InitLoaderScreen(C4CFN_StartupBackgroundMain))
245 {
246 LogFatal(id: C4ResStrTableKey::IDS_PRC_ERRLOADER); return false;
247 }
248 }
249
250 Game.SetInitProgress(fDoUseStartupDialog ? 10.0f : 1.0f);
251
252#ifdef ENABLE_SOUND
253 try
254 {
255 if (!AudioSystem)
256 {
257 AudioSystem.reset(p: C4AudioSystem::NewInstance(
258 maxChannels: Config.Sound.MaxChannels,
259 preferLinearResampling: Config.Sound.PreferLinearResampling
260 ));
261 }
262 }
263 catch (const std::runtime_error &e)
264 {
265 spdlog::error(msg: e.what());
266 Log(id: C4ResStrTableKey::IDS_PRC_NOAUDIO);
267 }
268#endif
269
270 // Music
271 MusicSystem.emplace();
272
273 Game.SetInitProgress(fDoUseStartupDialog ? 20.0f : 2.0f);
274
275 // Sound
276 SoundSystem.emplace();
277
278 // Toasts
279 try
280 {
281 if (!ToastSystem)
282 {
283 ToastSystem = C4ToastSystem::NewInstance();
284 }
285 }
286 catch (const std::runtime_error &e)
287 {
288 spdlog::warn(fmt: "Failed to initialize toast system: {}", args: e.what());
289 }
290
291 Game.SetInitProgress(fDoUseStartupDialog ? 30.0f : 3.0f);
292
293 AppState = fDoUseStartupDialog ? C4AS_Startup : C4AS_StartGame;
294
295 return true;
296}
297
298bool C4Application::ProcessCallback(const char *szMessage, int iProcess)
299{
300 Console.Out(text: szMessage);
301 return true;
302}
303
304void C4Application::Clear()
305{
306 Game.Clear();
307 NextMission.Clear();
308 // close system group (System.c4g)
309 SystemGroup.Close();
310 // Close timers
311 sec1TimerCallbacks.clear();
312 // Log
313 if (ResStrTable) // Avoid (double and undefined) message on (second?) shutdown...
314 Log(id: C4ResStrTableKey::IDS_PRC_DEINIT);
315 // Clear external language packs and string table
316 Languages.Clear();
317 Languages.ClearLanguage();
318 // gamepad clear
319 delete pGamePadControl; pGamePadControl = nullptr;
320 // music system clear
321 SoundSystem.reset();
322 MusicSystem.reset();
323 AudioSystem.reset();
324 ToastSystem.reset();
325 // Clear direct draw (late, because it's needed for e.g. Log)
326 delete DDraw; DDraw = nullptr;
327 // Close window
328 FullScreen.Clear();
329 Console.Clear();
330 // The very final stuff
331 CStdApp::Clear();
332}
333
334bool C4Application::OpenGame()
335{
336 if (isFullScreen)
337 {
338 // Open game
339 return Game.Init();
340 }
341 else
342 {
343 // Execute command line
344 if (Game.ScenarioFilename[0] || Game.DirectJoinAddress[0])
345 return Console.OpenGame(szCmdLine);
346 }
347 // done; success
348 return true;
349}
350
351void C4Application::Quit()
352{
353 // Clear definitions passed by frontend for this round
354 Config.General.Definitions[0] = 0;
355#ifdef _WIN32
356 if (pWindow)
357 {
358 // store if window is maximized and where it is positioned
359 WINDOWPLACEMENT placement;
360 GetWindowPlacement(pWindow->hWindow, &placement);
361 Config.Graphics.Maximized = placement.showCmd == SW_SHOWMAXIMIZED;
362 Config.Graphics.PositionX = placement.rcNormalPosition.left;
363 Config.Graphics.PositionY = placement.rcNormalPosition.top;
364 }
365#endif
366 // Save config if there was no loading error
367 if (Config.fConfigLoaded) Config.Save();
368 // quit app
369 CStdApp::Quit();
370 AppState = C4AS_Quit;
371}
372
373void C4Application::QuitGame()
374{
375 // reinit desired? Do restart
376 if (UseStartupDialog || NextMission)
377 {
378 // backup last start params
379 bool fWasNetworkActive = Game.NetworkActive;
380 StdStrBuf password;
381 if (fWasNetworkActive) password.Copy(pnData: Game.Network.GetPassword());
382 // the rest isn't changed by Clear()
383 decltype(Game.DefinitionFilenames) defs{Game.DefinitionFilenames};
384 // stop game
385 Game.Clear();
386 Game.Default();
387 AppState = C4AS_PreInit;
388 // if a next mission is desired, set to start it
389 if (NextMission)
390 {
391 SCopy(szSource: NextMission.getData(), sTarget: Game.ScenarioFilename, _MAX_PATH);
392 SReplaceChar(str: Game.ScenarioFilename, fc: '\\', DirSep[0]); // linux/mac: make sure we are using forward slashes
393 Game.fLobby = Game.NetworkActive = fWasNetworkActive;
394 if (fWasNetworkActive) Game.Network.SetPassword(password.getData());
395 Game.DefinitionFilenames = defs;
396 Game.FixedDefinitions = true;
397 Game.fObserve = false;
398 NextMission.Clear();
399 }
400 }
401 else
402 {
403 Quit();
404 }
405}
406
407void C4Application::Execute()
408{
409 CStdApp::Execute();
410 // Recursive execution check
411 static int32_t iRecursionCount = 0;
412 ++iRecursionCount;
413 // Exec depending on game state
414 switch (AppState)
415 {
416 case C4AS_Quit:
417 // Do nothing, HandleMessage will return HR_Failure soon
418 break;
419 case C4AS_PreInit:
420 if (!PreInit()) Quit();
421 break;
422 case C4AS_Startup:
423 if (pWindow)
424 {
425 pWindow->SetProgress(100);
426 }
427
428#ifdef USE_CONSOLE
429 // Console engines just stay in this state until aborted or new commands arrive on stdin
430#else
431 AppState = C4AS_Game;
432 // if no scenario or direct join has been specified, get game startup parameters by startup dialog
433 Game.Parameters.ScenarioTitle.CopyValidated(szFromVal: LoadResStr(id: C4ResStrTableKey::IDS_PRC_INITIALIZE));
434 if (!C4Startup::Execute()) { Quit(); --iRecursionCount; return; }
435 AppState = C4AS_StartGame;
436#endif
437 break;
438 case C4AS_StartGame:
439 // immediate progress to next state; OpenGame will enter HandleMessage-loops in startup and lobby!
440 AppState = C4AS_Game;
441 // first-time game initialization
442 if (!OpenGame())
443 {
444 // set error flag (unless this was a lobby user abort)
445 if (!C4GameLobby::UserAbort)
446 Game.fQuitWithError = true;
447 // no start: Regular QuitGame; this may reset the engine to startup mode if desired
448 QuitGame();
449 }
450 break;
451 case C4AS_Game:
452 {
453 uint32_t iThisGameTick = timeGetTime();
454 // Game (do additional timing check)
455 if (Game.IsRunning && iRecursionCount <= 1) if (Game.GameGo || !iExtraGameTickDelay || (iThisGameTick > iLastGameTick + iExtraGameTickDelay))
456 {
457 // Execute
458 Game.Execute();
459 // Save back time
460 iLastGameTick = iThisGameTick;
461 }
462 // Graphics
463 if (!Game.DoSkipFrame)
464 {
465 uint32_t iPreGfxTime = timeGetTime();
466 // Fullscreen mode
467 if (isFullScreen)
468 FullScreen.Execute();
469 // Console mode
470 else
471 Console.Execute();
472 // Automatic frame skip if graphics are slowing down the game (skip max. every 2nd frame)
473 Game.DoSkipFrame = Game.Parameters.AutoFrameSkip && ((iPreGfxTime + iGameTickDelay) < timeGetTime());
474 }
475 else
476 Game.DoSkipFrame = false;
477 // Sound
478 SoundSystem->Execute();
479 // Gamepad
480 if (pGamePadControl) pGamePadControl->Execute();
481 break;
482 }
483 case C4AS_None:
484 assert(!"Unhandled switch case");
485 }
486
487 --iRecursionCount;
488}
489
490void C4Application::OnNetworkEvents()
491{
492 InteractiveThread.ProcessEvents();
493}
494
495void C4Application::DoSec1Timers()
496{
497 for (auto it = sec1TimerCallbacks.begin(); it != sec1TimerCallbacks.end(); )
498 {
499 if ((*it)->IsReleased())
500 {
501 it = sec1TimerCallbacks.erase(position: it);
502 }
503 else
504 {
505 (*it++)->OnSec1Timer();
506 }
507 }
508}
509
510void C4Application::SetGameTickDelay(int iDelay)
511{
512 // Remember delay
513 iGameTickDelay = iDelay;
514 // Smaller than minimum refresh delay?
515 if (iDelay < Config.Graphics.MaxRefreshDelay)
516 {
517 // Set critical timer
518 ResetTimer(uDelay: iDelay);
519 // No additional breaking needed
520 iExtraGameTickDelay = 0;
521 }
522 else
523 {
524 // Do some magic to get as near as possible to the requested delay
525 int iGraphDelay = (std::max)(a: 1, b: iDelay);
526 iGraphDelay /= (iGraphDelay + Config.Graphics.MaxRefreshDelay - 1) / Config.Graphics.MaxRefreshDelay;
527 // Set critical timer
528 ResetTimer(uDelay: iGraphDelay);
529 // Slow down game tick
530 iExtraGameTickDelay = iDelay - iGraphDelay / 2;
531 }
532}
533
534bool C4Application::SetResolution(int32_t iNewResX, int32_t iNewResY)
535{
536 const auto scale = GetScale();
537 iNewResX = static_cast<int32_t>(ceilf(x: iNewResX / scale));
538 iNewResY = static_cast<int32_t>(ceilf(x: iNewResY / scale));
539
540 if (iNewResX != Config.Graphics.ResX || iNewResY != Config.Graphics.ResY)
541 {
542 Config.Graphics.ResX = iNewResX;
543 Config.Graphics.ResY = iNewResY;
544
545 // ask graphics system to change it
546 if (lpDDraw) lpDDraw->OnResolutionChanged();
547 // notify game
548 Game.OnResolutionChanged();
549 }
550 return true;
551}
552
553bool C4Application::SetGameFont(const char *szFontFace, int32_t iFontSize)
554{
555#ifndef USE_CONSOLE
556 // safety
557 if (!szFontFace || !*szFontFace || iFontSize < 1 || SLen(sptr: szFontFace) >= static_cast<int>(sizeof(Config.General.RXFontName))) return false;
558 // first, check if the selected font can be created at all
559 // check regular font only - there's no reason why the other fonts couldn't be created
560 CStdFont TestFont;
561 if (!Game.FontLoader.InitFont(rFont&: TestFont, szFontName: szFontFace, eType: C4FontLoader::C4FT_Main, iSize: iFontSize, pGfxGroups: &Game.GraphicsResource.Files))
562 return false;
563 // OK; reinit all fonts
564 StdStrBuf sOldFont; sOldFont.Copy(pnData: Config.General.RXFontName);
565 int32_t iOldFontSize = Config.General.RXFontSize;
566 SCopy(szSource: szFontFace, sTarget: Config.General.RXFontName);
567 Config.General.RXFontSize = iFontSize;
568 if (!Game.GraphicsResource.InitFonts() || (C4Startup::Get() && !C4Startup::Get()->Graphics.InitFonts()))
569 {
570 // failed :o
571 // shouldn't happen. Better restore config.
572 SCopy(szSource: sOldFont.getData(), sTarget: Config.General.RXFontName);
573 Config.General.RXFontSize = iOldFontSize;
574 return false;
575 }
576#endif
577 // save changes
578 return true;
579}
580
581void C4Application::AddSec1Timer(C4Sec1TimerCallbackBase *callback)
582{
583 sec1TimerCallbacks.emplace_back(args&: callback);
584}
585
586void C4Application::OnCommand(const char *szCmd)
587{
588 // Find parameters
589 const char *szPar = strchr(s: szCmd, c: ' ');
590 while (szPar && *szPar == ' ') szPar++;
591
592 if (SEqual(szStr1: szCmd, szStr2: "/quit"))
593 {
594 Quit();
595 return;
596 }
597
598 switch (AppState)
599 {
600 case C4AS_Startup:
601 // Open a new game
602 if (SEqual2(szStr1: szCmd, szStr2: "/open "))
603 {
604 // Try to start the game with given parameters
605 AppState = C4AS_Game;
606 Game.ParseCommandLine(szCmdLine: szPar);
607 UseStartupDialog = true;
608 if (!Game.Init())
609 AppState = C4AS_Startup;
610 }
611
612 break;
613
614 case C4AS_Game:
615 // Clear running game
616 if (SEqual(szStr1: szCmd, szStr2: "/close"))
617 {
618 Game.Clear();
619 Game.Default();
620 AppState = C4AS_PreInit;
621 UseStartupDialog = true;
622 return;
623 }
624
625 // Lobby commands
626 if (Game.Network.isLobbyActive())
627 {
628 if (SEqual2(szStr1: szCmd, szStr2: "/start"))
629 {
630 // timeout given?
631 int32_t iTimeout = Config.Lobby.CountdownTime;
632 if (!Game.Network.isHost())
633 Log(id: C4ResStrTableKey::IDS_MSG_CMD_HOSTONLY);
634 else if (szPar && (!sscanf(s: szPar, format: "%d", &iTimeout) || iTimeout < 0))
635 Log(id: C4ResStrTableKey::IDS_MSG_CMD_START_USAGE);
636 else
637 // start new countdown (aborts previous if necessary)
638 Game.Network.StartLobbyCountdown(iCountdownTime: iTimeout);
639 break;
640 }
641 }
642
643 // Normal commands
644 Game.MessageInput.ProcessInput(szText: szCmd);
645 break;
646
647 case C4AS_None: case C4AS_PreInit: case C4AS_StartGame: case C4AS_Quit:
648 assert(!"Unhandled switch case");
649 }
650}
651
652void C4Application::SetNextMission(const char *szMissionFilename)
653{
654 // set next mission if any is desired
655 if (szMissionFilename)
656 NextMission.Copy(pnData: szMissionFilename);
657 else
658 NextMission.Clear();
659}
660
661#ifdef WITH_GLIB
662std::shared_ptr<spdlog::logger> C4Application::CreateGLibLogger()
663{
664 return LogSystem.CreateLogger(config&: Config.Logging.GLib);
665}
666#endif
667