1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-07-17 01:32:21 +02:00

Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
Xilmi
2024-09-05 15:54:03 +02:00
50 changed files with 1219 additions and 131 deletions

View File

@ -140,10 +140,10 @@
"vcmi.client.errors.invalidMap" : "{Błędna mapa lub kampania}\n\nNie udało się stworzyć gry! Wybrana mapa lub kampania jest niepoprawna lub uszkodzona. Powód:\n%s", "vcmi.client.errors.invalidMap" : "{Błędna mapa lub kampania}\n\nNie udało się stworzyć gry! Wybrana mapa lub kampania jest niepoprawna lub uszkodzona. Powód:\n%s",
"vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.", "vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.",
"vcmi.server.errors.disconnected" : "{Błąd sieciowy}\n\nUtracono połączenie z serwerem!",
"vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej",
"vcmi.server.errors.modsToEnable" : "{Następujące mody są wymagane do wczytania gry}", "vcmi.server.errors.modsToEnable" : "{Następujące mody są wymagane do wczytania gry}",
"vcmi.server.errors.modsToDisable" : "{Następujące mody muszą zostać wyłączone}", "vcmi.server.errors.modsToDisable" : "{Następujące mody muszą zostać wyłączone}",
"vcmi.server.confirmReconnect" : "Połączyć ponownie z ostatnią sesją?",
"vcmi.server.errors.modNoDependency" : "Nie udało się wczytać moda {'%s'}!\n Jest on zależny od moda {'%s'} który nie jest aktywny!\n", "vcmi.server.errors.modNoDependency" : "Nie udało się wczytać moda {'%s'}!\n Jest on zależny od moda {'%s'} który nie jest aktywny!\n",
"vcmi.server.errors.modConflict" : "Nie udało się wczytać moda {'%s'}!\n Konflikty z aktywnym modem {'%s'}!\n", "vcmi.server.errors.modConflict" : "Nie udało się wczytać moda {'%s'}!\n Konflikty z aktywnym modem {'%s'}!\n",
"vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!", "vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!",
@ -162,6 +162,38 @@
"vcmi.systemOptions.otherGroup" : "Inne ustawienia", // unused right now "vcmi.systemOptions.otherGroup" : "Inne ustawienia", // unused right now
"vcmi.systemOptions.townsGroup" : "Ekran miasta", "vcmi.systemOptions.townsGroup" : "Ekran miasta",
"vcmi.statisticWindow.statistics" : "Statystyki",
"vcmi.statisticWindow.tsvCopy" : "Kopiuj do schowka",
"vcmi.statisticWindow.selectView" : "Tryb widoku",
"vcmi.statisticWindow.value" : "Wartość",
"vcmi.statisticWindow.title.overview" : "Przegląd",
"vcmi.statisticWindow.title.resources" : "Surowce",
"vcmi.statisticWindow.title.income" : "Przychód",
"vcmi.statisticWindow.title.numberOfHeroes" : "Lb. bohaterów",
"vcmi.statisticWindow.title.numberOfTowns" : "Lb. miast",
"vcmi.statisticWindow.title.numberOfArtifacts" : "Lb. artefaktów",
"vcmi.statisticWindow.title.numberOfDwellings" : "Lb. siedlisk",
"vcmi.statisticWindow.title.numberOfMines" : "Lb. kopalni",
"vcmi.statisticWindow.title.armyStrength" : "Siła armii",
"vcmi.statisticWindow.title.experience" : "Doświadczenie",
"vcmi.statisticWindow.title.resourcesSpentArmy" : "Koszty armii",
"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Koszty budowy",
"vcmi.statisticWindow.title.mapExplored" : "Odkrycie mapy",
"vcmi.statisticWindow.param.playerName" : "Imię",
"vcmi.statisticWindow.param.daysSurvived" : "Przeżytych dni",
"vcmi.statisticWindow.param.maxHeroLevel" : "Maks. poziom bohatera",
"vcmi.statisticWindow.param.battleWinRatioHero" : "Zwycięstw (vs. bohaterom)",
"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Zwycięstw (vs. neutralnym)",
"vcmi.statisticWindow.param.battlesHero" : "Walk (vs. bohaterom)",
"vcmi.statisticWindow.param.battlesNeutral" : "Walk (vs. neutralnym)",
"vcmi.statisticWindow.param.maxArmyStrength" : "Maks. siła armii",
"vcmi.statisticWindow.param.tradeVolume" : "Wielkość handlu",
"vcmi.statisticWindow.param.obeliskVisited" : "Lb. obelisków",
"vcmi.statisticWindow.icon.townCaptured" : "Miasto zdobyte",
"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Najsilniejszy bohater przeciwnika pokonany",
"vcmi.statisticWindow.icon.grailFound" : "Gral znaleziony",
"vcmi.statisticWindow.icon.defeated" : "Pokonany",
"vcmi.systemOptions.fullscreenBorderless.hover" : "Pełny ekran (bez ramek)", "vcmi.systemOptions.fullscreenBorderless.hover" : "Pełny ekran (bez ramek)",
"vcmi.systemOptions.fullscreenBorderless.help" : "{Pełny ekran w trybie okna}\n\nVCMI będzie działać w trybie okna pełnoekranowego. W tym trybie gra będzie zawsze używać rozdzielczości pulpitu, ignorując wybraną rozdzielczość.", "vcmi.systemOptions.fullscreenBorderless.help" : "{Pełny ekran w trybie okna}\n\nVCMI będzie działać w trybie okna pełnoekranowego. W tym trybie gra będzie zawsze używać rozdzielczości pulpitu, ignorując wybraną rozdzielczość.",
"vcmi.systemOptions.fullscreenExclusive.hover" : "Pełny ekran (tradycyjny)", "vcmi.systemOptions.fullscreenExclusive.hover" : "Pełny ekran (tradycyjny)",
@ -628,5 +660,7 @@
"core.bonus.WATER_IMMUNITY.name": "Odporność: Woda", "core.bonus.WATER_IMMUNITY.name": "Odporność: Woda",
"core.bonus.WATER_IMMUNITY.description": "Odporny na wszystkie czary szkoły wody", "core.bonus.WATER_IMMUNITY.description": "Odporny na wszystkie czary szkoły wody",
"core.bonus.WIDE_BREATH.name": "Szerokie zionięcie", "core.bonus.WIDE_BREATH.name": "Szerokie zionięcie",
"core.bonus.WIDE_BREATH.description": "Szeroki atak zionięciem (wiele heksów)" "core.bonus.WIDE_BREATH.description": "Szeroki atak zionięciem (wiele heksów)",
"core.bonus.DISINTEGRATE.name": "Rozpadanie",
"core.bonus.DISINTEGRATE.description": "Po śmierci nie pozostaje żaden trup"
} }

View File

@ -532,7 +532,10 @@ void CServerHandler::sendGuiAction(ui8 action) const
void CServerHandler::sendRestartGame() const void CServerHandler::sendRestartGame() const
{ {
GH.windows().createAndPushWindow<CLoadingScreen>(); if(si->campState && !si->campState->getLoadingBackground().empty())
GH.windows().createAndPushWindow<CLoadingScreen>(si->campState->getLoadingBackground());
else
GH.windows().createAndPushWindow<CLoadingScreen>();
LobbyRestartGame endGame; LobbyRestartGame endGame;
sendLobbyPack(endGame); sendLobbyPack(endGame);
@ -576,7 +579,12 @@ void CServerHandler::sendStartGame(bool allowOnlyAI) const
verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool()); verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool());
if(!settings["session"]["headless"].Bool()) if(!settings["session"]["headless"].Bool())
GH.windows().createAndPushWindow<CLoadingScreen>(); {
if(si->campState && !si->campState->getLoadingBackground().empty())
GH.windows().createAndPushWindow<CLoadingScreen>(si->campState->getLoadingBackground());
else
GH.windows().createAndPushWindow<CLoadingScreen>();
}
LobbyPrepareStartGame lpsg; LobbyPrepareStartGame lpsg;
sendLobbyPack(lpsg); sendLobbyPack(lpsg);

View File

@ -35,6 +35,7 @@
#include "../lib/CConfigHandler.h" #include "../lib/CConfigHandler.h"
#include "../lib/texts/CGeneralTextHandler.h" #include "../lib/texts/CGeneralTextHandler.h"
#include "../lib/serializer/Connection.h" #include "../lib/serializer/Connection.h"
#include "../lib/campaign/CampaignState.h"
void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack) void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack)
{ {
@ -203,7 +204,10 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState &
if(!lobby->bonusSel && handler.si->campState && handler.getState() == EClientState::LOBBY_CAMPAIGN) if(!lobby->bonusSel && handler.si->campState && handler.getState() == EClientState::LOBBY_CAMPAIGN)
{ {
lobby->bonusSel = std::make_shared<CBonusSelection>(); lobby->bonusSel = std::make_shared<CBonusSelection>();
GH.windows().pushWindow(lobby->bonusSel); if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty())
GH.windows().createAndPushWindow<CampaignIntroVideo>(handler.si->campState->getIntroVideo(), handler.si->campState->getIntroVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getIntroVideoRim(), lobby->bonusSel);
else
GH.windows().pushWindow(lobby->bonusSel);
} }
if(lobby->bonusSel) if(lobby->bonusSel)

View File

@ -28,6 +28,7 @@
#include "../widgets/MiscWidgets.h" #include "../widgets/MiscWidgets.h"
#include "../widgets/ObjectLists.h" #include "../widgets/ObjectLists.h"
#include "../widgets/TextControls.h" #include "../widgets/TextControls.h"
#include "../widgets/VideoWidget.h"
#include "../windows/GUIClasses.h" #include "../windows/GUIClasses.h"
#include "../windows/InfoWindows.h" #include "../windows/InfoWindows.h"
#include "../render/IImage.h" #include "../render/IImage.h"
@ -58,6 +59,41 @@
#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGHeroInstance.h"
CampaignIntroVideo::CampaignIntroVideo(VideoPath video, ImagePath rim, std::shared_ptr<CBonusSelection> bonusSel)
: CWindowObject(BORDERED), bonusSel(bonusSel)
{
OBJECT_CONSTRUCTION;
addUsedEvents(LCLICK | KEYBOARD);
pos = center(Rect(0, 0, 800, 600));
videoPlayer = std::make_shared<VideoWidgetOnce>(Point(80, 186), video, true, [this](){ exit(); });
setBackground(rim);
CCS->musich->stopMusic();
}
void CampaignIntroVideo::exit()
{
close();
if (!CSH->si->campState->getMusic().empty())
CCS->musich->playMusic(CSH->si->campState->getMusic(), true, false);
GH.windows().pushWindow(bonusSel);
}
void CampaignIntroVideo::clickPressed(const Point & cursorPosition)
{
exit();
}
void CampaignIntroVideo::keyPressed(EShortcut key)
{
exit();
}
std::shared_ptr<CampaignState> CBonusSelection::getCampaign() std::shared_ptr<CampaignState> CBonusSelection::getCampaign()
{ {
return CSH->si->campState; return CSH->si->campState;
@ -93,7 +129,9 @@ CBonusSelection::CBonusSelection()
labelCampaignDescription = std::make_shared<CLabel>(481, 63, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]); labelCampaignDescription = std::make_shared<CLabel>(481, 63, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]);
campaignDescription = std::make_shared<CTextBox>(getCampaign()->getDescriptionTranslated(), Rect(480, 86, 286, 117), 1); campaignDescription = std::make_shared<CTextBox>(getCampaign()->getDescriptionTranslated(), Rect(480, 86, 286, 117), 1);
mapName = std::make_shared<CLabel>(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated(), 285); bool videoButtonActive = CSH->getState() == EClientState::GAMEPLAY;
int availableSpace = videoButtonActive ? 225 : 285;
mapName = std::make_shared<CLabel>(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated(), availableSpace );
labelMapDescription = std::make_shared<CLabel>(481, 253, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]); labelMapDescription = std::make_shared<CLabel>(481, 253, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]);
mapDescription = std::make_shared<CTextBox>("", Rect(480, 278, 292, 108), 1); mapDescription = std::make_shared<CTextBox>("", Rect(480, 278, 292, 108), 1);

