Просмотр исходного кода

In-memory assets generation

All assets generation (large spellbook, terrain animations, etc) are now
done in memory and used as it, without saving to disk.

This should slightly improve load times since there is no encode png /
decode png, and should help with avoiding strange bug when vcmi fails to
load recently saved assets.

If needed, such assets can be force-dumped on disk using already
existing console command
Ivan Savenko 10 месяцев назад
Родитель
Сommit
cca4c0888c

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

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

+ 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){

+ 0 - 4
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"
@@ -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;

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

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

+ 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
 	{