Browse Source

Merge branch 'beta' into 'develop'

Ivan Savenko 8 tháng trước cách đây
mục cha
commit
b36f5e4026
38 tập tin đã thay đổi với 381 bổ sung342 xóa
  1. 18 0
      ChangeLog.md
  2. 2 2
      Mods/vcmi/Content/Sprites/stackWindow/switchModeIcons.json
  3. 31 30
      Mods/vcmi/Content/config/czech.json
  4. 11 3
      client/CPlayerInterface.cpp
  5. 1 2
      client/ClientCommandManager.cpp
  6. 0 3
      client/adventureMap/AdventureMapInterface.cpp
  7. 12 6
      client/battle/BattleFieldController.cpp
  8. 6 1
      client/battle/BattleInterfaceClasses.cpp
  9. 0 2
      client/battle/BattleStacksController.cpp
  10. 2 0
      client/globalLobby/GlobalLobbyLoginWindow.cpp
  11. 1 1
      client/gui/CIntObject.cpp
  12. 3 0
      client/lobby/OptionsTab.cpp
  13. 0 3
      client/lobby/OptionsTabBase.cpp
  14. 1 5
      client/mainmenu/CMainMenu.cpp
  15. 146 227
      client/render/AssetGenerator.cpp
  16. 42 9
      client/render/AssetGenerator.h
  17. 6 0
      client/render/CanvasImage.cpp
  18. 2 0
      client/render/CanvasImage.h
  19. 2 0
      client/render/IRenderHandler.h
  20. 37 3
      client/renderSDL/RenderHandler.cpp
  21. 8 2
      client/renderSDL/RenderHandler.h
  22. 4 0
      client/renderSDL/SDLImage.cpp
  23. 6 0
      client/renderSDL/SDLImageScaler.cpp
  24. 0 3
      client/widgets/Images.cpp
  25. 5 1
      client/windows/CCreatureWindow.cpp
  26. 0 2
      client/windows/CSpellWindow.cpp
  27. 1 1
      client/windows/settings/SettingsMainWindow.h
  28. 0 3
      clientapp/EntryPoint.cpp
  29. 6 0
      debian/changelog
  30. 1 1
      docs/Readme.md
  31. 1 0
      launcher/eu.vcmi.VCMI.metainfo.xml
  32. 1 1
      launcher/modManager/modstate.cpp
  33. 14 2
      lib/texts/TextOperations.cpp
  34. 2 2
      mapeditor/inspector/towneventdialog.cpp
  35. 2 0
      mapeditor/mainwindow.cpp
  36. 1 1
      mapeditor/mainwindow.ui
  37. 2 2
      mapeditor/mapsettings/timedevent.cpp
  38. 4 24
      mapeditor/translation/czech.ts

+ 18 - 0
ChangeLog.md

@@ -1,5 +1,23 @@
 # VCMI Project Changelog
 
+## 1.6.4 -> 1.6.5
+
+### General
+
+* Fixed corrupted graphics of generated assets like water tiles on mobile systems
+* All generated assets are now used directly from memory without saving them to disk
+* Launcher will now correctly show screenshots for already installed mods
+* Fixed broken icons in commander information dialog
+
+### Stability
+
+* Fixed regression causing crashes in combat when touchscreen input is in use
+* Fixed regression causing crash on attempt to upscale empty image
+* Fixed crash on some creature abilities from mods that cast targeted spells on unit with battle propagator
+* Fixed crash on accepting next turn in multiplayer when local player has game settings window open
+* Fixed crash in multiplayer when one player changes his starting options while another player has hero overview window open
+* Fixed crash on double-clicking login to global lobby button
+
 ## 1.6.3 -> 1.6.4
 
 ### General

+ 2 - 2
Mods/vcmi/Content/Sprites/stackWindow/switchModeIcons.json

@@ -1,7 +1,7 @@
 {
 	"images" :
 	[
-		{ "frame" : 0, "file" : "SECSK32:69"},
-		{ "frame" : 1, "file" : "SECSK32:28"}
+		{ "frame" : 0, "defFile" : "SECSK32", "defFrame" : 69 },
+		{ "frame" : 1, "defFile" : "SECSK32", "defFrame" : 28 }
 	]
 }

+ 31 - 30
Mods/vcmi/Content/config/czech.json

@@ -199,6 +199,7 @@
 	"vcmi.lobby.preview.error.invite" : "Nebyl jste pozván do této mísnosti.",
 	"vcmi.lobby.preview.error.mods" : "Použváte jinou sadu modifikací.",
 	"vcmi.lobby.preview.error.version" : "Používáte jinou verzi VCMI.",
+	"vcmi.lobby.channel.add" : "Přidat kanál",
 	"vcmi.lobby.room.new" : "Nová hra",
 	"vcmi.lobby.room.load" : "Načíst hru",
 	"vcmi.lobby.room.type" : "Druh místnosti",
@@ -326,9 +327,9 @@
 	"vcmi.adventureOptions.smoothDragging.help" : "{Plynulé posouvání mapy}\n\nPokud je tato možnost aktivována, posouvání mapy bude plynulé.",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Přeskočit efekty mizení",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Přeskočit efekty mizení}\n\nKdyž je povoleno, přeskočí se efekty mizení objektů a podobné efekty (sběr surovin, nalodění atd.). V některých případech zrychlí uživatelské rozhraní na úkor estetiky. Obzvláště užitečné v PvP hrách. Pro maximální rychlost pohybu je toto nastavení aktivní bez ohledu na další volby.",
-	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
-	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
-	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed1.hover" : "",
+	"vcmi.adventureOptions.mapScrollSpeed5.hover" : "",
+	"vcmi.adventureOptions.mapScrollSpeed6.hover" : "",
 	"vcmi.adventureOptions.mapScrollSpeed1.help" : "Nastavit posouvání mapy na velmi pomalé",
 	"vcmi.adventureOptions.mapScrollSpeed5.help" : "Nastavit posouvání mapy na velmi rychlé",
 	"vcmi.adventureOptions.mapScrollSpeed6.help" : "Nastavit posouvání mapy na okamžité",
@@ -337,16 +338,16 @@
 
 	"vcmi.battleOptions.queueSizeLabel.hover" : "Zobrazit frontu pořadí tahů",
 	"vcmi.battleOptions.queueSizeNoneButton.hover" : "VYPNUTO",
-	"vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO",
+	"vcmi.battleOptions.queueSizeAutoButton.hover" : "AUTO",
 	"vcmi.battleOptions.queueSizeSmallButton.hover" : "MALÁ",
 	"vcmi.battleOptions.queueSizeBigButton.hover" : "VELKÁ",
 	"vcmi.battleOptions.queueSizeNoneButton.help" : "Nezobrazovat frontu pořadí tahů.",
 	"vcmi.battleOptions.queueSizeAutoButton.help" : "Nastavit automaticky velikost fronty pořadí tahů podle rozlišení obrazovky hry (Při výšce herního rozlišení menší než 700 pixelů je použita velikost MALÁ, jinak velikost VELKÁ)",
 	"vcmi.battleOptions.queueSizeSmallButton.help" : "Zobrazit MALOU frontu pořadí tahů.",
 	"vcmi.battleOptions.queueSizeBigButton.help" : "Zobrazit VELKOU frontu pořadí tahů (není podporováno, pokud výška rozlišení hry není alespoň 700 pixelů).",
-	"vcmi.battleOptions.animationsSpeed1.hover": "",
-	"vcmi.battleOptions.animationsSpeed5.hover": "",
-	"vcmi.battleOptions.animationsSpeed6.hover": "",
+	"vcmi.battleOptions.animationsSpeed1.hover" : "",
+	"vcmi.battleOptions.animationsSpeed5.hover" : "",
+	"vcmi.battleOptions.animationsSpeed6.hover" : "",
 	"vcmi.battleOptions.animationsSpeed1.help" : "Nastavit rychlost animací na velmi pomalé.",
 	"vcmi.battleOptions.animationsSpeed5.help" : "Nastavit rychlost animací na velmi rychlé.",
 	"vcmi.battleOptions.animationsSpeed6.help" : "Nastavit rychlost animací na okamžité.",