View File

@ -12,6 +12,7 @@
#include "../windows/CWindowObject.h" #include "../windows/CWindowObject.h"
#include "../lib/campaign/CampaignConstants.h" #include "../lib/campaign/CampaignConstants.h"
#include "../lib/filesystem/ResourcePath.h"
VCMI_LIB_NAMESPACE_BEGIN VCMI_LIB_NAMESPACE_BEGIN
@ -28,6 +29,22 @@ class CLabel;
class CFlagBox; class CFlagBox;
class ISelectionScreenInfo; class ISelectionScreenInfo;
class ExtraOptionsTab; class ExtraOptionsTab;
class VideoWidgetOnce;
class CBonusSelection;
class CampaignIntroVideo : public CWindowObject
{
std::shared_ptr<VideoWidgetOnce> videoPlayer;
std::shared_ptr<CBonusSelection> bonusSel;
void exit();
public:
CampaignIntroVideo(VideoPath video, ImagePath rim, std::shared_ptr<CBonusSelection> bonusSel);
void clickPressed(const Point & cursorPosition) override;
void keyPressed(EShortcut key) override;
};
/// Campaign screen where you can choose one out of three starting bonuses /// Campaign screen where you can choose one out of three starting bonuses
class CBonusSelection : public CWindowObject class CBonusSelection : public CWindowObject

View File

@ -601,6 +601,12 @@ void RandomMapTab::loadOptions()
{ {
w->setItem(mapGenOptions->getMapTemplate()); w->setItem(mapGenOptions->getMapTemplate());
} }
} else
{
// Default settings
mapGenOptions->setRoadEnabled(RoadId(Road::DIRT_ROAD), true);
mapGenOptions->setRoadEnabled(RoadId(Road::GRAVEL_ROAD), true);
mapGenOptions->setRoadEnabled(RoadId(Road::COBBLESTONE_ROAD), true);
} }
updateMapInfoByHost(); updateMapInfoByHost();

View File

@ -787,6 +787,8 @@ bool SelectionTab::isMapSupported(const CMapInfo & info)
return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["supported"].Bool(); return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["supported"].Bool();
case EMapFormat::SOD: case EMapFormat::SOD:
return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["supported"].Bool(); return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["supported"].Bool();
case EMapFormat::CHR:
return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_CHRONICLES)["supported"].Bool();
case EMapFormat::WOG: case EMapFormat::WOG:
return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)["supported"].Bool(); return CGI->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)["supported"].Bool();
case EMapFormat::HOTA: case EMapFormat::HOTA:

View File

@ -629,7 +629,12 @@ void CSimpleJoinScreen::startConnection(const std::string & addr, ui16 port)
} }
CLoadingScreen::CLoadingScreen() CLoadingScreen::CLoadingScreen()
: CWindowObject(BORDERED, getBackground()) : CLoadingScreen(getBackground())
{
}
CLoadingScreen::CLoadingScreen(ImagePath background)
: CWindowObject(BORDERED, background)
{ {
OBJECT_CONSTRUCTION; OBJECT_CONSTRUCTION;

View File

@ -192,6 +192,7 @@ class CLoadingScreen : virtual public CWindowObject, virtual public Load::Progre
public: public:
CLoadingScreen(); CLoadingScreen();
CLoadingScreen(ImagePath background);
~CLoadingScreen(); ~CLoadingScreen();
void tick(uint32_t msPassed) override; void tick(uint32_t msPassed) override;

View File

@ -16,15 +16,17 @@
#include "../render/Colors.h" #include "../render/Colors.h"
#include "../render/IScreenHandler.h" #include "../render/IScreenHandler.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/Rect.h" #include "../../lib/Rect.h"
#include "../../lib/VCMI_Lib.h"
#include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/Filesystem.h"
#include "../../lib/modding/CModHandler.h" #include "../../lib/modding/CModHandler.h"
#include "../../lib/texts/Languages.h" #include "../../lib/texts/Languages.h"
#include "../../lib/texts/TextOperations.h" #include "../../lib/texts/TextOperations.h"
#include "../../lib/vcmi_endian.h" #include "../../lib/vcmi_endian.h"
#include "../../lib/VCMI_Lib.h"
#include <SDL_surface.h> #include <SDL_surface.h>
#include <SDL_image.h>
struct AtlasLayout struct AtlasLayout
{ {
@ -199,10 +201,20 @@ CBitmapFont::CBitmapFont(const std::string & filename):
if (GH.screenHandler().getScalingFactor() != 1) if (GH.screenHandler().getScalingFactor() != 1)
{ {
auto scaledSurface = CSDL_Ext::scaleSurfaceIntegerFactor(atlasImage, GH.screenHandler().getScalingFactor()); static const std::map<std::string, EScalingAlgorithm> filterNameToEnum = {
{ "nearest", EScalingAlgorithm::NEAREST},
{ "bilinear", EScalingAlgorithm::BILINEAR},
{ "xbrz", EScalingAlgorithm::XBRZ}
};
auto filterName = settings["video"]["fontUpscalingFilter"].String();
EScalingAlgorithm algorithm = filterNameToEnum.at(filterName);
auto scaledSurface = CSDL_Ext::scaleSurfaceIntegerFactor(atlasImage, GH.screenHandler().getScalingFactor(), algorithm);
SDL_FreeSurface(atlasImage); SDL_FreeSurface(atlasImage);
atlasImage = scaledSurface; atlasImage = scaledSurface;
} }
IMG_SavePNG(atlasImage, ("/home/ivan/font_" + filename).c_str());
} }
CBitmapFont::~CBitmapFont() CBitmapFont::~CBitmapFont()

View File

@ -27,19 +27,25 @@ std::pair<std::unique_ptr<ui8[]>, ui64> CTrueTypeFont::loadData(const JsonNode &
return CResourceHandler::get()->load(ResourcePath(filename, EResType::TTF_FONT))->readAll(); return CResourceHandler::get()->load(ResourcePath(filename, EResType::TTF_FONT))->readAll();
} }
int CTrueTypeFont::getPointSize(const JsonNode & config) const
{
int scalingFactor = getScalingFactor();
if (config.isNumber())
return config.Integer() * scalingFactor;
else
return config[scalingFactor-1].Integer();
}
TTF_Font * CTrueTypeFont::loadFont(const JsonNode &config) TTF_Font * CTrueTypeFont::loadFont(const JsonNode &config)
{ {
int pointSizeBase = static_cast<int>(config["size"].Float());
int scalingFactor = getScalingFactor();
int pointSize = pointSizeBase * scalingFactor;
if(!TTF_WasInit() && TTF_Init()==-1) if(!TTF_WasInit() && TTF_Init()==-1)
throw std::runtime_error(std::string("Failed to initialize true type support: ") + TTF_GetError() + "\n"); throw std::runtime_error(std::string("Failed to initialize true type support: ") + TTF_GetError() + "\n");
return TTF_OpenFontRW(SDL_RWFromConstMem(data.first.get(), (int)data.second), 1, pointSize); return TTF_OpenFontRW(SDL_RWFromConstMem(data.first.get(), data.second), 1, getPointSize(config["size"]));
} }
int CTrueTypeFont::getFontStyle(const JsonNode &config) int CTrueTypeFont::getFontStyle(const JsonNode &config) const
{ {
const JsonVector & names = config["style"].Vector(); const JsonVector & names = config["style"].Vector();
int ret = 0; int ret = 0;

View File

@ -30,7 +30,8 @@ class CTrueTypeFont final : public IFont
std::pair<std::unique_ptr<ui8[]>, ui64> loadData(const JsonNode & config); std::pair<std::unique_ptr<ui8[]>, ui64> loadData(const JsonNode & config);
TTF_Font * loadFont(const JsonNode & config); TTF_Font * loadFont(const JsonNode & config);
int getFontStyle(const JsonNode & config); int getPointSize(const JsonNode & config) const;
int getFontStyle(const JsonNode & config) const;
void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override; void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override;
public: public:

View File

@ -270,30 +270,10 @@ std::shared_ptr<ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palet
if (factor <= 0) if (factor <= 0)
throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor)); throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor));
if (palette && surf->format->palette) if (palette && surf && surf->format->palette)
SDL_SetSurfacePalette(surf, palette); SDL_SetSurfacePalette(surf, palette);
/// Convert current surface to ARGB format suitable for xBRZ SDL_Surface * scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ);
/// TODO: skip its creation if this is format matches current surface (even if unlikely)
SDL_Surface * intermediate = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ARGB8888, 0);
SDL_Surface * scaled = CSDL_Ext::newSurface(Point(surf->w * factor, surf->h * factor), intermediate);
assert(intermediate->pitch == intermediate->w * 4);
assert(scaled->pitch == scaled->w * 4);
const uint32_t * srcPixels = static_cast<const uint32_t*>(intermediate->pixels);
uint32_t * dstPixels = static_cast<uint32_t*>(scaled->pixels);
// avoid excessive granulation - xBRZ prefers at least 8-16 lines per task
// TODO: compare performance and size of images, recheck values for potentially better parameters
const int granulation = std::clamp(surf->h / 64 * 8, 8, 64);
tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate->h, granulation), [factor, srcPixels, dstPixels, intermediate](const tbb::blocked_range<size_t> & r)
{
xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, xbrz::ColorFormat::ARGB, {}, r.begin(), r.end());
});
SDL_FreeSurface(intermediate);
auto ret = std::make_shared<SDLImageShared>(scaled); auto ret = std::make_shared<SDLImageShared>(scaled);
@ -307,7 +287,7 @@ std::shared_ptr<ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palet
// erase our own reference // erase our own reference
SDL_FreeSurface(scaled); SDL_FreeSurface(scaled);
if (surf->format->palette) if (surf && surf->format->palette)
SDL_SetSurfacePalette(surf, originalPalette); SDL_SetSurfacePalette(surf, originalPalette);
return ret; return ret;

View File

