Browse Source

Merge pull request #5544 from IvanSavenko/fix_shutdown

Fix shutdown
Ivan Savenko 7 months ago
parent
commit
cb7c2d2f07

+ 6 - 7
AI/Nullkiller/AIGateway.cpp

@@ -10,6 +10,7 @@
 #include "StdInc.h"
 
 #include "../../lib/ArtifactUtils.h"
+#include "../../lib/AsyncRunner.h"
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/StartInfo.h"
 #include "../../lib/entities/building/CBuilding.h"
@@ -32,8 +33,6 @@
 #include "AIGateway.h"
 #include "Goals/Goals.h"
 
-static tbb::task_arena executeActionAsyncArena;
-
 namespace NKAI
 {
 
@@ -73,7 +72,7 @@ AIGateway::AIGateway()
 	destinationTeleport = ObjectInstanceID();
 	destinationTeleportPos = int3(-1);
 	nullkiller.reset(new Nullkiller());
-	asyncTasks = std::make_unique<tbb::task_group>();
+	asyncTasks = std::make_unique<AsyncRunner>();
 }
 
 AIGateway::~AIGateway()
@@ -593,11 +592,11 @@ void AIGateway::yourTurn(QueryID queryID)
 
 	nullkiller->makingTurnInterrupption.reset();
 
-	executeActionAsyncArena.enqueue(asyncTasks->defer([this]()
+	asyncTasks->run([this]()
 	{
 		ScopedThreadName guard("NKAI::makingTurn");
 		makeTurn();
-	}));
+	});
 }
 
 void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID)
@@ -1608,13 +1607,13 @@ void AIGateway::executeActionAsync(const std::string & description, const std::f
 	if (!asyncTasks)
 		throw std::runtime_error("Attempt to execute task on shut down AI state!");
 
-	executeActionAsyncArena.enqueue(asyncTasks->defer([this, description, whatToDo]()
+	asyncTasks->run([this, description, whatToDo]()
 	{
 		ScopedThreadName guard("NKAI::" + description);
 		SET_GLOBAL_STATE(this);
 		std::shared_lock gsLock(CGameState::mutex);
 		whatToDo();
-	}));
+	});
 }
 
 void AIGateway::lostHero(HeroPtr h)

+ 4 - 3
AI/Nullkiller/AIGateway.h

@@ -22,8 +22,9 @@
 #include "Pathfinding/AIPathfinder.h"
 #include "Engine/Nullkiller.h"
 
-#include <tbb/task_group.h>
-#include <tbb/task_arena.h>
+VCMI_LIB_NAMESPACE_BEGIN
+class AsyncRunner;
+VCMI_LIB_NAMESPACE_END
 
 namespace NKAI
 {
@@ -74,7 +75,7 @@ public:
 	AIStatus status;
 	std::string battlename;
 	std::shared_ptr<CCallback> myCb;
-	std::unique_ptr<tbb::task_group> asyncTasks;
+	std::unique_ptr<AsyncRunner> asyncTasks;
 
 public:
 	ObjectInstanceID selectedObject;

+ 6 - 7
AI/VCAI/VCAI.cpp

@@ -15,6 +15,7 @@
 #include "Goals/Goals.h"
 
 #include "../../lib/ArtifactUtils.h"
+#include "../../lib/AsyncRunner.h"
 #include "../../lib/CThreadHelper.h"
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/StartInfo.h"
@@ -37,8 +38,6 @@
 
 #include "AIhelper.h"
 
-static tbb::task_arena executeActionAsyncArena;
-
 extern FuzzyHelper * fh;
 
 const double SAFE_ATTACK_CONSTANT = 1.5;
@@ -78,7 +77,7 @@ struct SetGlobalState
 VCAI::VCAI()
 {
 	LOG_TRACE(logAi);
-	asyncTasks = std::make_unique<tbb::task_group>();
+	asyncTasks = std::make_unique<AsyncRunner>();
 	destinationTeleport = ObjectInstanceID();
 	destinationTeleportPos = int3(-1);
 
@@ -653,11 +652,11 @@ void VCAI::yourTurn(QueryID queryID)
 	status.startedTurn();
 
 	makingTurnInterrupption.reset();
-	executeActionAsyncArena.enqueue(asyncTasks->defer([this]()
+	asyncTasks->run([this]()
 	{
 		ScopedThreadName guard("VCAI::makingTurn");
 		makeTurn();
-	}));
+	});
 }
 
 void VCAI::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID)
@@ -2510,13 +2509,13 @@ void VCAI::executeActionAsync(const std::string & description, const std::functi
 	if (!asyncTasks)
 		throw std::runtime_error("Attempt to execute task on shut down AI state!");
 
-	executeActionAsyncArena.enqueue(asyncTasks->defer([this, description, whatToDo]()
+	asyncTasks->run([this, description, whatToDo]()
 	{
 		ScopedThreadName guard("VCAI::" + description);
 		SET_GLOBAL_STATE(this);
 		std::shared_lock gsLock(CGameState::mutex);
 		whatToDo();
-	}));
+	});
 }
 
 void VCAI::lostHero(HeroPtr h)

+ 2 - 4
AI/VCAI/VCAI.h

@@ -23,13 +23,11 @@
 #include "../../lib/spells/CSpellHandler.h"
 #include "Pathfinding/AIPathfinder.h"
 
-#include <tbb/task_group.h>
-#include <tbb/task_arena.h>
-
 VCMI_LIB_NAMESPACE_BEGIN
 
 struct QuestInfo;
 class PathfinderCache;
+class AsyncRunner;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -108,7 +106,7 @@ public:
 
 	std::shared_ptr<CCallback> myCb;
 
-	std::unique_ptr<tbb::task_group> asyncTasks;
+	std::unique_ptr<AsyncRunner> asyncTasks;
 	ThreadInterruption makingTurnInterrupption;
 
 public:

+ 0 - 1
client/CMT.h

