瀏覽代碼

Merge pull request #1 from vcmi/develop

Update branch
George King 7 月之前
父節點
當前提交
4a0d8a2015
共有 81 個文件被更改,包括 683 次插入973 次删除
  1. 20 0
      .editorconfig
  2. 6 7
      AI/Nullkiller/AIGateway.cpp
  3. 4 3
      AI/Nullkiller/AIGateway.h
  4. 6 7
      AI/VCAI/VCAI.cpp
  5. 2 4
      AI/VCAI/VCAI.h
  6. 0 1
      client/CMT.h
  7. 0 2
      client/CServerHandler.cpp
  8. 28 27
      client/GameEngine.cpp
  9. 15 5
      client/GameEngine.h
  10. 3 0
      client/GameEngineUser.h
  11. 19 0
      client/GameInstance.cpp
  12. 10 0
      client/GameInstance.h
  13. 1 1
      client/NetPacksClient.cpp
  14. 2 8
      client/adventureMap/AdventureMapShortcuts.cpp
  15. 5 4
      client/eventsSDL/InputHandler.cpp
  16. 6 6
      client/lobby/SelectionTab.cpp
  17. 1 9
      client/mainmenu/CMainMenu.cpp
  18. 13 1
      client/mainmenu/CPrologEpilogVideo.cpp
  19. 3 0
      client/mainmenu/CPrologEpilogVideo.h
  20. 3 2
      client/media/CAudioBase.cpp
  21. 1 3
      client/media/CMusicHandler.cpp
  22. 7 0
      client/media/CSoundHandler.cpp
  23. 0 3
      client/render/IScreenHandler.h
  24. 8 4
      client/renderSDL/SDLImage.cpp
  25. 6 1
      client/renderSDL/ScreenHandler.cpp
  26. 1 3
      client/renderSDL/ScreenHandler.h
  27. 9 3
      client/windows/CSpellWindow.cpp
  28. 69 16
      client/windows/GUIClasses.cpp
  29. 2 0
      client/windows/GUIClasses.h
  30. 2 5
      client/windows/settings/SettingsMainWindow.cpp
  31. 42 111
      clientapp/EntryPoint.cpp
  32. 6 1
      config/schemas/settings.json
  33. 12 0
      launcher/settingsView/csettingsview_moc.cpp
  34. 1 6
      launcher/settingsView/csettingsview_moc.h
  35. 77 57
      launcher/settingsView/csettingsview_moc.ui
  36. 44 0
      lib/AsyncRunner.h
  37. 1 11
      lib/CArtifactInstance.h
  38. 4 2
      lib/CConsoleHandler.cpp
  39. 2 0
      lib/CConsoleHandler.h
  40. 1 0
      lib/CMakeLists.txt
  41. 3 33
      lib/CPlayerState.h
  42. 13 27
      lib/GameLibrary.cpp
  43. 31 33
      lib/GameLibrary.h
  44. 3 31
      lib/StartInfo.h
  45. 0 3
      lib/VCMIDirs.cpp
  46. 18 8
      lib/battle/BattleHexArray.h
  47. 1 10
      lib/bonuses/Limiters.h
  48. 13 32
      lib/campaign/CampaignState.h
  49. 0 9
      lib/gameState/CGameState.cpp
  50. 1 11
      lib/gameState/CGameState.h
  51. 5 12
      lib/gameState/GameStatistics.h
  52. 1 4
      lib/mapObjects/CBank.h
  53. 1 1
      lib/mapObjects/CGCreature.h
  54. 0 8
      lib/mapObjects/CGHeroInstance.h
  55. 1 55
      lib/mapObjects/CGMarket.h
  56. 0 6
      lib/mapObjects/CGObjectInstance.h
  57. 4 34
      lib/mapObjects/CGTownInstance.h
  58. 0 5
      lib/mapObjects/IMarket.h
  59. 1 10
      lib/mapObjects/TownBuildingInstance.h
  60. 2 22
      lib/mapping/CMap.h
  61. 5 55
      lib/mapping/CMapDefines.h
  62. 5 16
      lib/mapping/CMapHeader.h
  63. 4 0
      lib/mapping/CMapInfo.cpp
  64. 1 1
      lib/modding/CModHandler.cpp
  65. 1 1
      lib/modding/CModHandler.h
  66. 1 2
      lib/networkPacks/PacksForClient.h
  67. 2 7
      lib/rewardable/Configuration.h
  68. 1 2
      lib/rewardable/Reward.h
  69. 13 25
      lib/serializer/BinaryDeserializer.h
  70. 15 26
      lib/serializer/BinarySerializer.h
  71. 1 40
      lib/serializer/ESerializationVersion.h
  72. 0 22
      lib/serializer/SerializerReflection.cpp
  73. 1 1
      lib/spells/effects/Catapult.cpp
  74. 25 22
      lib/texts/Languages.h
  75. 1 39
      lib/texts/TextLocalizationContainer.h
  76. 40 18
      lib/texts/TextOperations.cpp
  77. 11 2
      lib/texts/TextOperations.h
  78. 8 12
      mapeditor/mainwindow.cpp
  79. 2 11
      server/processors/TurnOrderProcessor.h
  80. 3 2
      serverapp/EntryPoint.cpp
  81. 3 2
      test/CVcmiTestConfig.cpp

+ 20 - 0
.editorconfig

@@ -0,0 +1,20 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = tab
+insert_final_newline = true
+spelling_language = en-US
+trim_trailing_whitespace = true
+
+[{*.py,CMakePresets.json}]
+indent_style = space
+indent_size = 4
+
+[*.{md,yml}]
+indent_style = space
+indent_size = 2
+
+[*.ui]
+indent_style = space
+indent_size = 1

+ 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;
 		}
 	}

+ 6 - 6
client/lobby/SelectionTab.cpp

@@ -60,7 +60,7 @@ bool mapSorter::operator()(const std::shared_ptr<ElementInfo> aaa, const std::sh
 		{
 			if(boost::algorithm::starts_with(aaa->folderName, "..") || boost::algorithm::starts_with(bbb->folderName, ".."))
 				return boost::algorithm::starts_with(aaa->folderName, "..");
-			return boost::ilexicographical_compare(aaa->folderName, bbb->folderName);
+			return TextOperations::compareLocalizedStrings(aaa->folderName, bbb->folderName);
 		}
 	}
 
@@ -115,13 +115,13 @@ bool mapSorter::operator()(const std::shared_ptr<ElementInfo> aaa, const std::sh
 			return (a->victoryIconIndex < b->victoryIconIndex);
 			break;
 		case _name: //by name
-			return boost::ilexicographical_compare(a->name.toString(), b->name.toString());
+			return TextOperations::compareLocalizedStrings(aaa->name, bbb->name);
 		case _fileName: //by filename
-			return boost::ilexicographical_compare(aaa->fileURI, bbb->fileURI);
+			return TextOperations::compareLocalizedStrings(aaa->fileURI, bbb->fileURI);
 		case _changeDate: //by changedate
 			return aaa->lastWrite < bbb->lastWrite;
 		default:
-			return boost::ilexicographical_compare(a->name.toString(), b->name.toString());
+			return TextOperations::compareLocalizedStrings(aaa->name, bbb->name);
 		}
 	}
 	else //if we are sorting campaigns
@@ -131,9 +131,9 @@ bool mapSorter::operator()(const std::shared_ptr<ElementInfo> aaa, const std::sh
 		case _numOfMaps: //by number of maps in campaign
 			return aaa->campaign->scenariosCount() < bbb->campaign->scenariosCount();
 		case _name: //by name
-			return boost::ilexicographical_compare(aaa->campaign->getNameTranslated(), bbb->campaign->getNameTranslated());
+			return TextOperations::compareLocalizedStrings(aaa->campaign->getNameTranslated(), bbb->campaign->getNameTranslated());
 		default:
-			return boost::ilexicographical_compare(aaa->campaign->getNameTranslated(), bbb->campaign->getNameTranslated());
+			return TextOperations::compareLocalizedStrings(aaa->campaign->getNameTranslated(), bbb->campaign->getNameTranslated());
 		}
 	}
 }

+ 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

+ 13 - 1
client/mainmenu/CPrologEpilogVideo.cpp

@@ -14,6 +14,7 @@
 #include "../media/IMusicPlayer.h"
 #include "../media/ISoundPlayer.h"
 #include "../GameEngine.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/VideoWidget.h"
 #include "../widgets/Images.h"
@@ -92,7 +93,7 @@ void CPrologEpilogVideo::show(Canvas & to)
 		text->showAll(to); // blit text over video, if needed
 }
 
-void CPrologEpilogVideo::clickPressed(const Point & cursorPosition)
+void CPrologEpilogVideo::exit()
 {
 	ENGINE->music().setVolume(ENGINE->music().getVolume() * 2); // restore background volume
 	close();
@@ -102,3 +103,14 @@ void CPrologEpilogVideo::clickPressed(const Point & cursorPosition)
 	if(exitCb)
 		exitCb();
 }
+
+void CPrologEpilogVideo::clickPressed(const Point & cursorPosition)
+{
+	exit();
+}
+
+void CPrologEpilogVideo::keyPressed(EShortcut key)
+{
+	if(key == EShortcut::GLOBAL_RETURN)
+		exit();
+}

+ 3 - 0
client/mainmenu/CPrologEpilogVideo.h

@@ -30,6 +30,8 @@ class CPrologEpilogVideo : public CWindowObject
 	std::shared_ptr<VideoWidget> videoPlayer;
 	std::shared_ptr<CFilledTexture> backgroundAroundMenu;
 
+	void exit();
+
 	bool voiceStopped = false;
 
 public:
@@ -37,5 +39,6 @@ public:
 
 	void tick(uint32_t msPassed) override;
 	void clickPressed(const Point & cursorPosition) override;
+	void keyPressed(EShortcut key) override;
 	void show(Canvas & to) override;
 };

+ 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;
 }
 

+ 6 - 1
client/renderSDL/ScreenHandler.cpp

@@ -212,6 +212,11 @@ ScreenHandler::ScreenHandler()
 	else
 		SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight");
 
+#ifdef VCMI_IOS
+	if(!settings["general"]["ignoreMuteSwitch"].Bool())
+		SDL_SetHint(SDL_HINT_AUDIO_CATEGORY, "AVAudioSessionCategoryAmbient");
+#endif
+
 	if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER))
 	{
 		logGlobal->error("Something was wrong: %s", SDL_GetError());
@@ -595,7 +600,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;
 

+ 9 - 3
client/windows/CSpellWindow.cpp

@@ -92,7 +92,7 @@ public:
 				return false;
 		}
 
-		return A->getNameTranslated() < B->getNameTranslated();
+		return TextOperations::compareLocalizedStrings(A->getNameTranslated(), B->getNameTranslated());
 	}
 };
 