@@ -762,28 +763,28 @@
 	"core.bonus.MECHANICAL.name" : "Mechanický",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.name" : "Trojitý dech",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.description" : "Útok trojitým dechem (útok přes 3 směry)",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Odolnost vůči kouzlům",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air": "Odolnost vůči kouzlům vzduchu",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire": "Odolnost vůči kouzlům ohně",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water": "Odolnost vůči kouzlům vody",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth": "Odolnost vůči kouzlům země",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Poškození ze všech kouzel sníženo o ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air": "Poškození kouzel magie vzduchu sníženo o ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire": "Poškození kouzel magie ohně sníženo o ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water": "Poškození kouzel magie vody sníženo o ${val}%.",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth": "Poškození kouzel magie země sníženo o ${val}%.",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Imunita vůči kouzlům",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Vzdušná imunita",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Ohnivá imunita",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Vodní imunita",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Zemská imunita",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "Jednotka je imunní vůči všem kouzlům.",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "Jednotka je imunní vůči všem kouzlům magie vzduchu.",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "Jednotka je imunní vůči všem kouzlům magie ohně.",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "Jednotka je imunní vůči všem kouzlům magie vody.",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "Jednotka je imunní vůči všem kouzlům magie země.",
-	"core.bonus.OPENING_BATTLE_SPELL.name": "Začíná kouzlem",
-	"core.bonus.OPENING_BATTLE_SPELL.description": "Sesílá ${subtype.spell} na začátku bitvy.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name" : "Odolnost vůči kouzlům",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air" : "Odolnost vůči kouzlům vzduchu",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire" : "Odolnost vůči kouzlům ohně",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water" : "Odolnost vůči kouzlům vody",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth" : "Odolnost vůči kouzlům země",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description" : "Poškození ze všech kouzel sníženo o ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air" : "Poškození kouzel magie vzduchu sníženo o ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire" : "Poškození kouzel magie ohně sníženo o ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water" : "Poškození kouzel magie vody sníženo o ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth" : "Poškození kouzel magie země sníženo o ${val}%.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name" : "Imunita vůči kouzlům",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air" : "Vzdušná imunita",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire" : "Ohnivá imunita",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water" : "Vodní imunita",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth" : "Zemská imunita",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description" : "Jednotka je imunní vůči všem kouzlům.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air" : "Jednotka je imunní vůči všem kouzlům magie vzduchu.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire" : "Jednotka je imunní vůči všem kouzlům magie ohně.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water" : "Jednotka je imunní vůči všem kouzlům magie vody.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth" : "Jednotka je imunní vůči všem kouzlům magie země.",
+	"core.bonus.OPENING_BATTLE_SPELL.name" : "Začíná kouzlem",
+	"core.bonus.OPENING_BATTLE_SPELL.description" : "Sesílá ${subtype.spell} na začátku bitvy.",
 	
 	"spell.core.castleMoat.name" : "Hradní příkop",
 	"spell.core.castleMoatTrigger.name" : "Hradní příkop",
@@ -806,4 +807,4 @@
 	"spell.core.strongholdMoatTrigger.name" : "Dřevěné bodce",
 	"spell.core.summonDemons.name" : "Přivolání démonů",
 	"spell.core.towerMoat.name" : "Pozemní mina"
-}
+}

+ 11 - 3
client/CPlayerInterface.cpp

@@ -62,6 +62,7 @@
 #include "windows/CTutorialWindow.h"
 #include "windows/GUIClasses.h"
 #include "windows/InfoWindows.h"
+#include "windows/settings/SettingsMainWindow.h"
 
 #include "../CCallback.h"
 
@@ -187,6 +188,7 @@ void CPlayerInterface::closeAllDialogs()
 	while(true)
 	{
 		auto adventureWindow = GH.windows().topWindow<AdventureMapInterface>();
+		auto settingsWindow = GH.windows().topWindow<SettingsMainWindow>();
 		auto infoWindow = GH.windows().topWindow<CInfoWindow>();
 		auto topWindow = GH.windows().topWindow<WindowBase>();
 
@@ -196,10 +198,16 @@ void CPlayerInterface::closeAllDialogs()
 		if(infoWindow && infoWindow->ID != QueryID::NONE)
 			break;
 
-		if (topWindow == nullptr)
-			throw std::runtime_error("Invalid or non-existing top window! Total windows: " + std::to_string(GH.windows().count()));
+		if (settingsWindow)
+		{
+			settingsWindow->close();
+			continue;
+		}
 
-		topWindow->close();
+		if (topWindow)
+			topWindow->close();
+		else
+			GH.windows().popWindows(1); // does not inherits from WindowBase, e.g. settings dialog
 	}
 }
 

+ 1 - 2
client/ClientCommandManager.cpp

@@ -18,7 +18,6 @@
 #include "gui/CGuiHandler.h"
 #include "gui/WindowHandler.h"
 #include "render/IRenderHandler.h"
-#include "render/AssetGenerator.h"
 #include "ClientNetPackVisitors.h"
 #include "../lib/CConfigHandler.h"
 #include "../lib/gameState/CGameState.h"
@@ -510,7 +509,7 @@ void ClientCommandManager::handleVsLog(std::istringstream & singleWordBuffer)
 
 void ClientCommandManager::handleGenerateAssets()
 {
-	AssetGenerator::generateAll();
+	GH.renderHandler().exportGeneratedAssets();
 	printCommandMessage("All assets generated");
 }
 

+ 0 - 3
client/adventureMap/AdventureMapInterface.cpp

@@ -34,7 +34,6 @@
 #include "../render/IImage.h"
 #include "../render/IRenderHandler.h"
 #include "../render/IScreenHandler.h"
-#include "../render/AssetGenerator.h"
 #include "../CMT.h"
 #include "../PlayerLocalState.h"
 #include "../CPlayerInterface.h"
@@ -65,8 +64,6 @@ AdventureMapInterface::AdventureMapInterface():
 	pos.w = GH.screenDimensions().x;
 	pos.h = GH.screenDimensions().y;
 
-	AssetGenerator::createPaletteShiftedSprites();
-
 	shortcuts = std::make_shared<AdventureMapShortcuts>(*this);
 
 	widget = std::make_shared<AdventureMapWidget>(shortcuts);

+ 12 - 6
client/battle/BattleFieldController.cpp

@@ -683,18 +683,24 @@ BattleHex::EDir BattleFieldController::selectAttackDirection(const BattleHex & m
 		// |    - -   |   - -    |    - -   |   - o o  |  o o -   |   - -    |    - -   |   o o
 
 		for (size_t i : { 1, 2, 3})
-			attackAvailability[i] = occupiableHexes.contains(neighbours[i]) && occupiableHexes.contains(neighbours[i].cloneInDirection(BattleHex::RIGHT, false));
+		{
+			BattleHex target = neighbours[i].cloneInDirection(BattleHex::RIGHT, false);
+			attackAvailability[i] = neighbours[i].isValid() && occupiableHexes.contains(neighbours[i]) && target.isValid() && occupiableHexes.contains(target);
+		}
 
 		for (size_t i : { 4, 5, 0})
-			attackAvailability[i] = occupiableHexes.contains(neighbours[i]) && occupiableHexes.contains(neighbours[i].cloneInDirection(BattleHex::LEFT, false));
+		{
+			BattleHex target = neighbours[i].cloneInDirection(BattleHex::LEFT, false);
+			attackAvailability[i] = neighbours[i].isValid() && occupiableHexes.contains(neighbours[i]) && target.isValid() && occupiableHexes.contains(target);
+		}
 
-		attackAvailability[6] = occupiableHexes.contains(neighbours[0]) && occupiableHexes.contains(neighbours[1]);
-		attackAvailability[7] = occupiableHexes.contains(neighbours[3]) && occupiableHexes.contains(neighbours[4]);
+		attackAvailability[6] = neighbours[0].isValid() && neighbours[1].isValid() && occupiableHexes.contains(neighbours[0]) && occupiableHexes.contains(neighbours[1]);
+		attackAvailability[7] = neighbours[3].isValid() && neighbours[4].isValid() && occupiableHexes.contains(neighbours[3]) && occupiableHexes.contains(neighbours[4]);
 	}
 	else
 	{
 		for (size_t i = 0; i < 6; ++i)
-			attackAvailability[i] = occupiableHexes.contains(neighbours[i]);
+			attackAvailability[i] = neighbours[i].isValid() && occupiableHexes.contains(neighbours[i]);
 
 		attackAvailability[6] = false;
 		attackAvailability[7] = false;
@@ -739,7 +745,7 @@ BattleHex::EDir BattleFieldController::selectAttackDirection(const BattleHex & m
 
 BattleHex BattleFieldController::fromWhichHexAttack(const BattleHex & attackTarget)
 {
-	BattleHex::EDir direction = selectAttackDirection(getHoveredHex());
+	BattleHex::EDir direction = selectAttackDirection(attackTarget);
 
 	const CStack * attacker = owner.stacksController->getActiveStack();
 

+ 6 - 1
client/battle/BattleInterfaceClasses.cpp

@@ -686,7 +686,12 @@ void StackInfoBasicPanel::initializeData(const CStack * stack)
 		if (hasGraphics)
 		{
 			//FIXME: support permanent duration
-			int duration = stack->getFirstBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)))->turnsRemain;
+			auto spellBonuses = stack->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)));
+
+			if (spellBonuses->empty())
+				throw std::runtime_error("Failed to find effects for spell " + effect.toSpell()->getJsonKey());
+
+			int duration = spellBonuses->front()->duration;
 
 			icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed));
 			if(settings["general"]["enableUiEnhancements"].Bool())