@ -638,8 +638,8 @@ SDL_Surface * CSDL_Ext::scaleSurface(SDL_Surface * surf, int width, int height)
if(!surf || !width || !height) if(!surf || !width || !height)
return nullptr; return nullptr;
if (surf->w * 2 == width && surf->h * 2 == height) // TODO: use xBRZ if possible? E.g. when scaling to 150% do 100% -> 200% via xBRZ and then linear downscale 200% -> 150%?
return scaleSurfaceIntegerFactor(surf, 2); // Need to investigate which is optimal for performance and for visuals
SDL_Surface * intermediate = SDL_ConvertSurface(surf, screen->format, 0); SDL_Surface * intermediate = SDL_ConvertSurface(surf, screen->format, 0);
SDL_Surface * ret = newSurface(Point(width, height), intermediate); SDL_Surface * ret = newSurface(Point(width, height), intermediate);
@ -654,7 +654,7 @@ SDL_Surface * CSDL_Ext::scaleSurface(SDL_Surface * surf, int width, int height)
return ret; return ret;
} }
SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor) SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor, EScalingAlgorithm algorithm)
{ {
if(surf == nullptr || factor == 0) if(surf == nullptr || factor == 0)
return nullptr; return nullptr;
@ -662,7 +662,7 @@ SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor
int newWidth = surf->w * factor; int newWidth = surf->w * factor;
int newHight = surf->h * factor; int newHight = surf->h * factor;
SDL_Surface * intermediate = SDL_ConvertSurface(surf, screen->format, 0); SDL_Surface * intermediate = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_ARGB8888, 0);
SDL_Surface * ret = newSurface(Point(newWidth, newHight), intermediate); SDL_Surface * ret = newSurface(Point(newWidth, newHight), intermediate);
assert(intermediate->pitch == intermediate->w * 4); assert(intermediate->pitch == intermediate->w * 4);
@ -675,10 +675,23 @@ SDL_Surface * CSDL_Ext::scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor
// TODO: compare performance and size of images, recheck values for potentially better parameters // TODO: compare performance and size of images, recheck values for potentially better parameters
const int granulation = std::clamp(surf->h / 64 * 8, 8, 64); const int granulation = std::clamp(surf->h / 64 * 8, 8, 64);
tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate->h, granulation), [factor, srcPixels, dstPixels, intermediate](const tbb::blocked_range<size_t> & r) switch (algorithm)
{ {
xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, xbrz::ColorFormat::ARGB, {}, r.begin(), r.end()); case EScalingAlgorithm::NEAREST:
}); xbrz::nearestNeighborScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h);
break;
case EScalingAlgorithm::BILINEAR:
xbrz::bilinearScale(srcPixels, intermediate->w, intermediate->h, dstPixels, ret->w, ret->h);
break;
case EScalingAlgorithm::XBRZ:
tbb::parallel_for(tbb::blocked_range<size_t>(0, intermediate->h, granulation), [factor, srcPixels, dstPixels, intermediate](const tbb::blocked_range<size_t> & r)
{
xbrz::scale(factor, srcPixels, dstPixels, intermediate->w, intermediate->h, xbrz::ColorFormat::ARGB, {}, r.begin(), r.end());
});
break;
default:
throw std::runtime_error("invalid scaling algorithm!");
}
SDL_FreeSurface(intermediate); SDL_FreeSurface(intermediate);

View File

@ -27,6 +27,13 @@ class Point;
VCMI_LIB_NAMESPACE_END VCMI_LIB_NAMESPACE_END
enum class EScalingAlgorithm : int8_t
{
NEAREST,
BILINEAR,
XBRZ
};
namespace CSDL_Ext namespace CSDL_Ext
{ {
@ -92,7 +99,7 @@ using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &,
// bilinear filtering. Always returns rgba surface // bilinear filtering. Always returns rgba surface
SDL_Surface * scaleSurface(SDL_Surface * surf, int width, int height); SDL_Surface * scaleSurface(SDL_Surface * surf, int width, int height);
SDL_Surface * scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor); SDL_Surface * scaleSurfaceIntegerFactor(SDL_Surface * surf, int factor, EScalingAlgorithm scaler);
template<int bpp> template<int bpp>
void convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect); void convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect);

View File