@@ -241,12 +241,18 @@ void CSpellWindow::processSpells()
 	mySpells.reserve(LIBRARY->spellh->objects.size());
 	for(auto const & spell : LIBRARY->spellh->objects)
 	{
-		bool searchTextFound = !searchBox || TextOperations::textSearchSimilar(searchBox->getText(), spell->getNameTranslated());
+		bool searchTextFound = !searchBox || TextOperations::textSearchSimilarityScore(searchBox->getText(), spell->getNameTranslated());
 
 		if(onSpellSelect)
 		{
-			if(spell->isCombat() == openOnBattleSpells && !spell->isSpecial() && !spell->isCreatureAbility() && searchTextFound && (showAllSpells->isSelected() || myHero->canCastThisSpell(spell.get())))
+			if(spell->isCombat() == openOnBattleSpells
+				&& !spell->isSpecial()
+				&& !spell->isCreatureAbility()
+				&& searchTextFound
+				&& (showAllSpells->isSelected() || myHero->canCastThisSpell(spell.get())))
+			{
 				mySpells.push_back(spell.get());
+			}
 			continue;
 		}
 

+ 69 - 16
client/windows/GUIClasses.cpp

@@ -39,6 +39,7 @@
 #include "../render/Canvas.h"
 #include "../render/IRenderHandler.h"
 #include "../render/IImage.h"
+#include "../render/IFont.h"
 
 #include "../../CCallback.h"
 
@@ -1531,7 +1532,11 @@ CObjectListWindow::CObjectListWindow(const std::vector<int> & _items, std::share
 	items.reserve(_items.size());
 
 	for(int id : _items)
-		items.push_back(std::make_pair(id, GAME->interface()->cb->getObjInstance(ObjectInstanceID(id))->getObjectName()));
+	{
+		std::string objectName = GAME->interface()->cb->getObjInstance(ObjectInstanceID(id))->getObjectName();
+		trimTextIfTooWide(objectName, id);
+		items.emplace_back(id, objectName);
+	}
 	itemsVisible = items;
 
 	init(titleWidget_, _title, _descr, searchBoxEnabled);
@@ -1550,8 +1555,12 @@ CObjectListWindow::CObjectListWindow(const std::vector<std::string> & _items, st
 
 	items.reserve(_items.size());
 
-	for(size_t i=0; i<_items.size(); i++)
-		items.push_back(std::make_pair(int(i), _items[i]));
+	for(size_t i = 0; i < _items.size(); i++)
+	{
+		std::string objectName = _items[i];
+		trimTextIfTooWide(objectName, static_cast<int>(i));
+		items.emplace_back(static_cast<int>(i), objectName);
+	}
 	itemsVisible = items;
 
 	init(titleWidget_, _title, _descr, searchBoxEnabled);
@@ -1570,7 +1579,7 @@ void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::stri
 	{
 		addChild(titleWidget.get());
 		titleWidget->pos.x = pos.w/2 + pos.x - titleWidget->pos.w/2;
-		titleWidget->pos.y =75 + pos.y - titleWidget->pos.h/2;
+		titleWidget->pos.y = 75 + pos.y - titleWidget->pos.h/2;
 	}
 	list = std::make_shared<CListBox>(std::bind(&CObjectListWindow::genItem, this, _1),
 		Point(14, 151), Point(0, 25), 9, itemsVisible.size(), 0, 1, Rect(262, -32, 256, 256) );
@@ -1590,21 +1599,64 @@ void CObjectListWindow::init(std::shared_ptr<CIntObject> titleWidget_, std::stri
 	searchBoxDescription = std::make_shared<CLabel>(r.center().x, r.center().y, FONT_SMALL, ETextAlignment::CENTER, grayedColor, LIBRARY->generaltexth->translate("vcmi.spellBook.search"));
 
 	searchBox = std::make_shared<CTextInput>(r, FONT_SMALL, ETextAlignment::CENTER, true);
-	searchBox->setCallback([this](const std::string & text){
-		searchBoxDescription->setEnabled(text.empty());
+	searchBox->setCallback(std::bind(&CObjectListWindow::itemsSearchCallback, this, std::placeholders::_1));
+}
+
+void CObjectListWindow::trimTextIfTooWide(std::string & text, int id) const
+{
+	int maxWidth = pos.w - 60;	// 60 px for scrollbar and borders
+	std::string idStr = '(' + std::to_string(id) + ')';
+	const auto & font = ENGINE->renderHandler().loadFont(FONT_SMALL);
+	std::string suffix = " ... " + idStr;
+
+	if(font->getStringWidth(text) >= maxWidth)
+	{
+		logGlobal->warn("Mapobject name '%s' is too long and probably needs to be fixed! Trimming...", 
+			text.substr(0, text.size() - idStr.size() + 1));
+
+		// Trim text until it fits
+		while(!text.empty())
+		{
+			std::string trimmedText = text + suffix;
+
+			if(font->getStringWidth(trimmedText) < maxWidth)
+				break;
+
+			TextOperations::trimRightUnicode(text);
+		}
 
-		itemsVisible.clear();
-		for(auto & item : items)
-			if(TextOperations::textSearchSimilar(text, item.second))
-				itemsVisible.push_back(item);
+		text += suffix;
+	}
+}
+
+void CObjectListWindow::itemsSearchCallback(const std::string & text)
+{
+	searchBoxDescription->setEnabled(text.empty());
 
-		selected = 0;
-		list->resize(itemsVisible.size());
-		list->scrollTo(0);
-		ok->block(!itemsVisible.size());
+	itemsVisible.clear();
+	std::vector<std::pair<int, decltype(items)::value_type>> rankedItems; // Store (score, item)
+
+	for(const auto & item : items)
+	{
+		if(auto score = TextOperations::textSearchSimilarityScore(text, item.second)) // Keep only relevant items
+			rankedItems.emplace_back(score.value(), item);
+	}
 
-		redraw();
+	// Sort: Lower score is better match
+	std::sort(rankedItems.begin(), rankedItems.end(), [](const auto & a, const auto & b)
+	{
+		return a.first < b.first;
 	});
+
+	for(const auto & rankedItem : rankedItems)
+		itemsVisible.push_back(rankedItem.second);
+
+	selected = 0;
+	list->resize(itemsVisible.size());
+	list->scrollTo(0);
+	ok->block(!itemsVisible.size());
+
+	redraw();
 }
 
 std::shared_ptr<CIntObject> CObjectListWindow::genItem(size_t index)
@@ -1734,7 +1786,8 @@ void VideoWindow::clickPressed(const Point & cursorPosition)
 
 void VideoWindow::keyPressed(EShortcut key)
 {
-	exit(true);
+	if(key == EShortcut::GLOBAL_RETURN)
+		exit(true);
 }
 
 void VideoWindow::notFocusedClick()

+ 2 - 0
client/windows/GUIClasses.h

@@ -197,6 +197,8 @@ class CObjectListWindow : public CWindowObject
 	std::vector< std::pair<int, std::string> > itemsVisible; //visible items present in list
 
 	void init(std::shared_ptr<CIntObject> titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled);
+	void trimTextIfTooWide(std::string & text, int id) const; // trim item's text to fit within window's width
+	void itemsSearchCallback(const std::string & text);
 	void exitPressed();
 public:
 	size_t selected;//index of currently selected item

+ 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);
 }

+ 6 - 1
config/schemas/settings.json

@@ -45,7 +45,8 @@
 				"audioMuteFocus",
 				"enableOverlay",
 				"lastKindomInterface",
-				"enableSubtitle"
+				"enableSubtitle",
+				"ignoreMuteSwitch"
 			],
 			"properties" : {
 				"playerName" : {
@@ -161,6 +162,10 @@
 				"enableSubtitle" : {
 					"type": "boolean",
 					"default": true
+				},
+				"ignoreMuteSwitch" : {
+					"type": "boolean",
+					"default": true
 				}
 			}
 		},

+ 12 - 0
launcher/settingsView/csettingsview_moc.cpp

@@ -130,6 +130,10 @@ void CSettingsView::loadSettings()
 	ui->labelHandleBackRightMouseButton->hide();
 	ui->buttonAllowPortrait->hide();
 	ui->labelAllowPortrait->hide();
+#endif
+#ifndef VCMI_IOS
+	ui->labelIgnoreMuteSwitch->hide();
+	ui->buttonIgnoreMuteSwitch->hide();
 #endif
 	fillValidScalingRange();
 
@@ -224,6 +228,8 @@ void CSettingsView::loadToggleButtonSettings()
 
 	setCheckbuttonState(ui->buttonHandleBackRightMouseButton, settings["input"]["handleBackRightMouseButton"].Bool());
 
+	setCheckbuttonState(ui->buttonIgnoreMuteSwitch, settings["general"]["ignoreMuteSwitch"].Bool());
+
 	std::string cursorType = settings["video"]["cursor"].String();
 	int cursorTypeIndex = vstd::find_pos(cursorTypesList, cursorType);
 	setCheckbuttonState(ui->buttonCursorType, cursorTypeIndex);
@@ -866,3 +872,9 @@ void CSettingsView::on_buttonHandleBackRightMouseButton_toggled(bool checked)
 	updateCheckbuttonText(ui->buttonHandleBackRightMouseButton);
 }
 
+void CSettingsView::on_buttonIgnoreMuteSwitch_toggled(bool checked)
+{
+	Settings node = settings.write["general"]["ignoreMuteSwitch"];
+	node->Bool() = checked;
+	updateCheckbuttonText(ui->buttonIgnoreMuteSwitch);
+}

+ 1 - 6
launcher/settingsView/csettingsview_moc.h

@@ -91,18 +91,13 @@ private slots:
 	void on_buttonFontScalable_clicked(bool checked);
 	void on_buttonFontOriginal_clicked(bool checked);
 
-
 	void on_buttonValidationOff_clicked(bool checked);
-
 	void on_buttonValidationBasic_clicked(bool checked);
-
 	void on_buttonValidationFull_clicked(bool checked);
-
 	void on_sliderScalingCursor_valueChanged(int value);
-
 	void on_buttonScalingAuto_toggled(bool checked);
-
 	void on_buttonHandleBackRightMouseButton_toggled(bool checked);
+	void on_buttonIgnoreMuteSwitch_toggled(bool checked);
 
 private:
 	Ui::CSettingsView * ui;

+ 77 - 57
launcher/settingsView/csettingsview_moc.ui

@@ -69,7 +69,7 @@
          </property>
         </widget>
        </item>
-       <item row="62" column="0">
+       <item row="63" column="0">
         <widget class="QLabel" name="labelModsValidation">
          <property name="text">
           <string>Mods Validation</string>
@@ -105,7 +105,7 @@
          </item>
         </widget>
        </item>
-       <item row="45" column="1" colspan="5">
+       <item row="46" column="1" colspan="5">
         <widget class="QSlider" name="sliderControllerSticksSensitivity">
          <property name="minimum">
           <number>500</number>
@@ -133,7 +133,7 @@
          </property>
         </widget>
        </item>
-       <item row="37" column="1" colspan="5">
+       <item row="38" column="1" colspan="5">
         <widget class="QPushButton" name="pushButtonResetTutorialTouchscreen">
          <property name="text">
           <string>Reset</string>
@@ -147,7 +147,7 @@
          </property>
         </widget>
        </item>
-       <item row="41" column="1" colspan="5">
+       <item row="42" column="1" colspan="5">
         <widget class="QSlider" name="sliderLongTouchDuration">
          <property name="minimum">
           <number>500</number>
@@ -172,7 +172,7 @@
          </property>
         </widget>
        </item>
-       <item row="56" column="1">
+       <item row="57" column="1">
         <widget class="QToolButton" name="buttonAutoCheck">
          <property name="enabled">
           <bool>true</bool>
@@ -239,7 +239,7 @@
          </property>
         </widget>
        </item>
-       <item row="57" column="1">
+       <item row="58" column="1">
         <widget class="QToolButton" name="buttonRepositoryDefault">
          <property name="enabled">
           <bool>true</bool>
@@ -268,14 +268,14 @@
          </property>
         </widget>
        </item>
-       <item row="40" column="0">
+       <item row="41" column="0">
         <widget class="QLabel" name="labelHapticFeedback">
          <property name="text">
           <string>Haptic Feedback</string>
          </property>
         </widget>
        </item>
-       <item row="43" column="0">
+       <item row="44" column="0">
         <widget class="QLabel" name="labelToleranceDistanceTouch">
          <property name="text">
           <string>Touch Tap Tolerance</string>
@@ -288,7 +288,7 @@
        <item row="1" column="1" colspan="5">
         <widget class="QComboBox" name="comboBoxLanguage"/>
        </item>
-       <item row="60" column="1" colspan="5">
+       <item row="61" column="1" colspan="5">
         <widget class="QSpinBox" name="spinBoxNetworkPortLobby">
          <property name="minimum">
           <number>1024</number>
@@ -301,7 +301,7 @@
          </property>
         </widget>
        </item>
-       <item row="62" column="5">
+       <item row="63" column="5">
         <widget class="QToolButton" name="buttonValidationFull">
          <property name="enabled">
           <bool>true</bool>
@@ -326,7 +326,7 @@
          </attribute>
         </widget>
        </item>
-       <item row="44" column="0">
+       <item row="45" column="0">
         <widget class="QLabel" name="labelInputMouse_3">
          <property name="font">
           <font>
@@ -379,7 +379,7 @@
          </property>
         </widget>
        </item>
-       <item row="49" column="1" colspan="5">
+       <item row="50" column="1" colspan="5">
         <widget class="QComboBox" name="comboBoxEnemyPlayerAI">
          <property name="currentText">
           <string notr="true">VCAI</string>
@@ -396,7 +396,7 @@
          </item>
         </widget>
        </item>
-       <item row="57" column="2" colspan="4">
+       <item row="58" column="2" colspan="4">
         <widget class="QLineEdit" name="lineEditRepositoryDefault">
          <property name="text">
           <string notr="true"/>
@@ -406,7 +406,7 @@
          </property>
         </widget>
        </item>
-       <item row="50" column="1" colspan="5">
+       <item row="51" column="1" colspan="5">
         <widget class="QComboBox" name="comboBoxAlliedPlayerAI">
          <property name="currentText">
           <string notr="true">VCAI</string>
@@ -430,7 +430,7 @@
          </property>
         </widget>
        </item>
-       <item row="48" column="0">
+       <item row="49" column="0">
         <widget class="QLabel" name="labelArtificialIntelligence">
          <property name="font">
           <font>
@@ -445,7 +445,7 @@
          </property>
         </widget>
        </item>
-       <item row="57" column="0">
+       <item row="58" column="0">
         <widget class="QLabel" name="labelRepositoryDefault">
          <property name="text">
           <string>Default repository</string>
@@ -471,7 +471,7 @@
          </property>
         </widget>
        </item>
-       <item row="39" column="0">
+       <item row="40" column="0">
         <widget class="QLabel" name="labelRelativeCursorSpeed">
          <property name="text">
           <string>Relative Pointer Speed</string>
@@ -485,7 +485,7 @@
          </property>
         </widget>
        </item>
-       <item row="55" column="1" colspan="5">
+       <item row="56" column="1" colspan="5">
         <widget class="QToolButton" name="buttonIgnoreSslErrors">
          <property name="enabled">
           <bool>true</bool>
@@ -588,7 +588,7 @@
          </property>
         </widget>
        </item>
-       <item row="58" column="1">
+       <item row="59" column="1">
         <widget class="QToolButton" name="buttonRepositoryExtra">
          <property name="sizePolicy">
           <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
@@ -607,21 +607,21 @@
          </property>
         </widget>
        </item>
-       <item row="56" column="2" colspan="4">
+       <item row="57" column="2" colspan="4">
         <widget class="QPushButton" name="refreshRepositoriesButton">
          <property name="text">
           <string>Refresh now</string>
          </property>
         </widget>
        </item>
-       <item row="50" column="0">
+       <item row="51" column="0">
         <widget class="QLabel" name="labelAlliedPlayerAI">
          <property name="text">
           <string>Adventure Map Allies</string>
          </property>
         </widget>
        </item>
-       <item row="51" column="0">
+       <item row="52" column="0">
         <widget class="QLabel" name="labelNeutralAI">
          <property name="text">
           <string>Neutral AI in battles</string>
@@ -659,7 +659,7 @@
          </property>
         </widget>
        </item>
-       <item row="59" column="1" colspan="5">
+       <item row="60" column="1" colspan="5">
         <widget class="QLineEdit" name="lineEditGameLobbyHost">
          <property name="text">
           <string notr="true"/>
@@ -673,7 +673,7 @@
          </property>
         </widget>
        </item>
-       <item row="38" column="0">
+       <item row="39" column="0">
         <widget class="QLabel" name="labelRelativeCursorMode">
          <property name="text">
           <string>Use Relative Pointer Mode</string>
@@ -683,14 +683,14 @@
        <item row="7" column="1" colspan="5">
         <widget class="QSpinBox" name="spinBoxAutoSaveLimit"/>
        </item>
-       <item row="52" column="0">
+       <item row="53" column="0">
         <widget class="QLabel" name="labelFriendlyAI">
          <property name="text">
           <string>Autocombat AI in battles</string>
          </property>
         </widget>
        </item>
-       <item row="62" column="1" colspan="2">
+       <item row="63" column="1" colspan="2">
         <widget class="QToolButton" name="buttonValidationOff">
          <property name="enabled">
           <bool>true</bool>
@@ -715,7 +715,7 @@
          </attribute>
         </widget>
        </item>
-       <item row="33" column="0">
+       <item row="34" column="0">
         <widget class="QLabel" name="labelInputMouse">
          <property name="font">
           <font>
@@ -737,7 +737,7 @@
          </property>
         </widget>
        </item>
-       <item row="40" column="1" colspan="5">
+       <item row="41" column="1" colspan="5">
         <widget class="QToolButton" name="buttonHapticFeedback">
          <property name="sizePolicy">
           <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
@@ -768,7 +768,7 @@
          </property>
         </widget>
        </item>
-       <item row="58" column="0">
+       <item row="59" column="0">
         <widget class="QLabel" name="labelRepositoryExtra">
          <property name="text">
           <string>Additional repository</string>
@@ -791,7 +791,7 @@
          </property>
         </widget>
        </item>
-       <item row="49" column="0">
+       <item row="50" column="0">
         <widget class="QLabel" name="labelEnemyPlayerAI">
          <property name="text">
           <string>Adventure Map Enemies</string>
@@ -819,7 +819,7 @@
          </property>
         </widget>
        </item>
-       <item row="36" column="0">
+       <item row="37" column="0">
         <widget class="QLabel" name="labelInputMouse_2">
          <property name="font">
           <font>
@@ -853,7 +853,7 @@
        <item row="24" column="1">
         <widget class="QLabel" name="labelScalingCursorValue"/>
        </item>
-       <item row="38" column="1" colspan="5">
+       <item row="39" column="1" colspan="5">
         <widget class="QToolButton" name="buttonRelativeCursorMode">
          <property name="sizePolicy">
           <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
@@ -876,28 +876,28 @@
          </property>
         </widget>
        </item>
-       <item row="59" column="0">
+       <item row="60" column="0">
         <widget class="QLabel" name="labelGameLobbyHost">
          <property name="text">
           <string>Online Lobby address</string>
          </property>
         </widget>
        </item>
-       <item row="60" column="0">
+       <item row="61" column="0">
         <widget class="QLabel" name="labelNetworkPortLobby">
          <property name="text">
           <string>Online Lobby port</string>
          </property>
         </widget>
        </item>
-       <item row="47" column="0">
+       <item row="48" column="0">
         <widget class="QLabel" name="labelToleranceDistanceController">
          <property name="text">
           <string>Controller Click Tolerance</string>
          </property>
         </widget>
        </item>
-       <item row="51" column="1" colspan="5">
+       <item row="52" column="1" colspan="5">
         <widget class="QComboBox" name="comboBoxNeutralAI">
          <property name="currentText">
           <string notr="true">BattleAI</string>
@@ -933,7 +933,7 @@
          </item>
         </widget>
        </item>
-       <item row="55" column="0">
+       <item row="56" column="0">
         <widget class="QLabel" name="labelIgnoreSslErrors">
          <property name="text">
           <string>Ignore SSL errors</string>
@@ -943,14 +943,14 @@
        <item row="21" column="1" colspan="5">
         <widget class="QComboBox" name="comboBoxDisplayIndex"/>
        </item>
-       <item row="37" column="0">
+       <item row="38" column="0">
         <widget class="QLabel" name="labelResetTutorialTouchscreen">
          <property name="text">
           <string>Show Tutorial again</string>
          </property>
         </widget>
        </item>
-       <item row="46" column="0">
+       <item row="47" column="0">
         <widget class="QLabel" name="labelControllerSticksAcceleration">
          <property name="text">
           <string>Sticks Acceleration</string>
@@ -972,7 +972,7 @@
          </property>
         </widget>
        </item>
-       <item row="53" column="1" colspan="5">
+       <item row="54" column="1" colspan="5">
         <widget class="QComboBox" name="comboBoxEnemyAI">
          <property name="editable">
           <bool>false</bool>
@@ -1006,21 +1006,21 @@
          </property>
         </widget>
        </item>
-       <item row="34" column="0">
+       <item row="35" column="0">
         <widget class="QLabel" name="labelToleranceDistanceMouse">
          <property name="text">
           <string>Mouse Click Tolerance</string>
          </property>
         </widget>
        </item>
-       <item row="35" column="0">
+       <item row="36" column="0">
         <widget class="QLabel" name="labelHandleBackRightMouseButton">
          <property name="text">
           <string>Handle back as right mouse button</string>
          </property>
         </widget>
        </item>
-       <item row="35" column="1" colspan="5">
+       <item row="36" column="1" colspan="5">
         <widget class="QToolButton" name="buttonHandleBackRightMouseButton">
          <property name="sizePolicy">
           <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
@@ -1036,7 +1036,7 @@
          </property>
         </widget>
        </item>
-       <item row="53" column="0">
+       <item row="54" column="0">
         <widget class="QLabel" name="labelEnemyAI">
          <property name="text">
           <string>Enemy AI in battles</string>
@@ -1050,7 +1050,7 @@
          </property>
         </widget>
        </item>
-       <item row="43" column="1" colspan="5">
+       <item row="44" column="1" colspan="5">
         <widget class="QSlider" name="sliderToleranceDistanceTouch">
          <property name="minimum">
           <number>0</number>
@@ -1092,7 +1092,7 @@
          </property>
         </widget>
        </item>
-       <item row="52" column="1" colspan="5">
+       <item row="53" column="1" colspan="5">
         <widget class="QComboBox" name="comboBoxFriendlyAI">
          <property name="editable">
           <bool>false</bool>
@@ -1112,7 +1112,7 @@
          </item>
         </widget>
        </item>
-       <item row="58" column="2" colspan="4">
+       <item row="59" column="2" colspan="4">
         <widget class="QLineEdit" name="lineEditRepositoryExtra">
          <property name="text">
           <string notr="true"/>
@@ -1164,7 +1164,7 @@
          </attribute>
         </widget>
        </item>
-       <item row="54" column="0">
+       <item row="55" column="0">
         <widget class="QLabel" name="labelNetwork">
          <property name="font">
           <font>
@@ -1179,7 +1179,7 @@
          </property>
         </widget>
        </item>
-       <item row="61" column="0">
+       <item row="62" column="0">
         <widget class="QLabel" name="labelMiscellaneous">
          <property name="font">
           <font>
@@ -1194,7 +1194,7 @@
          </property>
         </widget>
        </item>
-       <item row="46" column="1" colspan="5">
+       <item row="47" column="1" colspan="5">
         <widget class="QSlider" name="sliderControllerSticksAcceleration">
          <property name="minimum">
           <number>100</number>
@@ -1266,21 +1266,21 @@ Fullscreen Exclusive Mode - the game will cover the entirety of your screen and
          </property>
         </widget>
        </item>
-       <item row="41" column="0">
+       <item row="42" column="0">
         <widget class="QLabel" name="labelLongTouchDuration">
          <property name="text">
           <string>Long Touch Duration</string>
          </property>
         </widget>
        </item>
-       <item row="56" column="0">
+       <item row="57" column="0">
         <widget class="QLabel" name="labelAutoCheck">
          <property name="text">
           <string>Check on startup</string>
          </property>
         </widget>
        </item>
-       <item row="47" column="1" colspan="5">
+       <item row="48" column="1" colspan="5">
         <widget class="QSlider" name="sliderToleranceDistanceController">
          <property name="minimum">
           <number>0</number>
@@ -1324,7 +1324,7 @@ Fullscreen Exclusive Mode - the game will cover the entirety of your screen and
          </property>
         </widget>
        </item>
-       <item row="39" column="1" colspan="5">
+       <item row="40" column="1" colspan="5">
         <widget class="QSlider" name="sliderRelativeCursorSpeed">
          <property name="minimum">
           <number>100</number>
@@ -1366,7 +1366,7 @@ Fullscreen Exclusive Mode - the game will cover the entirety of your screen and
          </property>
         </widget>
        </item>
-       <item row="34" column="1" colspan="5">
+       <item row="35" column="1" colspan="5">
         <widget class="QSlider" name="slideToleranceDistanceMouse">
          <property name="minimum">
           <number>0</number>
@@ -1439,7 +1439,7 @@ Fullscreen Exclusive Mode - the game will cover the entirety of your screen and
          </property>
         </widget>
        </item>
-       <item row="45" column="0">
+       <item row="46" column="0">
         <widget class="QLabel" name="labelControllerSticksSensitivity">
          <property name="text">
           <string>Sticks Sensitivity</string>
@@ -1470,7 +1470,7 @@ Fullscreen Exclusive Mode - the game will cover the entirety of your screen and
        <item row="12" column="1" colspan="5">
         <widget class="QComboBox" name="comboBoxResolution"/>
        </item>
-       <item row="62" column="3" colspan="2">
+       <item row="63" column="3" colspan="2">
         <widget class="QToolButton" name="buttonValidationBasic">
          <property name="enabled">
           <bool>true</bool>
@@ -1495,6 +1495,26 @@ Fullscreen Exclusive Mode - the game will cover the entirety of your screen and
          </attribute>
         </widget>
        </item>
+       <item row="33" column="0">
+        <widget class="QLabel" name="labelIgnoreMuteSwitch">
+         <property name="text">
+          <string>Ignore mute switch</string>
+         </property>
+        </widget>
+       </item>
+       <item row="33" column="1" colspan="5">
+        <widget class="QToolButton" name="buttonIgnoreMuteSwitch">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="checkable">
+          <bool>true</bool>
+         </property>
+        </widget>
+       </item>
       </layout>
      </widget>
     </widget>

+ 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

+ 1 - 11
lib/CArtifactInstance.h

@@ -94,17 +94,7 @@ public:
 	{
 		h & static_cast<CBonusSystemNode&>(*this);
 		h & static_cast<CCombinedArtifactInstance&>(*this);
-		if (h.version >= Handler::Version::REMOVE_VLC_POINTERS)
-		{
-			h & artTypeID;
-		}
-		else
-		{
-			bool isNull = false;
-			h & isNull;
-			if (!isNull)
-				h & artTypeID;
-		}
+		h & artTypeID;
 		h & id;
 		BONUS_TREE_DESERIALIZATION_FIX
 	}

+ 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

+ 3 - 33
lib/CPlayerState.h

@@ -121,24 +121,8 @@ public:
 		h & resources;
 		h & status;
 		h & turnTimer;
-
-		if (h.version >= Handler::Version::LOCAL_PLAYER_STATE_DATA)
-			h & *playerLocalSettings;
-
-		if (h.version >= Handler::Version::PLAYER_STATE_OWNED_OBJECTS)
-		{
-			h & ownedObjects;
-		}
-		else
-		{
-			std::vector<const CGObjectInstance* > heroes;
-			std::vector<const CGObjectInstance* > towns;
-			std::vector<const CGObjectInstance* > dwellings;
-
-			h & heroes;
-			h & towns;
-			h & dwellings;
-		}
+		h & *playerLocalSettings;
+		h & ownedObjects;
 		h & quests;
 		h & visitedObjects;
 		h & visitedObjectsGlobal;
@@ -173,23 +157,9 @@ public:
 	{
 		h & id;
 		h & players;
-		if (h.version < Handler::Version::REMOVE_FOG_OF_WAR_POINTER)
-		{
-			struct Helper : public Serializeable
-			{
-				void serialize(Handler &h) const
-				{}
-			};
-			Helper helper;
-			auto ptrHelper = &helper;
-			h & ptrHelper;
-		}
-
 		h & fogOfWarMap;
 		h & static_cast<CBonusSystemNode&>(*this);
-
-		if (h.version >= Handler::Version::REWARDABLE_BANKS)
-			h & scoutedObjects;
+		h & scoutedObjects;
 	}
 
 };