@@ -16,4 +16,3 @@ extern SDL_Renderer * mainRenderer;
 /// Defined in clientapp EntryPoint
 /// TODO: decide on better location for this method
 [[noreturn]] void handleFatalError(const std::string & message, bool terminate);
-void handleQuit(bool ask = true);

+ 0 - 2
client/CServerHandler.cpp

@@ -703,8 +703,6 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared
 			entry->Bool() = true;
 		}
 
-		GAME->mainmenu()->makeActiveInterface();
-
 		if(!ourCampaign->isCampaignFinished())
 			GAME->mainmenu()->openCampaignLobby(ourCampaign);
 		else

+ 28 - 27
client/GameEngine.cpp

@@ -23,20 +23,17 @@
 #include "media/CVideoHandler.h"
 #include "media/CEmptyVideoPlayer.h"
 
-#include "CPlayerInterface.h"
 #include "adventureMap/AdventureMapInterface.h"
 #include "render/Canvas.h"
 #include "render/Colors.h"
-#include "render/Graphics.h"
 #include "render/IFont.h"
 #include "render/EFont.h"
 #include "renderSDL/ScreenHandler.h"
 #include "renderSDL/RenderHandler.h"
-#include "CMT.h"
 #include "GameEngineUser.h"
 #include "battle/BattleInterface.h"
 
-#include "../lib/CThreadHelper.h"
+#include "../lib/AsyncRunner.h"
 #include "../lib/CConfigHandler.h"
 
 #include <SDL_render.h>
@@ -58,7 +55,9 @@ ObjectConstruction::~ObjectConstruction()
 	ENGINE->captureChildren = !ENGINE->createdObj.empty();
 }
 
-void GameEngine::init()
+GameEngine::GameEngine()
+	: captureChildren(false)
+	, fakeStatusBar(std::make_shared<EmptyStatusBar>())
 {
 	inGuiThread = true;
 
@@ -81,9 +80,11 @@ void GameEngine::init()
 
 	soundPlayerInstance = std::make_unique<CSoundHandler>();
 	musicPlayerInstance = std::make_unique<CMusicHandler>();
-	sound().setVolume((ui32)settings["general"]["sound"].Float());
-	music().setVolume((ui32)settings["general"]["music"].Float());
+	sound().setVolume(settings["general"]["sound"].Integer());
+	music().setVolume(settings["general"]["music"].Integer());
 	cursorHandlerInstance = std::make_unique<CursorHandler>();
+
+	asyncTasks = std::make_unique<AsyncRunner>();
 }
 
 void GameEngine::handleEvents()
@@ -104,33 +105,33 @@ void GameEngine::fakeMouseMove()
 	});
 }
 
-void GameEngine::renderFrame()
+[[noreturn]] void GameEngine::mainLoop()
 {
+	for (;;)
 	{
-		std::scoped_lock interfaceLock(ENGINE->interfaceMutex);
-
-		engineUser->onUpdate();
+		input().fetchEvents();
+		updateFrame();
+		screenHandlerInstance->presentScreenTexture();
+		framerate().framerateDelay(); // holds a constant FPS
+	}
+}
 
-		handleEvents();
-		windows().simpleRedraw();
+void GameEngine::updateFrame()
+{
+	std::scoped_lock interfaceLock(ENGINE->interfaceMutex);
 
-		if (settings["video"]["showfps"].Bool())
-			drawFPSCounter();
+	engineUser->onUpdate();
 
-		screenHandlerInstance->updateScreenTexture();
+	handleEvents();
+	windows().simpleRedraw();
 
-		windows().onFrameRendered();
-		ENGINE->cursor().update();
-	}
+	if (settings["video"]["showfps"].Bool())
+		drawFPSCounter();
 
-	screenHandlerInstance->presentScreenTexture();
-	framerate().framerateDelay(); // holds a constant FPS
-}
+	screenHandlerInstance->updateScreenTexture();
 
-GameEngine::GameEngine()
-	: captureChildren(false)
-	, fakeStatusBar(std::make_shared<EmptyStatusBar>())
-{
+	windows().onFrameRendered();
+	ENGINE->cursor().update();
 }
 
 GameEngine::~GameEngine()
@@ -239,7 +240,7 @@ std::shared_ptr<IStatusBar> GameEngine::statusbar()
 	return locked;
 }
 
-void GameEngine::setStatusbar(std::shared_ptr<IStatusBar> newStatusBar)
+void GameEngine::setStatusbar(const std::shared_ptr<IStatusBar> & newStatusBar)
 {
 	currentStatusBar = newStatusBar;
 }

+ 15 - 5
client/GameEngine.h

@@ -11,6 +11,7 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 class Point;
+class AsyncRunner;
 class Rect;
 VCMI_LIB_NAMESPACE_END
 
@@ -52,9 +53,14 @@ private:
 	std::unique_ptr<IMusicPlayer> musicPlayerInstance;
 	std::unique_ptr<CursorHandler> cursorHandlerInstance;
 	std::unique_ptr<IVideoPlayer> videoPlayerInstance;
+	std::unique_ptr<AsyncRunner> asyncTasks;
 
 	IGameEngineUser *engineUser = nullptr;
 
+	void updateFrame();
+	void handleEvents(); //takes events from queue and calls interested objects
+	void drawFPSCounter(); // draws the FPS to the upper left corner of the screen
+
 public:
 	std::mutex interfaceMutex;
 
@@ -66,6 +72,7 @@ public:
 	EventDispatcher & events();
 	InputHandler & input();
 
+	AsyncRunner & async() { return *asyncTasks; }
 	IGameEngineUser & user() { return *engineUser; }
 	ISoundPlayer & sound() { return *soundPlayerInstance; }
 	IMusicPlayer & music() { return *musicPlayerInstance; }
@@ -97,7 +104,9 @@ public:
 	std::shared_ptr<IStatusBar> statusbar();
 
 	/// Set currently active status bar