@ -73,20 +73,19 @@ std::vector<std::string> CMessage::breakText(std::string text, size_t maxLineWid
// each iteration generates one output line // each iteration generates one output line
while(text.length()) while(text.length())
{ {
ui32 lineWidth = 0; //in characters or given char metric
ui32 wordBreak = -1; //last position for line break (last space character) ui32 wordBreak = -1; //last position for line break (last space character)
ui32 currPos = 0; //current position in text ui32 currPos = 0; //current position in text
bool opened = false; //set to true when opening brace is found bool opened = false; //set to true when opening brace is found
std::string color; //color found std::string color; //color found
size_t symbolSize = 0; // width of character, in bytes size_t symbolSize = 0; // width of character, in bytes
size_t glyphWidth = 0; // width of printable glyph, pixels
std::string printableString;
// loops till line is full or end of text reached // loops till line is full or end of text reached
while(currPos < text.length() && text[currPos] != 0x0a && lineWidth < maxLineWidth) while(currPos < text.length() && text[currPos] != 0x0a)
{ {
symbolSize = TextOperations::getUnicodeCharacterSize(text[currPos]); symbolSize = TextOperations::getUnicodeCharacterSize(text[currPos]);
glyphWidth = graphics->fonts[font]->getGlyphWidth(text.data() + currPos);
// candidate for line break // candidate for line break
if(ui8(text[currPos]) <= ui8(' ')) if(ui8(text[currPos]) <= ui8(' '))
@ -116,7 +115,14 @@ std::vector<std::string> CMessage::breakText(std::string text, size_t maxLineWid
color = ""; color = "";
} }
else else
lineWidth += glyphWidth; {
std::string newPrintableString = printableString;
newPrintableString.append(text.data() + currPos, symbolSize);
if (graphics->fonts[font]->getStringWidth(newPrintableString) < maxLineWidth)
printableString.append(text.data() + currPos, symbolSize);
else
break;
}
currPos += symbolSize; currPos += symbolSize;
} }

View File

@ -0,0 +1,254 @@
{
"MAPS/HC1_MAIN" : { // Heroes Chronicles 1
"regions":
{
"background": "chronicles_1/CamBkHc",
"prefix": "chronicles_1/HcSc",
"suffix": ["1", "2", "3"],
"color_suffix_length": 0,
"desc": [
{ "infix": "1", "x": 27, "y": 43 },
{ "infix": "2", "x": 231, "y": 43 },
{ "infix": "3", "x": 27, "y": 178 },
{ "infix": "4", "x": 231, "y": 178 },
{ "infix": "5", "x": 27, "y": 312 },
{ "infix": "6", "x": 231, "y": 312 },
{ "infix": "7", "x": 27, "y": 447 },
{ "infix": "8", "x": 231, "y": 447 }
]
},
"scenarioCount": 8,
"scenarios": [
{ "voiceProlog": "chronicles_1/ABVOFL4" },
{ "voiceProlog": "chronicles_1/H3X2UAE" },
{ "voiceProlog": "chronicles_1/H3X2BBA" },
{ "voiceProlog": "chronicles_1/H3X2RND" },
{ "voiceProlog": "chronicles_1/G1C" },
{ "voiceProlog": "chronicles_1/G2C" },
{ "voiceProlog": "chronicles_1/ABVOFL3" },
{ "voiceProlog": "chronicles_1/H3X2BBF", "voiceEpilog": "chronicles_1/N1C_D" }
],
"loadingBackground": "chronicles_1/LoadBar",
"introVideoRim": "chronicles_1/INTRORIM",
"introVideo": "chronicles_1/Intro"
},
"MAPS/HC2_MAIN" : { // Heroes Chronicles 2
"regions":
{
"background": "chronicles_2/CamBkHc",
"prefix": "chronicles_2/HcSc",
"suffix": ["1", "2", "3"],
"color_suffix_length": 0,
"desc": [
{ "infix": "1", "x": 27, "y": 43 },
{ "infix": "2", "x": 231, "y": 43 },
{ "infix": "3", "x": 27, "y": 178 },
{ "infix": "4", "x": 231, "y": 178 },
{ "infix": "5", "x": 27, "y": 312 },
{ "infix": "6", "x": 231, "y": 312 },
{ "infix": "7", "x": 27, "y": 447 },
{ "infix": "8", "x": 231, "y": 447 }
]
},
"scenarioCount": 8,
"scenarios": [
{ "voiceProlog": "chronicles_2/H3X2ELB" },
{ "voiceProlog": "chronicles_2/H3X2NBA" },
{ "voiceProlog": "chronicles_2/H3X2RNA" },
{ "voiceProlog": "chronicles_2/ABVOAB8" },
{ "voiceProlog": "chronicles_2/H3X2UAL" },
{ "voiceProlog": "chronicles_2/E1A" },
{ "voiceProlog": "chronicles_2/ABVOAB2" },
{ "voiceProlog": "chronicles_2/G1A", "voiceEpilog": "chronicles_2/S1C" }
],
"loadingBackground": "chronicles_2/LoadBar",
"introVideoRim": "chronicles_2/INTRORIM",
"introVideo": "chronicles_2/Intro"
},
"MAPS/HC3_MAIN" : { // Heroes Chronicles 3
"regions":
{
"background": "chronicles_3/CamBkHc",
"prefix": "chronicles_3/HcSc",
"suffix": ["1", "2", "3"],
"color_suffix_length": 0,
"desc": [
{ "infix": "1", "x": 27, "y": 43 },
{ "infix": "2", "x": 231, "y": 43 },
{ "infix": "3", "x": 27, "y": 178 },
{ "infix": "4", "x": 231, "y": 178 },
{ "infix": "5", "x": 27, "y": 312 },
{ "infix": "6", "x": 231, "y": 312 },
{ "infix": "7", "x": 27, "y": 447 },
{ "infix": "8", "x": 231, "y": 447 }
]
},
"scenarioCount": 8,
"scenarios": [
{ "voiceProlog": "chronicles_3/G2C" },
{ "voiceProlog": "chronicles_3/ABVOAB1" },
{ "voiceProlog": "chronicles_3/G2D" },
{ "voiceProlog": "chronicles_3/E1B" },
{ "voiceProlog": "chronicles_3/ABVOAB2" },
{ "voiceProlog": "chronicles_3/ABVOAB4" },
{ "voiceProlog": "chronicles_3/ABVOAB6" },
{ "voiceProlog": "chronicles_3/G3B", "voiceEpilog": "chronicles_3/ABVOFL2" }
],
"loadingBackground": "chronicles_3/LoadBar",
"introVideoRim": "chronicles_3/INTRORIM",
"introVideo": "chronicles_3/Intro"
},
"MAPS/HC4_MAIN" : { // Heroes Chronicles 4
"regions":
{
"background": "chronicles_4/CamBkHc",
"prefix": "chronicles_4/HcSc",
"suffix": ["1", "2", "3"],
"color_suffix_length": 0,
"desc": [
{ "infix": "1", "x": 27, "y": 43 },
{ "infix": "2", "x": 231, "y": 43 },
{ "infix": "3", "x": 27, "y": 178 },
{ "infix": "4", "x": 231, "y": 178 },
{ "infix": "5", "x": 27, "y": 312 },
{ "infix": "6", "x": 231, "y": 312 },
{ "infix": "7", "x": 27, "y": 447 },
{ "infix": "8", "x": 231, "y": 447 }
]
},
"scenarioCount": 8,
"scenarios": [
{ "voiceProlog": "chronicles_4/ABVOAB1" },
{ "voiceProlog": "chronicles_4/ABVODB4" },
{ "voiceProlog": "chronicles_4/H3X2ELC" },
{ "voiceProlog": "chronicles_4/ABVODS2" },
{ "voiceProlog": "chronicles_4/ABVODS1" },
{ "voiceProlog": "chronicles_4/ABVODS3" },
{ "voiceProlog": "chronicles_4/ABVODS4" },
{ "voiceProlog": "chronicles_4/H3X2NBD", "voiceEpilog": "chronicles_4/S1C" }
],
"loadingBackground": "chronicles_4/LoadBar",
"introVideoRim": "chronicles_4/INTRORIM",
"introVideo": "chronicles_4/Intro"
},
"MAPS/HC5_MAIN" : { // Heroes Chronicles 5
"regions":
{
"background": "chronicles_5/CamBkHc",
"prefix": "chronicles_5/HcSc",
"suffix": ["1", "2", "3"],
"color_suffix_length": 0,
"desc": [
{ "infix": "1", "x": 34, "y": 184 },
{ "infix": "2", "x": 235, "y": 184 },
{ "infix": "3", "x": 34, "y": 320 },
{ "infix": "4", "x": 235, "y": 320 },
{ "infix": "5", "x": 129, "y": 459 }
]
},
"scenarioCount": 5,
"scenarios": [
{ "voiceProlog": "chronicles_5/ABVOAB1" },
{ "voiceProlog": "chronicles_5/H3X2RNA" },
{ "voiceProlog": "chronicles_5/ABVOFL2" },
{ "voiceProlog": "chronicles_5/ABVOFL4" },
{ "voiceProlog": "chronicles_5/H3X2UAH", "voiceEpilog": "chronicles_5/N1C_D" }
],
"loadingBackground": "chronicles_5/LoadBar",
"introVideoRim": "chronicles_5/INTRORIM",
"introVideo": "chronicles_5/Intro"
},
"MAPS/HC6_MAIN" : { // Heroes Chronicles 6
"regions":
{
"background": "chronicles_6/CamBkHc",
"prefix": "chronicles_6/HcSc",
"suffix": ["1", "2", "3"],
"color_suffix_length": 0,
"desc": [
{ "infix": "1", "x": 34, "y": 184 },
{ "infix": "2", "x": 235, "y": 184 },
{ "infix": "3", "x": 34, "y": 320 },
{ "infix": "4", "x": 235, "y": 320 },
{ "infix": "5", "x": 129, "y": 459 }
]
},
"scenarioCount": 5,
"scenarios": [
{ "voiceProlog": "chronicles_6/H3X2ELB" },
{ "voiceProlog": "chronicles_6/E1A" },
{ "voiceProlog": "chronicles_6/H3X2BBA" },
{ "voiceProlog": "chronicles_6/ABVOAB2" },
{ "voiceProlog": "chronicles_6/ABVOAB5", "voiceEpilog": "chronicles_6/ABVODB2" }
],
"loadingBackground": "chronicles_6/LoadBar",
"introVideoRim": "chronicles_6/INTRORIM",
"introVideo": "chronicles_6/Intro"
},
"MAPS/HC7_MAIN" : { // Heroes Chronicles 7
"regions":
{
"background": "chronicles_7/CamBkHc",
"prefix": "chronicles_7/HcSc",
"suffix": ["1", "2", "3"],
"color_suffix_length": 0,
"desc": [
{ "infix": "1", "x": 27, "y": 43 },
{ "infix": "2", "x": 231, "y": 43 },
{ "infix": "3", "x": 27, "y": 178 },
{ "infix": "4", "x": 231, "y": 178 },
{ "infix": "5", "x": 27, "y": 312 },
{ "infix": "6", "x": 231, "y": 312 },
{ "infix": "7", "x": 27, "y": 447 },
{ "infix": "8", "x": 231, "y": 447 }
]
},
"scenarioCount": 8,
"scenarios": [
{ "voiceProlog": "chronicles_7/ABVOFL2" },
{ "voiceProlog": "chronicles_7/ABVOFL3" },
{ "voiceProlog": "chronicles_7/N1C_D" },
{ "voiceProlog": "chronicles_7/S1C" },
{ "voiceProlog": "chronicles_7/H3X2UAB" },
{ "voiceProlog": "chronicles_7/E2C" },
{ "voiceProlog": "chronicles_7/H3X2NBE" },
{ "voiceProlog": "chronicles_7/ABVOFW4", "voiceEpilog": "chronicles_7/ABVOAB1" }
],
"loadingBackground": "chronicles_7/LoadBar",
"introVideoRim": "chronicles_7/INTRORIM",
"introVideo": "chronicles_7/Intro5"
},
"MAPS/HC8_MAIN" : { // Heroes Chronicles 8
"regions":
{
"background": "chronicles_8/CamBkHc",
"prefix": "chronicles_8/HcSc",
"suffix": ["1", "2", "3"],
"color_suffix_length": 0,
"desc": [
{ "infix": "1", "x": 27, "y": 43 },
{ "infix": "2", "x": 231, "y": 43 },
{ "infix": "3", "x": 27, "y": 178 },
{ "infix": "4", "x": 231, "y": 178 },
{ "infix": "5", "x": 27, "y": 312 },
{ "infix": "6", "x": 231, "y": 312 },
{ "infix": "7", "x": 27, "y": 447 },
{ "infix": "8", "x": 231, "y": 447 }
]
},
"scenarioCount": 8,
"scenarios": [
{ "voiceProlog": "chronicles_8/H3X2RNB" },
{ "voiceProlog": "chronicles_8/ABVOAB9" },
{ "voiceProlog": "chronicles_8/H3X2BBB" },
{ "voiceProlog": "chronicles_8/ABVODS1" },
{ "voiceProlog": "chronicles_8/H3X2ELA" },
{ "voiceProlog": "chronicles_8/E1B" },
{ "voiceProlog": "chronicles_8/H3X2BBD" },
{ "voiceProlog": "chronicles_8/H3X2ELE", "voiceEpilog": "chronicles_8/ABVOAB7" }
],
"loadingBackground": "chronicles_8/LoadBar",
"introVideoRim": "chronicles_8/INTRORIM",
"introVideo": "chronicles_8/Intro6"
}
}

View File

@ -19,7 +19,11 @@
// Should be in format: // Should be in format:
// <replaced bitmap font name, case-sensetive> : <true type font description> // <replaced bitmap font name, case-sensetive> : <true type font description>
// "file" - file to load font from, must be in data/ directory // "file" - file to load font from, must be in data/ directory
// "size" - point size of font // "size" - point size of font. Can be defined in two forms:
// a) single number, e.g. 10. In this case, game will automatically multiply font size by upscaling factor when xBRZ is in use,
// so xbrz 2x will use 20px, xbrz 3x will use 30px, etc
// b) list of scaling factors for each scaling mode, e.g. [ 10, 16, 22, 26]. In this case game will select point size according to xBRZ scaling factor
// so unscaled mode will use 10px, xbrz2 will use 16px, and xbrz3 will use 22
// "style" - italic and\or bold, indicates font style // "style" - italic and\or bold, indicates font style
// "blend" - if set to true, font will be antialiased // "blend" - if set to true, font will be antialiased
"trueType": "trueType":

View File

@ -271,6 +271,19 @@
"portraitYoungYog" : 162 "portraitYoungYog" : 162
} }
}, },
"chronicles" : {
"supported" : true,
"iconIndex" : 2,
"portraits" : {
"portraitTarnumBarbarian" : 163,
"portraitTarnumKnight" : 164,
"portraitTarnumWizard" : 165,
"portraitTarnumRanger" : 166,
"portraitTarnumOverlord" : 167,
"portraitTarnumBeastmaster" : 168
}
},
"jsonVCMI" : { "jsonVCMI" : {
"supported" : true, "supported" : true,
"iconIndex" : 3 "iconIndex" : 3

View File

@ -202,5 +202,179 @@
], ],
"skills" : [], "skills" : [],
"specialty" : {} "specialty" : {}
},
"portraitTarnumBarbarian" :
{
"class" : "barbarian",
"special" : true,
"images": {
"large" : "HPL137",
"small" : "HPS137",
"specialtySmall" : "default",
"specialtyLarge" : "default"
},
"texts" : {
"name" : "",
"biography" : "",
"specialty" : {
"description" : "",
"tooltip" : "",
"name" : ""
}
},
"army" : [
{
"creature" : "goblin",
"min" : 1,
"max" : 1
}
],
"skills" : [],
"specialty" : {}
},
"portraitTarnumKnight" :
{
"class" : "knight",
"special" : true,
"images": {
"large" : "HPL138",
"small" : "HPS138",
"specialtySmall" : "default",
"specialtyLarge" : "default"
},
"texts" : {
"name" : "",
"biography" : "",
"specialty" : {
"description" : "",
"tooltip" : "",
"name" : ""
}
},
"army" : [
{
"creature" : "pikeman",
"min" : 1,
"max" : 1
}
],
"skills" : [],
"specialty" : {}
},
"portraitTarnumWizard" :
{
"class" : "wizard",
"special" : true,
"images": {
"large" : "HPL139",
"small" : "HPS139",
"specialtySmall" : "default",
"specialtyLarge" : "default"
},
"texts" : {
"name" : "",
"biography" : "",
"specialty" : {
"description" : "",
"tooltip" : "",
"name" : ""
}
},
"army" : [
{
"creature" : "enchanter",
"min" : 1,
"max" : 1
}
],
"skills" : [],
"specialty" : {}
},
"portraitTarnumRanger" :
{
"class" : "ranger",
"special" : true,
"images": {
"large" : "HPL140",
"small" : "HPS140",
"specialtySmall" : "default",
"specialtyLarge" : "default"
},
"texts" : {
"name" : "",
"biography" : "",
"specialty" : {
"description" : "",
"tooltip" : "",
"name" : ""
}
},
"army" : [
{
"creature" : "sharpshooter",
"min" : 1,
"max" : 1
}
],
"skills" : [],
"specialty" : {}
},
"portraitTarnumOverlord" :
{
"class" : "overlord",
"special" : true,
"images": {
"large" : "HPL141",
"small" : "HPS141",
"specialtySmall" : "default",
"specialtyLarge" : "default"
},
"texts" : {
"name" : "",
"biography" : "",
"specialty" : {
"description" : "",
"tooltip" : "",
"name" : ""
}
},
"army" : [
{
"creature" : "troglodyte",
"min" : 1,
"max" : 1
}
],
"skills" : [],
"specialty" : {}
},
"portraitTarnumBeastmaster" :
{
"class" : "beastmaster",
"special" : true,
"images": {
"large" : "HPL142",
"small" : "HPS142",
"specialtySmall" : "default",
"specialtyLarge" : "default"
},
"texts" : {
"name" : "",
"biography" : "",
"specialty" : {
"description" : "",
"tooltip" : "",
"name" : ""
}
},
"army" : [
{
"creature" : "gnoll",
"min" : 1,
"max" : 1
}
],
"skills" : [],
"specialty" : {}
} }
} }

View File