+ 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

+ 3 - 31
lib/StartInfo.h

@@ -51,11 +51,7 @@ struct DLL_LINKAGE SimturnsInfo
 		h & requiredTurns;
 		h & optionalTurns;
 		h & allowHumanWithAI;
-
-		if (h.version >= Handler::Version::SAVE_COMPATIBILITY_FIXES)
-			h & ignoreAlliedContacts;
-		else
-			ignoreAlliedContacts = true;
+		h & ignoreAlliedContacts;
 	}
 };
 
@@ -108,14 +104,7 @@ struct DLL_LINKAGE PlayerSettings
 		h & heroNameTextId;
 		h & bonus;
 		h & color;
-		if (h.version >= Handler::Version::PLAYER_HANDICAP)
-			h & handicap;
-		else
-		{
-			enum EHandicap {NO_HANDICAP, MILD, SEVERE};
-			EHandicap handicapLegacy = NO_HANDICAP;
-			h & handicapLegacy;
-		}
+		h & handicap;
 		h & name;
 		h & connectedPlayerIDs;
 		h & compOnly;
@@ -173,24 +162,7 @@ struct DLL_LINKAGE StartInfo : public Serializeable
 		h & mode;
 		h & difficulty;
 		h & playerInfos;
-		if (h.version < Handler::Version::REMOVE_LIB_RNG)
-		{
-			uint32_t oldSeeds = 0;
-			h & oldSeeds;
-			h & oldSeeds;
-			h & oldSeeds;
-		}
-		if (h.version < Handler::Version::FOLDER_NAME_REWORK)
-		{
-			std::string startTimeLegacy;
-			h & startTimeLegacy;
-			struct std::tm tm;
-			std::istringstream ss(startTimeLegacy);
-			ss >> std::get_time(&tm, "%Y%m%dT%H%M%S");
-			startTime = mktime(&tm);
-		}
-		else
-			h & startTime;
+		h & startTime;
 		h & fileURI;
 		h & simturnsInfo;
 		h & turnTimerInfo;

