1 /*
2 * OpenClonk, http://www.openclonk.org
3 *
4 * Copyright (c) 1998-2000, Matthes Bender
5 * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
6 * Copyright (c) 2009-2016, The OpenClonk 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 initialize configuration and execute the game */
19
20 #include "C4Include.h"
21 #include "C4ForbidLibraryCompilation.h"
22 #include "game/C4Application.h"
23
24 #include "C4Version.h"
25 #include "editor/C4Console.h"
26 #include "game/C4FullScreen.h"
27 #include "game/C4GraphicsSystem.h"
28 #include "graphics/C4Draw.h"
29 #include "graphics/C4GraphicsResource.h"
30 #include "graphics/StdPNG.h"
31 #include "gui/C4GameLobby.h"
32 #include "gui/C4GfxErrorDlg.h"
33 #include "gui/C4MessageInput.h"
34 #ifdef _WIN32
35 #include "gui/C4UpdateDlg.h"
36 #endif
37 #include "gui/C4Startup.h"
38 #include "landscape/C4Particles.h"
39 #include "network/C4Network2.h"
40 #include "network/C4Network2IRC.h"
41 #include "platform/C4GamePadCon.h"
42
43 #include <getopt.h>
44
45 static C4Network2IRCClient ApplicationIRCClient;
46 const std::string C4Application::Revision{ C4REVISION };
47
C4Application()48 C4Application::C4Application():
49 IRCClient(ApplicationIRCClient)
50 {
51 }
52
~C4Application()53 C4Application::~C4Application()
54 {
55 // clear gamepad
56 if (pGamePadControl) delete pGamePadControl;
57 // Close log
58 CloseLog();
59 }
60
DoInit(int argc,char * argv[])61 bool C4Application::DoInit(int argc, char * argv[])
62 {
63 assert(AppState == C4AS_None);
64 // Config overwrite by parameter
65 StdStrBuf sConfigFilename;
66 for (int32_t iPar=0; iPar < argc; iPar++)
67 if (SEqual2NoCase(argv[iPar], "--config="))
68 sConfigFilename.Copy(argv[iPar] + 9);
69 // Config check
70 Config.Init();
71 Config.Load(sConfigFilename.getData());
72 Config.Save();
73 // sometimes, the configuration can become corrupted due to loading errors or w/e
74 // check this and reset defaults if necessary
75 if (Config.IsCorrupted())
76 {
77 if (sConfigFilename)
78 {
79 // custom config corrupted: Fail
80 Log("ERROR: Custom configuration corrupted - program abort!\n");
81 return false;
82 }
83 else
84 {
85 // default config corrupted: Restore default
86 Log("Warning: Configuration corrupted - restoring default!\n");
87 Config.Default();
88 Config.Save();
89 Config.Load();
90 }
91 }
92 // Open log
93 OpenLog();
94
95 // Engine header message
96 Log(C4ENGINECAPTION);
97 LogF("Version: %s %s (%s - %s)", C4VERSION, C4_OS, GetRevision(), C4REVISION_TS);
98 LogF(R"(ExePath: "%s")", Config.General.ExePath.getData());
99 LogF(R"(SystemDataPath: "%s")", Config.General.SystemDataPath);
100 LogF(R"(UserDataPath: "%s")", Config.General.UserDataPath);
101
102 // Init C4Group
103 C4Group_SetProcessCallback(&ProcessCallback);
104 C4Group_SetTempPath(Config.General.TempPath.getData());
105 C4Group_SetSortList(C4CFN_FLS);
106
107 // Cleanup temp folders left behind
108 Config.CleanupTempUpdateFolder();
109
110 // Initialize game data paths
111 Reloc.Init();
112
113 // init system group
114 if (!Reloc.Open(SystemGroup, C4CFN_System))
115 {
116 // Error opening system group - no LogFatal, because it needs language table.
117 // This will *not* use the FatalErrors stack, but this will cause the game
118 // to instantly halt, anyway.
119 const char *szMessage = "Error opening system group file (System.ocg)!";
120 Log(szMessage);
121 // Fatal error, game cannot start - have player notice
122 MessageDialog(szMessage);
123 return false;
124 }
125 // Parse command line
126 ParseCommandLine(argc, argv);
127
128 // Open additional logs that depend on command line
129 OpenExtraLogs();
130
131 // Init external language packs
132 Languages.Init();
133 // Load language string table
134 if (!Languages.LoadLanguage(Config.General.LanguageEx))
135 // No language table was loaded - bad luck...
136 if (!Languages.HasStringTable())
137 Log("WARNING: No language string table loaded!");
138
139 #if defined(WIN32) && defined(WITH_AUTOMATIC_UPDATE)
140 // Windows: handle incoming updates directly, even before starting up the gui
141 // because updates will be applied in the console anyway.
142 if (!Application.IncomingUpdate.empty())
143 if (C4UpdateDlg::ApplyUpdate(Application.IncomingUpdate.c_str(), false, nullptr))
144 return true;
145 #endif
146
147 // Fixup resolution
148 if (!Config.Graphics.Windowed)
149 ApplyResolutionConstraints();
150
151 // activate
152 Active=true;
153
154 // Init carrier window
155 if (!isEditor)
156 {
157 if (!(pWindow = FullScreen.Init(this)))
158 { Clear(); ShowGfxErrorDialog(); return false; }
159 }
160 else
161 {
162 if (!(pWindow = Console.Init(this)))
163 { Clear(); return false; }
164 }
165
166 // init timers (needs window)
167 Add(pGameTimer = new C4ApplicationGameTimer());
168
169 // Initialize OpenGL
170 bool success = DDrawInit(this, GetConfigWidth(), GetConfigHeight(), Config.Graphics.Monitor);
171 if (!success) { LogFatal(LoadResStr("IDS_ERR_DDRAW")); Clear(); ShowGfxErrorDialog(); return false; }
172
173 if (!isEditor)
174 {
175 if (!SetVideoMode(Application.GetConfigWidth(), Application.GetConfigHeight(), Config.Graphics.RefreshRate, Config.Graphics.Monitor, !Config.Graphics.Windowed))
176 pWindow->SetSize(Config.Graphics.WindowX, Config.Graphics.WindowY);
177 }
178
179 // Initialize gamepad
180 if (!pGamePadControl && Config.General.GamepadEnabled)
181 pGamePadControl = new C4GamePadControl();
182
183 AppState = C4AS_PreInit;
184
185 return true;
186 }
187
ClearCommandLine()188 void C4Application::ClearCommandLine()
189 {
190 *Game.PlayerFilenames = 0;
191 }
192
ParseCommandLine(int argc,char * argv[])193 void C4Application::ParseCommandLine(int argc, char * argv[])
194 {
195 argv0 = argv[0];
196 StdStrBuf CmdLine("Command line:");
197 for(int i = 0; i < argc; ++i) {
198 CmdLine.Append(" ");
199 CmdLine.Append(argv[i]);
200 }
201 Log(CmdLine.getData());
202
203 ClearCommandLine();
204 Game.NetworkActive = false;
205 isEditor = 2;
206 int c;
207 while (true)
208 {
209
210 static struct option long_options[] =
211 {
212 // option, w/ argument?, set directly, set to...
213 {"editor", no_argument, &isEditor, 1},
214 {"fullscreen", no_argument, &isEditor, 0},
215 {"debugwait", no_argument, &Game.DebugWait, 1},
216 {"update", no_argument, &CheckForUpdates, 1},
217 {"noruntimejoin", no_argument, &Config.Network.NoRuntimeJoin, 1},
218 {"runtimejoin", no_argument, &Config.Network.NoRuntimeJoin, 0},
219 {"noleague", no_argument, &Config.Network.LeagueServerSignUp, 0},
220 {"league", no_argument, &Config.Network.LeagueServerSignUp, 1},
221 {"nosignup", no_argument, &Config.Network.MasterServerSignUp, 0},
222 {"signup", no_argument, &Config.Network.MasterServerSignUp, 1},
223
224 {"debugrecread", required_argument, nullptr, 'K'},
225 {"debugrecwrite", required_argument, nullptr, 'w'},
226
227 {"client", required_argument, nullptr, 'c'},
228 {"host", no_argument, nullptr, 'h'},
229 {"debughost", required_argument, nullptr, 'H'},
230 {"debugpass", required_argument, nullptr, 'P'},
231 {"debug", required_argument, nullptr, 'D'},
232 {"data", required_argument, nullptr, 'd'},
233 {"startup", required_argument, nullptr, 's'},
234 {"stream", required_argument, nullptr, 'e'},
235 {"recdump", required_argument, nullptr, 'R'},
236 {"comment", required_argument, nullptr, 'm'},
237 {"pass", required_argument, nullptr, 'p'},
238 {"udpport", required_argument, nullptr, 'u'},
239 {"tcpport", required_argument, nullptr, 't'},
240 {"join", required_argument, nullptr, 'j'},
241 {"language", required_argument, nullptr, 'L'},
242 {"scenpar", required_argument, nullptr, 'S'},
243
244 {"observe", no_argument, nullptr, 'o'},
245 {"nonetwork", no_argument, nullptr, 'N'},
246 {"network", no_argument, nullptr, 'n'},
247 {"record", no_argument, nullptr, 'r'},
248
249 {"lobby", optional_argument, nullptr, 'l'},
250
251 {"debug-opengl", no_argument, &Config.Graphics.DebugOpenGL, 1},
252 {"config", required_argument, nullptr, 0},
253 {nullptr, 0, nullptr, 0}
254 };
255 int option_index = 0;
256 c = getopt_long (argc, argv, "abc:d:f:",
257 long_options, &option_index);
258 // no more options
259 if (c == -1)
260 break;
261 switch (c)
262 {
263 case 0:
264 // Signup
265 if (SEqualNoCase(long_options[option_index].name, "signup"))
266 {
267 Game.NetworkActive = true;
268 }
269 // League
270 if (SEqualNoCase(long_options[option_index].name, "league"))
271 {
272 Game.NetworkActive = true;
273 Config.Network.MasterServerSignUp = true;
274 }
275 // Config: Already handled earlier.
276 break;
277 // Lobby
278 case 'l':
279 Game.fLobby = true;
280 // lobby timeout specified? (e.g. --lobby=120)
281 if (optarg)
282 {
283 Game.iLobbyTimeout = atoi(optarg);
284 if (Game.iLobbyTimeout < 0) Game.iLobbyTimeout = 0;
285 }
286 break;
287 case 'o': Game.fObserve = true; break;
288 // Direct join
289 case 'j':
290 Game.NetworkActive = true;
291 SCopy(optarg, Game.DirectJoinAddress, _MAX_PATH);
292 break;
293 case 'K':
294 if (optarg && optarg[0])
295 {
296 LogF("Reading from DebugRec file '%s'", optarg);
297 SCopy(optarg, Config.General.DebugRecExternalFile, _MAX_PATH);
298 }
299 else
300 Log("Reading DebugRec from CtrlRec file in scenario record");
301 Config.General.DebugRec = 1;
302 Config.General.DebugRecWrite = 0;
303 break;
304 case 'w':
305 if (optarg && optarg[0])
306 {
307 LogF("Writing to DebugRec file '%s'", optarg);
308 SCopy(optarg, Config.General.DebugRecExternalFile, _MAX_PATH);
309 }
310 else
311 Log("Writing DebugRec to CtrlRec file in scenario record");
312 Config.General.DebugRec = 1;
313 Config.General.DebugRecWrite = 1;
314 break;
315 case 'r': Game.Record = true; break;
316 case 'n': Game.NetworkActive = true; break;
317 case 'N': Game.NetworkActive = false; break;
318 // Language override by parameter
319 case 'L': SCopy(optarg, Config.General.LanguageEx, CFG_MaxString);
320 // port overrides
321 case 't': Config.Network.PortTCP = atoi(optarg); break;
322 case 'u': Config.Network.PortUDP = atoi(optarg); break;
323 // network game password
324 case 'p': Network.SetPassword(optarg); break;
325 // network game comment
326 case 'm': Config.Network.Comment.CopyValidated(optarg); break;
327 // record dump
328 case 'R': Game.RecordDumpFile.Copy(optarg); break;
329 // record stream
330 case 'e': Game.RecordStream.Copy(optarg); break;
331 // startup start screen
332 case 's': C4Startup::SetStartScreen(optarg); break;
333 // additional read-only data path
334 case 'd': Reloc.AddPath(optarg); break;
335 // debug options
336 case 'D': Game.DebugPort = atoi(optarg); break;
337 case 'P': Game.DebugPassword = optarg; break;
338 case 'H': Game.DebugHost = optarg; break;
339 // set custom scenario parameter by command line
340 case 'S':
341 {
342 StdStrBuf sopt, soptval; sopt.Copy(optarg);
343 int32_t val=1;
344 if (sopt.SplitAtChar('=', &soptval)) val=atoi(soptval.getData());
345 Game.StartupScenarioParameters.SetValue(sopt.getData(), val, false);
346 }
347 break;
348 // debug configs
349 case 'h':
350 Game.NetworkActive = true;
351 Game.fLobby = true;
352 Config.Network.PortTCP = 11112;
353 Config.Network.PortUDP = 11113;
354 Config.Network.MasterServerSignUp = Config.Network.LeagueServerSignUp = false;
355 break;
356 case 'c':
357 Game.NetworkActive = true;
358 SCopy("localhost", Game.DirectJoinAddress, _MAX_PATH);
359 Game.fLobby = true;
360 Config.Network.PortTCP = 11112 + 2*(atoi(optarg)+1);
361 Config.Network.PortUDP = 11113 + 2*(atoi(optarg)+1);
362 break;
363 case '?': /* getopt_long already printed an error message. */ break;
364 default: assert(!"unexpected getopt_long return value");
365 }
366 }
367 if (!Config.Network.MasterServerSignUp)
368 Config.Network.LeagueServerSignUp = false;
369 if (Game.fObserve || Game.fLobby)
370 Game.NetworkActive = true;
371
372 while (optind < argc)
373 {
374 char * szParameter = argv[optind++];
375 { // Strip trailing / that result from tab-completing unpacked c4groups
376 int iLen = SLen(szParameter);
377 if (iLen > 5 && szParameter[iLen-1] == '/' && szParameter[iLen-5] == '.' && szParameter[iLen-4] == 'o' && szParameter[iLen-3] == 'c')
378 {
379 szParameter[iLen-1] = '\0';
380 }
381 }
382 // Scenario file
383 if (SEqualNoCase(GetExtension(szParameter),"ocs"))
384 {
385 if(IsGlobalPath(szParameter))
386 Game.SetScenarioFilename(szParameter);
387 else
388 Game.SetScenarioFilename((std::string(GetWorkingDirectory()) + DirSep + szParameter).c_str());
389
390 continue;
391 }
392 if (SEqualNoCase(GetFilename(szParameter),"scenario.txt"))
393 {
394 Game.SetScenarioFilename(szParameter);
395 continue;
396 }
397 // Player file
398 if (SEqualNoCase(GetExtension(szParameter),"ocp"))
399 {
400 if(IsGlobalPath(szParameter))
401 SAddModule(Game.PlayerFilenames, szParameter);
402 else
403 SAddModule(Game.PlayerFilenames, (std::string(GetWorkingDirectory()) + DirSep + szParameter).c_str());
404
405 continue;
406 }
407 // Definition file
408 if (SEqualNoCase(GetExtension(szParameter),"ocd"))
409 {
410 SAddModule(Game.DefinitionFilenames,szParameter);
411 continue;
412 }
413 // Key file
414 if (SEqualNoCase(GetExtension(szParameter),"c4k"))
415 {
416 Application.IncomingKeyfile = szParameter;
417 continue;
418 }
419 // Update file
420 if (SEqualNoCase(GetExtension(szParameter),"ocu"))
421 {
422 Application.IncomingUpdate = szParameter;
423 continue;
424 }
425 // record stream
426 if (SEqualNoCase(GetExtension(szParameter),"c4r"))
427 {
428 Game.RecordStream.Copy(szParameter);
429 }
430 // Direct join by URL
431 if (SEqual2NoCase(szParameter, "clonk:"))
432 {
433 // Store address
434 SCopy(szParameter + 6, Game.DirectJoinAddress, _MAX_PATH);
435 SClearFrontBack(Game.DirectJoinAddress, '/');
436 // Special case: if the target address is "update" then this is used for update initiation by url
437 if (SEqualNoCase(Game.DirectJoinAddress, "update"))
438 {
439 Application.CheckForUpdates = true;
440 Game.DirectJoinAddress[0] = 0;
441 continue;
442 }
443 // Self-enable network
444 Game.NetworkActive = true;
445 continue;
446 }
447 }
448
449 #ifdef _WIN32
450 // Clean up some forward/backward slach confusion since many internal OC file functions cannot handle both
451 SReplaceChar(Game.ScenarioFilename, AltDirectorySeparator, DirectorySeparator);
452 SReplaceChar(Game.PlayerFilenames, AltDirectorySeparator, DirectorySeparator);
453 SReplaceChar(Game.DefinitionFilenames, AltDirectorySeparator, DirectorySeparator);
454 std::replace(begin(IncomingKeyfile), end(IncomingKeyfile), AltDirectorySeparator, DirectorySeparator);
455 std::replace(begin(IncomingUpdate), end(IncomingUpdate), AltDirectorySeparator, DirectorySeparator);
456 Game.RecordStream.ReplaceChar(AltDirectorySeparator, DirectorySeparator);
457 #endif
458
459 // Default to editor if scenario given, player mode otherwise
460 if (isEditor == 2)
461 isEditor = !!*Game.ScenarioFilename && !Config.General.OpenScenarioInGameMode;
462
463 // record?
464 Game.Record = Game.Record || (Config.Network.LeagueServerSignUp && Game.NetworkActive);
465
466 // startup dialog required?
467 QuitAfterGame = !isEditor && Game.HasScenario();
468 }
469
ApplyResolutionConstraints()470 void C4Application::ApplyResolutionConstraints()
471 {
472 // Not changing the resolution always works anyway
473 if (Config.Graphics.ResX == -1 && Config.Graphics.ResY == -1)
474 return;
475 // Enumerate display modes
476 int32_t idx = -1, iXRes, iYRes, iBitDepth, iRefreshRate;
477 int32_t best_match = -1;
478 uint32_t best_delta = ~0;
479 while (GetIndexedDisplayMode(++idx, &iXRes, &iYRes, &iBitDepth, &iRefreshRate, Config.Graphics.Monitor))
480 {
481 if (iBitDepth != C4Draw::COLOR_DEPTH) continue;
482 uint32_t delta = std::abs(Config.Graphics.ResX*Config.Graphics.ResY - iXRes*iYRes);
483 if (!delta && iBitDepth == C4Draw::COLOR_DEPTH && iRefreshRate == Config.Graphics.RefreshRate)
484 return; // Exactly the expected mode
485 if (delta < best_delta)
486 {
487 // Better match than before
488 best_match = idx;
489 best_delta = delta;
490 }
491 }
492 if (best_match != -1)
493 {
494 // Apply next-best mode
495 GetIndexedDisplayMode(best_match, &iXRes, &iYRes, &iBitDepth, &iRefreshRate, Config.Graphics.Monitor);
496 if (iXRes != Config.Graphics.ResX || iYRes != Config.Graphics.ResY)
497 // Don't warn if only bit depth changes
498 // Also, lang table not loaded yet
499 LogF("Warning: The selected resolution %dx%d is not available and has been changed to %dx%d.", Config.Graphics.ResX, Config.Graphics.ResY, iXRes, iYRes);
500 Config.Graphics.ResX = iXRes; Config.Graphics.ResY = iYRes;
501 Config.Graphics.RefreshRate = iRefreshRate;
502 }
503 }
504
PreInit()505 bool C4Application::PreInit()
506 {
507 // startup dialog: Only use if no next mission has been provided
508 bool fUseStartupDialog = !Game.HasScenario();
509
510 // Load graphics early, before we draw anything, since we need shaders
511 // loaded to draw.
512 Game.SetInitProgress(0.0f);
513 Log(LoadResStr("IDS_PRC_GFXRES"));
514 if (!GraphicsResource.Init()) return false;
515 Game.SetInitProgress(fUseStartupDialog ? 10.0f : 1.0f);
516
517 // Startup message board
518 if (!isEditor)
519 if (Config.Graphics.ShowStartupMessages || Game.NetworkActive)
520 {
521 C4Facet cgo; cgo.Set(FullScreen.pSurface,0,0,C4GUI::GetScreenWdt(), C4GUI::GetScreenHgt());
522 GraphicsSystem.MessageBoard->Init(cgo,true);
523 }
524
525 // init loader: Black screen for first start if a video is to be shown; otherwise default spec
526 if (fUseStartupDialog && !isEditor)
527 {
528 if (!::GraphicsSystem.InitLoaderScreen(C4CFN_StartupBackgroundMain))
529 { LogFatal(LoadResStr("IDS_PRC_ERRLOADER")); return false; }
530 }
531 Game.SetInitProgress(fUseStartupDialog ? 20.0f : 2.0f);
532
533 if (!Game.PreInit()) return false;
534
535 // Music
536 if (!MusicSystem.Init("frontend"))
537 Log(LoadResStr("IDS_PRC_NOMUSIC"));
538
539 Game.SetInitProgress(fUseStartupDialog ? 34.0f : 2.0f);
540
541 // Sound
542 if (!SoundSystem.Init())
543 Log(LoadResStr("IDS_PRC_NOSND"));
544
545 // Play some music! - after sound init because sound system might be needed by music system
546 if (fUseStartupDialog && !isEditor && Config.Sound.FEMusic)
547 MusicSystem.Play();
548
549 Game.SetInitProgress(fUseStartupDialog ? 35.0f : 3.0f);
550
551 if (fUseStartupDialog)
552 {
553 AppState = C4AS_Startup;
554 // default record?
555 Game.Record = Game.Record || Config.General.DefRec;
556 // if no scenario or direct join has been specified, get game startup parameters by startup dialog
557 if (!isEditor)
558 C4Startup::InitStartup();
559 }
560 // directly launch scenario / network game
561 else
562 {
563 AppState = C4AS_StartGame;
564 }
565
566 return true;
567 }
568
ProcessCallback(const char * szMessage,int iProcess)569 bool C4Application::ProcessCallback(const char *szMessage, int iProcess)
570 {
571 Console.Out(szMessage);
572 return true;
573 }
574
Clear()575 void C4Application::Clear()
576 {
577 Game.Clear();
578 NextMission.clear();
579 // stop timer
580 if (pGameTimer)
581 {
582 Remove(pGameTimer);
583 delete pGameTimer; pGameTimer = nullptr;
584 }
585 // quit irc
586 IRCClient.Close();
587 // close system group (System.ocg)
588 SystemGroup.Close();
589 // Log
590 if (::Languages.HasStringTable()) // Avoid (double and undefined) message on (second?) shutdown...
591 Log(LoadResStr("IDS_PRC_DEINIT"));
592 // Clear external language packs and string table
593 Languages.Clear();
594 Languages.ClearLanguage();
595 // gamepad clear
596 if (pGamePadControl) { delete pGamePadControl; pGamePadControl=nullptr; }
597 // music system clear
598 MusicSystem.Clear();
599 SoundSystem.Clear();
600 RestoreVideoMode();
601 // clear editcursor holding graphics before clearing draw
602 ::Console.EditCursor.Clear();
603 // Clear direct draw (late, because it's needed for e.g. Log)
604 if (pDraw) { delete pDraw; pDraw=nullptr; }
605 // Close window
606 FullScreen.Clear();
607 Console.Clear();
608 // There might be pending saves - do them after the fullscreen windows got closed
609 // so the app just remains as a lingering process until saving is done
610 CPNGFile::WaitForSaves();
611 // The very final stuff
612 C4AbstractApp::Clear();
613 }
614
Quit()615 void C4Application::Quit()
616 {
617 // Participants should not be cleared for usual startup dialog
618
619 // Save config if there was no loading error
620 if (Config.fConfigLoaded) Config.Save();
621 // make sure startup data is unloaded
622 C4Startup::Unload();
623 // fonts are loaded at start and never unloaded
624 ::GraphicsResource.ClearFonts();
625 // quit app
626 C4AbstractApp::Quit();
627 AppState = C4AS_Quit;
628 }
629
OpenGame(const char * scenario)630 void C4Application::OpenGame(const char * scenario)
631 {
632 if (AppState == C4AS_Startup)
633 {
634 if (scenario) Game.SetScenarioFilename(scenario);
635 AppState = C4AS_StartGame;
636 }
637 else
638 {
639 SetNextMission(scenario);
640 AppState = C4AS_AfterGame;
641 }
642
643 }
644
QuitGame()645 void C4Application::QuitGame()
646 {
647 // reinit desired? Do restart
648 if (!QuitAfterGame || !NextMission.empty())
649 {
650 AppState = C4AS_AfterGame;
651 }
652 else
653 {
654 Quit();
655 }
656 }
657
GameTick()658 void C4Application::GameTick()
659 {
660 // Exec depending on game state
661 switch (AppState)
662 {
663 case C4AS_None:
664 assert(AppState != C4AS_None);
665 break;
666 case C4AS_Quit:
667 // Do nothing, the main loop will exit soon
668 break;
669 case C4AS_PreInit:
670 if (!PreInit()) Quit();
671 break;
672 case C4AS_Startup:
673 SoundSystem.Execute();
674 MusicSystem.Execute();
675 if (pGamePadControl) pGamePadControl->Execute();
676 // wait for the user to start a game
677 break;
678 case C4AS_StartGame:
679 // immediate progress to next state; OpenGame will enter HandleMessage-loops in startup and lobby!
680 C4Startup::CloseStartup();
681 AppState = C4AS_Game;
682 #ifdef WITH_QT_EDITOR
683 // Notify console
684 if (isEditor) ::Console.OnStartGame();
685 #endif
686 // first-time game initialization
687 if (!Game.Init())
688 {
689 // set error flag (unless this was a lobby user abort)
690 if (!C4GameLobby::UserAbort)
691 Game.fQuitWithError = true;
692 // no start: Regular QuitGame; this may reset the engine to startup mode if desired
693 QuitGame();
694 break;
695 }
696 if(Config.Graphics.Windowed == 2 && FullScreenMode())
697 Application.SetVideoMode(GetConfigWidth(), GetConfigHeight(), Config.Graphics.RefreshRate, Config.Graphics.Monitor, true);
698 if (!isEditor)
699 pWindow->GrabMouse(true);
700 // Gamepad events have to be polled here so that the controller
701 // connection state is always up-to-date before players are
702 // joining.
703 if (pGamePadControl) pGamePadControl->Execute();
704 break;
705 case C4AS_AfterGame:
706 // stop game
707 Game.Clear();
708 if(Config.Graphics.Windowed == 2 && NextMission.empty() && !isEditor)
709 Application.SetVideoMode(GetConfigWidth(), GetConfigHeight(), Config.Graphics.RefreshRate, Config.Graphics.Monitor, false);
710 if (!isEditor)
711 pWindow->GrabMouse(false);
712 AppState = C4AS_PreInit;
713 // if a next mission is desired, set to start it
714 if (!NextMission.empty())
715 {
716 Game.SetScenarioFilename(NextMission.c_str());
717 Game.fLobby = Game.NetworkActive;
718 Game.fObserve = false;
719 NextMission.clear();
720 }
721 break;
722 case C4AS_Game:
723 // Game
724 if (Game.IsRunning)
725 Game.Execute();
726 // Sound
727 SoundSystem.Execute();
728 MusicSystem.Execute();
729 // Gamepad
730 if (pGamePadControl) pGamePadControl->Execute();
731 break;
732 }
733 }
734
Draw()735 void C4Application::Draw()
736 {
737 // Graphics
738
739 // Fullscreen mode
740 if (!isEditor)
741 FullScreen.Execute();
742 // Console mode
743 else
744 Console.Execute();
745 }
746
SetGameTickDelay(int iDelay)747 void C4Application::SetGameTickDelay(int iDelay)
748 {
749 if (!pGameTimer) return;
750 pGameTimer->SetGameTickDelay(iDelay);
751 }
752
OnResolutionChanged(unsigned int iXRes,unsigned int iYRes)753 void C4Application::OnResolutionChanged(unsigned int iXRes, unsigned int iYRes)
754 {
755 // notify game
756 if (pDraw)
757 {
758 Game.OnResolutionChanged(iXRes, iYRes);
759 pDraw->OnResolutionChanged(iXRes, iYRes);
760 }
761 if (pWindow)
762 {
763 if (pWindow->pSurface)
764 pWindow->pSurface->UpdateSize(iXRes, iYRes);
765 if (!FullScreenMode())
766 {
767 C4Rect r;
768 pWindow->GetSize(&r);
769 Config.Graphics.WindowX = r.Wdt;
770 Config.Graphics.WindowY = r.Hgt;
771 }
772 }
773 }
774
OnKeyboardLayoutChanged()775 void C4Application::OnKeyboardLayoutChanged()
776 {
777 // re-resolve all keys
778 Game.OnKeyboardLayoutChanged();
779 if (AppState == C4AS_Startup) C4Startup::Get()->OnKeyboardLayoutChanged();
780 }
781
SetGameFont(const char * szFontFace,int32_t iFontSize)782 bool C4Application::SetGameFont(const char *szFontFace, int32_t iFontSize)
783 {
784 #ifndef USE_CONSOLE
785 // safety
786 if (!szFontFace || !*szFontFace || iFontSize<1 || SLen(szFontFace)>=static_cast<int>(sizeof Config.General.RXFontName)) return false;
787 // first, check if the selected font can be created at all
788 // check regular font only - there's no reason why the other fonts couldn't be created
789 CStdFont TestFont;
790 if (!::FontLoader.InitFont(&TestFont, szFontFace, C4FontLoader::C4FT_Main, iFontSize, &::GraphicsResource.Files))
791 return false;
792 // OK; reinit all fonts
793 StdStrBuf sOldFont; sOldFont.Copy(Config.General.RXFontName);
794 int32_t iOldFontSize = Config.General.RXFontSize;
795 SCopy(szFontFace, Config.General.RXFontName);
796 Config.General.RXFontSize = iFontSize;
797 if (!::GraphicsResource.InitFonts() || !C4Startup::Get()->Graphics.InitFonts())
798 {
799 // failed :o
800 // shouldn't happen. Better restore config.
801 SCopy(sOldFont.getData(), Config.General.RXFontName);
802 Config.General.RXFontSize = iOldFontSize;
803 return false;
804 }
805 #endif
806 // save changes
807 return true;
808 }
809
OnCommand(const char * szCmd)810 void C4Application::OnCommand(const char *szCmd)
811 {
812 if (AppState == C4AS_Game)
813 ::MessageInput.ProcessInput(szCmd);
814 else if (AppState == C4AS_Startup)
815 {
816 AppState = C4AS_PreInit;
817 Game.SetScenarioFilename(szCmd);
818 }
819 }
820
Activate()821 void C4Application::Activate()
822 {
823 #ifdef USE_WIN32_WINDOWS
824 BringWindowToTop(FullScreen.hWindow);
825 ShowWindow(FullScreen.hWindow, SW_SHOW);
826 #endif
827 }
828
SetNextMission(const char * szMissionFilename)829 void C4Application::SetNextMission(const char *szMissionFilename)
830 {
831 // set next mission if any is desired
832 if (szMissionFilename)
833 {
834 NextMission = szMissionFilename;
835 // scenarios tend to use the wrong slash
836 std::replace(begin(NextMission), end(NextMission), AltDirectorySeparator, DirectorySeparator);
837 }
838 else
839 NextMission.clear();
840 }
841
NextTick()842 void C4Application::NextTick()
843 {
844 if (!pGameTimer) return;
845 pGameTimer->Set();
846 }
847
FullScreenMode()848 bool C4Application::FullScreenMode()
849 {
850 if(isEditor)
851 return false;
852 if(!Config.Graphics.Windowed)
853 return true;
854 if(Config.Graphics.Windowed == 2 && Game.IsRunning)
855 return true;
856 return false;
857 }
858
859 // *** C4ApplicationGameTimer
860
C4ApplicationGameTimer()861 C4ApplicationGameTimer::C4ApplicationGameTimer()
862 : CStdMultimediaTimerProc(26),
863 tLastGameTick(C4TimeMilliseconds::NegativeInfinity)
864 {
865 }
866
SetGameTickDelay(uint32_t iDelay)867 void C4ApplicationGameTimer::SetGameTickDelay(uint32_t iDelay)
868 {
869 // Remember delay
870 iGameTickDelay = iDelay;
871 // Smaller than minimum refresh delay?
872 if (iDelay < uint32_t(Config.Graphics.MaxRefreshDelay))
873 {
874 // Set critical timer
875 SetDelay(iDelay);
876 // No additional breaking needed
877 iExtraGameTickDelay = 0;
878 }
879 else
880 {
881 // Set critical timer
882 SetDelay(Config.Graphics.MaxRefreshDelay);
883 // Slow down game tick
884 iExtraGameTickDelay = iDelay;
885 }
886 }
887
Execute(int iTimeout,pollfd *)888 bool C4ApplicationGameTimer::Execute(int iTimeout, pollfd *)
889 {
890 // Check timer and reset
891 if (!CheckAndReset()) return true;
892 C4TimeMilliseconds tNow = C4TimeMilliseconds::Now();
893 // Execute
894 if (tNow >= tLastGameTick + iExtraGameTickDelay || Game.GameGo)
895 {
896 if (iGameTickDelay)
897 tLastGameTick += iGameTickDelay;
898 else
899 tLastGameTick = tNow;
900
901 // Compensate if things get too slow
902 if (tNow > tLastGameTick + iGameTickDelay)
903 tLastGameTick += (tNow - tLastGameTick) / 2;
904
905 Application.GameTick();
906 }
907 // Draw
908 if (!Game.DoSkipFrame)
909 {
910 C4TimeMilliseconds tPreGfxTime = C4TimeMilliseconds::Now();
911
912 Application.Draw();
913
914 // Automatic frame skip if graphics are slowing down the game (skip max. every 2nd frame)
915 Game.DoSkipFrame = Game.Parameters.AutoFrameSkip && (tPreGfxTime + iGameTickDelay < C4TimeMilliseconds::Now());
916 } else {
917 Game.DoSkipFrame=false;
918 }
919 return true;
920 }
921
IsLowPriority()922 bool C4ApplicationGameTimer::IsLowPriority() { return true; }
923