+ 0 - 2
client/battle/BattleStacksController.cpp

@@ -27,7 +27,6 @@
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../media/ISoundPlayer.h"
-#include "../render/AssetGenerator.h"
 #include "../render/Colors.h"
 #include "../render/Canvas.h"
 #include "../render/IRenderHandler.h"
@@ -80,7 +79,6 @@ BattleStacksController::BattleStacksController(BattleInterface & owner):
 	stackToActivate(nullptr),
 	animIDhelper(0)
 {
-	AssetGenerator::createCombatUnitNumberWindow();
 	//preparing graphics for displaying amounts of creatures
 	amountNormal     = GH.renderHandler().loadImage(ImagePath::builtin("combatUnitNumberWindowDefault"), EImageBlitMode::COLORKEY);
 	amountPositive   = GH.renderHandler().loadImage(ImagePath::builtin("combatUnitNumberWindowPositive"), EImageBlitMode::COLORKEY);

+ 2 - 0
client/globalLobby/GlobalLobbyLoginWindow.cpp

@@ -116,6 +116,7 @@ void GlobalLobbyLoginWindow::onLogin()
 		onConnectionSuccess();
 
 	buttonClose->block(true);
+	buttonLogin->block(true);
 }
 
 void GlobalLobbyLoginWindow::onConnectionSuccess()
@@ -142,4 +143,5 @@ void GlobalLobbyLoginWindow::onConnectionFailed(const std::string & reason)
 
 	labelStatus->setText(formatter.toString());
 	buttonClose->block(false);
+	buttonLogin->block(false);
 }

+ 1 - 1
client/gui/CIntObject.cpp

@@ -345,7 +345,7 @@ void WindowBase::close()
 	if(!GH.windows().isTopWindow(this))
 	{
 		auto topWindow = GH.windows().topWindow<IShowActivatable>().get();
-		throw std::runtime_error(std::string("Only top interface can be closed! Top window is ") + typeid(*this).name() + " but attempted to close " + typeid(*topWindow).name());
+		throw std::runtime_error(std::string("Only top interface can be closed! Top window is ") + typeid(*topWindow).name() + " but attempted to close " + typeid(*this).name());
 	}
 	GH.windows().popWindows(1);
 }

+ 3 - 0
client/lobby/OptionsTab.cpp

@@ -68,6 +68,9 @@ void OptionsTab::recreate()
 	entries.clear();
 	humanPlayers = 0;
 