+ 0 - 3
lib/VCMIDirs.cpp

@@ -171,9 +171,6 @@ class VCMIDirsWIN32 final : public IVCMIDirs
 
 void VCMIDirsWIN32::init()
 {
-	std::locale::global(boost::locale::generator().generate("en_US.UTF-8"));
-	boost::filesystem::path::imbue(std::locale());
-
 	// Call base (init dirs)
 	IVCMIDirs::init();
 

+ 18 - 8
lib/battle/BattleHexArray.h

@@ -77,7 +77,7 @@ public:
 
 	void insert(const BattleHex & hex) noexcept
 	{
-		if(contains(hex))
+		if(!isValidToInsert(hex))
 			return;
 
 		presenceFlags.set(hex.toInt());
@@ -86,6 +86,9 @@ public:
 
 	void set(size_type index, const BattleHex & hex)
 	{
+		if(!isValidToInsert(hex))
+			return;
+
 		if(index >= internalStorage.size())
 		{
 			logGlobal->error("Invalid BattleHexArray::set index parameter. It is " + std::to_string(index)
@@ -94,9 +97,6 @@ public:
 				+ " and current size is " + std::to_string(internalStorage.size()));
 		}
 
-		if(contains(hex))
-			return;
-
 		presenceFlags.set(hex.toInt());
 		internalStorage[index] = hex;
 	}
@@ -198,7 +198,7 @@ public:
 	{
 		static const BattleHexArray invalid;
 
-		if (hex.isValid())
+		if(hex.isValid())
 			return allNeighbouringTiles[hex.toInt()];
 		else
 			return invalid;
@@ -209,7 +209,7 @@ public:
 	{
 		static const BattleHexArray invalid;
 
-		if (hex.isValid())
+		if(hex.isValid())
 			return neighbouringTiles[hex.toInt()];
 		else
 			return invalid;
@@ -223,13 +223,23 @@ public:
 		return neighbouringTilesDoubleWide.at(side)[hex.toInt()];
 	}
 
-	/// note: returns true when param is ivalid BattleHex
+	[[nodiscard]] inline bool isValidToInsert(const BattleHex & hex) const noexcept
+	{
+		if(!hex.isValid())
+			return false;
+
+		if(contains(hex))
+			return false;
+
+		return true;
+	}
+
 	[[nodiscard]] inline bool contains(const BattleHex & hex) const noexcept
 	{
 		if(hex.isValid())
 			return presenceFlags.test(hex.toInt());
 		
-		return true;
+		return false;
 	}
 
 	template <typename Serializer>

+ 1 - 10
lib/bonuses/Limiters.h

@@ -108,16 +108,7 @@ public:
 	template <typename Handler> void serialize(Handler &h)
 	{
 		h & static_cast<ILimiter&>(*this);
-
-		if (h.version < Handler::Version::REMOVE_TOWN_PTR)
-		{
-			bool isNull = false;
-			h & isNull;
-			if(!isNull)
-				h & creatureID;
-		}
-		else
-			h & creatureID;
+		h & creatureID;
 		h & includeUpgrades;
 	}
 };

+ 13 - 32
lib/campaign/CampaignState.h

@@ -46,16 +46,8 @@ class DLL_LINKAGE CampaignRegions
 		template <typename Handler> void serialize(Handler &h)
 		{
 			h & infix;
-			if (h.version >= Handler::Version::REGION_LABEL)
-			{
-				h & pos;
-				h & labelPos;
-			}
-			else
-			{
-				h & pos.x;
-				h & pos.y;
-			}
+			h & pos;
+			h & labelPos;
 		}
 
 		static CampaignRegions::RegionDescription fromJson(const JsonNode & node);
@@ -79,11 +71,8 @@ public:
 		h & campPrefix;
 		h & colorSuffixLength;
 		h & regions;
-		if (h.version >= Handler::Version::CAMPAIGN_REGIONS)
-		{
-			h & campSuffix;
-			h & campBackground;
-		}
+		h & campSuffix;
+		h & campBackground;
 	}
 
 	static CampaignRegions fromJson(const JsonNode & node);
@@ -150,27 +139,20 @@ public:
 		h & numberOfScenarios;
 		h & name;
 		h & description;
-		if (h.version >= Handler::Version::MAP_FORMAT_ADDITIONAL_INFOS)
-		{
-			h & author;
-			h & authorContact;
-			h & campaignVersion;
-			h & creationDateTime;
-		}
+		h & author;
+		h & authorContact;
+		h & campaignVersion;
+		h & creationDateTime;
 		h & difficultyChosenByPlayer;
 		h & filename;
 		h & modName;
 		h & music;
 		h & encoding;
 		h & textContainer;
-		if (h.version >= Handler::Version::CHRONICLES_SUPPORT)
-		{
-			h & loadingBackground;
-			h & videoRim;
-			h & introVideo;
-		}
-		if (h.version >= Handler::Version::CAMPAIGN_OUTRO_SUPPORT)
-			h & outroVideo;
+		h & loadingBackground;
+		h & videoRim;
+		h & introVideo;
+		h & outroVideo;
 	}
 };
 
