File: main.cpp

package info (click to toggle)
endless-sky 0.10.16-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 414,608 kB
  • sloc: cpp: 73,435; python: 893; xml: 666; sh: 271; makefile: 28
file content (646 lines) | stat: -rw-r--r-- 19,132 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
/* main.cpp
Copyright (c) 2014 by Michael Zahniser

Main function for Endless Sky, a space exploration and combat RPG.

Endless Sky is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.

Endless Sky is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with
this program. If not, see <https://www.gnu.org/licenses/>.
*/

#include "audio/Audio.h"
#include "Command.h"
#include "Conversation.h"
#include "DataFile.h"
#include "DataNode.h"
#include "Engine.h"
#include "Files.h"
#include "text/Font.h"
#include "FrameTimer.h"
#include "GameData.h"
#include "GameLoadingPanel.h"
#include "GameWindow.h"
#include "Logger.h"
#include "MainPanel.h"
#include "MenuPanel.h"
#include "Panel.h"
#include "PlayerInfo.h"
#include "Plugins.h"
#include "Preferences.h"
#include "PrintData.h"
#include "Screen.h"
#include "image/SpriteSet.h"
#include "shader/SpriteShader.h"
#include "TaskQueue.h"
#include "test/Test.h"
#include "test/TestContext.h"
#include "UI.h"

#ifdef _WIN32
#include "windows/TimerResolutionGuard.h"
#include "windows/WinVersion.h"
#endif

#include <chrono>
#include <iostream>
#include <map>

#include <cassert>
#include <future>
#include <exception>
#include <string>

#ifdef _WIN32
#define STRICT
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif

using namespace std;

void PrintHelp();
void PrintVersion();
void GameLoop(PlayerInfo &player, TaskQueue &queue, const Conversation &conversation,
	const string &testToRun, bool debugMode);
Conversation LoadConversation(const PlayerInfo &player);
void PrintTestsTable();
#ifdef _WIN32
void InitConsole();
#endif