+	for (auto heroOverview : GH.windows().findWindows<CHeroOverview>())
+		heroOverview->close();
+
 	for (auto selectionWindow : GH.windows().findWindows<SelectionWindow>())
 	{
 		selectionWindow->reopen();

+ 0 - 3
client/lobby/OptionsTabBase.cpp

@@ -18,7 +18,6 @@
 #include "../widgets/TextControls.h"
 #include "../CServerHandler.h"
 #include "../CGameInfo.h"
-#include "../render/AssetGenerator.h"
 
 #include "../../lib/StartInfo.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
@@ -69,8 +68,6 @@ std::vector<SimturnsInfo> OptionsTabBase::getSimturnsPresets() const
 
 OptionsTabBase::OptionsTabBase(const JsonPath & configPath)
 {
-	AssetGenerator::createAdventureOptionsCleanBackground();
-
 	recActions = 0;
 
 	auto setTimerPresetCallback = [this](int index){

+ 1 - 5
client/mainmenu/CMainMenu.cpp

@@ -38,7 +38,6 @@
 #include "../widgets/VideoWidget.h"
 #include "../windows/InfoWindows.h"
 #include "../CServerHandler.h"
-#include "../render/AssetGenerator.h"
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
@@ -278,7 +277,7 @@ CMenuEntry::CMenuEntry(CMenuScreen * parent, const JsonNode & config)
 			for (const auto& item : campaign["items"].Vector()) {
 				std::string filename = item["file"].String();
 
-				if (CResourceHandler::get()->existsResource(ResourcePath(filename + ".h3c"))) {
+				if (CResourceHandler::get()->existsResource(ResourcePath(filename, EResType::CAMPAIGN))) {
 					fileExists = true;
 					break; 
 				}
@@ -428,9 +427,6 @@ void CMainMenu::openCampaignScreen(std::string name)
 {
 	auto const & config = CMainMenuConfig::get().getCampaigns();
 
-	AssetGenerator::createCampaignBackground();
-	AssetGenerator::createChroniclesCampaignImages();
-
 	if(!vstd::contains(config.Struct(), name))
 	{
 		logGlobal->error("Unknown campaign set: %s", name);

+ 146 - 227
client/render/AssetGenerator.cpp

@@ -29,36 +29,60 @@
 #include "../lib/RoadHandler.h"
 #include "../lib/TerrainHandler.h"
 
-void AssetGenerator::clear()
+AssetGenerator::AssetGenerator()
+{
+}
+
+void AssetGenerator::initialize()
 {
 	// clear to avoid non updated sprites after mod change (if base imnages are used)
 	if(boost::filesystem::is_directory(VCMIDirs::get().userDataPath() / "Generated"))
 		boost::filesystem::remove_all(VCMIDirs::get().userDataPath() / "Generated");
+
+	imageFiles[ImagePath::builtin("AdventureOptionsBackgroundClear.png")] = [this](){ return createAdventureOptionsCleanBackground();};
+	imageFiles[ImagePath::builtin("SpellBookLarge.png")] = [this](){ return createBigSpellBook();};
+
+	imageFiles[ImagePath::builtin("combatUnitNumberWindowDefault.png")]  = [this](){ return createCombatUnitNumberWindow(0.6f, 0.2f, 1.0f);};
+	imageFiles[ImagePath::builtin("combatUnitNumberWindowNeutral.png")]  = [this](){ return createCombatUnitNumberWindow(1.0f, 1.0f, 2.0f);};
+	imageFiles[ImagePath::builtin("combatUnitNumberWindowPositive.png")] = [this](){ return createCombatUnitNumberWindow(0.2f, 1.0f, 0.2f);};
+	imageFiles[ImagePath::builtin("combatUnitNumberWindowNegative.png")] = [this](){ return createCombatUnitNumberWindow(1.0f, 0.2f, 0.2f);};
+
+	imageFiles[ImagePath::builtin("CampaignBackground8.png")] = [this](){ return createCampaignBackground();};
+
+	for (PlayerColor color(0); color < PlayerColor::PLAYER_LIMIT; ++color)
+		imageFiles[ImagePath::builtin("DialogBoxBackground_" + color.toString())] = [this, color](){ return createPlayerColoredBackground(color);};
+
+	for(int i = 1; i < 9; i++)
+		imageFiles[ImagePath::builtin("CampaignHc" + std::to_string(i) + "Image.png")] = [this, i](){ return createChroniclesCampaignImages(i);};
+
+	createPaletteShiftedSprites();
 }
 
-void AssetGenerator::generateAll()
+std::shared_ptr<ISharedImage> AssetGenerator::generateImage(const ImagePath & image)
 {
-	createBigSpellBook();
-	createAdventureOptionsCleanBackground();
-	for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
-		createPlayerColoredBackground(PlayerColor(i));
-	createCombatUnitNumberWindow();
-	createCampaignBackground();
-	createChroniclesCampaignImages();
-	createPaletteShiftedSprites();
+	if (imageFiles.count(image))
+		return imageFiles.at(image)()->toSharedImage(); // TODO: cache?
+	else
+		return nullptr;
 }
 
-void AssetGenerator::createAdventureOptionsCleanBackground()
+std::map<ImagePath, std::shared_ptr<ISharedImage>> AssetGenerator::generateAllImages()
 {
-	std::string filename = "data/AdventureOptionsBackgroundClear.png";
+	std::map<ImagePath, std::shared_ptr<ISharedImage>> result;
+
+	for (const auto & entry : imageFiles)
+		result[entry.first] = entry.second()->toSharedImage();
 
-	if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation
-		return;
+	return result;
+}
 
-	if(!CResourceHandler::get("local")->createResource(filename))
-		return;
-	ResourcePath savePath(filename, EResType::IMAGE);
+std::map<AnimationPath, AssetGenerator::AnimationLayoutMap> AssetGenerator::generateAllAnimations()
+{
+	return animationFiles;
+}
 
+AssetGenerator::CanvasPtr AssetGenerator::createAdventureOptionsCleanBackground()
+{
 	auto locator = ImageLocator(ImagePath::builtin("ADVOPTBK"), EImageBlitMode::OPAQUE);
 
 	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
@@ -74,20 +98,11 @@ void AssetGenerator::createAdventureOptionsCleanBackground()
 	canvas.draw(img, Point(53, 567), Rect(53, 520, 339, 3));
 	canvas.draw(img, Point(53, 520), Rect(53, 264, 339, 47));
 
-	image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
+	return image;
 }
 
-void AssetGenerator::createBigSpellBook()
+AssetGenerator::CanvasPtr AssetGenerator::createBigSpellBook()
 {
-	std::string filename = "data/SpellBookLarge.png";
-
-	if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation
-		return;
-
-	if(!CResourceHandler::get("local")->createResource(filename))
-		return;
-	ResourcePath savePath(filename, EResType::IMAGE);
-
 	auto locator = ImageLocator(ImagePath::builtin("SpelBack"), EImageBlitMode::OPAQUE);
 
 	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
@@ -135,21 +150,11 @@ void AssetGenerator::createBigSpellBook()
 	canvas.draw(img, Point(575, 465), Rect(417, 406, 37, 45));
 	canvas.draw(img, Point(667, 465), Rect(478, 406, 37, 47));
 
-	image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
+	return image;
 }
 
-void AssetGenerator::createPlayerColoredBackground(const PlayerColor & player)
+AssetGenerator::CanvasPtr AssetGenerator::createPlayerColoredBackground(const PlayerColor & player)
 {
-	std::string filename = "data/DialogBoxBackground_" + player.toString() + ".png";
-
-	if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation
-		return;
-
-	if(!CResourceHandler::get("local")->createResource(filename))
-		return;
-
-	ResourcePath savePath(filename, EResType::IMAGE);
-
 	auto locator = ImageLocator(ImagePath::builtin("DiBoxBck"), EImageBlitMode::OPAQUE);
 
 	std::shared_ptr<IImage> texture = GH.renderHandler().loadImage(locator);
@@ -169,71 +174,44 @@ void AssetGenerator::createPlayerColoredBackground(const PlayerColor & player)
 
 	assert(player.isValidPlayer());
 	if (!player.isValidPlayer())
-	{
-		logGlobal->error("Unable to colorize to invalid player color %d!", player.getNum());
-		return;
-	}
+		throw std::runtime_error("Unable to colorize to invalid player color" + std::to_string(player.getNum()));
 
 	texture->adjustPalette(filters[player.getNum()], 0);
-	texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
-}
-
-void AssetGenerator::createCombatUnitNumberWindow()
-{
-	std::string filenameToSave = "data/combatUnitNumberWindow";
 
-	ResourcePath savePathDefault(filenameToSave + "Default.png", EResType::IMAGE);
-	ResourcePath savePathNeutral(filenameToSave + "Neutral.png", EResType::IMAGE);
-	ResourcePath savePathPositive(filenameToSave + "Positive.png", EResType::IMAGE);
-	ResourcePath savePathNegative(filenameToSave + "Negative.png", EResType::IMAGE);
-
-	if(CResourceHandler::get()->existsResource(savePathDefault)) // overridden by mod, no generation
-		return;
+	auto image = GH.renderHandler().createImage(texture->dimensions(), CanvasScalingPolicy::IGNORE);
+	Canvas canvas = image->getCanvas();
+	canvas.draw(texture, Point(0,0));
 
-	if(!CResourceHandler::get("local")->createResource(savePathDefault.getOriginalName() + ".png") ||
-	   !CResourceHandler::get("local")->createResource(savePathNeutral.getOriginalName() + ".png") ||
-	   !CResourceHandler::get("local")->createResource(savePathPositive.getOriginalName() + ".png") ||
-	   !CResourceHandler::get("local")->createResource(savePathNegative.getOriginalName() + ".png"))
-		return;
+	return image;
+}
 
+AssetGenerator::CanvasPtr AssetGenerator::createCombatUnitNumberWindow(float multR, float multG, float multB)
+{
 	auto locator = ImageLocator(ImagePath::builtin("CMNUMWIN"), EImageBlitMode::OPAQUE);
 	locator.layer = EImageBlitMode::OPAQUE;
 
 	std::shared_ptr<IImage> texture = GH.renderHandler().loadImage(locator);
 
-	static const auto shifterNormal   = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f );
-	static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f );
-	static const auto shifterNegative = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 0.2f, 0.2f );
-	static const auto shifterNeutral  = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 1.0f, 0.2f );
+	const auto shifter= ColorFilter::genRangeShifter(0.f, 0.f, 0.f, multR, multG, multB);
 
 	// do not change border color
 	static const int32_t ignoredMask = 1 << 26;
 
-	texture->adjustPalette(shifterNormal, ignoredMask);
-	texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePathDefault));
-	texture->adjustPalette(shifterPositive, ignoredMask);
-	texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePathPositive));
-	texture->adjustPalette(shifterNegative, ignoredMask);
-	texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePathNegative));
-	texture->adjustPalette(shifterNeutral, ignoredMask);
-	texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePathNeutral));
-}
-
-void AssetGenerator::createCampaignBackground()
-{
-	std::string filename = "data/CampaignBackground8.png";
+	texture->adjustPalette(shifter, ignoredMask);
 
-	if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation
-		return;
+	auto image = GH.renderHandler().createImage(texture->dimensions(), CanvasScalingPolicy::IGNORE);
+	Canvas canvas = image->getCanvas();
+	canvas.draw(texture, Point(0,0));
 
-	if(!CResourceHandler::get("local")->createResource(filename))
-		return;
-	ResourcePath savePath(filename, EResType::IMAGE);
+	return image;
+}
 
+AssetGenerator::CanvasPtr AssetGenerator::createCampaignBackground()
+{
 	auto locator = ImageLocator(ImagePath::builtin("CAMPBACK"), EImageBlitMode::OPAQUE);
 
 	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
-	auto image = GH.renderHandler().createImage(Point(800, 600), CanvasScalingPolicy::IGNORE);
+	auto image = GH.renderHandler().createImage(Point(200, 116), CanvasScalingPolicy::IGNORE);
 	Canvas canvas = image->getCanvas();
 
 	canvas.draw(img, Point(0, 0), Rect(0, 0, 800, 600));
@@ -264,171 +242,112 @@ void AssetGenerator::createCampaignBackground()
 	std::shared_ptr<IImage> imgSkull = GH.renderHandler().loadImage(locatorSkull);
 	canvas.draw(imgSkull, Point(562, 509), Rect(178, 108, 43, 19));
 
-	image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
+	return image;
 }
 