@ -167,6 +167,7 @@
"targetfps", "targetfps",
"vsync", "vsync",
"upscalingFilter", "upscalingFilter",
"fontUpscalingFilter",
"downscalingFilter" "downscalingFilter"
], ],
"properties" : { "properties" : {
@ -231,6 +232,11 @@
"type" : "boolean", "type" : "boolean",
"default" : true "default" : true
}, },
"fontUpscalingFilter" : {
"type" : "string",
"enum" : [ "nearest", "bilinear", "xbrz" ],
"default" : "nearest"
},
"upscalingFilter" : { "upscalingFilter" : {
"type" : "string", "type" : "string",
"enum" : [ "auto", "none", "xbrz2", "xbrz3", "xbrz4" ], "enum" : [ "auto", "none", "xbrz2", "xbrz3", "xbrz4" ],

View File

@ -52,6 +52,9 @@ In header are parameters describing campaign properties
- `"campaignVersion"` is creator defined version - `"campaignVersion"` is creator defined version
- `"creationDateTime"` unix time of campaign creation - `"creationDateTime"` unix time of campaign creation
- `"allowDifficultySelection"` is a boolean field (`true`/`false`) which allows or disallows to choose difficulty before scenario start - `"allowDifficultySelection"` is a boolean field (`true`/`false`) which allows or disallows to choose difficulty before scenario start
- `"loadingBackground"` is for setting a different loading screen background
- `"introVideo"` is for defining an optional intro video
- `"introVideoRim"` is for the Rim around the optional video (default is INTRORIM)
## Scenario description ## Scenario description

View File

@ -12,10 +12,12 @@ set(launcher_SRCS
modManager/cmodlistview_moc.cpp modManager/cmodlistview_moc.cpp
modManager/cmodmanager.cpp modManager/cmodmanager.cpp
modManager/imageviewer_moc.cpp modManager/imageviewer_moc.cpp
modManager/chroniclesextractor.cpp
settingsView/csettingsview_moc.cpp settingsView/csettingsview_moc.cpp
firstLaunch/firstlaunch_moc.cpp firstLaunch/firstlaunch_moc.cpp
main.cpp main.cpp
helper.cpp helper.cpp
innoextract.cpp
mainwindow_moc.cpp mainwindow_moc.cpp
languages.cpp languages.cpp
launcherdirs.cpp launcherdirs.cpp
@ -42,6 +44,7 @@ set(launcher_HEADERS
modManager/cmodlistview_moc.h modManager/cmodlistview_moc.h
modManager/cmodmanager.h modManager/cmodmanager.h
modManager/imageviewer_moc.h modManager/imageviewer_moc.h
modManager/chroniclesextractor.h
settingsView/csettingsview_moc.h settingsView/csettingsview_moc.h
firstLaunch/firstlaunch_moc.h firstLaunch/firstlaunch_moc.h
mainwindow_moc.h mainwindow_moc.h
@ -51,6 +54,7 @@ set(launcher_HEADERS
updatedialog_moc.h updatedialog_moc.h
main.h main.h
helper.h helper.h
innoextract.h
prepare.h prepare.h
) )

View File

@ -21,11 +21,7 @@
#include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/Filesystem.h"
#include "../helper.h" #include "../helper.h"
#include "../languages.h" #include "../languages.h"
#include "../innoextract.h"
#ifdef ENABLE_INNOEXTRACT
#include "cli/extract.hpp"
#include "setup/version.hpp"
#endif
#ifdef VCMI_IOS #ifdef VCMI_IOS
#include "ios/selectdirectory.h" #include "ios/selectdirectory.h"
@ -386,44 +382,11 @@ void FirstLaunchView::extractGogData()
if(isGogGalaxyExe(tmpFileExe)) if(isGogGalaxyExe(tmpFileExe))
errorText = tr("You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer!"); errorText = tr("You've provided GOG Galaxy installer! This file doesn't contain the game. Please download the offline backup game installer!");
::extract_options o; if(errorText.isEmpty())
o.extract = true; errorText = Innoextract::extract(tmpFileExe, tempDir.path(), [this](float progress) {
ui->progressBarGog->setValue(progress * 100);
// standard settings qApp->processEvents();
o.gog_galaxy = true; });
o.codepage = 0U;
o.output_dir = tempDir.path().toStdString();
o.extract_temp = true;
o.extract_unknown = true;
o.filenames.set_expand(true);
o.preserve_file_times = true; // also correctly closes file -> without it: on Windows the files are not written completely
try
{
if(errorText.isEmpty())
process_file(tmpFileExe.toStdString(), o, [this](float progress) {
ui->progressBarGog->setValue(progress * 100);
qApp->processEvents();
});
}
catch(const std::ios_base::failure & e)
{
errorText = tr("Stream error while extracting files!\nerror reason: ");
errorText += e.what();
}
catch(const format_error & e)
{
errorText = e.what();
}
catch(const std::runtime_error & e)
{
errorText = e.what();
}
catch(const setup::version_error &)
{
errorText = tr("Not a supported Inno Setup installer!");
}
ui->progressBarGog->setVisible(false); ui->progressBarGog->setVisible(false);
ui->pushButtonGogInstall->setVisible(true); ui->pushButtonGogInstall->setVisible(true);

62
launcher/innoextract.cpp Normal file
View File

@ -0,0 +1,62 @@
/*
* innoextract.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "innoextract.h"
#ifdef ENABLE_INNOEXTRACT
#include "cli/extract.hpp"
#include "setup/version.hpp"
#endif
QString Innoextract::extract(QString installer, QString outDir, std::function<void (float percent)> cb)
{
QString errorText{};
#ifdef ENABLE_INNOEXTRACT
::extract_options o;
o.extract = true;
// standard settings
o.gog_galaxy = true;
o.codepage = 0U;
o.output_dir = outDir.toStdString();
o.extract_temp = true;
o.extract_unknown = true;
o.filenames.set_expand(true);
o.preserve_file_times = true; // also correctly closes file -> without it: on Windows the files are not written completely
try
{
process_file(installer.toStdString(), o, cb);
}
catch(const std::ios_base::failure & e)
{
errorText = tr("Stream error while extracting files!\nerror reason: ");
errorText += e.what();
}
catch(const format_error & e)
{
errorText = e.what();
}
catch(const std::runtime_error & e)
{
errorText = e.what();
}
catch(const setup::version_error &)
{
errorText = tr("Not a supported Inno Setup installer!");
}
#else
errorText = tr("VCMI was compiled without innoextract support, which is needed to extract exe files!");
#endif
return errorText;
}

16
launcher/innoextract.h Normal file
View File

@ -0,0 +1,16 @@
/*
* innoextract.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
class Innoextract : public QObject
{
public:
static QString extract(QString installer, QString outDir, std::function<void (float percent)> cb = nullptr);
};

View File

@ -0,0 +1,225 @@
/*
* chroniclesextractor.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "chroniclesextractor.h"
#include "../../lib/VCMIDirs.h"
#include "../../lib/filesystem/CArchiveLoader.h"
#include "../innoextract.h"
ChroniclesExtractor::ChroniclesExtractor(QWidget *p, std::function<void(float percent)> cb) :
parent(p), cb(cb)
{
}
bool ChroniclesExtractor::createTempDir()
{
tempDir = QDir(pathToQString(VCMIDirs::get().userDataPath()));
if(tempDir.cd("tmp"))
{
tempDir.removeRecursively(); // remove if already exists (e.g. previous run)
tempDir.cdUp();
}
tempDir.mkdir("tmp");
if(!tempDir.cd("tmp"))
return false; // should not happen - but avoid deleting wrong folder in any case
return true;
}
void ChroniclesExtractor::removeTempDir()
{
tempDir.removeRecursively();
}
int ChroniclesExtractor::getChronicleNo(QFile & file)
{
if(!file.open(QIODevice::ReadOnly))
{
QMessageBox::critical(parent, tr("File cannot opened"), file.errorString());
return 0;
}
QByteArray magic{"MZ"};
QByteArray magicFile = file.read(magic.length());
if(!magicFile.startsWith(magic))
{
QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select an gog installer file!"));
return 0;
}
QByteArray dataBegin = file.read(1'000'000);
int chronicle = 0;
for (const auto& kv : chronicles) {
if(dataBegin.contains(kv.second))
{
chronicle = kv.first;
break;
}
}
if(!chronicle)
{
QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select an chronicle installer file!"));
return 0;
}
return chronicle;
}
bool ChroniclesExtractor::extractGogInstaller(QString file)
{
QString errorText = Innoextract::extract(file, tempDir.path(), [this](float progress) {
float overallProgress = ((1.0 / static_cast<float>(fileCount)) * static_cast<float>(extractionFile)) + (progress / static_cast<float>(fileCount));
if(cb)
cb(overallProgress);
});
if(!errorText.isEmpty())
{
QMessageBox::critical(parent, tr("Extracting error!"), errorText);
return false;
}
return true;
}
void ChroniclesExtractor::createBaseMod() const
{
QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods"));
dir.mkdir("chronicles");
dir.cd("chronicles");
dir.mkdir("Mods");
QJsonObject mod
{
{ "modType", "Expansion" },
{ "name", tr("Heroes Chronicles") },
{ "description", tr("Heroes Chronicles") },
{ "author", "3DO" },
{ "version", "1.0" },
{ "contact", "vcmi.eu" },
};
QFile jsonFile(dir.filePath("mod.json"));
jsonFile.open(QFile::WriteOnly);
jsonFile.write(QJsonDocument(mod).toJson());
}
void ChroniclesExtractor::createChronicleMod(int no)
{
QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "Mods" / ("chronicles_" + std::to_string(no))));
dir.removeRecursively();
dir.mkpath(".");
QByteArray tmpChronicles = chronicles.at(no);
tmpChronicles.replace('\0', "");
QJsonObject mod
{
{ "modType", "Expansion" },
{ "name", QString::number(no) + " - " + QString(tmpChronicles) },
{ "description", tr("Heroes Chronicles") + " - " + QString::number(no) + " - " + QString(tmpChronicles) },
{ "author", "3DO" },
{ "version", "1.0" },
{ "contact", "vcmi.eu" },
};
QFile jsonFile(dir.filePath("mod.json"));
jsonFile.open(QFile::WriteOnly);
jsonFile.write(QJsonDocument(mod).toJson());
dir.cd("content");
extractFiles(no);
}
void ChroniclesExtractor::extractFiles(int no) const
{
QByteArray tmpChronicles = chronicles.at(no);
tmpChronicles.replace('\0', "");
std::string chroniclesDir = "chronicles_" + std::to_string(no);
QDir tmpDir = tempDir.filePath(tempDir.entryList({"app"}, QDir::Filter::Dirs).front());
tmpDir.setPath(tmpDir.filePath(tmpDir.entryList({QString(tmpChronicles)}, QDir::Filter::Dirs).front()));
tmpDir.setPath(tmpDir.filePath(tmpDir.entryList({"data"}, QDir::Filter::Dirs).front()));
auto basePath = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "Mods" / chroniclesDir / "content";
QDir outDirDataPortraits(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "Data"));
QDir outDirData(pathToQString(basePath / "Data" / chroniclesDir));
QDir outDirSprites(pathToQString(basePath / "Sprites" / chroniclesDir));
QDir outDirVideo(pathToQString(basePath / "Video" / chroniclesDir));
QDir outDirSounds(pathToQString(basePath / "Sounds" / chroniclesDir));
QDir outDirMaps(pathToQString(basePath / "Maps"));
auto extract = [](QDir scrDir, QDir dest, QString file, std::vector<std::string> files = {}){
CArchiveLoader archive("", scrDir.filePath(scrDir.entryList({file}).front()).toStdString(), false);
for(auto & entry : archive.getEntries())
if(files.empty())
archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true);
else
{
for(const auto & item : files)
if(boost::algorithm::to_lower_copy(entry.second.name).find(boost::algorithm::to_lower_copy(item)) != std::string::npos)
archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true);
}
};
extract(tmpDir, outDirData, "xBitmap.lod");
extract(tmpDir, outDirData, "xlBitmap.lod");
extract(tmpDir, outDirSprites, "xSprite.lod");
extract(tmpDir, outDirSprites, "xlSprite.lod");
extract(tmpDir, outDirVideo, "xVideo.vid");
extract(tmpDir, outDirSounds, "xSound.snd");
tmpDir.cdUp();
if(tmpDir.entryList({"maps"}, QDir::Filter::Dirs).size()) // special case for "The World Tree": the map is in the "Maps" folder instead of inside the lod
{
QDir tmpDirMaps = tmpDir.filePath(tmpDir.entryList({"maps"}, QDir::Filter::Dirs).front());
for(const auto & entry : tmpDirMaps.entryList())
QFile(tmpDirMaps.filePath(entry)).copy(outDirData.filePath(entry));
}
tmpDir.cdUp();
QDir tmpDirData = tmpDir.filePath(tmpDir.entryList({"data"}, QDir::Filter::Dirs).front());
auto tarnumPortraits = std::vector<std::string>{"HPS137", "HPS138", "HPS139", "HPS140", "HPS141", "HPS142", "HPL137", "HPL138", "HPL139", "HPL140", "HPL141", "HPL142"};
extract(tmpDirData, outDirDataPortraits, "bitmap.lod", tarnumPortraits);
extract(tmpDirData, outDirData, "lbitmap.lod", std::vector<std::string>{"INTRORIM"});
if(!outDirMaps.exists())
outDirMaps.mkpath(".");
QString campaignFileName = "Hc" + QString::number(no) + "_Main.h3c";
QFile(outDirData.filePath(outDirData.entryList({"Main.h3c"}).front())).copy(outDirMaps.filePath(campaignFileName));
}
void ChroniclesExtractor::installChronicles(QStringList exe)
{
extractionFile = -1;
fileCount = exe.size();
for(QString f : exe)
{
extractionFile++;
QFile file(f);
int chronicleNo = getChronicleNo(file);
if(!chronicleNo)
continue;
if(!createTempDir())
continue;
if(!extractGogInstaller(f))
continue;
createBaseMod();
createChronicleMod(chronicleNo);
removeTempDir();
}
}

View File

@ -0,0 +1,47 @@
/*
* chroniclesextractor.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../StdInc.h"
class ChroniclesExtractor : public QObject
{
Q_OBJECT
QWidget *parent;
std::function<void(float percent)> cb;
QDir tempDir;
int extractionFile;
int fileCount;
bool createTempDir();
void removeTempDir();
int getChronicleNo(QFile & file);
bool extractGogInstaller(QString filePath);
void createBaseMod() const;
void createChronicleMod(int no);
void extractFiles(int no) const;
const std::map<int, QByteArray> chronicles = {
{1, QByteArray{reinterpret_cast<const char*>(u"Warlords of the Wasteland"), 50}},
{2, QByteArray{reinterpret_cast<const char*>(u"Conquest of the Underworld"), 52}},
{3, QByteArray{reinterpret_cast<const char*>(u"Masters of the Elements"), 46}},
{4, QByteArray{reinterpret_cast<const char*>(u"Clash of the Dragons"), 40}},
{5, QByteArray{reinterpret_cast<const char*>(u"The World Tree"), 28}},
{6, QByteArray{reinterpret_cast<const char*>(u"The Fiery Moon"), 28}},
{7, QByteArray{reinterpret_cast<const char*>(u"Revolt of the Beastmasters"), 52}},
{8, QByteArray{reinterpret_cast<const char*>(u"The Sword of Frost"), 36}}
};
public:
void installChronicles(QStringList exe);
ChroniclesExtractor(QWidget *p, std::function<void(float percent)> cb = nullptr);
};

View File

@ -20,6 +20,7 @@
#include "cmodlistmodel_moc.h" #include "cmodlistmodel_moc.h"
#include "cmodmanager.h" #include "cmodmanager.h"
#include "cdownloadmanager_moc.h" #include "cdownloadmanager_moc.h"
#include "chroniclesextractor.h"
#include "../settingsView/csettingsview_moc.h" #include "../settingsView/csettingsview_moc.h"
#include "../launcherdirs.h" #include "../launcherdirs.h"
#include "../jsonutils.h" #include "../jsonutils.h"
@ -29,6 +30,9 @@
#include "../../lib/CConfigHandler.h" #include "../../lib/CConfigHandler.h"
#include "../../lib/texts/Languages.h" #include "../../lib/texts/Languages.h"
#include "../../lib/modding/CModVersion.h" #include "../../lib/modding/CModVersion.h"
#include "../../lib/filesystem/Filesystem.h"
#include <future>
static double mbToBytes(double mb) static double mbToBytes(double mb)
{ {
@ -55,7 +59,7 @@ void CModListView::dragEnterEvent(QDragEnterEvent* event)
{ {
if(event->mimeData()->hasUrls()) if(event->mimeData()->hasUrls())
for(const auto & url : event->mimeData()->urls()) for(const auto & url : event->mimeData()->urls())
for(const auto & ending : QStringList({".zip", ".h3m", ".h3c", ".vmap", ".vcmp", ".json"})) for(const auto & ending : QStringList({".zip", ".h3m", ".h3c", ".vmap", ".vcmp", ".json", ".exe"}))
if(url.fileName().endsWith(ending, Qt::CaseInsensitive)) if(url.fileName().endsWith(ending, Qt::CaseInsensitive))
{ {
event->acceptProposedAction(); event->acceptProposedAction();
@ -636,8 +640,16 @@ void CModListView::on_installFromFileButton_clicked()
// https://bugreports.qt.io/browse/QTBUG-98651 // https://bugreports.qt.io/browse/QTBUG-98651
QTimer::singleShot(0, this, [this] QTimer::singleShot(0, this, [this]
{ {
QString filter = tr("All supported files") + " (*.h3m *.vmap *.h3c *.vcmp *.zip *.json);;" + tr("Maps") + " (*.h3m *.vmap);;" + tr("Campaigns") + " (*.h3c *.vcmp);;" + tr("Configs") + " (*.json);;" + tr("Mods") + " (*.zip)"; QString filter = tr("All supported files") + " (*.h3m *.vmap *.h3c *.vcmp *.zip *.json *.exe);;" +
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files (configs, mods, maps, campaigns) to install..."), QDir::homePath(), filter); tr("Maps") + " (*.h3m *.vmap);;" +
tr("Campaigns") + " (*.h3c *.vcmp);;" +
tr("Configs") + " (*.json);;" +
tr("Mods") + " (*.zip);;" +
tr("Gog files") + " (*.exe)";
#if defined(VCMI_MOBILE)
filter = tr("All files (*.*)"); //Workaround for sometimes incorrect mime for some extensions (e.g. for exe)
#endif
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select files (configs, mods, maps, campaigns, gog files) to install..."), QDir::homePath(), filter);
for(const auto & file : files) for(const auto & file : files)
{ {
@ -786,6 +798,7 @@ void CModListView::installFiles(QStringList files)
QStringList mods; QStringList mods;
QStringList maps; QStringList maps;
QStringList images; QStringList images;
QStringList exe;
QVector<QVariantMap> repositories; QVector<QVariantMap> repositories;
// TODO: some better way to separate zip's with mods and downloaded repository files // TODO: some better way to separate zip's with mods and downloaded repository files
@ -795,6 +808,8 @@ void CModListView::installFiles(QStringList files)
mods.push_back(filename); mods.push_back(filename);
else if(filename.endsWith(".h3m", Qt::CaseInsensitive) || filename.endsWith(".h3c", Qt::CaseInsensitive) || filename.endsWith(".vmap", Qt::CaseInsensitive) || filename.endsWith(".vcmp", Qt::CaseInsensitive)) else if(filename.endsWith(".h3m", Qt::CaseInsensitive) || filename.endsWith(".h3c", Qt::CaseInsensitive) || filename.endsWith(".vmap", Qt::CaseInsensitive) || filename.endsWith(".vcmp", Qt::CaseInsensitive))
maps.push_back(filename); maps.push_back(filename);
if(filename.endsWith(".exe", Qt::CaseInsensitive))
exe.push_back(filename);
else if(filename.endsWith(".json", Qt::CaseInsensitive)) else if(filename.endsWith(".json", Qt::CaseInsensitive))
{ {
//download and merge additional files //download and merge additional files
@ -832,6 +847,35 @@ void CModListView::installFiles(QStringList files)
if(!maps.empty()) if(!maps.empty())
installMaps(maps); installMaps(maps);
if(!exe.empty())
{
ui->progressBar->setFormat(tr("Installing chronicles"));
float prog = 0.0;
auto futureExtract = std::async(std::launch::async, [this, exe, &prog]()
{
ChroniclesExtractor ce(this, [&prog](float progress) { prog = progress; });
ce.installChronicles(exe);
return true;
});
while(futureExtract.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready)
{
emit extractionProgress(static_cast<int>(prog * 1000.f), 1000);
qApp->processEvents();
}
if(futureExtract.get())
{
//update
CResourceHandler::get("initial")->updateFilteredFiles([](const std::string &){ return true; });
manager->loadMods();
modModel->reloadRepositories();
emit modsChanged();
}
}
if(!images.empty()) if(!images.empty())
loadScreenshots(); loadScreenshots();
} }

View File

@ -79,6 +79,7 @@ void GameSettings::load(const JsonNode & input)
{EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA, "mapFormat", "restorationOfErathia" }, {EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA, "mapFormat", "restorationOfErathia" },
{EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE, "mapFormat", "armageddonsBlade" }, {EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE, "mapFormat", "armageddonsBlade" },
{EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH, "mapFormat", "shadowOfDeath" }, {EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH, "mapFormat", "shadowOfDeath" },
{EGameSettings::MAP_FORMAT_CHRONICLES, "mapFormat", "chronicles" },
{EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS, "mapFormat", "hornOfTheAbyss" }, {EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS, "mapFormat", "hornOfTheAbyss" },
{EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS, "mapFormat", "inTheWakeOfGods" }, {EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS, "mapFormat", "inTheWakeOfGods" },
{EGameSettings::MAP_FORMAT_JSON_VCMI, "mapFormat", "jsonVCMI" }, {EGameSettings::MAP_FORMAT_JSON_VCMI, "mapFormat", "jsonVCMI" },

View File

@ -58,6 +58,7 @@ enum class EGameSettings
MAP_FORMAT_RESTORATION_OF_ERATHIA, MAP_FORMAT_RESTORATION_OF_ERATHIA,
MAP_FORMAT_ARMAGEDDONS_BLADE, MAP_FORMAT_ARMAGEDDONS_BLADE,
MAP_FORMAT_SHADOW_OF_DEATH, MAP_FORMAT_SHADOW_OF_DEATH,
MAP_FORMAT_CHRONICLES,
MAP_FORMAT_HORN_OF_THE_ABYSS, MAP_FORMAT_HORN_OF_THE_ABYSS,
MAP_FORMAT_JSON_VCMI, MAP_FORMAT_JSON_VCMI,
MAP_FORMAT_IN_THE_WAKE_OF_GODS, MAP_FORMAT_IN_THE_WAKE_OF_GODS,

View File

@ -104,8 +104,8 @@ int BonusList::totalValue() const
}; };
BonusCollection accumulated; BonusCollection accumulated;
bool hasIndepMax = false; int indexMaxCount = 0;
bool hasIndepMin = false; int indexMinCount = 0;
std::array<int, vstd::to_underlying(BonusSource::NUM_BONUS_SOURCE)> percentToSource = {}; std::array<int, vstd::to_underlying(BonusSource::NUM_BONUS_SOURCE)> percentToSource = {};
@ -141,12 +141,12 @@ int BonusList::totalValue() const
case BonusValueType::ADDITIVE_VALUE: case BonusValueType::ADDITIVE_VALUE:
accumulated.additive += valModified; accumulated.additive += valModified;
break; break;
case BonusValueType::INDEPENDENT_MAX: case BonusValueType::INDEPENDENT_MAX: // actual meaning: at least this value
hasIndepMax = true; indexMaxCount++;
vstd::amax(accumulated.indepMax, valModified); vstd::amax(accumulated.indepMax, valModified);
break; break;
case BonusValueType::INDEPENDENT_MIN: case BonusValueType::INDEPENDENT_MIN: // actual meaning: at most this value
hasIndepMin = true; indexMinCount++;
vstd::amin(accumulated.indepMin, valModified); vstd::amin(accumulated.indepMin, valModified);
break; break;
} }
@ -156,21 +156,18 @@ int BonusList::totalValue() const
accumulated.base += accumulated.additive; accumulated.base += accumulated.additive;
auto valFirst = applyPercentage(accumulated.base ,accumulated.percentToAll); auto valFirst = applyPercentage(accumulated.base ,accumulated.percentToAll);
if(hasIndepMin && hasIndepMax && accumulated.indepMin < accumulated.indepMax) if(indexMinCount && indexMaxCount && accumulated.indepMin < accumulated.indepMax)
accumulated.indepMax = accumulated.indepMin; accumulated.indepMax = accumulated.indepMin;
const int notIndepBonuses = static_cast<int>(std::count_if(bonuses.cbegin(), bonuses.cend(), [](const std::shared_ptr<Bonus>& b) const int notIndepBonuses = bonuses.size() - indexMaxCount - indexMinCount;
{
return b->valType != BonusValueType::INDEPENDENT_MAX && b->valType != BonusValueType::INDEPENDENT_MIN;
}));
if(notIndepBonuses) if(notIndepBonuses)
return std::clamp(valFirst, accumulated.indepMax, accumulated.indepMin); return std::clamp(valFirst, accumulated.indepMax, accumulated.indepMin);
if (hasIndepMin) if (indexMinCount)
return accumulated.indepMin; return accumulated.indepMin;
if (hasIndepMax) if (indexMaxCount)
return accumulated.indepMax; return accumulated.indepMax;
return 0; return 0;

View File

@ -18,7 +18,7 @@ enum class CampaignVersion : uint8_t
AB = 5, AB = 5,
SoD = 6, SoD = 6,
WoG = 6, WoG = 6,
// Chr = 7, // Heroes Chronicles, likely identical to SoD, untested Chr = 7,
VCMI = 1, VCMI = 1,
VCMI_MIN = 1, VCMI_MIN = 1,

View File

@ -38,12 +38,14 @@ void CampaignHandler::readCampaign(Campaign * ret, const std::vector<ui8> & inpu
CBinaryReader reader(&stream); CBinaryReader reader(&stream);
readHeaderFromMemory(*ret, reader, filename, modName, encoding); readHeaderFromMemory(*ret, reader, filename, modName, encoding);
ret->overrideCampaign();
for(int g = 0; g < ret->numberOfScenarios; ++g) for(int g = 0; g < ret->numberOfScenarios; ++g)
{ {
auto scenarioID = static_cast<CampaignScenarioID>(ret->scenarios.size()); auto scenarioID = static_cast<CampaignScenarioID>(ret->scenarios.size());
ret->scenarios[scenarioID] = readScenarioFromMemory(reader, *ret); ret->scenarios[scenarioID] = readScenarioFromMemory(reader, *ret);
} }
ret->overrideCampaignScenarios();
} }
else // text format (json) else // text format (json)
{ {
@ -166,6 +168,9 @@ void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader
ret.filename = filename; ret.filename = filename;
ret.modName = modName; ret.modName = modName;
ret.encoding = encoding; ret.encoding = encoding;
ret.loadingBackground = ImagePath::fromJson(reader["loadingBackground"]);
ret.introVideoRim = ImagePath::fromJson(reader["introVideoRim"]);
ret.introVideo = VideoPath::fromJson(reader["introVideo"]);
} }
CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader)
@ -392,7 +397,8 @@ void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader
{ {
ret.version = static_cast<CampaignVersion>(reader.readUInt32()); ret.version = static_cast<CampaignVersion>(reader.readUInt32());
ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19] ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19]
ret.loadLegacyData(campId); if(ret.version != CampaignVersion::Chr) // For chronicles: Will be overridden later; Chronicles uses own logic (reusing OH3 ID's)
ret.loadLegacyData(campId);
ret.name.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "name")); ret.name.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "name"));
ret.description.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "description")); ret.description.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "description"));
ret.author.appendRawString(""); ret.author.appendRawString("");

View File

@ -20,6 +20,7 @@
#include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/CGHeroInstance.h"
#include "../serializer/JsonDeserializer.h" #include "../serializer/JsonDeserializer.h"
#include "../serializer/JsonSerializer.h" #include "../serializer/JsonSerializer.h"
#include "../json/JsonUtils.h"
VCMI_LIB_NAMESPACE_BEGIN VCMI_LIB_NAMESPACE_BEGIN
@ -138,6 +139,12 @@ void CampaignHeader::loadLegacyData(ui8 campId)
numberOfScenarios = VLC->generaltexth->getCampaignLength(campId); numberOfScenarios = VLC->generaltexth->getCampaignLength(campId);
} }
void CampaignHeader::loadLegacyData(CampaignRegions regions, int numOfScenario)
{
campaignRegions = regions;
numberOfScenarios = numOfScenario;
}
bool CampaignHeader::playerSelectedDifficulty() const bool CampaignHeader::playerSelectedDifficulty() const
{ {
return difficultyChosenByPlayer; return difficultyChosenByPlayer;
@ -198,6 +205,21 @@ AudioPath CampaignHeader::getMusic() const
return music; return music;
} }
ImagePath CampaignHeader::getLoadingBackground() const
{
return loadingBackground;
}
ImagePath CampaignHeader::getIntroVideoRim() const
{
return introVideoRim;
}
VideoPath CampaignHeader::getIntroVideo() const
{
return introVideo;
}
const CampaignRegions & CampaignHeader::getRegions() const const CampaignRegions & CampaignHeader::getRegions() const
{ {
return campaignRegions; return campaignRegions;
@ -455,6 +477,45 @@ std::set<CampaignScenarioID> Campaign::allScenarios() const
return result; return result;
} }
void Campaign::overrideCampaign()
{
const JsonNode node = JsonUtils::assembleFromFiles("config/campaignOverrides.json");
for (auto & entry : node.Struct())
if(filename == entry.first)
{
if(!entry.second["regions"].isNull() && !entry.second["scenarioCount"].isNull())
loadLegacyData(CampaignRegions::fromJson(entry.second["regions"]), entry.second["scenarioCount"].Integer());
if(!entry.second["loadingBackground"].isNull())
loadingBackground = ImagePath::builtin(entry.second["loadingBackground"].String());
if(!entry.second["introVideoRim"].isNull())
introVideoRim = ImagePath::builtin(entry.second["introVideoRim"].String());
if(!entry.second["introVideo"].isNull())
introVideo = VideoPath::builtin(entry.second["introVideo"].String());
}
}
void Campaign::overrideCampaignScenarios()
{
const JsonNode node = JsonUtils::assembleFromFiles("config/campaignOverrides.json");
for (auto & entry : node.Struct())
if(filename == entry.first)
{
if(!entry.second["scenarios"].isNull())
{
auto sc = entry.second["scenarios"].Vector();
for(int i = 0; i < sc.size(); i++)
{
auto it = scenarios.begin();
std::advance(it, i);
if(!sc.at(i)["voiceProlog"].isNull())
it->second.prolog.prologVoice = AudioPath::builtin(sc.at(i)["voiceProlog"].String());
if(!sc.at(i)["voiceEpilog"].isNull())
it->second.epilog.prologVoice = AudioPath::builtin(sc.at(i)["voiceEpilog"].String());
}
}
}
}
int Campaign::scenariosCount() const int Campaign::scenariosCount() const
{ {
return allScenarios().size(); return allScenarios().size();

View File

@ -83,6 +83,7 @@ public:
class DLL_LINKAGE CampaignHeader : public boost::noncopyable class DLL_LINKAGE CampaignHeader : public boost::noncopyable
{ {
friend class CampaignHandler; friend class CampaignHandler;
friend class Campaign;
CampaignVersion version = CampaignVersion::NONE; CampaignVersion version = CampaignVersion::NONE;
CampaignRegions campaignRegions; CampaignRegions campaignRegions;
@ -96,11 +97,15 @@ class DLL_LINKAGE CampaignHeader : public boost::noncopyable
std::string filename; std::string filename;
std::string modName; std::string modName;
std::string encoding; std::string encoding;
ImagePath loadingBackground;
ImagePath introVideoRim;
VideoPath introVideo;
int numberOfScenarios = 0; int numberOfScenarios = 0;
bool difficultyChosenByPlayer = false; bool difficultyChosenByPlayer = false;
void loadLegacyData(ui8 campId); void loadLegacyData(ui8 campId);
void loadLegacyData(CampaignRegions regions, int numOfScenario);
TextContainerRegistrable textContainer; TextContainerRegistrable textContainer;
@ -118,6 +123,9 @@ public:
std::string getModName() const; std::string getModName() const;
std::string getEncoding() const; std::string getEncoding() const;
AudioPath getMusic() const; AudioPath getMusic() const;
ImagePath getLoadingBackground() const;
ImagePath getIntroVideoRim() const;
VideoPath getIntroVideo() const;
const CampaignRegions & getRegions() const; const CampaignRegions & getRegions() const;
TextContainerRegistrable & getTexts(); TextContainerRegistrable & getTexts();
@ -142,6 +150,12 @@ public:
h & music; h & music;
h & encoding; h & encoding;
h & textContainer; h & textContainer;
if (h.version >= Handler::Version::CHRONICLES_SUPPORT)
{
h & loadingBackground;
h & introVideoRim;
h & introVideo;
}
} }
}; };
@ -247,6 +261,9 @@ public:
std::set<CampaignScenarioID> allScenarios() const; std::set<CampaignScenarioID> allScenarios() const;
int scenariosCount() const; int scenariosCount() const;
void overrideCampaign();
void overrideCampaignScenarios();
template <typename Handler> void serialize(Handler &h) template <typename Handler> void serialize(Handler &h)
{ {
h & static_cast<CampaignHeader&>(*this); h & static_cast<CampaignHeader&>(*this);

View File

@ -197,6 +197,11 @@ std::string CArchiveLoader::getMountPoint() const
return mountPoint; return mountPoint;
} }
const std::unordered_map<ResourcePath, ArchiveEntry> & CArchiveLoader::getEntries() const
{
return entries;
}
std::unordered_set<ResourcePath> CArchiveLoader::getFilteredFiles(std::function<bool(const ResourcePath &)> filter) const std::unordered_set<ResourcePath> CArchiveLoader::getFilteredFiles(std::function<bool(const ResourcePath &)> filter) const
{ {
std::unordered_set<ResourcePath> foundID; std::unordered_set<ResourcePath> foundID;
@ -209,7 +214,7 @@ std::unordered_set<ResourcePath> CArchiveLoader::getFilteredFiles(std::function<
return foundID; return foundID;
} }
void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, const ArchiveEntry & entry) const void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, const ArchiveEntry & entry, bool absolute) const
{ {
si64 currentPosition = fileStream.tell(); // save filestream position si64 currentPosition = fileStream.tell(); // save filestream position
@ -217,7 +222,7 @@ void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInput
fileStream.seek(entry.offset); fileStream.seek(entry.offset);
fileStream.read(data.data(), entry.fullSize); fileStream.read(data.data(), entry.fullSize);
boost::filesystem::path extractedFilePath = createExtractedFilePath(outputSubFolder, entry.name); boost::filesystem::path extractedFilePath = createExtractedFilePath(outputSubFolder, entry.name, absolute);
// writeToOutputFile // writeToOutputFile
std::ofstream out(extractedFilePath.string(), std::ofstream::binary); std::ofstream out(extractedFilePath.string(), std::ofstream::binary);
@ -227,17 +232,17 @@ void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInput
fileStream.seek(currentPosition); // restore filestream position fileStream.seek(currentPosition); // restore filestream position
} }
void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry) const void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry, bool absolute) const
{ {
std::unique_ptr<CInputStream> inputStream = load(ResourcePath(mountPoint + entry.name)); std::unique_ptr<CInputStream> inputStream = load(ResourcePath(mountPoint + entry.name));
entry.offset = 0; entry.offset = 0;
extractToFolder(outputSubFolder, *inputStream, entry); extractToFolder(outputSubFolder, *inputStream, entry, absolute);
} }
boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName) boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName, bool absolute)
{ {
boost::filesystem::path extractionFolderPath = VCMIDirs::get().userExtractedPath() / outputSubFolder; boost::filesystem::path extractionFolderPath = absolute ? outputSubFolder : VCMIDirs::get().userExtractedPath() / outputSubFolder;
boost::filesystem::path extractedFilePath = extractionFolderPath / entryName; boost::filesystem::path extractedFilePath = extractionFolderPath / entryName;
boost::filesystem::create_directories(extractionFolderPath); boost::filesystem::create_directories(extractionFolderPath);

View File

@ -63,12 +63,13 @@ public:
std::unique_ptr<CInputStream> load(const ResourcePath & resourceName) const override; std::unique_ptr<CInputStream> load(const ResourcePath & resourceName) const override;
bool existsResource(const ResourcePath & resourceName) const override; bool existsResource(const ResourcePath & resourceName) const override;
std::string getMountPoint() const override; std::string getMountPoint() const override;
const std::unordered_map<ResourcePath, ArchiveEntry> & getEntries() const;
void updateFilteredFiles(std::function<bool(const std::string &)> filter) const override {} void updateFilteredFiles(std::function<bool(const std::string &)> filter) const override {}
std::unordered_set<ResourcePath> getFilteredFiles(std::function<bool(const ResourcePath &)> filter) const override; std::unordered_set<ResourcePath> getFilteredFiles(std::function<bool(const ResourcePath &)> filter) const override;
/** Extracts one archive entry to the specified subfolder. Used for Video and Sound */ /** Extracts one archive entry to the specified subfolder. Used for Video and Sound */
void extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, const ArchiveEntry & entry) const; void extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, const ArchiveEntry & entry, bool absolute = false) const;
/** Extracts one archive entry to the specified subfolder. Used for Images, Sprites, etc */ /** Extracts one archive entry to the specified subfolder. Used for Images, Sprites, etc */
void extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry) const; void extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry, bool absolute = false) const;
private: private:
/** /**
@ -105,6 +106,6 @@ private:
}; };
/** Constructs the file path for the extracted file. Creates the subfolder hierarchy aswell **/ /** Constructs the file path for the extracted file. Creates the subfolder hierarchy aswell **/
boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName); boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName, bool absolute);
VCMI_LIB_NAMESPACE_END VCMI_LIB_NAMESPACE_END