@@ -374,8 +356,7 @@ public:
 		h & chosenCampaignBonuses;
 		h & campaignSet;
 		h & mapTranslations;
-		if (h.version >= Handler::Version::HIGHSCORE_PARAMETERS)
-			h & highscoreParameters;
+		h & highscoreParameters;
 	}
 };
 

+ 0 - 9
lib/gameState/CGameState.cpp

@@ -351,15 +351,6 @@ void CGameState::initCampaign()
 	map = campaign->getCurrentMap();
 }
 
-void CGameState::generateOwnedObjectsAfterDeserialize()
-{
-	for (auto & object : map->objects)
-	{
-		if (object && object->asOwnable() && object->getOwner().isValidPlayer())
-			players.at(object->getOwner()).addOwnedObject(object.get());
-	}
-}
-
 void CGameState::initGlobalBonuses()
 {
 	const JsonNode & baseBonuses = getSettings().getValue(EGameSettings::BONUSES_GLOBAL);

+ 1 - 11
lib/gameState/CGameState.h

@@ -171,21 +171,13 @@ public:
 		h & day;
 		h & map;
 		h & players;
-		if (h.version < Handler::Version::PLAYER_STATE_OWNED_OBJECTS)
-			generateOwnedObjectsAfterDeserialize();
 		h & teams;
 		h & heroesPool;
 		h & globalEffects;
-		if (h.version < Handler::Version::REMOVE_LIB_RNG)
-		{
-			std::string oldStateOfRNG;
-			h & oldStateOfRNG;
-		}
 		h & currentRumor;
 		h & campaign;
 		h & allocatedArtifacts;
-		if (h.version >= Handler::Version::STATISTICS)
-			h & statistic;
+		h & statistic;
 
 		BONUS_TREE_DESERIALIZATION_FIX
 	}
@@ -213,8 +205,6 @@ private:
 	void initVisitingAndGarrisonedHeroes();
 	void initCampaign();
 
-	void generateOwnedObjectsAfterDeserialize();
-
 	// ----- bonus system handling -----
 
 	void buildBonusSystemTree();

+ 5 - 12
lib/gameState/GameStatistics.h

@@ -63,8 +63,7 @@ struct DLL_LINKAGE StatisticDataSetEntry
 		h & timestamp;
 		h & day;
 		h & player;
-		if(h.version >= Handler::Version::STATISTICS_SCREEN)
-			h & playerName;
+		h & playerName;
 		h & team;
 		h & isHuman;
 		h & status;
@@ -92,11 +91,8 @@ struct DLL_LINKAGE StatisticDataSetEntry
 		h & spentResourcesForArmy;
 		h & spentResourcesForBuildings;
 		h & tradeVolume;
-		if(h.version >= Handler::Version::STATISTICS_SCREEN)
-		{
-			h & eventCapturedTown;
-			h & eventDefeatedStrongestHero;
-		}
+		h & eventCapturedTown;
+		h & eventDefeatedStrongestHero;
 		h & movementPointsUsed;
 	}
 };
@@ -136,11 +132,8 @@ public:
 			h & spentResourcesForBuildings;
 			h & tradeVolume;
 			h & movementPointsUsed;
-			if(h.version >= Handler::Version::STATISTICS_SCREEN)
-			{
-				h & lastCapturedTownDay;
-				h & lastDefeatedStrongestHeroDay;
-			}
+			h & lastCapturedTownDay;
+			h & lastDefeatedStrongestHeroDay;
 		}
 	};
 	std::vector<StatisticDataSetEntry> data;

+ 1 - 4
lib/mapObjects/CBank.h

@@ -51,10 +51,7 @@ public:
 		h & bankConfig;
 		h & resetDuration;
 		h & coastVisitable;
-		if (h.version >= Handler::Version::BANK_UNIT_PLACEMENT)
-			h & regularUnitPlacement;
-		else if (!h.saving)
-			regularUnitPlacement = false;
+		h & regularUnitPlacement;
 	}
 
 	friend class CBankInstanceConstructor;

+ 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

+ 0 - 8
lib/mapObjects/CGHeroInstance.h

@@ -371,14 +371,6 @@ public:
 		h & skillsInfo;
 		h & visitedTown;
 		h & boat;
-		if (h.version < Handler::Version::REMOVE_TOWN_PTR)
-		{
-			HeroTypeID type;
-			bool isNull = false;
-			h & isNull;
-			if(!isNull)
-				h & type;
-		}
 		h & commander;
 		h & visitedObjects;
 		BONUS_TREE_DESERIALIZATION_FIX

+ 1 - 55
lib/mapObjects/CGMarket.h

@@ -36,37 +36,6 @@ public:
 	int getMarketEfficiency() const override;
 	int availableUnits(EMarketMode mode, int marketItemSerial) const override; //-1 if unlimited
 	std::set<EMarketMode> availableModes() const override;
-
-	template <typename Handler>
-	void serialize(Handler &h)
-	{
-		h & static_cast<CGObjectInstance&>(*this);
-		if (h.version < Handler::Version::NEW_MARKETS)
-		{
-			std::set<EMarketMode> marketModes;
-			h & marketModes;
-		}
-
-		if (h.version < Handler::Version::MARKET_TRANSLATION_FIX)
-		{
-			int unused = 0;
-			h & unused;
-		}
-
-		if (h.version < Handler::Version::NEW_MARKETS)
-		{
-			std::string speech;
-			std::string title;
-			h & speech;
-			h & title;
-		}
-	}
-
-	template <typename Handler> void serializeArtifactsAltar(Handler &h)
-	{
-		serialize(h);
-		IMarket::serializeArtifactsAltar(h);
-	}
 };
 
 class DLL_LINKAGE CGBlackMarket : public CGMarket
@@ -82,24 +51,7 @@ public:
 	template <typename Handler> void serialize(Handler &h)
 	{
 		h & static_cast<CGMarket&>(*this);
-		if (h.version < Handler::Version::REMOVE_VLC_POINTERS)
-		{
-			int32_t size = 0;
-			h & size;
-			for (int32_t i = 0; i < size; ++i)
-			{
-				bool isNull = false;
-				ArtifactID artifact;
-				h & isNull;
-				if (!isNull)
-					h & artifact;
-				artifacts.push_back(artifact);
-			}
-		}
-		else
-		{
-			h & artifacts;
-		}
+		h & artifacts;
 	}
 };
 
@@ -119,12 +71,6 @@ public:
 	{
 		h & static_cast<CGMarket&>(*this);
 		h & skills;
-		if (h.version >= Handler::Version::NEW_MARKETS && h.version < Handler::Version::MARKET_TRANSLATION_FIX)
-		{
-			std::string temp;
-			h & temp;
-			h & temp;
-		}
 	}
 };
 

+ 0 - 6
lib/mapObjects/CGObjectInstance.h

@@ -143,12 +143,6 @@ public:
 	template <typename Handler> void serialize(Handler &h)
 	{
 		h & instanceName;
-		if (h.version < Handler::Version::REMOVE_OBJECT_TYPENAME)
-		{
-			std::string unused;
-			h & unused;
-			h & unused;
-		}
 		h & pos;
 		h & ID;
 		subID.serializeIdentifier(h, ID);

+ 4 - 34
lib/mapObjects/CGTownInstance.h

@@ -94,43 +94,13 @@ public:
 		h & obligatorySpells;
 		h & spells;
 		h & events;
-
-		if (h.version >= Handler::Version::SPELL_RESEARCH)
-		{
-			h & spellResearchCounterDay;
-			h & spellResearchAcceptedCounter;
-			h & spellResearchAllowed;
-		}
-
-		if (h.version >= Handler::Version::NEW_TOWN_BUILDINGS)
-		{
-			h & rewardableBuildings;
-		}
-		else
-		{
-			std::vector<TownRewardableBuildingInstance*> oldVector;
-			h & oldVector;
-			rewardableBuildings = convertOldBuildings(oldVector);
-		}
-
-		if (h.version < Handler::Version::REMOVE_TOWN_PTR)
-		{
-			FactionID faction;
-			bool isNull = false;
-			h & isNull;
-			if (!isNull)
-				h & faction;
-		}
-
+		h & spellResearchCounterDay;
+		h & spellResearchAcceptedCounter;
+		h & spellResearchAllowed;
+		h & rewardableBuildings;
 		h & townAndVis;
 		BONUS_TREE_DESERIALIZATION_FIX
 
-		if (h.version < Handler::Version::NEW_TOWN_BUILDINGS)
-		{
-			std::set<BuildingID> overriddenBuildings;
-			h & overriddenBuildings;
-		}
-
 		if(!h.saving)
 			postDeserialize();
 	}