-void AssetGenerator::createChroniclesCampaignImages()
+AssetGenerator::CanvasPtr AssetGenerator::createChroniclesCampaignImages(int chronicle)
 {
-	for(int i = 1; i < 9; i++)
-	{
-		std::string filename = "data/CampaignHc" + std::to_string(i) + "Image.png";
-
-		if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation
-			continue;
-			
-		auto imgPathBg = ImagePath::builtin("data/chronicles_" + std::to_string(i) + "/GamSelBk");
-		if(!CResourceHandler::get()->existsResource(imgPathBg)) // Chronicle episode not installed
-			continue;
-
-		if(!CResourceHandler::get("local")->createResource(filename))
-			continue;
-		ResourcePath savePath(filename, EResType::IMAGE);
+	auto imgPathBg = ImagePath::builtin("data/chronicles_" + std::to_string(chronicle) + "/GamSelBk");
+	auto locator = ImageLocator(imgPathBg, EImageBlitMode::OPAQUE);
 
-		auto locator = ImageLocator(imgPathBg, EImageBlitMode::OPAQUE);
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
+	auto image = GH.renderHandler().createImage(Point(800, 600), CanvasScalingPolicy::IGNORE);
+	Canvas canvas = image->getCanvas();
 
-		std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
-		auto image = GH.renderHandler().createImage(Point(800, 600), CanvasScalingPolicy::IGNORE);
-		Canvas canvas = image->getCanvas();
-		
-		switch (i)
-		{
-		case 1:
-			canvas.draw(img, Point(0, 0), Rect(149, 144, 200, 116));
-			break;
-		case 2:
-			canvas.draw(img, Point(0, 0), Rect(156, 150, 200, 116));
-			break;
-		case 3:
-			canvas.draw(img, Point(0, 0), Rect(171, 153, 200, 116));
-			break;
-		case 4:
-			canvas.draw(img, Point(0, 0), Rect(35, 358, 200, 116));
-			break;
-		case 5:
-			canvas.draw(img, Point(0, 0), Rect(216, 248, 200, 116));
-			break;
-		case 6:
-			canvas.draw(img, Point(0, 0), Rect(58, 234, 200, 116));
-			break;
-		case 7:
-			canvas.draw(img, Point(0, 0), Rect(184, 219, 200, 116));
-			break;
-		case 8:
-			canvas.draw(img, Point(0, 0), Rect(268, 210, 200, 116));
-
-			//skull
-			auto locatorSkull = ImageLocator(ImagePath::builtin("CampSP1"), EImageBlitMode::OPAQUE);
-			std::shared_ptr<IImage> imgSkull = GH.renderHandler().loadImage(locatorSkull);
-			canvas.draw(imgSkull, Point(162, 94), Rect(162, 94, 41, 22));
-			canvas.draw(img, Point(162, 94), Rect(424, 304, 14, 4));
-			canvas.draw(img, Point(162, 98), Rect(424, 308, 10, 4));
-			canvas.draw(img, Point(158, 102), Rect(424, 312, 10, 4));
-			canvas.draw(img, Point(154, 106), Rect(424, 316, 10, 4));
-			break;
-		}
+	std::array sourceRect = {
+		Rect(149, 144, 200, 116),
+		Rect(156, 150, 200, 116),
+		Rect(171, 153, 200, 116),
+		Rect(35, 358, 200, 116),
+		Rect(216, 248, 200, 116),
+		Rect(58, 234, 200, 116),
+		Rect(184, 219, 200, 116),
+		Rect(268, 210, 200, 116),
+	};
+	
+	canvas.draw(img, Point(0, 0), sourceRect.at(chronicle-1));
 
-		image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
+	if (chronicle == 8)
+	{
+		//skull
+		auto locatorSkull = ImageLocator(ImagePath::builtin("CampSP1"), EImageBlitMode::OPAQUE);
+		std::shared_ptr<IImage> imgSkull = GH.renderHandler().loadImage(locatorSkull);
+		canvas.draw(imgSkull, Point(162, 94), Rect(162, 94, 41, 22));
+		canvas.draw(img, Point(162, 94), Rect(424, 304, 14, 4));
+		canvas.draw(img, Point(162, 98), Rect(424, 308, 10, 4));
+		canvas.draw(img, Point(158, 102), Rect(424, 312, 10, 4));
+		canvas.draw(img, Point(154, 106), Rect(424, 316, 10, 4));
 	}
+
+	return image;
 }
 
 void AssetGenerator::createPaletteShiftedSprites()
 {
-	std::vector<std::string> tiles;
-	std::vector<std::vector<std::variant<TerrainPaletteAnimation, RiverPaletteAnimation>>> paletteAnimations;
 	for(auto entity : VLC->terrainTypeHandler->objects)
 	{
-		if(entity->paletteAnimation.size())
-		{
-			tiles.push_back(entity->tilesFilename.getName());
-			std::vector<std::variant<TerrainPaletteAnimation, RiverPaletteAnimation>> tmpAnim;
-			for(auto & animEntity : entity->paletteAnimation)
-				tmpAnim.push_back(animEntity);
-			paletteAnimations.push_back(tmpAnim);
-		}
+		if(entity->paletteAnimation.empty())
+			continue;
+
+		std::vector<PaletteAnimation> paletteShifts;
+		for(auto & animEntity : entity->paletteAnimation)
+			paletteShifts.push_back({animEntity.start, animEntity.length});
+
+		generatePaletteShiftedAnimation(entity->tilesFilename, paletteShifts);
+
 	}
 	for(auto entity : VLC->riverTypeHandler->objects)
 	{
-		if(entity->paletteAnimation.size())
-		{
-			tiles.push_back(entity->tilesFilename.getName());
-			std::vector<std::variant<TerrainPaletteAnimation, RiverPaletteAnimation>> tmpAnim;
-			for(auto & animEntity : entity->paletteAnimation)
-				tmpAnim.push_back(animEntity);
-			paletteAnimations.push_back(tmpAnim);
-		}
+		if(entity->paletteAnimation.empty())
+			continue;
+
+		std::vector<PaletteAnimation> paletteShifts;
+		for(auto & animEntity : entity->paletteAnimation)
+			paletteShifts.push_back({animEntity.start, animEntity.length});
+
+		generatePaletteShiftedAnimation(entity->tilesFilename, paletteShifts);
 	}
+}
 
-	for(int i = 0; i < tiles.size(); i++)
-	{
-		auto sprite = tiles[i];
+void AssetGenerator::generatePaletteShiftedAnimation(const AnimationPath & sprite, const std::vector<PaletteAnimation> & paletteAnimations)
+{
+	AnimationLayoutMap layout;
 
-		JsonNode config;
-		config["basepath"].String() = sprite + "_Shifted/";
-		config["images"].Vector();
+	auto animation = GH.renderHandler().loadAnimation(sprite, EImageBlitMode::COLORKEY);
 
-		auto filename = AnimationPath::builtin(sprite).addPrefix("SPRITES/");
-		auto filenameNew = AnimationPath::builtin(sprite + "_Shifted").addPrefix("SPRITES/");
+	int paletteTransformLength = 1;
+	for (const auto & transform : paletteAnimations)
+		paletteTransformLength = std::lcm(paletteTransformLength, transform.length);
 
-		if(CResourceHandler::get()->existsResource(ResourcePath(filenameNew.getName(), EResType::JSON))) // overridden by mod, no generation
-			return;
-		
-		auto anim = GH.renderHandler().loadAnimation(filename, EImageBlitMode::COLORKEY);
-		for(int j = 0; j < anim->size(); j++)
+	for(int tileIndex = 0; tileIndex < animation->size(); tileIndex++)
+	{
+		for(int paletteIndex = 0; paletteIndex < paletteTransformLength; paletteIndex++)
 		{
-			int maxLen = 1;
-			for(int k = 0; k < paletteAnimations[i].size(); k++)
-			{
-				auto element = paletteAnimations[i][k];
-				if(std::holds_alternative<TerrainPaletteAnimation>(element))
-					maxLen = std::lcm(maxLen, std::get<TerrainPaletteAnimation>(element).length);
-				else
-					maxLen = std::lcm(maxLen, std::get<RiverPaletteAnimation>(element).length);
-			}
-			for(int l = 0; l < maxLen; l++)
-			{
-				std::string spriteName = sprite + boost::str(boost::format("%02d") % j) + "_" + std::to_string(l) + ".png";
-				std::string filenameNewImg = "sprites/" + sprite + "_Shifted" + "/" + spriteName;
-				ResourcePath savePath(filenameNewImg, EResType::IMAGE);
-
-				if(!CResourceHandler::get("local")->createResource(filenameNewImg))
-					return;
-
-				auto imgLoc = anim->getImageLocator(j, 0);
-				auto img = GH.renderHandler().loadImage(imgLoc);
-				for(int k = 0; k < paletteAnimations[i].size(); k++)
-				{
-					auto element = paletteAnimations[i][k];
-					if(std::holds_alternative<TerrainPaletteAnimation>(element))
-					{
-						auto tmp = std::get<TerrainPaletteAnimation>(element);
-						img->shiftPalette(tmp.start, tmp.length, l % tmp.length);
-					}
-					else
-					{
-						auto tmp = std::get<RiverPaletteAnimation>(element);
-						img->shiftPalette(tmp.start, tmp.length, l % tmp.length);
-					}
-				}
-				
-				auto image = GH.renderHandler().createImage(Point(32, 32), CanvasScalingPolicy::IGNORE);
-				Canvas canvas = image->getCanvas();
-				canvas.draw(img, Point((32 - img->dimensions().x) / 2, (32 - img->dimensions().y) / 2));
-				image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
-
-				JsonNode node(JsonMap{
-					{ "group", JsonNode(l) },
-					{ "frame", JsonNode(j) },
-					{ "file", JsonNode(spriteName) }
-				});
-				config["images"].Vector().push_back(node);
-			}
+			ImagePath spriteName = ImagePath::builtin(sprite.getName() + boost::str(boost::format("%02d") % tileIndex) + "_" + std::to_string(paletteIndex) + ".png");
+			layout[paletteIndex].push_back(ImageLocator(spriteName, EImageBlitMode::SIMPLE));
+
+			imageFiles[spriteName]  = [=](){ return createPaletteShiftedImage(sprite, paletteAnimations, tileIndex, paletteIndex);};
 		}
+	}
 
-		ResourcePath savePath(filenameNew.getOriginalName(), EResType::JSON);
-		if(!CResourceHandler::get("local")->createResource(filenameNew.getOriginalName() + ".json"))
-			return;
+	AnimationPath shiftedPath = AnimationPath::builtin("SPRITES/" + sprite.getName() + "_SHIFTED");
+	animationFiles[shiftedPath] = layout;
+}
+
+AssetGenerator::CanvasPtr AssetGenerator::createPaletteShiftedImage(const AnimationPath & source, const std::vector<PaletteAnimation> & palette, int frameIndex, int paletteShiftCounter)
+{
+	auto animation = GH.renderHandler().loadAnimation(source, EImageBlitMode::COLORKEY);
+
+	auto imgLoc = animation->getImageLocator(frameIndex, 0);
+	auto img = GH.renderHandler().loadImage(imgLoc);
+
+	for(const auto & element : palette)
+		img->shiftPalette(element.start, element.length, paletteShiftCounter % element.length);
+
+	auto image = GH.renderHandler().createImage(Point(32, 32), CanvasScalingPolicy::IGNORE);
+	Canvas canvas = image->getCanvas();
+	canvas.draw(img, Point((32 - img->dimensions().x) / 2, (32 - img->dimensions().y) / 2));
+
+	return image;
 
-		std::fstream file(CResourceHandler::get("local")->getResourceName(savePath)->c_str(), std::ofstream::out | std::ofstream::trunc);
-		file << config.toString();
-	}
 }

+ 42 - 9
client/render/AssetGenerator.h

@@ -9,20 +9,53 @@
  */
 #pragma once
 
+#include "ImageLocator.h"
+
 VCMI_LIB_NAMESPACE_BEGIN
 class PlayerColor;
 VCMI_LIB_NAMESPACE_END
 
+class ISharedImage;
+class CanvasImage;
+
 class AssetGenerator
 {
 public:
-	static void clear();
-	static void generateAll();
-	static void createAdventureOptionsCleanBackground();
-	static void createBigSpellBook();
-	static void createPlayerColoredBackground(const PlayerColor & player);
-	static void createCombatUnitNumberWindow();
-	static void createCampaignBackground();
-	static void createChroniclesCampaignImages();
-	static void createPaletteShiftedSprites();
+	using AnimationLayoutMap = std::map<size_t, std::vector<ImageLocator>>;
+	using CanvasPtr = std::shared_ptr<CanvasImage>;
+
+	AssetGenerator();
+
+	void initialize();
+
+	std::shared_ptr<ISharedImage> generateImage(const ImagePath & image);
+
+	std::map<ImagePath, std::shared_ptr<ISharedImage>> generateAllImages();
+	std::map<AnimationPath, AnimationLayoutMap> generateAllAnimations();
+
+private:
+	using ImageGenerationFunctor = std::function<CanvasPtr()>;
+
+	struct PaletteAnimation
+	{
+		/// index of first color to cycle
+		int32_t start;
+		/// total numbers of colors to cycle
+		int32_t length;
+	};
+
+	std::map<ImagePath, ImageGenerationFunctor> imageFiles;
+	std::map<AnimationPath, AnimationLayoutMap> animationFiles;
+
+	CanvasPtr createAdventureOptionsCleanBackground();
+	CanvasPtr createBigSpellBook();
+	CanvasPtr createPlayerColoredBackground(const PlayerColor & player);
+	CanvasPtr createCombatUnitNumberWindow(float multR, float multG, float multB);
+	CanvasPtr createCampaignBackground();
+	CanvasPtr createChroniclesCampaignImages(int chronicle);
+	CanvasPtr createPaletteShiftedImage(const AnimationPath & source, const std::vector<PaletteAnimation> & animation, int frameIndex, int paletteShiftCounter);
+
+	void createPaletteShiftedSprites();
+	void generatePaletteShiftedAnimation(const AnimationPath & source, const std::vector<PaletteAnimation> & animation);
+
 };

+ 6 - 0
client/render/CanvasImage.cpp

@@ -14,6 +14,7 @@
 #include "../render/IScreenHandler.h"
 #include "../renderSDL/SDL_Extensions.h"
 #include "../renderSDL/SDLImageScaler.h"
+#include "../renderSDL/SDLImage.h"
 
 #include <SDL_image.h>
 #include <SDL_surface.h>
@@ -61,3 +62,8 @@ Point CanvasImage::dimensions() const
 {
 	return {surface->w, surface->h};
 }
+
+std::shared_ptr<ISharedImage> CanvasImage::toSharedImage()
+{
+	return std::make_shared<SDLImageShared>(surface);
+}

+ 2 - 0
client/render/CanvasImage.h

@@ -34,6 +34,8 @@ public:
 	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override{};
 	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override{};
 
+	std::shared_ptr<ISharedImage> toSharedImage();
+
 private:
 	SDL_Surface * surface;
 	CanvasScalingPolicy scalingPolicy;

+ 2 - 0
client/render/IRenderHandler.h

@@ -50,4 +50,6 @@ public:
 
 	/// Returns font with specified identifer
 	virtual std::shared_ptr<const IFont> loadFont(EFonts font) = 0;
+
+	virtual void exportGeneratedAssets() = 0;
 };