-	void setStatusbar(std::shared_ptr<IStatusBar>);
+	void setStatusbar(const std::shared_ptr<IStatusBar> &);
+
+	/// Sets engine user that is used as target of callback for events received by engine
 	void setEngineUser(IGameEngineUser * user);
 
 	bool captureChildren; //all newly created objects will get their parents from stack and will be added to parents children list
@@ -106,16 +115,17 @@ public:
 	GameEngine();
 	~GameEngine();
 
-	void init();
-	void renderFrame();
+	/// Performs main game loop till game shutdown
+	/// This method never returns, to abort main loop throw GameShutdownException
+	[[noreturn]] void mainLoop();
 
 	/// called whenever SDL_WINDOWEVENT_RESTORED is reported or the user selects a different resolution, requiring to center/resize all windows
 	void onScreenResize(bool resolutionChanged);
 
-	void handleEvents(); //takes events from queue and calls interested objects
+	/// Simulate mouse movement to force refresh UI state that updates on mouse move
 	void fakeMouseMove();
-	void drawFPSCounter(); // draws the FPS to the upper left corner of the screen
 
+	/// Returns true for calls made from main (GUI) thread, false othervice
 	bool amIGuiThread();
 
 	/// Calls provided functor in main thread on next execution frame

+ 3 - 0
client/GameEngineUser.h

@@ -20,6 +20,9 @@ public:
 	/// Called on every game tick for game to update its state
 	virtual void onUpdate() = 0;
 
+	/// Called when app shutdown has been requested in any way - exit button, Alt-F4, etc
+	virtual void onShutdownRequested(bool askForConfirmation) = 0;
+
 	/// Returns true if all input events should be captured and ignored
 	virtual bool capturedAllEvents() = 0;
 };

+ 19 - 0
client/GameInstance.cpp

@@ -11,12 +11,16 @@
 #include "GameInstance.h"
 
 #include "CPlayerInterface.h"
+#include "CMT.h"
 #include "CServerHandler.h"
 #include "mapView/mapHandler.h"
 #include "globalLobby/GlobalLobbyClient.h"
 #include "mainmenu/CMainMenu.h"
+#include "windows/InfoWindows.h"
 
 #include "../lib/CConfigHandler.h"
+#include "../lib/GameLibrary.h"
+#include "../lib/texts/CGeneralTextHandler.h"
 
 std::unique_ptr<GameInstance> GAME = nullptr;
 
@@ -93,3 +97,18 @@ bool GameInstance::capturedAllEvents()
 	else
 		return false;
 }
+
+void GameInstance::onShutdownRequested(bool ask)
+{
+	auto doQuit = [](){ throw GameShutdownException(); };
+
+	if(!ask)
+		doQuit();
+	else
+	{
+		if (interface())
+			interface()->showYesNoDialog(LIBRARY->generaltexth->allTexts[69], doQuit, nullptr);
+		else
+			CInfoWindow::showYesNoDialog(LIBRARY->generaltexth->allTexts[69], {}, doQuit, {}, PlayerColor(1));
+	}
+}

+ 10 - 0
client/GameInstance.h

@@ -21,6 +21,15 @@ VCMI_LIB_NAMESPACE_BEGIN
 class INetworkHandler;
 VCMI_LIB_NAMESPACE_END
 
+class GameShutdownException final : public std::exception
+{
+public:
+	const char* what() const noexcept final
+	{
+		return "Game shutdown has been requested";
+	}
+};
+
 class GameInstance final : boost::noncopyable, public IGameEngineUser
 {
 	std::unique_ptr<CServerHandler> serverInstance;
@@ -45,6 +54,7 @@ public:
 	void onGlobalLobbyInterfaceActivated() final;
 	void onUpdate() final;
 	bool capturedAllEvents() final;
+	void onShutdownRequested(bool askForConfirmation) final;
 };
 
 extern std::unique_ptr<GameInstance> GAME;

+ 1 - 1
client/NetPacksClient.cpp

@@ -418,7 +418,7 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
 	{
 		logAi->info("Red player %s. Ending game.", pack.victoryLossCheckResult.victory() ? "won" : "lost");
 
-		handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not
+		GAME->onShutdownRequested(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not
 	}
 }
 

+ 2 - 8
client/adventureMap/AdventureMapShortcuts.cpp

@@ -354,14 +354,8 @@ void AdventureMapShortcuts::quitGame()
 {
 	GAME->interface()->showYesNoDialog(
 		LIBRARY->generaltexth->allTexts[578],
-		[]()
-		{
-			ENGINE->dispatchMainThread( []()
-			{
-				handleQuit(false);
-			});
-		},
-		0
+		[](){ GAME->onShutdownRequested(false);},
+		nullptr
 		);
 }
 

+ 5 - 4
client/eventsSDL/InputHandler.cpp

@@ -19,6 +19,7 @@
 #include "InputSourceGameController.h"
 
 #include "../GameEngine.h"
+#include "../GameEngineUser.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/EventDispatcher.h"
 #include "../gui/MouseButton.h"
@@ -194,9 +195,9 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 	{
 		std::scoped_lock interfaceLock(ENGINE->interfaceMutex);
 #ifdef VCMI_ANDROID
-		handleQuit(false);
+		ENGINE->user().onShutdownRequested(false);
 #else
-		handleQuit(true);
+		ENGINE->user().onShutdownRequested(true);
 #endif
 		return;
 	}
@@ -206,14 +207,14 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 		{
 			// FIXME: dead code? Looks like intercepted by OS/SDL and delivered as SDL_Quit instead?
 			std::scoped_lock interfaceLock(ENGINE->interfaceMutex);
-			handleQuit(true);
+			ENGINE->user().onShutdownRequested(true);
 			return;
 		}
 
 		if(ev.key.keysym.scancode == SDL_SCANCODE_AC_BACK && !settings["input"]["handleBackRightMouseButton"].Bool())
 		{
 			std::scoped_lock interfaceLock(ENGINE->interfaceMutex);
-			handleQuit(true);
+			ENGINE->user().onShutdownRequested(true);
 			return;
 		}
 	}