// Entry point for the EndlessSky executable
int main(int argc, char *argv[])
{
#ifdef _WIN32
	WinVersion::Init();
	// Handle command-line arguments
	if(argc > 1)
		InitConsole();
#endif
	PlayerInfo player;
	Conversation conversation;
	bool debugMode = false;
	bool loadOnly = false;
	bool checkAssets = false;
	bool printTests = false;
	bool printData = false;
	bool noTestMute = false;
	string testToRunName;

	// Whether the game has encountered errors while loading.
	bool hasErrors = false;
	// Ensure that we log errors to the errors.txt file.
	Logger::SetLogErrorCallback([&hasErrors](const string &errorMessage) {
		static const string PARSING_PREFIX = "Parsing: ";
		if(errorMessage.substr(0, PARSING_PREFIX.length()) != PARSING_PREFIX)
			hasErrors = true;
		Files::LogErrorToFile(errorMessage);
	});

	for(const char *const *it = argv + 1; *it; ++it)
	{
		string arg = *it;
		if(arg == "-h" || arg == "--help")
		{
			PrintHelp();
			return 0;
		}
		else if(arg == "-v" || arg == "--version")
		{
			PrintVersion();
			return 0;
		}
		else if(arg == "-t" || arg == "--talk")
			conversation = LoadConversation(player);
		else if(arg == "-d" || arg == "--debug")
			debugMode = true;
		else if(arg == "-p" || arg == "--parse-save")
			loadOnly = true;
		else if(arg == "--parse-assets")
			checkAssets = true;
		else if(arg == "--test" && *++it)
			testToRunName = *it;
		else if(arg == "--tests")
			printTests = true;
		else if(arg == "--nomute")
			noTestMute = true;
	}
	printData = PrintData::IsPrintDataArgument(argv);
	Files::Init(argv);

	// Whether we are running an integration test.
	const bool isTesting = !testToRunName.empty();
	try {
		// Load plugin preferences before game data if any.
		Plugins::LoadSettings();

		TaskQueue queue;

		// Begin loading the game data.
		bool isConsoleOnly = loadOnly || printTests || printData;
		auto dataFuture = GameData::BeginLoad(queue, player, isConsoleOnly, debugMode,
			isConsoleOnly || checkAssets || (isTesting && !debugMode));

		// If we are not using the UI, or performing some automated task, we should load
		// all data now.
		if(isConsoleOnly || checkAssets || isTesting)
			dataFuture.wait();

		if(isTesting && !GameData::Tests().Has(testToRunName))
		{
			Logger::LogError("Test \"" + testToRunName + "\" not found.");
			return 1;
		}

		if(printData)
		{
			PrintData::Print(argv, player);
			return 0;
		}
		if(printTests)
		{
			PrintTestsTable();
			return 0;
		}

		if(loadOnly || checkAssets)
		{
			if(checkAssets)
			{
				Audio::LoadSounds(GameData::Sources());
				while(GameData::GetProgress() < 1.)
				{
					queue.ProcessSyncTasks();
					std::this_thread::yield();
				}
				if(GameData::IsLoaded())
				{
					// Now that we have finished loading all the basic sprites and sounds, we can look for invalid file paths,
					// e.g. due to capitalization errors or other typos.
					SpriteSet::CheckReferences();
					Audio::CheckReferences(true);
				}
			}

			// Set the game's initial internal state.
			GameData::FinishLoading();

			// Reference check the universe, as known to the player. If no player found,
			// then check the default state of the universe.
			if(!player.LoadRecent())
				GameData::CheckReferences();
			cout << "Parse completed with " << (hasErrors ? "at least one" : "no") << " error(s)." << endl;
			if(checkAssets)
				Audio::Quit();
			return hasErrors;
		}
		assert(!isConsoleOnly && "Attempting to use UI when only data was loaded!");

		Preferences::Load();

		// Load global conditions:
		DataFile globalConditions(Files::Config() / "global conditions.txt");
		for(const DataNode &node : globalConditions)
			if(node.Token(0) == "conditions")
				GameData::GlobalConditions().Load(node);

		if(!GameWindow::Init(isTesting && !debugMode))
			return 1;

		GameData::LoadSettings();

#ifdef _WIN32
		TimerResolutionGuard windowsTimerGuard;
#endif

		if(!isTesting || debugMode)
		{
			GameData::LoadShaders();

			// Show something other than a blank window.
			GameWindow::Step();
		}

		Audio::Init(GameData::Sources());

		if(isTesting && !noTestMute)
			Audio::SetVolume(0, SoundCategory::MASTER);

		// This is the main loop where all the action begins.
		GameLoop(player, queue, conversation, testToRunName, debugMode);
	}
	catch(Test::known_failure_tag)
	{
		// This is not an error. Simply exit successfully.
	}
	catch(const exception &error)
	{
		Audio::Quit();
		GameWindow::ExitWithError(error.what(), !isTesting);
		return 1;
	}

	// Remember the window state and preferences if quitting normally.
	Preferences::Set("maximized", GameWindow::IsMaximized());
	Preferences::Set("fullscreen", GameWindow::IsFullscreen());
	Screen::SetRaw(GameWindow::Width(), GameWindow::Height());
	Preferences::Save();
	Plugins::Save();

	Audio::Quit();
	GameWindow::Quit();

	return 0;
}