View File

@ -536,7 +536,14 @@ void CGDwelling::serializeJsonOptions(JsonSerializeFormat & handler)
const IOwnableObject * CGDwelling::asOwnable() const const IOwnableObject * CGDwelling::asOwnable() const
{ {
return this; switch (ID.toEnum())
{
case Obj::WAR_MACHINE_FACTORY:
case Obj::REFUGEE_CAMP:
return nullptr; // can't be owned
default:
return this;
}
} }
ResourceSet CGDwelling::dailyIncome() const ResourceSet CGDwelling::dailyIncome() const

View File

@ -1227,6 +1227,21 @@ BoatId CGShipyard::getBoatType() const
return createdBoat; return createdBoat;
} }
const IOwnableObject * CGShipyard::asOwnable() const
{
return this;
}
ResourceSet CGShipyard::dailyIncome() const
{
return {};
}
std::vector<CreatureID> CGShipyard::providedCreatures() const
{
return {};
}
void CGDenOfthieves::onHeroVisit (const CGHeroInstance * h) const void CGDenOfthieves::onHeroVisit (const CGHeroInstance * h) const
{ {
cb->showObjectWindow(this, EOpenWindowMode::THIEVES_GUILD, h, false); cb->showObjectWindow(this, EOpenWindowMode::THIEVES_GUILD, h, false);

View File

@ -346,7 +346,7 @@ public:
} }
}; };
class DLL_LINKAGE CGShipyard : public CGObjectInstance, public IShipyard class DLL_LINKAGE CGShipyard : public CGObjectInstance, public IShipyard, public IOwnableObject
{ {
friend class ShipyardInstanceConstructor; friend class ShipyardInstanceConstructor;
@ -358,6 +358,10 @@ protected:
const IObjectInterface * getObject() const override; const IObjectInterface * getObject() const override;
BoatId getBoatType() const override; BoatId getBoatType() const override;
const IOwnableObject * asOwnable() const final;
ResourceSet dailyIncome() const override;
std::vector<CreatureID> providedCreatures() const override;
public: public:
using CGObjectInstance::CGObjectInstance; using CGObjectInstance::CGObjectInstance;

View File

@ -172,6 +172,8 @@ int CMapInfo::getMapSizeFormatIconId() const
return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["iconIndex"].Integer(); return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["iconIndex"].Integer();
case EMapFormat::SOD: case EMapFormat::SOD:
return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["iconIndex"].Integer(); return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["iconIndex"].Integer();
case EMapFormat::CHR:
return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_CHRONICLES)["iconIndex"].Integer();
case EMapFormat::WOG: case EMapFormat::WOG:
return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)["iconIndex"].Integer(); return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)["iconIndex"].Integer();
case EMapFormat::HOTA: case EMapFormat::HOTA:

View File

@ -157,6 +157,7 @@ std::unique_ptr<IMapLoader> CMapService::getMapLoader(std::unique_ptr<CInputStre
case static_cast<int>(EMapFormat::AB) : case static_cast<int>(EMapFormat::AB) :
case static_cast<int>(EMapFormat::ROE) : case static_cast<int>(EMapFormat::ROE) :
case static_cast<int>(EMapFormat::SOD) : case static_cast<int>(EMapFormat::SOD) :
case static_cast<int>(EMapFormat::CHR) :
case static_cast<int>(EMapFormat::HOTA) : case static_cast<int>(EMapFormat::HOTA) :
return std::unique_ptr<IMapLoader>(new CMapLoaderH3M(mapName, modName, encoding, stream.get())); return std::unique_ptr<IMapLoader>(new CMapLoaderH3M(mapName, modName, encoding, stream.get()));
default : default :

View File

@ -25,6 +25,8 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::find(EMapFormat format, uint32_t hota
return getFeaturesAB(); return getFeaturesAB();
case EMapFormat::SOD: case EMapFormat::SOD:
return getFeaturesSOD(); return getFeaturesSOD();
case EMapFormat::CHR:
return getFeaturesCHR();
case EMapFormat::WOG: case EMapFormat::WOG:
return getFeaturesWOG(); return getFeaturesWOG();
case EMapFormat::HOTA: case EMapFormat::HOTA:
@ -107,6 +109,16 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesSOD()
return result; return result;
} }
MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesCHR()
{
MapFormatFeaturesH3M result = getFeaturesSOD();
result.levelCHR = true;
result.heroesPortraitsCount = 169; // +6x tarnum
return result;
}
MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesWOG() MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesWOG()
{ {
MapFormatFeaturesH3M result = getFeaturesSOD(); MapFormatFeaturesH3M result = getFeaturesSOD();

View File

@ -21,6 +21,7 @@ public:
static MapFormatFeaturesH3M getFeaturesROE(); static MapFormatFeaturesH3M getFeaturesROE();
static MapFormatFeaturesH3M getFeaturesAB(); static MapFormatFeaturesH3M getFeaturesAB();
static MapFormatFeaturesH3M getFeaturesSOD(); static MapFormatFeaturesH3M getFeaturesSOD();
static MapFormatFeaturesH3M getFeaturesCHR();
static MapFormatFeaturesH3M getFeaturesWOG(); static MapFormatFeaturesH3M getFeaturesWOG();
static MapFormatFeaturesH3M getFeaturesHOTA(uint32_t hotaVersion); static MapFormatFeaturesH3M getFeaturesHOTA(uint32_t hotaVersion);
@ -64,6 +65,7 @@ public:
bool levelROE = false; bool levelROE = false;
bool levelAB = false; bool levelAB = false;
bool levelSOD = false; bool levelSOD = false;
bool levelCHR = false;
bool levelWOG = false; bool levelWOG = false;
bool levelHOTA0 = false; bool levelHOTA0 = false;
bool levelHOTA1 = false; bool levelHOTA1 = false;

View File

@ -19,7 +19,7 @@ enum class EMapFormat : uint8_t
ROE = 0x0e, // 14 ROE = 0x0e, // 14
AB = 0x15, // 21 AB = 0x15, // 21
SOD = 0x1c, // 28 SOD = 0x1c, // 28
// CHR = 0x1d, // 29 Heroes Chronicles, presumably - identical to SoD, untested CHR = 0x1d, // 29
HOTA = 0x20, // 32 HOTA = 0x20, // 32
WOG = 0x33, // 51 WOG = 0x33, // 51
VCMI = 0x64 VCMI = 0x64

View File

@ -135,6 +135,8 @@ static MapIdentifiersH3M generateMapping(EMapFormat format)
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)); identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE));
if(features.levelSOD) if(features.levelSOD)
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)); identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH));
if(features.levelCHR)
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_CHRONICLES));
if(features.levelWOG) if(features.levelWOG)
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)); identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS));
if(features.levelHOTA0) if(features.levelHOTA0)
@ -161,6 +163,7 @@ static std::map<EMapFormat, MapIdentifiersH3M> generateMappings()
addMapping(EMapFormat::ROE); addMapping(EMapFormat::ROE);
addMapping(EMapFormat::AB); addMapping(EMapFormat::AB);
addMapping(EMapFormat::SOD); addMapping(EMapFormat::SOD);
addMapping(EMapFormat::CHR);
addMapping(EMapFormat::HOTA); addMapping(EMapFormat::HOTA);
addMapping(EMapFormat::WOG); addMapping(EMapFormat::WOG);