+ 1 - 9
client/mainmenu/CMainMenu.cpp

@@ -64,14 +64,6 @@
 
 ISelectionScreenInfo * SEL = nullptr;
 
-static void do_quit()
-{
-	ENGINE->dispatchMainThread([]()
-	{
-		handleQuit(false);
-	});
-}
-
 CMenuScreen::CMenuScreen(const JsonNode & configNode)
 	: CWindowObject(BORDERED), config(configNode)
 {
@@ -210,7 +202,7 @@ static std::function<void()> genCommand(CMenuScreen * menu, std::vector<std::str
 			break;
 			case 4: //exit
 			{
-				return []() { CInfoWindow::showYesNoDialog(LIBRARY->generaltexth->allTexts[69], std::vector<std::shared_ptr<CComponent>>(), do_quit, 0, PlayerColor(1)); };
+				return []() { CInfoWindow::showYesNoDialog(LIBRARY->generaltexth->allTexts[69], std::vector<std::shared_ptr<CComponent>>(), [](){GAME->onShutdownRequested(false);}, 0, PlayerColor(1)); };
 			}
 			break;
 			case 5: //highscores

+ 3 - 2
client/media/CAudioBase.cpp

@@ -37,7 +37,8 @@ CAudioBase::~CAudioBase()
 	--initializationCounter;
 
 	if(initializationCounter == 0 && initializeSuccess)
+	{
 		Mix_CloseAudio();
-
-	initializeSuccess = false;
+		initializeSuccess = false;
+	}
 }

+ 1 - 3
client/media/CMusicHandler.cpp

@@ -91,7 +91,6 @@ CMusicHandler::~CMusicHandler()
 		std::scoped_lock guard(mutex);
 
 		Mix_HookMusicFinished(nullptr);
-		current->stop();
 
 		current.reset();
 		next.reset();
@@ -233,8 +232,7 @@ MusicEntry::~MusicEntry()
 
 	if(loop == 0 && Mix_FadingMusic() != MIX_NO_FADING)
 	{
-		assert(0);
-		logGlobal->error("Attempt to delete music while fading out!");
+		logGlobal->trace("Halting playback of music file %s", currentName.getOriginalName());
 		Mix_HaltMusic();
 	}
 

+ 7 - 0
client/media/CSoundHandler.cpp

@@ -56,6 +56,7 @@ CSoundHandler::~CSoundHandler()
 {
 	if(isInitialized())
 	{
+		Mix_ChannelFinished(nullptr);
 		Mix_HaltChannel(-1);
 
 		for(auto & chunk : soundChunks)
@@ -63,6 +64,12 @@ CSoundHandler::~CSoundHandler()
 			if(chunk.second.first)
 				Mix_FreeChunk(chunk.second.first);
 		}
+
+		for(auto & chunk : soundChunksRaw)
+		{
+			if(chunk.second.first)
+				Mix_FreeChunk(chunk.second.first);
+		}
 	}
 }
 

+ 0 - 3
client/render/IScreenHandler.h

@@ -25,9 +25,6 @@ public:
 	/// Updates window state after fullscreen state has been changed in settings
 	virtual void onScreenResize() = 0;
 
-	/// De-initializes window state
-	virtual void close() = 0;
-
 	/// Fills screen with black color, erasing any existing content
 	virtual void clearScreen() = 0;
 

+ 8 - 4
client/renderSDL/SDLImage.cpp

@@ -20,10 +20,10 @@
 #include "../GameEngine.h"
 #include "../render/IScreenHandler.h"
 
+#include "../../lib/AsyncRunner.h"
 #include "../../lib/CConfigHandler.h"
 
 #include <tbb/parallel_for.h>
-#include <tbb/task_arena.h>
 
 #include <SDL_image.h>
 #include <SDL_surface.h>
@@ -256,8 +256,6 @@ std::shared_ptr<const ISharedImage> SDLImageShared::scaleInteger(int factor, SDL
 
 SDLImageShared::SDLImageShared(const SDLImageShared * from, int integerScaleFactor, EScalingAlgorithm algorithm)
 {
-	static tbb::task_arena upscalingArena;
-
 	upscalingInProgress = true;
 
 	auto scaler = std::make_shared<SDLImageScaler>(from->surf, Rect(from->margins, from->fullSize), true);
@@ -273,7 +271,7 @@ SDLImageShared::SDLImageShared(const SDLImageShared * from, int integerScaleFact
 	};
 
 	if(settings["video"]["asyncUpscaling"].Bool())
-		upscalingArena.enqueue(scalingTask);
+		ENGINE->async().run(scalingTask);
 	else
 		scalingTask();
 }
@@ -401,6 +399,9 @@ std::shared_ptr<const ISharedImage> SDLImageShared::horizontalFlip() const
 	ret->margins.y = fullSize.y - surf->h - margins.y;
 	ret->fullSize = fullSize;
 
+	// erase our own reference
+	SDL_FreeSurface(flipped);
+
 	return ret;
 }
 
@@ -419,6 +420,9 @@ std::shared_ptr<const ISharedImage> SDLImageShared::verticalFlip() const
 	ret->margins.y = margins.y;
 	ret->fullSize = fullSize;
 
+	// erase our own reference
+	SDL_FreeSurface(flipped);
+
 	return ret;
 }
 

+ 1 - 1
client/renderSDL/ScreenHandler.cpp

@@ -595,7 +595,7 @@ void ScreenHandler::destroyWindow()
 	}
 }
 