+ 37 - 3
client/renderSDL/RenderHandler.cpp

@@ -16,6 +16,7 @@
 
 #include "../gui/CGuiHandler.h"
 
+#include "../render/AssetGenerator.h"
 #include "../render/CAnimation.h"
 #include "../render/CanvasImage.h"
 #include "../render/CDefFile.h"
@@ -43,6 +44,13 @@
 #include <vcmi/SkillService.h>
 #include <vcmi/spells/Service.h>
 
+RenderHandler::RenderHandler()
+	:assetGenerator(std::make_unique<AssetGenerator>())
+{
+}
+
+RenderHandler::~RenderHandler() = default;
+
 std::shared_ptr<CDefFile> RenderHandler::getAnimationFile(const AnimationPath & path)
 {
 	AnimationPath actualPath = boost::starts_with(path.getName(), "SPRITES") ? path : path.addPrefix("SPRITES/");
@@ -201,12 +209,28 @@ std::shared_ptr<ScalableImageShared> RenderHandler::loadImageImpl(const ImageLoc
 	return scaledImage;
 }
 
-std::shared_ptr<SDLImageShared> RenderHandler::loadImageFromFileUncached(const ImageLocator & locator)
+std::shared_ptr<ISharedImage> RenderHandler::loadImageFromFileUncached(const ImageLocator & locator)
 {
 	if(locator.image)
 	{
-		// TODO: create EmptySharedImage class that will be instantiated if image does not exists or fails to load
-		return std::make_shared<SDLImageShared>(*locator.image);
+		auto imagePath = *locator.image;
+		auto imagePathSprites = imagePath.addPrefix("SPRITES/");
+		auto imagePathData = imagePath.addPrefix("DATA/");
+
+		if(CResourceHandler::get()->existsResource(imagePathSprites))
+			return std::make_shared<SDLImageShared>(imagePathSprites);
+
+		if(CResourceHandler::get()->existsResource(imagePathData))
+			return std::make_shared<SDLImageShared>(imagePathData);
+
+		if(CResourceHandler::get()->existsResource(imagePath))
+			return std::make_shared<SDLImageShared>(imagePath);
+
+		auto generated = assetGenerator->generateImage(imagePath);
+		if (generated)
+			return generated;
+
+		return std::make_shared<SDLImageShared>(ImagePath::builtin("DEFAULT"));
 	}
 
 	if(locator.defFile)
@@ -423,6 +447,10 @@ static void detectOverlappingBuildings(RenderHandler * renderHandler, const Fact
 
 void RenderHandler::onLibraryLoadingFinished(const Services * services)
 {
+	assert(animationLayouts.empty());
+	assetGenerator->initialize();
+	animationLayouts = assetGenerator->generateAllAnimations();
+
 	addImageListEntries(services->creatures());
 	addImageListEntries(services->heroTypes());
 	addImageListEntries(services->artifacts());
@@ -469,3 +497,9 @@ std::shared_ptr<const IFont> RenderHandler::loadFont(EFonts font)
 	fonts[font] = loadedFont;
 	return loadedFont;
 }
+
+void RenderHandler::exportGeneratedAssets()
+{
+	for (const auto & entry : assetGenerator->generateAllImages())
+		entry.second->exportBitmap(VCMIDirs::get().userDataPath() / "Generated" / (entry.first.getOriginalName() + ".png"), nullptr);
+}

+ 8 - 2
client/renderSDL/RenderHandler.h

@@ -18,8 +18,9 @@ VCMI_LIB_NAMESPACE_END
 class CDefFile;
 class SDLImageShared;
 class ScalableImageShared;
+class AssetGenerator;
 
-class RenderHandler : public IRenderHandler
+class RenderHandler final : public IRenderHandler
 {
 	using AnimationLayoutMap = std::map<size_t, std::vector<ImageLocator>>;
 
@@ -27,6 +28,7 @@ class RenderHandler : public IRenderHandler
 	std::map<AnimationPath, AnimationLayoutMap> animationLayouts;
 	std::map<SharedImageLocator, std::shared_ptr<ScalableImageShared>> imageFiles;
 	std::map<EFonts, std::shared_ptr<const IFont>> fonts;
+	std::unique_ptr<AssetGenerator> assetGenerator;
 
 	std::shared_ptr<CDefFile> getAnimationFile(const AnimationPath & path);
 	AnimationLayoutMap & getAnimationLayout(const AnimationPath & path, int scalingFactor, EImageBlitMode mode);
@@ -38,13 +40,15 @@ class RenderHandler : public IRenderHandler
 
 	std::shared_ptr<ScalableImageShared> loadImageImpl(const ImageLocator & config);
 
-	std::shared_ptr<SDLImageShared> loadImageFromFileUncached(const ImageLocator & locator);
+	std::shared_ptr<ISharedImage> loadImageFromFileUncached(const ImageLocator & locator);
 
 	ImageLocator getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group, int scaling, EImageBlitMode mode);
 
 	int getScalingFactor() const;
 
 public:
+	RenderHandler();
+	~RenderHandler();
 
 	// IRenderHandler implementation
 	void onLibraryLoadingFinished(const Services * services) override;
@@ -61,4 +65,6 @@ public:
 
 	/// Returns font with specified identifer
 	std::shared_ptr<const IFont> loadFont(EFonts font) override;
+
+	void exportGeneratedAssets() override;
 };

+ 4 - 0
client/renderSDL/SDLImage.cpp

@@ -306,6 +306,10 @@ std::shared_ptr<const ISharedImage> SDLImageShared::scaleTo(const Point & size,
 
 void SDLImageShared::exportBitmap(const boost::filesystem::path& path, SDL_Palette * palette) const
 {
+	auto directory = path;
+	directory.remove_filename();
+	boost::filesystem::create_directories(directory);
+
 	assert(upscalingInProgress == false);
 	if (!surf)
 		return;

+ 6 - 0
client/renderSDL/SDLImageScaler.cpp

@@ -120,6 +120,9 @@ const Rect & SDLImageOptimizer::getResultDimensions() const
 
 void SDLImageScaler::scaleSurface(Point targetDimensions, EScalingAlgorithm algorithm)
 {
+	if (!intermediate)
+		return; // may happen on scaling of empty images
+
 	if(!targetDimensions.x || !targetDimensions.y)
 		throw std::runtime_error("invalid scaling dimensions!");
 
@@ -144,6 +147,9 @@ void SDLImageScaler::scaleSurface(Point targetDimensions, EScalingAlgorithm algo
 
 void SDLImageScaler::scaleSurfaceIntegerFactor(int factor, EScalingAlgorithm algorithm)
 {
+	if (!intermediate)
+		return; // may happen on scaling of empty images
+
 	if(factor == 0)
 		throw std::runtime_error("invalid scaling factor!");
 

+ 0 - 3
client/widgets/Images.cpp

@@ -13,7 +13,6 @@
 #include "MiscWidgets.h"
 
 #include "../gui/CGuiHandler.h"
-#include "../render/AssetGenerator.h"
 #include "../render/IImage.h"
 #include "../render/IRenderHandler.h"
 #include "../render/CAnimation.h"
@@ -184,8 +183,6 @@ FilledTexturePlayerColored::FilledTexturePlayerColored(Rect position)
 
 void FilledTexturePlayerColored::setPlayerColor(PlayerColor player)
 {
-	AssetGenerator::createPlayerColoredBackground(player);
-
 	ImagePath imagePath = ImagePath::builtin("DialogBoxBackground_" + player.toString() + ".bmp");
 
 	texture = GH.renderHandler().loadImage(imagePath, EImageBlitMode::COLORKEY);

+ 5 - 1
client/windows/CCreatureWindow.cpp

@@ -234,7 +234,11 @@ CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int
 			spellText = CGI->generaltexth->allTexts[610]; //"%s, duration: %d rounds."
 			boost::replace_first(spellText, "%s", spell->getNameTranslated());
 			//FIXME: support permanent duration
-			int duration = battleStack->getFirstBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)))->turnsRemain;
+			auto spellBonuses = battleStack->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)));
+			if (spellBonuses->empty())
+				throw std::runtime_error("Failed to find effects for spell " + effect.toSpell()->getJsonKey());
+
+			int duration = spellBonuses->front()->duration;
 			boost::replace_first(spellText, "%d", std::to_string(duration));
 
 			spellIcons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed));

