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