From cca4c0888c6700308e71c9f892c9beb49b44b96c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 29 Jan 2025 10:03:58 +0000 Subject: [PATCH 01/17] 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 --- client/ClientCommandManager.cpp | 3 +- client/adventureMap/AdventureMapInterface.cpp | 3 - client/battle/BattleStacksController.cpp | 2 - client/lobby/OptionsTabBase.cpp | 3 - client/mainmenu/CMainMenu.cpp | 4 - client/render/AssetGenerator.cpp | 391 +++++++----------- client/render/AssetGenerator.h | 51 ++- client/render/CanvasImage.cpp | 6 + client/render/CanvasImage.h | 2 + client/render/IRenderHandler.h | 2 + client/renderSDL/RenderHandler.cpp | 40 +- client/renderSDL/RenderHandler.h | 10 +- client/renderSDL/SDLImage.cpp | 4 + client/widgets/Images.cpp | 3 - client/windows/CSpellWindow.cpp | 2 - clientapp/EntryPoint.cpp | 3 - 16 files changed, 257 insertions(+), 272 deletions(-) diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index fc41ffab1..c6f31e881 100644 --- a/client/ClientCommandManager.cpp +++ b/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"); } diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 55dd66e1d..4d54817f7 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/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(*this); widget = std::make_shared(shortcuts); diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 6c981d621..d0be307cc 100644 --- a/client/battle/BattleStacksController.cpp +++ b/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); diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index 8335d9f9d..ad148b7a2 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/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 OptionsTabBase::getSimturnsPresets() const OptionsTabBase::OptionsTabBase(const JsonPath & configPath) { - AssetGenerator::createAdventureOptionsCleanBackground(); - recActions = 0; auto setTimerPresetCallback = [this](int index){ diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 05b2f71cb..e5e8af87c 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/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); diff --git a/client/render/AssetGenerator.cpp b/client/render/AssetGenerator.cpp index 20882069d..6f264b0ac 100644 --- a/client/render/AssetGenerator.cpp +++ b/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"); -} -void AssetGenerator::generateAll() -{ - createBigSpellBook(); - createAdventureOptionsCleanBackground(); - for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) - createPlayerColoredBackground(PlayerColor(i)); - createCombatUnitNumberWindow(); - createCampaignBackground(); - createChroniclesCampaignImages(); + 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::createAdventureOptionsCleanBackground() +std::shared_ptr AssetGenerator::generateImage(const ImagePath & image) { - std::string filename = "data/AdventureOptionsBackgroundClear.png"; + if (imageFiles.count(image)) + return imageFiles.at(image)()->toSharedImage(); // TODO: cache? + else + return nullptr; +} - if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation - return; +std::map> AssetGenerator::generateAllImages() +{ + std::map> result; - if(!CResourceHandler::get("local")->createResource(filename)) - return; - ResourcePath savePath(filename, EResType::IMAGE); + for (const auto & entry : imageFiles) + result[entry.first] = entry.second()->toSharedImage(); + return result; +} + +std::map AssetGenerator::generateAllAnimations() +{ + return animationFiles; +} + +AssetGenerator::CanvasPtr AssetGenerator::createAdventureOptionsCleanBackground() +{ auto locator = ImageLocator(ImagePath::builtin("ADVOPTBK"), EImageBlitMode::OPAQUE); std::shared_ptr 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 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 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)); + + auto image = GH.renderHandler().createImage(texture->dimensions(), CanvasScalingPolicy::IGNORE); + Canvas canvas = image->getCanvas(); + canvas.draw(texture, Point(0,0)); + + return image; } -void AssetGenerator::createCombatUnitNumberWindow() +AssetGenerator::CanvasPtr AssetGenerator::createCombatUnitNumberWindow(float multR, float multG, float multB) { - 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; - - 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; - auto locator = ImageLocator(ImagePath::builtin("CMNUMWIN"), EImageBlitMode::OPAQUE); locator.layer = EImageBlitMode::OPAQUE; std::shared_ptr 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)); + texture->adjustPalette(shifter, ignoredMask); + + auto image = GH.renderHandler().createImage(texture->dimensions(), CanvasScalingPolicy::IGNORE); + Canvas canvas = image->getCanvas(); + canvas.draw(texture, Point(0,0)); + + return image; } -void AssetGenerator::createCampaignBackground() +AssetGenerator::CanvasPtr AssetGenerator::createCampaignBackground() { - std::string filename = "data/CampaignBackground8.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("CAMPBACK"), EImageBlitMode::OPAQUE); std::shared_ptr 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 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++) + auto imgPathBg = ImagePath::builtin("data/chronicles_" + std::to_string(chronicle) + "/GamSelBk"); + auto locator = ImageLocator(imgPathBg, EImageBlitMode::OPAQUE); + + std::shared_ptr img = GH.renderHandler().loadImage(locator); + auto image = GH.renderHandler().createImage(Point(800, 600), CanvasScalingPolicy::IGNORE); + Canvas canvas = image->getCanvas(); + + 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)); + + if (chronicle == 8) { - 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 locator = ImageLocator(imgPathBg, EImageBlitMode::OPAQUE); - - std::shared_ptr 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 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; - } - - image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath)); + //skull + auto locatorSkull = ImageLocator(ImagePath::builtin("CampSP1"), EImageBlitMode::OPAQUE); + std::shared_ptr 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 tiles; - std::vector>> paletteAnimations; for(auto entity : VLC->terrainTypeHandler->objects) { - if(entity->paletteAnimation.size()) - { - tiles.push_back(entity->tilesFilename.getName()); - std::vector> tmpAnim; - for(auto & animEntity : entity->paletteAnimation) - tmpAnim.push_back(animEntity); - paletteAnimations.push_back(tmpAnim); - } + if(entity->paletteAnimation.empty()) + continue; + + std::vector 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> tmpAnim; - for(auto & animEntity : entity->paletteAnimation) - tmpAnim.push_back(animEntity); - paletteAnimations.push_back(tmpAnim); - } - } + if(entity->paletteAnimation.empty()) + continue; - for(int i = 0; i < tiles.size(); i++) - { - auto sprite = tiles[i]; + std::vector paletteShifts; + for(auto & animEntity : entity->paletteAnimation) + paletteShifts.push_back({animEntity.start, animEntity.length}); - JsonNode config; - config["basepath"].String() = sprite + "_Shifted/"; - config["images"].Vector(); - - auto filename = AnimationPath::builtin(sprite).addPrefix("SPRITES/"); - auto filenameNew = AnimationPath::builtin(sprite + "_Shifted").addPrefix("SPRITES/"); - - 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++) - { - int maxLen = 1; - for(int k = 0; k < paletteAnimations[i].size(); k++) - { - auto element = paletteAnimations[i][k]; - if(std::holds_alternative(element)) - maxLen = std::lcm(maxLen, std::get(element).length); - else - maxLen = std::lcm(maxLen, std::get(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(element)) - { - auto tmp = std::get(element); - img->shiftPalette(tmp.start, tmp.length, l % tmp.length); - } - else - { - auto tmp = std::get(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); - } - } - - ResourcePath savePath(filenameNew.getOriginalName(), EResType::JSON); - if(!CResourceHandler::get("local")->createResource(filenameNew.getOriginalName() + ".json")) - return; - - std::fstream file(CResourceHandler::get("local")->getResourceName(savePath)->c_str(), std::ofstream::out | std::ofstream::trunc); - file << config.toString(); + generatePaletteShiftedAnimation(entity->tilesFilename, paletteShifts); } } + +void AssetGenerator::generatePaletteShiftedAnimation(const AnimationPath & sprite, const std::vector & paletteAnimations) +{ + AnimationLayoutMap layout; + + auto animation = GH.renderHandler().loadAnimation(sprite, EImageBlitMode::COLORKEY); + + int paletteTransformLength = 1; + for (const auto & transform : paletteAnimations) + paletteTransformLength = std::lcm(paletteTransformLength, transform.length); + + for(int tileIndex = 0; tileIndex < animation->size(); tileIndex++) + { + for(int paletteIndex = 0; paletteIndex < paletteTransformLength; paletteIndex++) + { + 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);}; + } + } + + AnimationPath shiftedPath = AnimationPath::builtin("SPRITES/" + sprite.getName() + "_SHIFTED"); + animationFiles[shiftedPath] = layout; +} + +AssetGenerator::CanvasPtr AssetGenerator::createPaletteShiftedImage(const AnimationPath & source, const std::vector & 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; + +} diff --git a/client/render/AssetGenerator.h b/client/render/AssetGenerator.h index 51584754a..c0646b727 100644 --- a/client/render/AssetGenerator.h +++ b/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>; + using CanvasPtr = std::shared_ptr; + + AssetGenerator(); + + void initialize(); + + std::shared_ptr generateImage(const ImagePath & image); + + std::map> generateAllImages(); + std::map generateAllAnimations(); + +private: + using ImageGenerationFunctor = std::function; + + struct PaletteAnimation + { + /// index of first color to cycle + int32_t start; + /// total numbers of colors to cycle + int32_t length; + }; + + std::map imageFiles; + std::map 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 & animation, int frameIndex, int paletteShiftCounter); + + void createPaletteShiftedSprites(); + void generatePaletteShiftedAnimation(const AnimationPath & source, const std::vector & animation); + }; diff --git a/client/render/CanvasImage.cpp b/client/render/CanvasImage.cpp index 07e9b3474..dc6d619bb 100644 --- a/client/render/CanvasImage.cpp +++ b/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 #include @@ -61,3 +62,8 @@ Point CanvasImage::dimensions() const { return {surface->w, surface->h}; } + +std::shared_ptr CanvasImage::toSharedImage() +{ + return std::make_shared(surface); +} diff --git a/client/render/CanvasImage.h b/client/render/CanvasImage.h index 18c342832..095a4276b 100644 --- a/client/render/CanvasImage.h +++ b/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 toSharedImage(); + private: SDL_Surface * surface; CanvasScalingPolicy scalingPolicy; diff --git a/client/render/IRenderHandler.h b/client/render/IRenderHandler.h index cefd02890..2210bd797 100644 --- a/client/render/IRenderHandler.h +++ b/client/render/IRenderHandler.h @@ -50,4 +50,6 @@ public: /// Returns font with specified identifer virtual std::shared_ptr loadFont(EFonts font) = 0; + + virtual void exportGeneratedAssets() = 0; }; diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index 285c539cd..92469f4d3 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/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 #include +RenderHandler::RenderHandler() + :assetGenerator(std::make_unique()) +{ +} + +RenderHandler::~RenderHandler() = default; + std::shared_ptr RenderHandler::getAnimationFile(const AnimationPath & path) { AnimationPath actualPath = boost::starts_with(path.getName(), "SPRITES") ? path : path.addPrefix("SPRITES/"); @@ -201,12 +209,28 @@ std::shared_ptr RenderHandler::loadImageImpl(const ImageLoc return scaledImage; } -std::shared_ptr RenderHandler::loadImageFromFileUncached(const ImageLocator & locator) +std::shared_ptr 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(*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(imagePathSprites); + + if(CResourceHandler::get()->existsResource(imagePathData)) + return std::make_shared(imagePathData); + + if(CResourceHandler::get()->existsResource(imagePath)) + return std::make_shared(imagePath); + + auto generated = assetGenerator->generateImage(imagePath); + if (generated) + return generated; + + return std::make_shared(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 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); +} diff --git a/client/renderSDL/RenderHandler.h b/client/renderSDL/RenderHandler.h index 4a7e73c6a..b795e5a4e 100644 --- a/client/renderSDL/RenderHandler.h +++ b/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>; @@ -27,6 +28,7 @@ class RenderHandler : public IRenderHandler std::map animationLayouts; std::map> imageFiles; std::map> fonts; + std::unique_ptr assetGenerator; std::shared_ptr getAnimationFile(const AnimationPath & path); AnimationLayoutMap & getAnimationLayout(const AnimationPath & path, int scalingFactor, EImageBlitMode mode); @@ -38,13 +40,15 @@ class RenderHandler : public IRenderHandler std::shared_ptr loadImageImpl(const ImageLocator & config); - std::shared_ptr loadImageFromFileUncached(const ImageLocator & locator); + std::shared_ptr 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 loadFont(EFonts font) override; + + void exportGeneratedAssets() override; }; diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index 16adecf69..d584f4683 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -306,6 +306,10 @@ std::shared_ptr 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; diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index 6ebabdc0f..6693300e3 100644 --- a/client/widgets/Images.cpp +++ b/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); diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 56168d707..f3033e864 100644 --- a/client/windows/CSpellWindow.cpp +++ b/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(ImagePath::builtin("SpellBookLarge"), 0, 0); updateShadow(); } diff --git a/clientapp/EntryPoint.cpp b/clientapp/EntryPoint.cpp index b4081ce66..a0cbd7b16 100644 --- a/clientapp/EntryPoint.cpp +++ b/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 { From 5b579f69127fb1f3909f18c5fc5cca723997323d Mon Sep 17 00:00:00 2001 From: George King <98261225+GeorgeK1ng@users.noreply.github.com> Date: Sat, 1 Feb 2025 09:57:11 +0100 Subject: [PATCH 02/17] Fix VCMP campaign detection for buttons --- client/mainmenu/CMainMenu.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 05b2f71cb..6fe00de67 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -278,7 +278,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; } From 5ff9baf565826cc1e4e1afcc1d82311239d47fd4 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 1 Feb 2025 11:50:48 +0000 Subject: [PATCH 03/17] Additional checks for potentially invalid hexes --- client/battle/BattleFieldController.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index ed99d5298..412215deb 100644 --- a/client/battle/BattleFieldController.cpp +++ b/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(); From 386679294fa334ed69381536017308fbefc19c78 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 1 Feb 2025 11:51:15 +0000 Subject: [PATCH 04/17] Fix crash on stack being affected by spell with battle-wide propagator --- client/battle/BattleInterfaceClasses.cpp | 7 ++++++- client/windows/CCreatureWindow.cpp | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 596bdbd1e..1f57228da 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/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(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); if(settings["general"]["enableUiEnhancements"].Bool()) diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index 286ed0f8a..9279a080c 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/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(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); From cf44186a11f434db448d6d1e478dc700e2a6a48d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 1 Feb 2025 12:17:17 +0000 Subject: [PATCH 05/17] Fix crash on accepting turn in MP with settings window open --- client/CPlayerInterface.cpp | 14 +++++++++++--- client/gui/CIntObject.cpp | 2 +- client/windows/settings/SettingsMainWindow.h | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 6b6db20b9..61fda3b47 100644 --- a/client/CPlayerInterface.cpp +++ b/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(); + auto settingsWindow = GH.windows().topWindow(); auto infoWindow = GH.windows().topWindow(); auto topWindow = GH.windows().topWindow(); @@ -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 } } diff --git a/client/gui/CIntObject.cpp b/client/gui/CIntObject.cpp index 14e3d09a1..d38238ce6 100644 --- a/client/gui/CIntObject.cpp +++ b/client/gui/CIntObject.cpp @@ -345,7 +345,7 @@ void WindowBase::close() if(!GH.windows().isTopWindow(this)) { auto topWindow = GH.windows().topWindow().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); } diff --git a/client/windows/settings/SettingsMainWindow.h b/client/windows/settings/SettingsMainWindow.h index 7b26921df..5e59695d0 100644 --- a/client/windows/settings/SettingsMainWindow.h +++ b/client/windows/settings/SettingsMainWindow.h @@ -29,7 +29,6 @@ private: std::shared_ptr 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; }; From 3d6c58c366f881577dc6451a1abcbcb9e840f68d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 1 Feb 2025 13:55:09 +0000 Subject: [PATCH 06/17] Fix crash on scaling empty surface --- client/renderSDL/SDLImageScaler.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/renderSDL/SDLImageScaler.cpp b/client/renderSDL/SDLImageScaler.cpp index dfa906e0d..d2a958f5f 100644 --- a/client/renderSDL/SDLImageScaler.cpp +++ b/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!"); From 5810ff2b41e53f964920dd760cb15bba2925fe29 Mon Sep 17 00:00:00 2001 From: George King <98261225+GeorgeK1ng@users.noreply.github.com> Date: Sat, 1 Feb 2025 15:42:38 +0100 Subject: [PATCH 07/17] Updated Czech translation --- Mods/vcmi/Content/config/czech.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Mods/vcmi/Content/config/czech.json b/Mods/vcmi/Content/config/czech.json index 9732e0bfe..74300aece 100644 --- a/Mods/vcmi/Content/config/czech.json +++ b/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", From 7f09e22242eeae79df6f3bd6dfa85da1cb957ab8 Mon Sep 17 00:00:00 2001 From: George King <98261225+GeorgeK1ng@users.noreply.github.com> Date: Sat, 1 Feb 2025 16:23:59 +0100 Subject: [PATCH 08/17] Improved Ground / Undergound menu text --- mapeditor/mainwindow.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 8210b77ca..b4daf03a8 100644 --- a/mapeditor/mainwindow.cpp +++ b/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")); } } From 93024102f46c7e370e646e1baf6f0187fcd635be Mon Sep 17 00:00:00 2001 From: George King <98261225+GeorgeK1ng@users.noreply.github.com> Date: Sat, 1 Feb 2025 16:25:25 +0100 Subject: [PATCH 09/17] Improved Ground / Undergound menu text --- mapeditor/mainwindow.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 31431bd8d..c0cc0382a 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -1067,7 +1067,7 @@ - U/G + View underground View underground From 18c21cd0aeaf34129e2726769be5b5d84a0206ee Mon Sep 17 00:00:00 2001 From: George King <98261225+GeorgeK1ng@users.noreply.github.com> Date: Sat, 1 Feb 2025 16:26:55 +0100 Subject: [PATCH 10/17] Update czech.json --- Mods/vcmi/Content/config/czech.json | 60 ++++++++++++++--------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/Mods/vcmi/Content/config/czech.json b/Mods/vcmi/Content/config/czech.json index 74300aece..05e1e02da 100644 --- a/Mods/vcmi/Content/config/czech.json +++ b/Mods/vcmi/Content/config/czech.json @@ -327,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é", @@ -338,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é.", @@ -763,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", @@ -807,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" -} \ No newline at end of file +} From 82f963bc12928d15737d6285f5401c4bcce425cb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 1 Feb 2025 18:09:34 +0000 Subject: [PATCH 11/17] Fix crash on double-clicking lobby login button --- client/globalLobby/GlobalLobbyLoginWindow.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/globalLobby/GlobalLobbyLoginWindow.cpp b/client/globalLobby/GlobalLobbyLoginWindow.cpp index d76f80d17..88bdb7f3c 100644 --- a/client/globalLobby/GlobalLobbyLoginWindow.cpp +++ b/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); } From 977e15d15c06e6891c5dc28fa828b111ab6aab1f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 1 Feb 2025 18:09:53 +0000 Subject: [PATCH 12/17] Erase any active windows before deleting guihandler --- client/gui/CGuiHandler.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index 6608ca44d..9dfe71703 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -134,6 +134,7 @@ CGuiHandler::~CGuiHandler() // enforce deletion order on shutdown // all UI elements including adventure map must be destroyed before Gui Handler // proper solution would be removal of adventureInt global + windowHandlerInstance->clear(); adventureInt.reset(); } From 4b20128144be5c0f7371f7bf66b132381cc2d3f8 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 1 Feb 2025 18:10:22 +0000 Subject: [PATCH 13/17] Close hero overview before recreating advanced options dialog --- client/lobby/OptionsTab.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 97bb2383b..dd00f5442 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -68,6 +68,9 @@ void OptionsTab::recreate() entries.clear(); humanPlayers = 0; + for (auto heroOverview : GH.windows().findWindows()) + heroOverview->close(); + for (auto selectionWindow : GH.windows().findWindows()) { selectionWindow->reopen(); From a95ab5a7ce97cda84fb0b8dfaca8bda7b0f1b314 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 1 Feb 2025 18:10:42 +0000 Subject: [PATCH 14/17] Add better debugging for text conversion failure --- lib/texts/TextOperations.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/texts/TextOperations.cpp b/lib/texts/TextOperations.cpp index 8410867d0..d29dbefdf 100644 --- a/lib/texts/TextOperations.cpp +++ b/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(text, encoding); + try { + return boost::locale::conv::to_utf(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(text, encoding); + try { + return boost::locale::conv::from_utf(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) From 61e5dbde20a36778887fa1613c6434ccc1705dea Mon Sep 17 00:00:00 2001 From: godric3 Date: Sun, 2 Feb 2025 17:03:58 +0100 Subject: [PATCH 15/17] map editor: fix resource reward in events --- mapeditor/inspector/towneventdialog.cpp | 4 ++-- mapeditor/mapsettings/timedevent.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mapeditor/inspector/towneventdialog.cpp b/mapeditor/inspector/towneventdialog.cpp index 7d5ff496d..e2fa1805d 100644 --- a/mapeditor/inspector/towneventdialog.cpp +++ b/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 (ui->resourcesTable->cellWidget(i, 1)); - res[itemType->text()] = QVariant::fromValue(itemQty->value()); + res[itemType] = QVariant::fromValue(itemQty->value()); } return res; } diff --git a/mapeditor/mapsettings/timedevent.cpp b/mapeditor/mapsettings/timedevent.cpp index 5cc14afe6..6df7957f1 100644 --- a/mapeditor/mapsettings/timedevent.cpp +++ b/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; From fb4451df74f3b25389f309a27bdb5e24a08937e0 Mon Sep 17 00:00:00 2001 From: George King <98261225+GeorgeK1ng@users.noreply.github.com> Date: Sun, 2 Feb 2025 18:51:19 +0100 Subject: [PATCH 16/17] Updated Czech translation for Map Editor --- mapeditor/translation/czech.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mapeditor/translation/czech.ts b/mapeditor/translation/czech.ts index f238b42f9..d45ae6556 100644 --- a/mapeditor/translation/czech.ts +++ b/mapeditor/translation/czech.ts @@ -464,7 +464,7 @@ General - Všeobecné + Nastavení @@ -474,7 +474,7 @@ Players settings - Hráčské nastavení + Hráči @@ -500,7 +500,7 @@ Validate - Ověřit + Validátor @@ -2457,7 +2457,7 @@ Map validation results - Výsledky ověření mapy + Výsledky validátoru From b44259647c44ad057a4eba4a30093c03cf135c34 Mon Sep 17 00:00:00 2001 From: George King <98261225+GeorgeK1ng@users.noreply.github.com> Date: Sun, 2 Feb 2025 18:53:31 +0100 Subject: [PATCH 17/17] Removed vanished elements --- mapeditor/translation/czech.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/mapeditor/translation/czech.ts b/mapeditor/translation/czech.ts index d45ae6556..74e0adbd2 100644 --- a/mapeditor/translation/czech.ts +++ b/mapeditor/translation/czech.ts @@ -2660,10 +2660,6 @@ Map size Velikost mapy - - Two level map - Dvouvrstvá mapa - Height @@ -2689,14 +2685,6 @@ Players Hráči - - 0 - 0 - - - Human/Computer - Hráč/počítač - S (36x36) @@ -2735,10 +2723,6 @@ Random Náhodně - - Computer only - Pouze počítač - Human teams @@ -2851,10 +2835,6 @@ OK OK - - Ok - Dobře - Cancel