-void ScreenHandler::close()
+ScreenHandler::~ScreenHandler()
 {
 	if(settings["general"]["notifications"].Bool())
 		NotificationHandler::destroy();

+ 1 - 3
client/renderSDL/ScreenHandler.h

@@ -96,13 +96,11 @@ public:
 
 	/// Creates and initializes screen, window and SDL state
 	ScreenHandler();
+	~ScreenHandler();
 
 	/// Updates and potentially recreates target screen to match selected fullscreen status
 	void onScreenResize() final;
 
-	/// De-initializes and destroys screen, window and SDL state
-	void close() final;
-
 	/// Fills screen with black color, erasing any existing content
 	void clearScreen() final;
 

+ 2 - 5
client/windows/settings/SettingsMainWindow.cpp

@@ -125,12 +125,9 @@ void SettingsMainWindow::quitGameButtonCallback()
 		[this]()
 		{
 			close();
-			ENGINE->dispatchMainThread( []()
-			{
-				handleQuit(false);
-			});
+			ENGINE->user().onShutdownRequested(false);
 		},
-		0
+		nullptr
 	);
 }
 

+ 42 - 111
clientapp/EntryPoint.cpp

@@ -17,24 +17,19 @@
 #include "../client/CMT.h"
 #include "../client/CPlayerInterface.h"
 #include "../client/CServerHandler.h"
-#include "../client/eventsSDL/InputHandler.h"
 #include "../client/GameEngine.h"
 #include "../client/GameInstance.h"
 #include "../client/gui/CursorHandler.h"
 #include "../client/gui/WindowHandler.h"
 #include "../client/mainmenu/CMainMenu.h"
-#include "../client/media/CEmptyVideoPlayer.h"
-#include "../client/media/CMusicHandler.h"
-#include "../client/media/CSoundHandler.h"
-#include "../client/media/CVideoHandler.h"
 #include "../client/render/Graphics.h"
 #include "../client/render/IRenderHandler.h"
-#include "../client/render/IScreenHandler.h"
-#include "../client/lobby/CBonusSelection.h"
 #include "../client/windows/CMessage.h"
 #include "../client/windows/InfoWindows.h"
 
+#include "../lib/AsyncRunner.h"
 #include "../lib/CConsoleHandler.h"
+#include "../lib/CConfigHandler.h"
 #include "../lib/CThreadHelper.h"
 #include "../lib/ExceptionsCommon.h"
 #include "../lib/filesystem/Filesystem.h"
@@ -42,7 +37,6 @@
 #include "../lib/modding/IdentifierStorage.h"
 #include "../lib/modding/CModHandler.h"
 #include "../lib/modding/ModDescription.h"
-#include "../lib/texts/CGeneralTextHandler.h"
 #include "../lib/texts/MetaString.h"
 #include "../lib/GameLibrary.h"
 #include "../lib/VCMIDirs.h"
@@ -68,20 +62,13 @@ namespace po_style = boost::program_options::command_line_style;
 static std::atomic<bool> headlessQuit = false;
 static std::optional<std::string> criticalInitializationError;
 
