1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-08-08 22:26:51 +02:00

Merge branch 'beta' into 'develop'

This commit is contained in:
Ivan Savenko
2025-02-03 16:29:10 +00:00
38 changed files with 390 additions and 351 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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");
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ISharedImage> 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<ImagePath, std::shared_ptr<ISharedImage>> AssetGenerator::generateAllImages()
{
std::map<ImagePath, std::shared_ptr<ISharedImage>> 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<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));
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<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));
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<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++)
auto imgPathBg = ImagePath::builtin("data/chronicles_" + std::to_string(chronicle) + "/GamSelBk");
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::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<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;
}
image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
//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;
for(int i = 0; i < tiles.size(); i++)
{
auto sprite = tiles[i];
std::vector<PaletteAnimation> 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<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);
}
}
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<PaletteAnimation> & 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<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;
}

View File

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

View File

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

View File

@@ -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;

View File

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

View File

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

View File

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

View File

@@ -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;

View File

@@ -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!");

View File

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

View File

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

View File

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

View File

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

View File

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

6
debian/changelog vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"));
}
}

View File

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

View File

@@ -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;

View File

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