View File

@ -30,9 +30,6 @@ CMapGenOptions::CMapGenOptions()
customizedPlayers(false) customizedPlayers(false)
{ {
initPlayersMap(); initPlayersMap();
setRoadEnabled(RoadId(Road::DIRT_ROAD), true);
setRoadEnabled(RoadId(Road::GRAVEL_ROAD), true);
setRoadEnabled(RoadId(Road::COBBLESTONE_ROAD), true);
} }
si32 CMapGenOptions::getWidth() const si32 CMapGenOptions::getWidth() const

View File

@ -56,6 +56,7 @@ enum class ESerializationVersion : int32_t
NEW_MARKETS, // 857 - reworked market classes NEW_MARKETS, // 857 - reworked market classes
PLAYER_STATE_OWNED_OBJECTS, // 858 - player state stores all owned objects in a single list PLAYER_STATE_OWNED_OBJECTS, // 858 - player state stores all owned objects in a single list
SAVE_COMPATIBILITY_FIXES, // 859 - implementation of previoulsy postponed changes to serialization SAVE_COMPATIBILITY_FIXES, // 859 - implementation of previoulsy postponed changes to serialization
CHRONICLES_SUPPORT, // 860 - support for heroes chronicles
CURRENT = SAVE_COMPATIBILITY_FIXES CURRENT = CHRONICLES_SUPPORT
}; };

View File

@ -326,7 +326,7 @@ SetAvailableCreatures NewTurnProcessor::generateTownGrowth(const CGTownInstance
if (weekType == EWeekType::PLAGUE) if (weekType == EWeekType::PLAGUE)
resultingCreatures = creaturesBefore / 2; resultingCreatures = creaturesBefore / 2;
else if (weekType == EWeekType::DOUBLE_GROWTH) else if (weekType == EWeekType::DOUBLE_GROWTH && vstd::contains(t->creatures.at(k).second, creatureWeek))
resultingCreatures = (creaturesBefore + creatureGrowth) * 2; resultingCreatures = (creaturesBefore + creatureGrowth) * 2;
else else
resultingCreatures = creaturesBefore + creatureGrowth; resultingCreatures = creaturesBefore + creatureGrowth;