-#ifndef VCMI_IOS
-void processCommand(const std::string &message);
-#endif
-[[noreturn]] static void quitApplication();
-static void mainLoop();
-
-static CBasicLogConfigurator *logConfig;
-
 static void init()
 {
-	CStopWatch tmh;
 	try
 	{
-		loadDLLClasses();
+		CStopWatch tmh;
+		LIBRARY->initializeLibrary();
+		logGlobal->info("Initializing VCMI_Lib: %d ms", tmh.getDiff());
 	}
 	catch (const DataLoadingException & e)
 	{
@@ -89,8 +76,6 @@ static void init()
 		return;
 	}
 
-	logGlobal->info("Initializing VCMI_Lib: %d ms", tmh.getDiff());
-
 	// Debug code to load all maps on start
 	//ClientCommandManager commandController;
 	//commandController.processCommand("translate maps", false);
@@ -241,7 +226,8 @@ int main(int argc, char * argv[])
 	// Init filesystem and settings
 	try
 	{
-		preinitDLL(false);
+		LIBRARY = new GameLibrary;
+		LIBRARY->initializeFilesystem(false);
 	}
 	catch (const DataLoadingException & e)
 	{
@@ -249,14 +235,14 @@ int main(int argc, char * argv[])
 	}
 
 	Settings session = settings.write["session"];
-	auto setSettingBool = [&](std::string key, std::string arg) {
+	auto setSettingBool = [&](const std::string & key, const std::string & arg) {
 		Settings s = settings.write(vstd::split(key, "/"));
 		if(vm.count(arg))
 			s->Bool() = true;
 		else if(s->isNull())
 			s->Bool() = false;
 	};
-	auto setSettingInteger = [&](std::string key, std::string arg, si64 defaultValue) {
+	auto setSettingInteger = [&](const std::string & key, const std::string & arg, si64 defaultValue) {
 		Settings s = settings.write(vstd::split(key, "/"));
 		if(vm.count(arg))
 			s->Integer() = vm[arg].as<si64>();
@@ -294,7 +280,7 @@ int main(int argc, char * argv[])
 	logGlobal->debug("settings = %s", settings.toJsonNode().toString());
 
 	// Some basic data validation to produce better error messages in cases of incorrect install
-	auto testFile = [](std::string filename, std::string message)
+	auto testFile = [](const std::string & filename, const std::string & message)
 	{
 		if (!CResourceHandler::get()->existsResource(ResourcePath(filename)))
 			handleFatalError(message, false);
@@ -308,13 +294,13 @@ int main(int argc, char * argv[])
 
 	srand ( (unsigned int)time(nullptr) );
 
-	ENGINE = std::make_unique<GameEngine>();
-
 	if(!settings["session"]["headless"].Bool())
-		ENGINE->init();
+		ENGINE = std::make_unique<GameEngine>();
 
 	GAME = std::make_unique<GameInstance>();
-	ENGINE->setEngineUser(GAME.get());
+
+	if (ENGINE)
+		ENGINE->setEngineUser(GAME.get());
 	
 #ifndef VCMI_NO_THREADED_LOAD
 	//we can properly play intro only in the main thread, so we have to move loading to the separate thread
@@ -345,7 +331,7 @@ int main(int argc, char * argv[])
 		handleFatalError(criticalInitializationError.value(), false);
 	}
 
-	if(!settings["session"]["headless"].Bool())
+	if (ENGINE)
 	{
 		pomtime.getDiff();
 		graphics = new Graphics(); // should be before curh
@@ -387,56 +373,32 @@ int main(int argc, char * argv[])
 			GAME->mainmenu()->playMusic();
 	}
 	
-	std::vector<std::string> names;
-
-	if(!settings["session"]["headless"].Bool())
-	{
-		checkForModLoadingFailure();
-		mainLoop();
-	}
-	else
-	{
-		while(!headlessQuit)
-			std::this_thread::sleep_for(std::chrono::milliseconds(200));
-
-		std::this_thread::sleep_for(std::chrono::milliseconds(500));
-
-		quitApplication();
-	}
-
-	return 0;
-}
-
-static void mainLoop()
-{
 #ifndef VCMI_UNIX
 	// on Linux, name of main thread is also name of our process. Which we don't want to change
 	setThreadName("MainGUI");
 #endif
 
-	while(1) //main SDL events loop
+	try
 	{
-		ENGINE->input().fetchEvents();
-		ENGINE->renderFrame();
-	}
-}
+		if (ENGINE)
+		{
+			checkForModLoadingFailure();
+			ENGINE->mainLoop();
+		}
+		else
+		{
+			while(!headlessQuit)
+				std::this_thread::sleep_for(std::chrono::milliseconds(200));
 
-[[noreturn]] static void quitApplicationImmediately(int error_code)
-{
-	// Perform quick exit without executing static destructors and let OS cleanup anything that we did not
-	// We generally don't care about them and this leads to numerous issues, e.g.
-	// destruction of locked mutexes (fails an assertion), even in third-party libraries (as well as native libs on Android)
-	// Android - std::quick_exit is available only starting from API level 21
-	// Mingw, macOS and iOS - std::quick_exit is unavailable (at least in current version of CI)
-#if (defined(__ANDROID_API__) && __ANDROID_API__ < 21) || (defined(__MINGW32__)) || defined(VCMI_APPLE)
-	::exit(error_code);
-#else
-	std::quick_exit(error_code);
-#endif
-}
+			std::this_thread::sleep_for(std::chrono::milliseconds(500));
+		}
+	}
+	catch (const GameShutdownException & )
+	{
+		// no-op - just break out of main loop
+		logGlobal->info("Main loop termination requested");
+	}
 
-[[noreturn]] static void quitApplication()
-{
 	GAME->server().endNetwork();
 
 	if(!settings["session"]["headless"].Bool())
@@ -444,7 +406,8 @@ static void mainLoop()
 		if(GAME->server().client)
 			GAME->server().endGameplay();
 
-		ENGINE->windows().clear();
+		if (ENGINE)
+			ENGINE->windows().clear();
 	}
 
 	GAME.reset();
@@ -452,51 +415,19 @@ static void mainLoop()
 	if(!settings["session"]["headless"].Bool())
 	{
 		CMessage::dispose();
-
 		vstd::clear_pointer(graphics);
 	}
 
-	vstd::clear_pointer(LIBRARY);
-
-	// sometimes leads to a hang. TODO: investigate
-	//vstd::clear_pointer(console);// should be removed after everything else since used by logging
+	// must be executed before reset - since unique_ptr resets pointer to null before calling destructor
+	ENGINE->async().wait();
 
-	if(!settings["session"]["headless"].Bool())
-		ENGINE->screenHandler().close();
+	ENGINE.reset();
 
-	if(logConfig != nullptr)
-	{
-		logConfig->deconfigure();
-		delete logConfig;
-		logConfig = nullptr;
-	}
-
-	//ENGINE.reset();
+	vstd::clear_pointer(LIBRARY);
+	logConfigurator.deconfigure();
 
 	std::cout << "Ending...\n";
-	quitApplicationImmediately(0);
-}
-
-void handleQuit(bool ask)
-{
-	if(!ask)
-	{
-		if(settings["session"]["headless"].Bool())
-		{
-			headlessQuit = true;
-		}
-		else
-		{
-			quitApplication();
-		}
-
-		return;
-	}
-
-	if (GAME->interface())
-		GAME->interface()->showYesNoDialog(LIBRARY->generaltexth->allTexts[69], quitApplication, nullptr);
-	else
-		CInfoWindow::showYesNoDialog(LIBRARY->generaltexth->allTexts[69], {}, quitApplication, {}, PlayerColor(1));
+	return 0;
 }
 
 /// Notify user about encountered fatal error and terminate the game
@@ -513,5 +444,5 @@ void handleFatalError(const std::string & message, bool terminate)
 	if (terminate)
 		throw std::runtime_error(message);
 	else
-		quitApplicationImmediately(1);
+		::exit(1);
 }

+ 44 - 0
lib/AsyncRunner.h