+ 0 - 2
client/windows/CSpellWindow.cpp

@@ -32,7 +32,6 @@
 #include "../widgets/Buttons.h"
 #include "../widgets/VideoWidget.h"
 #include "../adventureMap/AdventureMapInterface.h"
-#include "../render/AssetGenerator.h"
 
 #include "../../CCallback.h"
 
@@ -118,7 +117,6 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
 
 	if(isBigSpellbook)
 	{
-		AssetGenerator::createBigSpellBook();
 		background = std::make_shared<CPicture>(ImagePath::builtin("SpellBookLarge"), 0, 0);
 		updateShadow();
 	}

+ 1 - 1
client/windows/settings/SettingsMainWindow.h

@@ -29,7 +29,6 @@ private:
 	std::shared_ptr<CIntObject> createTab(size_t index);
 	void openTab(size_t index);
 
-	void close(); //TODO: copypaste of WindowBase::close(), consider changing Windowbase to IWindowbase with default close() implementation and changing WindowBase inheritance to CIntObject + IWindowBase
 
 	void loadGameButtonCallback();
 	void saveGameButtonCallback();
@@ -40,6 +39,7 @@ private:
 public:
 	SettingsMainWindow(BattleInterface * parentBattleInterface = nullptr);
 
+	void close(); //TODO: copypaste of WindowBase::close(), consider changing Windowbase to IWindowbase with default close() implementation and changing WindowBase inheritance to CIntObject + IWindowBase
 	void showAll(Canvas & to) override;
 	void onScreenResize() override;
 };

+ 0 - 3
clientapp/EntryPoint.cpp

@@ -27,7 +27,6 @@
 #include "../client/media/CMusicHandler.h"
 #include "../client/media/CSoundHandler.h"
 #include "../client/media/CVideoHandler.h"