+ 0 - 5
lib/mapObjects/IMarket.h

@@ -36,11 +36,6 @@ public:
 	CArtifactSet * getArtifactsStorage() const;
 	bool getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode) const; //val1 - how many units of id1 player has to give to receive val2 units
 
-	template <typename Handler> void serializeArtifactsAltar(Handler &h)
-	{
-		h & *altarArtifactsStorage;
-	}
-
 private:
 	std::unique_ptr<CArtifactSetAltar> altarArtifactsStorage;
 };

+ 1 - 10
lib/mapObjects/TownBuildingInstance.h

@@ -43,14 +43,6 @@ public:
 	template <typename Handler> void serialize(Handler &h)
 	{
 		h & bID;
-		if (h.version < Handler::Version::NEW_TOWN_BUILDINGS)
-		{
-			// compatibility code
-			si32 indexOnTV = 0; //identifies its index on towns vector
-			BuildingSubID::EBuildingSubID bType = BuildingSubID::NONE;
-			h & indexOnTV;
-			h & bType;
-		}
 	}
 
 private:
@@ -90,8 +82,7 @@ public:
 	template <typename Handler> void serialize(Handler &h)
 	{
 		h & static_cast<TownBuildingInstance&>(*this);
-		if (h.version >= Handler::Version::NEW_TOWN_BUILDINGS)
-			h & static_cast<Rewardable::Interface&>(*this);
+		h & static_cast<Rewardable::Interface&>(*this);
 		h & visitors;
 	}
 };

+ 2 - 22
lib/mapping/CMap.h

@@ -192,31 +192,11 @@ public:
 		// static members
 		h & obeliskCount;
 		h & obelisksVisited;
-
-		if (h.version < Handler::Version::REMOVE_VLC_POINTERS)
-		{
-			int32_t size = 0;
-			h & size;
-			for (int32_t i = 0; i < size; ++i)
-			{
-				bool isNull = false;
-				ArtifactID artifact;
-				h & isNull;
-				if (!isNull)
-					h & artifact;
-				townMerchantArtifacts.push_back(artifact);
-			}
-		}
-		else
-		{
-			h & townMerchantArtifacts;
-		}
+		h & townMerchantArtifacts;
 		h & townUniversitySkills;
 
 		h & instanceNames;
-
-		if (h.version >= Handler::Version::PER_MAP_GAME_SETTINGS)
-			h & *gameSettings;
+		h & *gameSettings;
 	}
 };
 

+ 5 - 55
lib/mapping/CMapDefines.h

@@ -52,26 +52,12 @@ public:
 		h & name;
 		h & message;
 		h & resources;
-		if (h.version >= Handler::Version::EVENTS_PLAYER_SET)
-		{
-			h & players;
-		}
-		else
-		{
-			ui8 playersMask = 0;
-			h & playersMask;
-			for (int i = 0; i < 8; ++i)
-				if ((playersMask & (1 << i)) != 0)
-					players.insert(PlayerColor(i));
-		}
+		h & players;
 		h & humanAffected;
 		h & computerAffected;
 		h & firstOccurrence;
 		h & nextOccurrence;
-		if(h.version >= Handler::Version::EVENT_OBJECTS_DELETION)
-		{
-			h & deletedObjectsInstances;
-		}
+		h & deletedObjectsInstances;
 	}
 	
 	virtual void serializeJson(JsonSerializeFormat & handler);
@@ -147,49 +133,13 @@ struct DLL_LINKAGE TerrainTile
 	template <typename Handler>
 	void serialize(Handler & h)
 	{
-		if (h.version >= Handler::Version::REMOVE_VLC_POINTERS)
-		{
-			h & terrainType;
-		}
-		else
-		{
-			bool isNull = false;
-			h & isNull;
-			if (!isNull)
-				h & terrainType;
-		}
+		h & terrainType;
 		h & terView;
-		if (h.version >= Handler::Version::REMOVE_VLC_POINTERS)
-		{
-			h & riverType;
-		}
-		else
-		{
-			bool isNull = false;
-			h & isNull;
-			if (!isNull)
-				h & riverType;
-		}
+		h & riverType;
 		h & riverDir;
-		if (h.version >= Handler::Version::REMOVE_VLC_POINTERS)
-		{
-			h & roadType;
-		}
-		else
-		{
-			bool isNull = false;
-			h & isNull;
-			if (!isNull)
-				h & roadType;
-		}
+		h & roadType;
 		h & roadDir;
 		h & extTileFlags;
-		if (h.version < Handler::Version::REMOVE_VLC_POINTERS)
-		{
-			bool unused = false;
-			h & unused;
-			h & unused;
-		}
 		h & visitableObjects;
 		h & blockingObjects;
 	}

+ 5 - 16
lib/mapping/CMapHeader.h

@@ -287,25 +287,14 @@ public:
 		h & mods;
 		h & name;
 		h & description;
-		if (h.version >= Handler::Version::MAP_FORMAT_ADDITIONAL_INFOS)
-		{
-			h & author;
-			h & authorContact;
-			h & mapVersion;
-			h & creationDateTime;
-		}
+		h & author;
+		h & authorContact;
+		h & mapVersion;
+		h & creationDateTime;
 		h & width;
 		h & height;
 		h & twoLevel;
-
-		if (h.version >= Handler::Version::SAVE_COMPATIBILITY_FIXES)
-			h & difficulty;
-		else
-		{
-			uint8_t difficultyInteger = static_cast<uint8_t>(difficulty);
-			h & difficultyInteger;
-			difficulty = static_cast<EMapDifficulty>(difficultyInteger);
-		}
+		h & difficulty;
 
 		h & levelLimit;
 		h & areAnyPlayers;

+ 4 - 0
lib/mapping/CMapInfo.cpp

@@ -48,6 +48,8 @@ void CMapInfo::mapInit(const std::string & fname)
 	originalFileURI = resource.getOriginalName();
 	fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(resource)).string();
 	mapHeader = mapService.loadMapHeader(resource);
+	lastWrite = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(resource));
+	date = TextOperations::getFormattedDateTimeLocal(lastWrite);
 	countPlayers();
 }
 
@@ -76,6 +78,8 @@ void CMapInfo::campaignInit()
 	originalFileURI = resource.getOriginalName();
 	fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(resource)).string();
 	campaign = CampaignHandler::getHeader(fileURI);
+	lastWrite = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(resource));
+	date = TextOperations::getFormattedDateTimeLocal(lastWrite);
 }
 
 void CMapInfo::countPlayers()

+ 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();

+ 1 - 2
lib/networkPacks/PacksForClient.h

@@ -481,8 +481,7 @@ struct DLL_LINKAGE PlayerEndsGame : public CPackForClient
 	{
 		h & player;
 		h & victoryLossCheckResult;
-		if (h.version >= Handler::Version::STATISTICS_SCREEN)
-			h & statistic;
+		h & statistic;
 	}
 };
 

+ 2 - 7
lib/rewardable/Configuration.h

@@ -194,13 +194,8 @@ struct DLL_LINKAGE Configuration
 		h & canRefuse;
 		h & showScoutedPreview;
 		h & infoWindowType;
-		if (h.version >= Handler::Version::REWARDABLE_BANKS)
-		{
-			h & coastVisitable;
-			h & guardsLayout;
-		}
-		else
-			coastVisitable = false;
+		h & coastVisitable;
+		h & guardsLayout;
 	}
 };
 

+ 1 - 2
lib/rewardable/Reward.h

@@ -129,8 +129,7 @@ struct DLL_LINKAGE Reward final
 		h & removeObject;
 		h & manaPercentage;
 		h & movePercentage;
-		if (h.version >= Handler::Version::REWARDABLE_GUARDS)
-			h & guards;
+		h & guards;
 		h & heroExperience;
 		h & heroLevel;
 		h & manaDiff;

+ 13 - 25
lib/serializer/BinaryDeserializer.h

@@ -162,10 +162,7 @@ public:
 		else
 		{
 			static_assert(!std::is_same_v<uint64_t, T>, "Serialization of unsigned 64-bit value may not work in some cases");
-			if (hasFeature(Version::COMPACT_INTEGER_SERIALIZATION))
-				data = loadEncodedInteger();
-			else
-				this->read(static_cast<void *>(&data), sizeof(data), reverseEndianness);
+			data = loadEncodedInteger();
 		}
 	}
 
@@ -444,32 +441,23 @@ public:
 	}
 	void load(std::string &data)
 	{
-		if (hasFeature(Version::COMPACT_STRING_SERIALIZATION))
-		{
-			int32_t length;
-			load(length);
+		int32_t length;
+		load(length);
 
-			if (length < 0)
-			{
-				int32_t stringID = -length - 1; // -1, -2 ... -> 0, 1 ...
-				data = loadedStrings[stringID];
-			}
-			if (length == 0)
-			{
-				data = {};
-			}
-			if (length > 0)
-			{
-				data.resize(length);
-				this->read(static_cast<void *>(data.data()), length, false);
-				loadedStrings.push_back(data);
-			}
+		if (length < 0)
+		{
+			int32_t stringID = -length - 1; // -1, -2 ... -> 0, 1 ...
+			data = loadedStrings[stringID];
 		}
-		else
+		if (length == 0)
+		{
+			data = {};
+		}
+		if (length > 0)
 		{
-			uint32_t length = readAndCheckLength();
 			data.resize(length);
 			this->read(static_cast<void *>(data.data()), length, false);
+			loadedStrings.push_back(data);
 		}
 	}
 

+ 15 - 26
lib/serializer/BinarySerializer.h

@@ -148,10 +148,7 @@ public:
 		}
 		else
 		{
-			if (hasFeature(Version::COMPACT_INTEGER_SERIALIZATION))
-				saveEncodedInteger(data);
-			else
-				this->write(static_cast<const void *>(&data), sizeof(data));
+			saveEncodedInteger(data);
 		}
 	}
 