@@ -0,0 +1,44 @@
+/*
+ * AsyncRunner.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include <tbb/task_arena.h>
+#include <tbb/task_group.h>
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+/// Helper class for running asynchronous tasks using TBB thread pool
+class AsyncRunner : boost::noncopyable
+{
+	tbb::task_arena arena;
+	tbb::task_group taskGroup;
+
+public:
+	/// Runs the provided functor asynchronously on a thread from the TBB worker pool.
+	template<typename Functor>
+	void run(Functor && f)
+	{
+		arena.enqueue(taskGroup.defer(std::forward<Functor>(f)));
+	}
+
+	/// Waits for all previously enqueued task.
+	/// Re-entrable - waiting for tasks does not prevent submitting new tasks
+	void wait()
+	{
+		taskGroup.wait();
+	}
+
+	~AsyncRunner()
+	{
+		wait();
+	}
+};
+
+VCMI_LIB_NAMESPACE_END

+ 4 - 2
lib/CConsoleHandler.cpp

@@ -251,8 +251,9 @@ int CConsoleHandler::run()
 				if ( cb )
 					cb(buffer, false);
 		}
-		else
-			std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+		std::unique_lock guard(shutdownMutex);
+		shutdownVariable.wait_for(guard, std::chrono::seconds(1));
 
 		if (shutdownPending)
 			return -1;
@@ -308,6 +309,7 @@ void CConsoleHandler::end()
 	{
 #ifndef _MSC_VER
 		shutdownPending = true;
+		shutdownVariable.notify_all();
 #else
 		TerminateThread(thread.native_handle(),0);
 #endif

+ 2 - 0
lib/CConsoleHandler.h

@@ -94,6 +94,8 @@ private:
 	//function to be called when message is received - string: message, bool: whether call was made from in-game console
 	std::function<void(const std::string &, bool)> cb;
 
+	std::condition_variable shutdownVariable;
+	std::mutex shutdownMutex;
 	std::atomic<bool> shutdownPending = false;
 
 	std::mutex smx;

+ 1 - 0
lib/CMakeLists.txt

@@ -693,6 +693,7 @@ set(lib_MAIN_HEADERS
 
 	AI_Base.h
 	ArtifactUtils.h
+	AsyncRunner.h
 	BattleFieldHandler.h
 	CAndroidVMHelper.h
 	CArtHandler.h

+ 13 - 27
lib/GameLibrary.cpp

@@ -46,20 +46,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 GameLibrary * LIBRARY = nullptr;
 
-DLL_LINKAGE void preinitDLL(bool extractArchives)
-{
-	LIBRARY = new GameLibrary();
-	LIBRARY->loadFilesystem(extractArchives);
-	settings.init("config/settings.json", "vcmi:settings");
-	persistentStorage.init("config/persistentStorage.json", "");
-	LIBRARY->loadModFilesystem();
 
-}
-
-DLL_LINKAGE void loadDLLClasses(bool onlyEssential)
-{
-	LIBRARY->init(onlyEssential);
-}
 
 const ArtifactService * GameLibrary::artifacts() const
 {
@@ -160,12 +147,21 @@ void GameLibrary::loadModFilesystem()
 	logGlobal->info("\tMod filesystems: %d ms", loadTime.getDiff());
 }
 
-template <class Handler> void createHandler(std::shared_ptr<Handler> & handler)
+template <class Handler>
+void createHandler(std::unique_ptr<Handler> & handler)
+{
+	handler = std::make_unique<Handler>();
+}
+
+void GameLibrary::initializeFilesystem(bool extractArchives)
 {
-	handler = std::make_shared<Handler>();
+	loadFilesystem(extractArchives);
+	settings.init("config/settings.json", "vcmi:settings");
+	persistentStorage.init("config/persistentStorage.json", "");
+	loadModFilesystem();
 }
 
-void GameLibrary::init(bool onlyEssential)
+void GameLibrary::initializeLibrary()
 {
 	createHandler(settingsHandler);
 	modh->initializeConfig();
@@ -194,7 +190,7 @@ void GameLibrary::init(bool onlyEssential)
 	createHandler(obstacleHandler);
 
 	modh->load();
-	modh->afterLoad(onlyEssential);
+	modh->afterLoad();
 }
 
 #if SCRIPTING_ENABLED
@@ -207,14 +203,4 @@ void GameLibrary::scriptsLoaded()
 GameLibrary::GameLibrary() = default;
 GameLibrary::~GameLibrary() = default;
 
-std::shared_ptr<CContentHandler> GameLibrary::getContent() const
-{
-	return modh->content;
-}
-
-void GameLibrary::setContent(std::shared_ptr<CContentHandler> content)
-{
-	modh->content = std::move(content);
-}
-
 VCMI_LIB_NAMESPACE_END

+ 31 - 33
lib/GameLibrary.h

@@ -51,10 +51,6 @@ namespace scripting
 /// Loads and constructs several handlers
 class DLL_LINKAGE GameLibrary final : public Services
 {
-
-	std::shared_ptr<CContentHandler> getContent() const;
-	void setContent(std::shared_ptr<CContentHandler> content);
-
 public:
 	const ArtifactService * artifacts() const override;
 	const CreatureService * creatures() const override;
@@ -76,38 +72,44 @@ public:
 	const IBonusTypeHandler * getBth() const; //deprecated
 	const CIdentifierStorage * identifiers() const;
 
-	std::shared_ptr<CArtHandler> arth;
-	std::shared_ptr<CBonusTypeHandler> bth;
-	std::shared_ptr<CHeroHandler> heroh;
-	std::shared_ptr<CHeroClassHandler> heroclassesh;
-	std::shared_ptr<CCreatureHandler> creh;
-	std::shared_ptr<CSpellHandler> spellh;
-	std::shared_ptr<CSkillHandler> skillh;
+	std::unique_ptr<CArtHandler> arth;
+	std::unique_ptr<CBonusTypeHandler> bth;
+	std::unique_ptr<CHeroHandler> heroh;
+	std::unique_ptr<CHeroClassHandler> heroclassesh;
+	std::unique_ptr<CCreatureHandler> creh;
+	std::unique_ptr<CSpellHandler> spellh;
+	std::unique_ptr<CSkillHandler> skillh;
 	// TODO: Remove ObjectHandler altogether?
-	std::shared_ptr<CObjectHandler> objh;
-	std::shared_ptr<CObjectClassesHandler> objtypeh;
-	std::shared_ptr<CTownHandler> townh;
-	std::shared_ptr<CGeneralTextHandler> generaltexth;
-	std::shared_ptr<CModHandler> modh;
-	std::shared_ptr<TerrainTypeHandler> terrainTypeHandler;
-	std::shared_ptr<RoadTypeHandler> roadTypeHandler;
-	std::shared_ptr<RiverTypeHandler> riverTypeHandler;
-	std::shared_ptr<CIdentifierStorage> identifiersHandler;
-	std::shared_ptr<CTerrainViewPatternConfig> terviewh;
-	std::shared_ptr<CRmgTemplateStorage> tplh;
-	std::shared_ptr<BattleFieldHandler> battlefieldsHandler;
-	std::shared_ptr<ObstacleHandler> obstacleHandler;
-	std::shared_ptr<GameSettings> settingsHandler;
-	std::shared_ptr<ObstacleSetHandler> biomeHandler;
+	std::unique_ptr<CObjectHandler> objh;
+	std::unique_ptr<CObjectClassesHandler> objtypeh;
+	std::unique_ptr<CTownHandler> townh;
+	std::unique_ptr<CGeneralTextHandler> generaltexth;
+	std::unique_ptr<CModHandler> modh;
+	std::unique_ptr<TerrainTypeHandler> terrainTypeHandler;
+	std::unique_ptr<RoadTypeHandler> roadTypeHandler;
+	std::unique_ptr<RiverTypeHandler> riverTypeHandler;
+	std::unique_ptr<CIdentifierStorage> identifiersHandler;
+	std::unique_ptr<CTerrainViewPatternConfig> terviewh;
+	std::unique_ptr<CRmgTemplateStorage> tplh;
+	std::unique_ptr<BattleFieldHandler> battlefieldsHandler;
+	std::unique_ptr<ObstacleHandler> obstacleHandler;
+	std::unique_ptr<GameSettings> settingsHandler;
+	std::unique_ptr<ObstacleSetHandler> biomeHandler;
 
 #if SCRIPTING_ENABLED
-	std::shared_ptr<scripting::ScriptHandler> scriptHandler;
+	std::unique_ptr<scripting::ScriptHandler> scriptHandler;
 #endif
 
-	GameLibrary(); //c-tor, loads .lods and NULLs handlers
+	GameLibrary();
 	~GameLibrary();
-	void init(bool onlyEssential); //uses standard config file
 
+	/// initializes settings and filesystem
+	void initializeFilesystem(bool extractArchives);
+
+	/// Loads all game entities
+	void initializeLibrary();
+
+private:
 	// basic initialization. should be called before init(). Can also extract original H3 archives
 	void loadFilesystem(bool extractArchives);
 	void loadModFilesystem();
@@ -119,8 +121,4 @@ public:
 
 extern DLL_LINKAGE GameLibrary * LIBRARY;
 
-DLL_LINKAGE void preinitDLL(bool extractArchives);
-DLL_LINKAGE void loadDLLClasses(bool onlyEssential = false);
-
-
 VCMI_LIB_NAMESPACE_END

+ 1 - 1
lib/mapObjects/CGCreature.h

@@ -27,7 +27,7 @@ public:
 		COMPLIANT = 0, FRIENDLY = 1, AGGRESSIVE = 2, HOSTILE = 3, SAVAGE = 4
 	};
 
-	ui32 identifier; //unique code for this monster (used in missions)
+	ui32 identifier = -1; //unique code for this monster (used in missions)
 	si8 character = 0; //character of this set of creatures (0 - the most friendly, 4 - the most hostile) => on init changed to -4 (compliant) ... 10 value (savage)
 	MetaString message; //message printed for attacking hero
 	TResources resources; // resources given to hero that has won with monsters

+ 1 - 1
lib/modding/CModHandler.cpp

@@ -309,7 +309,7 @@ void CModHandler::load()
 	logMod->info("\tAll game content loaded");
 }
 
-void CModHandler::afterLoad(bool onlyEssential)
+void CModHandler::afterLoad()
 {
 	JsonNode modSettings;
 	for (const auto & modEntry : getActiveMods())

+ 1 - 1
lib/modding/CModHandler.h

@@ -62,7 +62,7 @@ public:
 
 	/// load content from all available mods
 	void load();
-	void afterLoad(bool onlyEssential);
+	void afterLoad();
 
 	CModHandler();
 	~CModHandler();

+ 8 - 12
mapeditor/mainwindow.cpp

@@ -69,16 +69,6 @@ QPixmap pixmapFromJson(const QJsonValue &val)
   return p;
 }
 
-void init()
-{
-	loadDLLClasses();
-
-	Settings config = settings.write["session"]["editor"];
-	config->Bool() = true;
-
-	logGlobal->info("Initializing VCMI_Lib");
-}
-
 void MainWindow::loadUserSettings()
 {
 	//load window settings
@@ -190,7 +180,8 @@ MainWindow::MainWindow(QWidget* parent) :
 	logGlobal->info("The log file will be saved to %s", logPath);
 
 	//init
-	preinitDLL(extractionOptions.extractArchives);
+	LIBRARY = new GameLibrary();
+	LIBRARY->initializeFilesystem(extractionOptions.extractArchives);
 
 	// Initialize logging based on settings
 	logConfig->configure();
@@ -250,7 +241,12 @@ MainWindow::MainWindow(QWidget* parent) :
 	loadUserSettings(); //For example window size
 	setTitle();
 
-	init();
+	LIBRARY->initializeLibrary();
+
+	Settings config = settings.write["session"]["editor"];
+	config->Bool() = true;
+
+	logGlobal->info("Initializing VCMI_Lib");
 
 	graphics = new Graphics(); // should be before curh->init()
 	graphics->load();//must be after Content loading but should be in main thread

+ 3 - 2
serverapp/EntryPoint.cpp

@@ -78,10 +78,11 @@ int main(int argc, const char * argv[])
 
 	boost::program_options::variables_map opts;
 	handleCommandOptions(argc, argv, opts);
-	preinitDLL(false);
+	LIBRARY = new GameLibrary;
+	LIBRARY->initializeFilesystem(false);
 	logConfigurator.configure();
 
-	loadDLLClasses();
+	LIBRARY->initializeLibrary();
 	std::srand(static_cast<uint32_t>(time(nullptr)));
 
 	{

+ 3 - 2
test/CVcmiTestConfig.cpp

@@ -22,8 +22,9 @@
 
 void CVcmiTestConfig::SetUp()
 {
-	preinitDLL(true);
-	loadDLLClasses(true);
+	LIBRARY = new GameLibrary;
+	LIBRARY->initializeFilesystem(false);
+	LIBRARY->initializeLibrary();
 
 	/* TEST_DATA_DIR may be wrong, if yes below test don't run,
 	find your test data folder in your build and change TEST_DATA_DIR for it*/