-#include "../client/render/AssetGenerator.h"
 #include "../client/render/Graphics.h"
 #include "../client/render/IRenderHandler.h"
 #include "../client/render/IScreenHandler.h"
@@ -235,8 +234,6 @@ int main(int argc, char * argv[])
 	logGlobal->info("Creating console and configuring logger: %d ms", pomtime.getDiff());
 	logGlobal->info("The log file will be saved to %s", logPath);
 
-	AssetGenerator::clear();
-
 	// Init filesystem and settings
 	try
 	{

+ 6 - 0
debian/changelog

@@ -4,6 +4,12 @@ vcmi (1.7.0) jammy; urgency=medium
 
  -- Ivan Savenko <[email protected]>  Fri, 30 May 2025 12:00:00 +0200
 
+vcmi (1.6.5) jammy; urgency=medium
+
+  * New upstream release
+
+ -- Ivan Savenko <[email protected]>  Mon, 3 Feb 2025 12:00:00 +0200
+
 vcmi (1.6.4) jammy; urgency=medium
 
   * New upstream release

+ 1 - 1
docs/Readme.md

@@ -1,9 +1,9 @@
 # VCMI Project
 
 [![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush)
-[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.2/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.2)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.3/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.3)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.4/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.4)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.6.5/total)](https://github.com/vcmi/vcmi/releases/tag/1.6.5)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 
 VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities.

+ 1 - 0
launcher/eu.vcmi.VCMI.metainfo.xml

@@ -91,6 +91,7 @@
 	<launchable type="desktop-id">vcmilauncher.desktop</launchable>
 	<releases>
 		<release version="1.7.0" date="2025-05-30" type="development"/>
+		<release version="1.6.5" date="2025-02-03" type="stable"/>
 		<release version="1.6.4" date="2025-01-31" type="stable"/>
 		<release version="1.6.3" date="2025-01-10" type="stable"/>
 		<release version="1.6.2" date="2025-01-03" type="stable"/>

+ 1 - 1
launcher/modManager/modstate.cpp

@@ -71,7 +71,7 @@ QStringList ModState::getConflicts() const
 
 QStringList ModState::getScreenshots() const
 {
-	return stringListStdToQt(impl.getLocalizedValue("screenshots").convertTo<std::vector<std::string>>());
+	return stringListStdToQt(impl.getRepositoryValue("screenshots").convertTo<std::vector<std::string>>());
 }
 
 QString ModState::getBaseLanguage() const

+ 14 - 2
lib/texts/TextOperations.cpp

@@ -161,12 +161,24 @@ uint32_t TextOperations::getUnicodeCodepoint(char data, const std::string & enco
 
 std::string TextOperations::toUnicode(const std::string &text, const std::string &encoding)
 {
-	return boost::locale::conv::to_utf<char>(text, encoding);
+	try {
+		return boost::locale::conv::to_utf<char>(text, encoding);
+	}
+	catch (const boost::locale::conv::conversion_error &)
+	{
+		throw std::runtime_error("Failed to convert text '" + text + "' from encoding " + encoding );
+	}
 }
 
 std::string TextOperations::fromUnicode(const std::string &text, const std::string &encoding)
 {
-	return boost::locale::conv::from_utf<char>(text, encoding);
+	try {
+		return boost::locale::conv::from_utf<char>(text, encoding);
+	}
+	catch (const boost::locale::conv::conversion_error &)
+	{
+		throw std::runtime_error("Failed to convert text '" + text + "' to encoding " + encoding );
+	}
 }
 
 void TextOperations::trimRightUnicode(std::string & text, const size_t amount)

+ 2 - 2
mapeditor/inspector/towneventdialog.cpp

@@ -230,10 +230,10 @@ QVariantMap TownEventDialog::resourcesToVariant()
 	auto res = params.value("resources").toMap();
 	for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i)
 	{
-		auto * itemType = ui->resourcesTable->item(i, 0);
+		auto itemType = QString::fromStdString(GameConstants::RESOURCE_NAMES[i]);
 		auto * itemQty = static_cast<QSpinBox *> (ui->resourcesTable->cellWidget(i, 1));
 
-		res[itemType->text()] = QVariant::fromValue(itemQty->value());
+		res[itemType] = QVariant::fromValue(itemQty->value());
 	}
 	return res;
 }

+ 2 - 0
mapeditor/mainwindow.cpp

@@ -979,10 +979,12 @@ void MainWindow::on_actionLevel_triggered()
 		ui->minimapView->setScene(controller.miniScene(mapLevel));
 		if (mapLevel == 0)
 		{
+			ui->actionLevel->setText(tr("View underground"));
 			ui->actionLevel->setToolTip(tr("View underground"));
 		}
 		else
 		{
+			ui->actionLevel->setText(tr("View surface"));
 			ui->actionLevel->setToolTip(tr("View surface"));
 		}
 	}

+ 1 - 1
mapeditor/mainwindow.ui

@@ -1067,7 +1067,7 @@
   </action>
   <action name="actionLevel">
    <property name="text">
-    <string>U/G</string>
+    <string>View underground</string>
    </property>
    <property name="toolTip">
     <string>View underground</string>

+ 2 - 2
mapeditor/mapsettings/timedevent.cpp

@@ -96,9 +96,9 @@ void TimedEvent::on_TimedEvent_finished(int result)
 	auto res = target->data(Qt::UserRole).toMap().value("resources").toMap();
 	for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i)
 	{
-		auto * itemType = ui->resources->item(i, 0);
+		auto itemType = QString::fromStdString(GameConstants::RESOURCE_NAMES[i]);
 		auto * itemQty = ui->resources->item(i, 1);
-		res[itemType->text()] = QVariant::fromValue(itemQty->text().toInt());
+		res[itemType] = QVariant::fromValue(itemQty->text().toInt());
 	}
 	descriptor["resources"] = res;
 

+ 4 - 24
mapeditor/translation/czech.ts

@@ -464,7 +464,7 @@
     <message>
         <location filename="../mainwindow.ui" line="1141"/>
         <source>General</source>
-        <translation>Všeobecné</translation>
+        <translation>Nastavení</translation>
     </message>
     <message>
         <location filename="../mainwindow.ui" line="1144"/>
@@ -474,7 +474,7 @@
     <message>
         <location filename="../mainwindow.ui" line="1155"/>
         <source>Players settings</source>
-        <translation>Hráčské nastavení</translation>
+        <translation>Hráči</translation>
     </message>
     <message>
         <location filename="../mainwindow.ui" line="1166"/>
@@ -500,7 +500,7 @@
     <message>
         <location filename="../mainwindow.ui" line="1216"/>
         <source>Validate</source>
-        <translation>Ověřit</translation>
+        <translation>Validátor</translation>
     </message>
     <message>
         <location filename="../mainwindow.ui" line="1227"/>
@@ -2457,7 +2457,7 @@
     <message>
         <location filename="../validator.ui" line="17"/>
         <source>Map validation results</source>
-        <translation>Výsledky ověření mapy</translation>
+        <translation>Výsledky validátoru</translation>
     </message>
     <message>
         <location filename="../validator.cpp" line="54"/>
@@ -2660,10 +2660,6 @@
         <source>Map size</source>
         <translation>Velikost mapy</translation>
     </message>
-    <message>
-        <source>Two level map</source>
-        <translation type="vanished">Dvouvrstvá mapa</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="200"/>
         <source>Height</source>
@@ -2689,14 +2685,6 @@
         <source>Players</source>
         <translation>Hráči</translation>
     </message>
-    <message>
-        <source>0</source>
-        <translation type="vanished">0</translation>
-    </message>
-    <message>
-        <source>Human/Computer</source>
-        <translation type="vanished">Hráč/počítač</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="93"/>
         <source>S  (36x36)</source>
@@ -2735,10 +2723,6 @@
         <source>Random</source>
         <translation>Náhodně</translation>
     </message>
-    <message>
-        <source>Computer only</source>
-        <translation type="vanished">Pouze počítač</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="444"/>
         <source>Human teams</source>
@@ -2851,10 +2835,6 @@
         <source>OK</source>
         <translation>OK</translation>
     </message>
-    <message>
-        <source>Ok</source>
-        <translation type="vanished">Dobře</translation>
-    </message>
     <message>
         <location filename="../windownewmap.ui" line="1044"/>
         <source>Cancel</source>