@@ -325,36 +322,28 @@ public:
 
 	void save(const std::string &data)
 	{
-		if (hasFeature(Version::COMPACT_STRING_SERIALIZATION))
+		if (data.empty())
 		{
-			if (data.empty())
-			{
-				save(static_cast<uint32_t>(0));
-				return;
-			}
+			save(static_cast<uint32_t>(0));
+			return;
+		}
 
-			auto it = savedStrings.find(data);
+		auto it = savedStrings.find(data);
 
-			if (it == savedStrings.end())
-			{
-				save(static_cast<uint32_t>(data.length()));
-				this->write(static_cast<const void *>(data.data()), data.size());
+		if (it == savedStrings.end())
+		{
+			save(static_cast<uint32_t>(data.length()));
+			this->write(static_cast<const void *>(data.data()), data.size());
 
-				// -1, -2...
-				int32_t newStringID = -1 - savedStrings.size();
+			// -1, -2...
+			int32_t newStringID = -1 - savedStrings.size();
 
-				savedStrings[data] = newStringID;
-			}
-			else
-			{
-				int32_t index = it->second;
-				save(index);
-			}
+			savedStrings[data] = newStringID;
 		}
 		else
 		{
-			save(static_cast<uint32_t>(data.length()));
-			this->write(static_cast<const void *>(data.data()), data.size());
+			int32_t index = it->second;
+			save(index);
 		}
 	}
 

+ 1 - 40
lib/serializer/ESerializationVersion.h

@@ -31,47 +31,8 @@ enum class ESerializationVersion : int32_t
 {
 	NONE = 0,
 
-	RELEASE_150 = 840,
-	MINIMAL = RELEASE_150,
-
-	VOTING_SIMTURNS, // 841 - allow modification of simturns duration via vote
-	REMOVE_TEXT_CONTAINER_SIZE_T, // 842 Fixed serialization of size_t from text containers
-	BANK_UNIT_PLACEMENT, // 843 Banks have unit placement flag
-
-	RELEASE_156 = BANK_UNIT_PLACEMENT,
-
-	COMPACT_STRING_SERIALIZATION, // 844 - optimized serialization of previously encountered strings
-	COMPACT_INTEGER_SERIALIZATION, // 845 - serialize integers in forms similar to protobuf
-	REMOVE_FOG_OF_WAR_POINTER, // 846 - fog of war is serialized as reference instead of pointer
-	SIMPLE_TEXT_CONTAINER_SERIALIZATION, // 847 - text container is serialized using common routine instead of custom approach
-	MAP_FORMAT_ADDITIONAL_INFOS, // 848 - serialize new infos in map format
-	REMOVE_LIB_RNG, // 849 - removed random number generators from library classes
-	HIGHSCORE_PARAMETERS, // 850 - saves parameter for campaign
-	PLAYER_HANDICAP, // 851 - player handicap selection at game start
-	STATISTICS, // 852 - removed random number generators from library classes
-	CAMPAIGN_REGIONS, // 853 - configurable campaign regions
-	EVENTS_PLAYER_SET, // 854 - map & town events use std::set instead of bitmask to store player list
-	NEW_TOWN_BUILDINGS, // 855 - old bonusing buildings have been removed
-	STATISTICS_SCREEN, // 856 - extent statistic functions
-	NEW_MARKETS, // 857 - reworked market classes
-	PLAYER_STATE_OWNED_OBJECTS, // 858 - player state stores all owned objects in a single list
-	SAVE_COMPATIBILITY_FIXES, // 859 - implementation of previoulsy postponed changes to serialization
-	CHRONICLES_SUPPORT, // 860 - support for heroes chronicles
-	PER_MAP_GAME_SETTINGS, // 861 - game settings are now stored per-map
-	CAMPAIGN_OUTRO_SUPPORT, // 862 - support for campaign outro video
-	REWARDABLE_BANKS, // 863 - team state contains list of scouted objects, coast visitable rewardable objects
-	REGION_LABEL, // 864 - labels for campaign regions
-	SPELL_RESEARCH, // 865 - spell research
-	LOCAL_PLAYER_STATE_DATA, // 866 - player state contains arbitrary client-side data
-	REMOVE_TOWN_PTR, // 867 - removed pointer to CTown from CGTownInstance
-	REMOVE_OBJECT_TYPENAME, // 868 - remove typename from CGObjectInstance
-	REMOVE_VLC_POINTERS, // 869 removed remaining pointers to LIBRARY entities
-	FOLDER_NAME_REWORK, // 870 - rework foldername
-	REWARDABLE_GUARDS, // 871 - fix missing serialization of guards in rewardable objects
-	MARKET_TRANSLATION_FIX, // 872 - remove serialization of markets translateable strings
-	EVENT_OBJECTS_DELETION, //873 - allow events to remove map objects
-
 	RELEASE_160 = 873,
+	MINIMAL = RELEASE_160,
 
 	MAP_HEADER_DISPOSED_HEROES, // map header contains disposed heroes list
 	

+ 0 - 22
lib/serializer/SerializerReflection.cpp

@@ -63,24 +63,6 @@ public:
 	}
 };
 
-class SerializerCompatibilityBonusingBuilding final : public SerializerCompatibility<TownRewardableBuildingInstance, ESerializationVersion::NEW_TOWN_BUILDINGS>
-{
-	void loadPtr(BinaryDeserializer &ar, IGameCallback * cb, Serializeable * data) const override
-	{
-		auto * realPtr = dynamic_cast<TownRewardableBuildingInstance *>(data);
-		realPtr->serialize(ar);
-	}
-};
-
-class SerializerCompatibilityArtifactsAltar final : public SerializerCompatibility<CGMarket, ESerializationVersion::NEW_MARKETS>
-{
-	void loadPtr(BinaryDeserializer &ar, IGameCallback * cb, Serializeable * data) const override
-	{
-		auto * realPtr = dynamic_cast<CGMarket *>(data);
-		realPtr->serializeArtifactsAltar(ar);
-	}
-};
-
 template<typename Type>
 void CSerializationApplier::registerType(uint16_t ID)
 {
@@ -91,10 +73,6 @@ void CSerializationApplier::registerType(uint16_t ID)
 CSerializationApplier::CSerializationApplier()
 {
 	registerTypes(*this);
-
-	apps[54].reset(new SerializerCompatibilityBonusingBuilding);
-	apps[55].reset(new SerializerCompatibilityBonusingBuilding);
-	apps[81].reset(new SerializerCompatibilityArtifactsAltar);
 }
 
 CSerializationApplier & CSerializationApplier::getInstance()

+ 1 - 1
lib/spells/effects/Catapult.cpp

@@ -263,7 +263,7 @@ void Catapult::adjustHitChance()
 	vstd::abetween(wall, 0, 100);
 	vstd::abetween(crit, 0, 100);
 	vstd::abetween(hit, 0, 100 - crit);
-	vstd::amin(noDmg, 100 - hit - crit);
+	noDmg = 100 - hit - crit;
 }
 
 void Catapult::serializeJsonEffect(JsonSerializeFormat & handler)

+ 25 - 22
lib/texts/Languages.h

@@ -66,6 +66,9 @@ struct Options
 	/// encoding that is used by H3 for this language
 	std::string encoding;
 
+	/// proper locale name, e.g. "en_US.UTF-8"
+	std::string localeName;
+
 	/// primary IETF language tag
 	std::string tagIETF;
 