void GameLoop(PlayerInfo &player, TaskQueue &queue, const Conversation &conversation,
		const string &testToRunName, bool debugMode)
{
	// gamePanels is used for the main panel where you fly your spaceship.
	// All other game content related dialogs are placed on top of the gamePanels.
	// If there are both menuPanels and gamePanels, then the menuPanels take
	// priority over the gamePanels. The gamePanels will not be shown until
	// the stack of menuPanels is empty.
	UI gamePanels;

	// menuPanels is used for the panels related to pilot creation, preferences,
	// game loading and game saving.
	UI menuPanels;

	// Whether the game data is done loading. This is used to trigger any
	// tests to run.
	bool dataFinishedLoading = false;
	menuPanels.Push(new GameLoadingPanel(player, queue, conversation, gamePanels, dataFinishedLoading));

	bool showCursor = true;
	int cursorTime = 0;
	int frameRate = 60;
	FrameTimer timer(frameRate);
	bool isDebugPaused = false;
	bool isFastForward = false;

	// If fast forwarding, keep track of whether the current frame should be drawn.
	int skipFrame = 0;

	// Limit how quickly full-screen mode can be toggled.
	int toggleTimeout = 0;

	// Data to track progress of testing if/when a test is running.
	TestContext testContext;
	if(!testToRunName.empty())
		testContext = TestContext(GameData::Tests().Get(testToRunName));

	const bool isHeadless = (testContext.CurrentTest() && !debugMode);

	auto ProcessEvents = [&menuPanels, &gamePanels, &player, &cursorTime, &toggleTimeout, &debugMode, &isDebugPaused,
			&isFastForward]
	{
		SDL_Event event;
		while(SDL_PollEvent(&event))
		{
			UI &activeUI = (menuPanels.IsEmpty() ? gamePanels : menuPanels);

			// If the mouse moves, reset the cursor movement timeout.
			if(event.type == SDL_MOUSEMOTION)
				cursorTime = 0;

			if(debugMode && event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_BACKQUOTE)
			{
				isDebugPaused = !isDebugPaused;
				if(isDebugPaused)
					Audio::Pause();
				else
					Audio::Resume();
			}
			else if(event.type == SDL_KEYDOWN && menuPanels.IsEmpty()
					&& Command(event.key.keysym.sym).Has(Command::MENU)
					&& !gamePanels.IsEmpty() && gamePanels.Top()->IsInterruptible())
			{
				// User pressed the Menu key.
				menuPanels.Push(shared_ptr<Panel>(
					new MenuPanel(player, gamePanels)));
				UI::PlaySound(UI::UISound::NORMAL);
			}
			else if(event.type == SDL_QUIT)
				menuPanels.Quit();
			else if(event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
			{
				// The window has been resized. Adjust the raw screen size
				// and the OpenGL viewport to match. The order (window first, then UI panels)
				// matters as window changes can adjust the zoom factor.
				GameWindow::AdjustViewport();
				menuPanels.AdjustViewport();
				gamePanels.AdjustViewport();
			}
			else if(event.type == SDL_KEYDOWN && !toggleTimeout
					&& (Command(event.key.keysym.sym).Has(Command::FULLSCREEN)
					|| (event.key.keysym.sym == SDLK_RETURN && (event.key.keysym.mod & KMOD_ALT))))
			{
				toggleTimeout = 30;
				Preferences::ToggleScreenMode();
			}
			else if(activeUI.Handle(event))
			{
				// The UI handled the event.
			}
			else if(event.type == SDL_KEYDOWN && !event.key.repeat
					&& (Command(event.key.keysym.sym).Has(Command::FASTFORWARD))
					&& !Command(SDLK_CAPSLOCK).Has(Command::FASTFORWARD))
			{
				isFastForward = !isFastForward;
			}
		}

		// Special case: If fastforward is on capslock, update on mod state and not
		// on keypress.
		if(Command(SDLK_CAPSLOCK).Has(Command::FASTFORWARD))
			isFastForward = SDL_GetModState() & KMOD_CAPS;
	};

	// Game loop when running the game normally.
	if(!testContext.CurrentTest())
	{
		while(!menuPanels.IsDone())
		{
			if(toggleTimeout)
				--toggleTimeout;
			chrono::steady_clock::time_point start = chrono::steady_clock::now();

			ProcessEvents();

			SDL_Keymod mod = SDL_GetModState();
			Font::ShowUnderlines(mod & KMOD_ALT);

			// In full-screen mode, hide the cursor if inactive for ten seconds,
			// but only if the player is flying around in the main view.
			bool inFlight = (menuPanels.IsEmpty() && gamePanels.Root() == gamePanels.Top());
			++cursorTime;
			bool shouldShowCursor = (!GameWindow::IsFullscreen() || cursorTime < 600 || !inFlight);
			if(shouldShowCursor != showCursor)
			{
				showCursor = shouldShowCursor;
				SDL_ShowCursor(showCursor);
			}

			// Switch off fast-forward if the player is not in flight or flight-related screen
			// (for example when the boarding dialog shows up or when the player lands). The player
			// can switch fast-forward on again when flight is resumed.
			bool allowFastForward = !gamePanels.IsEmpty() && gamePanels.Top()->AllowsFastForward();
			if(Preferences::Has("Interrupt fast-forward") && !inFlight && isFastForward && !allowFastForward)
				isFastForward = false;

			// Tell all the panels to step forward, then draw them.
			((!isDebugPaused && menuPanels.IsEmpty()) ? gamePanels : menuPanels).StepAll();

			// Caps lock slows the frame rate in debug mode.
			// Slowing eases in and out over a couple of frames.
			if((mod & KMOD_CAPS) && inFlight && debugMode)
			{
				if(frameRate > 10)
				{
					frameRate = max(frameRate - 5, 10);
					timer.SetFrameRate(frameRate);
				}
			}
			else
			{
				if(frameRate < 60)
				{
					frameRate = min(frameRate + 5, 60);
					timer.SetFrameRate(frameRate);
				}

				if(isFastForward && inFlight)
				{
					skipFrame = (skipFrame + 1) % 3;
					if(skipFrame)
						continue;
				}
			}

			Audio::Step(isFastForward);

			// Events in this frame may have cleared out the menu, in which case
			// we should draw the game panels instead:
			(menuPanels.IsEmpty() ? gamePanels : menuPanels).DrawAll();

			MainPanel *mainPanel = static_cast<MainPanel *>(gamePanels.Root().get());
			if(mainPanel && mainPanel->GetEngine().IsPaused())
				SpriteShader::Draw(SpriteSet::Get("ui/paused"), Screen::TopLeft() + Point(10., 10.));
			else if(isFastForward)
				SpriteShader::Draw(SpriteSet::Get("ui/fast forward"), Screen::TopLeft() + Point(10., 10.));

			GameWindow::Step();

			// Lock the game loop to 60 FPS.
			timer.Wait();

			// If the player ended this frame in-game, count the elapsed time as played time.
			if(menuPanels.IsEmpty())
				player.AddPlayTime(chrono::steady_clock::now() - start);
		}
	}
	// Game loop when running the game as part of an integration test.
	else
	{
		int integrationStepCounter = 0;
		while(!menuPanels.IsDone())
		{
			ProcessEvents();

			// Handle any integration test steps.
			if(dataFinishedLoading)
			{
				// Run a single integration step every 30 frames.
				integrationStepCounter = (integrationStepCounter + 1) % 30;
				if(!integrationStepCounter)
				{
					assert(!gamePanels.IsEmpty() && "main panel missing?");

					// The main panel is always at the root of the game panels.
					MainPanel *mainPanel = static_cast<MainPanel *>(gamePanels.Root().get());

					// The engine needs to have finished calculating the current frame so
					// that it is safe to run any additional processing here.
					if(menuPanels.IsEmpty())
						mainPanel->GetEngine().Wait();

					// The current test that is running, if any.
					const Test *runningTest = testContext.CurrentTest();
					assert(runningTest && "no running test while running an integration test?");
					Command command;
					runningTest->Step(testContext, player, command);

					// Send any commands to the engine, if it is active.
					if(menuPanels.IsEmpty())
						mainPanel->GetEngine().GiveCommand(command);
				}
			}

			// Tell all the panels to step forward, then draw them.
			(menuPanels.IsEmpty() ? gamePanels : menuPanels).StepAll();

			if(!isHeadless)
			{
				Audio::Step(isFastForward);

				// Events in this frame may have cleared out the menu, in which case
				// we should draw the game panels instead:
				(menuPanels.IsEmpty() ? gamePanels : menuPanels).DrawAll();

				GameWindow::Step();

				// When we perform automated testing, then we run the game by default as quickly as possible.
				// Except when not in headless mode so that the user can follow along.
				timer.Wait();
			}
		}
	}

	// If player quit while landed on a planet, save the game if there are changes.
	if(player.GetPlanet() && gamePanels.CanSave())
		player.Save();
}



void PrintHelp()
{
	cerr << endl;
	cerr << "Command line options:" << endl;
	cerr << "    -h, --help: print this help message." << endl;
	cerr << "    -v, --version: print version information." << endl;
	cerr << "    -t, --talk: read and display a conversation from STDIN." << endl;
	cerr << "    -r, --resources <path>: load resources from given directory." << endl;
	cerr << "    -c, --config <path>: save user's files to given directory." << endl;
	cerr << "    -d, --debug: turn on debugging features (e.g. Caps Lock slows down instead of speeds up)." << endl;
	cerr << "    -p, --parse-save: load the most recent saved game and inspect it for content errors." << endl;
	cerr << "    --parse-assets: load all game data, images, and sounds,"
		" and the latest save game, and inspect data for errors." << endl;
	cerr << "    --tests: print table of available tests, then exit." << endl;
	cerr << "    --test <name>: run given test from resources directory." << endl;
	cerr << "    --nomute: don't mute the game while running tests." << endl;
	PrintData::Help();
	cerr << endl;
	cerr << "Report bugs to: <https://github.com/endless-sky/endless-sky/issues>" << endl;
	cerr << "Home page: <https://endless-sky.github.io>" << endl;
	cerr << endl;
}



void PrintVersion()
{
	cerr << endl;
	cerr << "Endless Sky ver. 0.10.16" << endl;
	cerr << "License GPLv3+: GNU GPL version 3 or later: <https://gnu.org/licenses/gpl.html>" << endl;
	cerr << "This is free software: you are free to change and redistribute it." << endl;
	cerr << "There is NO WARRANTY, to the extent permitted by law." << endl;
	cerr << endl;
	cerr << GameWindow::SDLVersions() << endl;
	cerr << endl;
}



Conversation LoadConversation(const PlayerInfo &player)
{
	const ConditionsStore *conditions = &player.Conditions();
	Conversation conversation;
	DataFile file(cin);
	for(const DataNode &node : file)
		if(node.Token(0) == "conversation")
		{
			conversation.Load(node, conditions);
			break;
		}

	map<string, string> subs = {
		{"<bunks>", "[N]"},
		{"<cargo>", "[N tons of Commodity]"},
		{"<commodity>", "[Commodity]"},
		{"<date>", "[Day Mon Year]"},
		{"<day>", "[The Nth of Month]"},
		{"<destination>", "[Planet in the Star system]"},
		{"<fare>", "[N passengers]"},
		{"<first>", "[First]"},
		{"<last>", "[Last]"},
		{"<origin>", "[Origin Planet]"},
		{"<passengers>", "[your passengers]"},
		{"<planet>", "[Planet]"},
		{"<ship>", "[Ship]"},
		{"<model>", "[Ship Model]"},
		{"<flagship>", "[Flagship]"},
		{"<flagship model>", "[Flagship Model]"},
		{"<system>", "[Star]"},
		{"<tons>", "[N tons]"}
	};
	return conversation.Instantiate(subs);
}



// This prints out the list of tests that are available and their status
// (active/missing feature/known failure)..
void PrintTestsTable()
{
	for(auto &it : GameData::Tests())
		if(it.second.GetStatus() != Test::Status::PARTIAL
				&& it.second.GetStatus() != Test::Status::BROKEN)
			cout << it.second.Name() << '\n';
	cout.flush();
}



#ifdef _WIN32
void InitConsole()
{
	const int UNINITIALIZED = -2;
	bool redirectStdout = _fileno(stdout) == UNINITIALIZED;
	bool redirectStderr = _fileno(stderr) == UNINITIALIZED;
	bool redirectStdin = _fileno(stdin) == UNINITIALIZED;

	// Bail if stdin, stdout, and stderr are already initialized (e.g. writing to a file)
	if(!redirectStdout && !redirectStderr && !redirectStdin)
		return;

	// Bail if we fail to attach to the console
	if(!AttachConsole(ATTACH_PARENT_PROCESS) && !AllocConsole())
		return;

	// Perform console redirection.
	if(redirectStdout)
	{
		FILE *fstdout = nullptr;
		freopen_s(&fstdout, "CONOUT$", "w", stdout);
		if(fstdout)
			setvbuf(stdout, nullptr, _IOFBF, 4096);
	}
	if(redirectStderr)
	{
		FILE *fstderr = nullptr;
		freopen_s(&fstderr, "CONOUT$", "w", stderr);
		if(fstderr)
			setvbuf(stderr, nullptr, _IOLBF, 1024);
	}
	if(redirectStdin)
	{
		FILE *fstdin = nullptr;
		freopen_s(&fstdin, "CONIN$", "r", stdin);
		if(fstdin)
			setvbuf(stdin, nullptr, _IONBF, 0);
	}
}
#endif