@@ -86,28 +89,28 @@ inline const auto & getLanguageList()
 {
 	static const std::array<Options, 22> languages
 	{ {
-		{ "bulgarian",   "Bulgarian",   "Български",  "CP1251", "bg", "bul", "%d.%m.%Y %H:%M",    EPluralForms::EN_2, true  },
-		{ "czech",       "Czech",       "Čeština",    "CP1250", "cs", "cze", "%d.%m.%Y %H:%M",    EPluralForms::CZ_3, true  },
-		{ "chinese",     "Chinese",     "简体中文",    "GBK",    "zh", "chi", "%Y-%m-%d %H:%M",    EPluralForms::VI_1, true  }, // Note: actually Simplified Chinese
-		{ "english",     "English",     "English",    "CP1252", "en", "eng", "%Y-%m-%d %H:%M",    EPluralForms::EN_2, true  }, // English uses international date/time format here
-		{ "finnish",     "Finnish",     "Suomi",      "CP1252", "fi", "fin", "%d.%m.%Y %H:%M",    EPluralForms::EN_2, true  },
-		{ "french",      "French",      "Français",   "CP1252", "fr", "fre", "%d/%m/%Y %H:%M",    EPluralForms::FR_2, true  },
-		{ "german",      "German",      "Deutsch",    "CP1252", "de", "ger", "%d.%m.%Y %H:%M",    EPluralForms::EN_2, true  },
-		{ "greek",       "Greek",       "ελληνικά",   "CP1253", "el", "ell", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, false },
-		{ "hungarian",   "Hungarian",   "Magyar",     "CP1250", "hu", "hun", "%Y. %m. %d. %H:%M", EPluralForms::EN_2, true  },
-		{ "italian",     "Italian",     "Italiano",   "CP1250", "it", "ita", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, true  },
-		{ "japanese",    "Japanese",    "日本語",      "JIS",    "ja", "jpn", "%Y年%m月%d日 %H:%M", EPluralForms::NONE, false },
-		{ "korean",      "Korean",      "한국어",      "CP949",  "ko", "kor", "%Y-%m-%d %H:%M",    EPluralForms::VI_1, true  },
-		{ "polish",      "Polish",      "Polski",     "CP1250", "pl", "pol", "%d.%m.%Y %H:%M",    EPluralForms::PL_3, true  },
-		{ "portuguese",  "Portuguese",  "Português",  "CP1252", "pt", "por", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, true  }, // Note: actually Brazilian Portuguese
-		{ "romanian",    "Romanian",    "Română",     "CP28606","ro", "rum", "%Y-%m-%d %H:%M",    EPluralForms::RO_3, false },
-		{ "russian",     "Russian",     "Русский",    "CP1251", "ru", "rus", "%d.%m.%Y %H:%M",    EPluralForms::UK_3, true  },
-		{ "spanish",     "Spanish",     "Español",    "CP1252", "es", "spa", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, true  },
-		{ "swedish",     "Swedish",     "Svenska",    "CP1252", "sv", "swe", "%Y-%m-%d %H:%M",    EPluralForms::EN_2, true  },
-		{ "norwegian",   "Norwegian",   "Norsk",      "CP1252", "no", "nor", "%d/%m/%Y %H:%M",    EPluralForms::EN_2, false },
-		{ "turkish",     "Turkish",     "Türkçe",     "CP1254", "tr", "tur", "%d.%m.%Y %H:%M",    EPluralForms::EN_2, true  },
-		{ "ukrainian",   "Ukrainian",   "Українська", "CP1251", "uk", "ukr", "%d.%m.%Y %H:%M",    EPluralForms::UK_3, true  },
-		{ "vietnamese",  "Vietnamese",  "Tiếng Việt", "UTF-8",  "vi", "vie", "%d/%m/%Y %H:%M",    EPluralForms::VI_1, true  }, // Fan translation uses special encoding
+		{ "bulgarian",   "Bulgarian",   "Български",  "CP1251", "bg_BG.UTF-8", "bg", "bul", "%d.%m.%Y %H:%M",    EPluralForms::EN_2, true  },
+		{ "czech",       "Czech",       "Čeština",		"CP1250", "cs_CZ.UTF-8", "cs", "cze", "%d.%m.%Y %H:%M",		EPluralForms::CZ_3, true },
+		{ "chinese",     "Chinese",     "简体中文",		"GBK",	  "zh_CN.UTF-8", "zh", "chi", "%Y-%m-%d %H:%M",		EPluralForms::VI_1, true }, // Note: actually Simplified Chinese
+		{ "english",     "English",     "English",		"CP1252", "en_US.UTF-8", "en", "eng", "%Y-%m-%d %H:%M",		EPluralForms::EN_2, true }, // English uses international date/time format here
+		{ "finnish",     "Finnish",     "Suomi",		"CP1252", "fi_FI.UTF-8", "fi", "fin", "%d.%m.%Y %H:%M",		EPluralForms::EN_2, true },
+		{ "french",      "French",      "Français",		"CP1252", "fr_FR.UTF-8", "fr", "fre", "%d/%m/%Y %H:%M",		EPluralForms::FR_2, true },
+		{ "german",      "German",      "Deutsch",		"CP1252", "de_DE.UTF-8", "de", "ger", "%d.%m.%Y %H:%M",		EPluralForms::EN_2, true },
+		{ "greek",       "Greek",       "ελληνικά",		"CP1253", "el_GR.UTF-8", "el", "ell", "%d/%m/%Y %H:%M",		EPluralForms::EN_2, false },
+		{ "hungarian",   "Hungarian",   "Magyar",		"CP1250", "hu_HU.UTF-8", "hu", "hun", "%Y. %m. %d. %H:%M",  EPluralForms::EN_2, true },
+		{ "italian",     "Italian",     "Italiano",		"CP1250", "it_IT.UTF-8", "it", "ita", "%d/%m/%Y %H:%M",		EPluralForms::EN_2, true },
+		{ "japanese",    "Japanese",    "日本語",		"JIS",    "ja_JP.UTF-8", "ja", "jpn", "%Y年%m月%d日 %H:%M",	EPluralForms::NONE, false },
+		{ "korean",      "Korean",      "한국어",		"CP949",  "ko_KR.UTF-8", "ko", "kor", "%Y-%m-%d %H:%M",		EPluralForms::VI_1, true },
+		{ "polish",      "Polish",      "Polski",		"CP1250", "pl_PL.UTF-8", "pl", "pol", "%d.%m.%Y %H:%M",		EPluralForms::PL_3, true },
+		{ "portuguese",  "Portuguese",  "Português",	"CP1252", "pt_BR.UTF-8", "pt", "por", "%d/%m/%Y %H:%M",		EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese
+		{ "romanian",    "Romanian",    "Română",     "CP28606", "ro_RO.UTF-8", "ro", "rum", "%Y-%m-%d %H:%M",    EPluralForms::RO_3, false },
+		{ "russian",     "Russian",     "Русский",		"CP1251", "ru_RU.UTF-8", "ru", "rus", "%d.%m.%Y %H:%M",		EPluralForms::UK_3, true },
+		{ "spanish",     "Spanish",     "Español",		"CP1252", "es_ES.UTF-8", "es", "spa", "%d/%m/%Y %H:%M",		EPluralForms::EN_2, true },
+		{ "swedish",     "Swedish",     "Svenska",		"CP1252", "sv_SE.UTF-8", "sv", "swe", "%Y-%m-%d %H:%M",		EPluralForms::EN_2, true },
+		{ "norwegian",   "Norwegian",   "Norsk Bokmål", "UTF-8",  "nb_NO.UTF-8", "nb", "nor", "%d/%m/%Y %H:%M",		EPluralForms::EN_2, false },
+		{ "turkish",     "Turkish",     "Türkçe",		"CP1254", "tr_TR.UTF-8", "tr", "tur", "%d.%m.%Y %H:%M",		EPluralForms::EN_2, true },
+		{ "ukrainian",   "Ukrainian",   "Українська",	"CP1251", "uk_UA.UTF-8", "uk", "ukr", "%d.%m.%Y %H:%M",		EPluralForms::UK_3, true },
+		{ "vietnamese",  "Vietnamese",  "Tiếng Việt",	"UTF-8",  "vi_VN.UTF-8", "vi", "vie", "%d/%m/%Y %H:%M",		EPluralForms::VI_1, true }, // Fan translation uses special encoding
 	} };
 	static_assert(languages.size() == static_cast<size_t>(ELanguages::COUNT), "Languages array is missing a value!");
 

+ 1 - 39
lib/texts/TextLocalizationContainer.h

@@ -93,45 +93,7 @@ public:
 	void serialize(Handler & h)
 	{
 		std::lock_guard globalLock(globalTextMutex);
-
-		if (h.version >= Handler::Version::SIMPLE_TEXT_CONTAINER_SERIALIZATION)
-		{
-			h & stringsLocalizations;
-		}
-		else
-		{
-			std::string key;
-			int64_t sz = stringsLocalizations.size();
-
-			if (h.version >= Handler::Version::REMOVE_TEXT_CONTAINER_SIZE_T)
-			{
-				int64_t size = sz;
-				h & size;
-				sz = size;
-			}
-			else
-			{
-				h & sz;
-			}
-
-			if(h.saving)
-			{
-				for(auto & s : stringsLocalizations)
-				{
-					key = s.first;
-					h & key;
-					h & s.second;
-				}
-			}
-			else
-			{
-				for(size_t i = 0; i < sz; ++i)
-				{
-					h & key;
-					h & stringsLocalizations[key];
-				}
-			}
-		}
+		h & stringsLocalizations;
 	}
 };
 

+ 40 - 18
lib/texts/TextOperations.cpp

@@ -10,7 +10,8 @@
 #include "StdInc.h"
 #include "TextOperations.h"
 
-#include "texts/CGeneralTextHandler.h"
+#include "../GameLibrary.h"
+#include "../texts/CGeneralTextHandler.h"
 #include "Languages.h"
 #include "CConfigHandler.h"
 
@@ -252,7 +253,7 @@ std::string TextOperations::getCurrentFormattedDateTimeLocal(std::chrono::second
 	return TextOperations::getFormattedDateTimeLocal(std::chrono::system_clock::to_time_t(timepoint));
 }
 
-int TextOperations::getLevenshteinDistance(const std::string & s, const std::string & t)
+int TextOperations::getLevenshteinDistance(std::string_view s, std::string_view t)
 {
 	int n = t.size();
 	int m = s.size();
@@ -300,30 +301,51 @@ int TextOperations::getLevenshteinDistance(const std::string & s, const std::str
 	return v0[n];
 }
 
-bool TextOperations::textSearchSimilar(const std::string & s, const std::string & t)
+DLL_LINKAGE std::string TextOperations::getLocaleName()
 {
-	boost::locale::generator gen;
-	std::locale loc = gen("en_US.UTF-8"); // support for UTF8 lowercase
-	
+	return Languages::getLanguageOptions(LIBRARY->generaltexth->getPreferredLanguage()).localeName;
+}
+
+DLL_LINKAGE bool TextOperations::compareLocalizedStrings(std::string_view str1, std::string_view str2)
+{
+	static const std::locale loc(getLocaleName());
+	static const std::collate<char> & col = std::use_facet<std::collate<char>>(loc);
+
+	return col.compare(str1.data(), str1.data() + str1.size(),
+					   str2.data(), str2.data() + str2.size()) < 0;
+}
+
+std::optional<int> TextOperations::textSearchSimilarityScore(const std::string & s, const std::string & t)
+{
+	static const std::locale loc = boost::locale::generator().generate(getLocaleName());
+
 	auto haystack = boost::locale::to_lower(t, loc);
 	auto needle = boost::locale::to_lower(s, loc);
 
-	if(boost::algorithm::contains(haystack, needle))
-		return true;
+	// 0 - Best possible match: text starts with the search string
+	if(haystack.rfind(needle, 0) == 0)
+		return 0;
 
-	if(needle.size() > haystack.size())
-		return false;
+	// 1 - Strong match: text contains the search string
+	if(haystack.find(needle) != std::string::npos)
+		return 1;
 
-	for(int i = 0; i < haystack.size() - needle.size() + 1; i++)
+	// Dynamic threshold: Reject if too many typos based on word length
+	int maxAllowedDistance = std::max(2, static_cast<int>(needle.size() / 2));
+
+	// Compute Levenshtein distance for fuzzy similarity
+	int minDist = std::numeric_limits<int>::max();
+	for(size_t i = 0; i <= haystack.size() - needle.size(); i++)
 	{
-		auto dist = getLevenshteinDistance(haystack.substr(i, needle.size()), needle);
-		if(needle.size() > 2 && dist <= 1)
-			return true;
-		else if(needle.size() > 4 && dist <= 2)
-			return true;
+		int dist = getLevenshteinDistance(haystack.substr(i, needle.size()), needle);
+		minDist = std::min(minDist, dist);
 	}
-	
-	return false;
+
+	// Apply scaling: Short words tolerate smaller distances
+	if(needle.size() > 2 && minDist <= 2)
+		minDist += 1;
+
+	return (minDist > maxAllowedDistance) ? std::nullopt : std::optional<int>{ minDist };
 }
 
 VCMI_LIB_NAMESPACE_END

+ 11 - 2
lib/texts/TextOperations.h

@@ -74,10 +74,19 @@ namespace TextOperations
 	/// Algorithm for detection of typos in words
 	/// Determines how 'different' two strings are - how many changes must be done to turn one string into another one
 	/// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
-	DLL_LINKAGE int getLevenshteinDistance(const std::string & s, const std::string & t);
+	DLL_LINKAGE int getLevenshteinDistance(std::string_view s, std::string_view t);
+
+	/// Retrieves the locale name based on the selected (in config) game language.
+	DLL_LINKAGE std::string getLocaleName();
+
+	/// Compares two strings using locale-aware collation based on the selected game language.
+	DLL_LINKAGE bool compareLocalizedStrings(std::string_view str1, std::string_view str2);
 
 	/// Check if texts have similarity when typing into search boxes
-	DLL_LINKAGE bool textSearchSimilar(const std::string & s, const std::string & t);
+	/// 0 -> Exact match or starts with typed-in text, 1 -> Close match or substring match, 
+	/// other values = Levenshtein distance, returns std::nullopt for unrelated word (bad match).
+	DLL_LINKAGE std::optional<int> textSearchSimilarityScore(const std::string & s, const std::string & t);
+
 };
 
 template<typename Arithmetic>

+ 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

+ 2 - 11
server/processors/TurnOrderProcessor.h

@@ -107,16 +107,7 @@ public:
 		h & awaitingPlayers;
 		h & actingPlayers;
 		h & actedPlayers;
-
-		if (h.version >= Handler::Version::VOTING_SIMTURNS)
-		{
-			h & simturnsMinDurationDays;
-			h & simturnsMaxDurationDays;
-		}
-		else if (!h.saving)
-		{
-			simturnsMinDurationDays.reset();
-			simturnsMaxDurationDays.reset();
-		}
+		h & simturnsMinDurationDays;
+		h & simturnsMaxDurationDays;
 	}
 };

+ 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*/