From f210bd9cb3b25454467af80f45550b21964dc7fa Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 6 Feb 2025 14:15:12 +0000 Subject: [PATCH 01/11] Fix crash on winning map on 7-day town timer running out for enemy --- server/processors/TurnOrderProcessor.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 899df2b04..36c3bffef 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -249,6 +249,8 @@ void TurnOrderProcessor::doStartNewDay() assert(awaitingPlayers.empty()); assert(actingPlayers.empty()); + gameHandler->onNewTurn(); + bool activePlayer = false; for (auto player : actedPlayers) { @@ -264,7 +266,6 @@ void TurnOrderProcessor::doStartNewDay() std::swap(actedPlayers, awaitingPlayers); - gameHandler->onNewTurn(); updateAndNotifyContactStatus(); tryStartTurnsForPlayers(); } From 436aa0a15fb5f8edbe57f15178a39f7a3a0c51af Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 6 Feb 2025 14:16:52 +0000 Subject: [PATCH 02/11] Add victory condition overrides for Elixir of Life Maps can only be won by finding required artifact, defeating enemies is not enough --- config/mapOverrides.json | 115 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/config/mapOverrides.json b/config/mapOverrides.json index 7d9206bf6..55a81df97 100644 --- a/config/mapOverrides.json +++ b/config/mapOverrides.json @@ -2370,5 +2370,120 @@ }, "victoryIconIndex" : 11, "victoryString" : "core.vcdesc.0" + }, + + "data/gelu:1" : { // Cutthroats + "defeatIconIndex" : 1, + "defeatString" : "core.lcdesc.2", + "triggeredEvents" : { + "heroesMustSurvive" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "noneOf", + [ "control", { "position" : [ 11, 8, 0 ], "type" : "hero" } ] // Gelu + ] + ], + "effect" : { + "messageToSend" : "core.genrltxt.5", + "type" : "defeat" + }, + "message" : "core.genrltxt.253" + }, + "specialVictory" : { + "condition" : [ "haveArtifact", { "type" : "artifact.ringOfVitality" } ], + "effect" : { + "messageToSend" : "core.genrltxt.281", + "type" : "victory" + }, + "message" : "core.genrltxt.280" + }, + "standardDefeat" : { + "condition" : [ "daysWithoutTown", { "value" : 7 } ], + "effect" : { + "messageToSend" : "core.genrltxt.8", + "type" : "defeat" + }, + "message" : "core.genrltxt.7" + } + }, + "victoryIconIndex" : 0, + "victoryString" : "core.vcdesc.1" + }, + "data/gelu:2" : { // Valley of the Dragon Lords + "defeatIconIndex" : 1, + "defeatString" : "core.lcdesc.2", + "triggeredEvents" : { + "heroesMustSurvive" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "noneOf", + [ "control", { "position" : [ 62, 12, 0 ], "type" : "hero" } ] // Gelu + ] + ], + "effect" : { + "messageToSend" : "core.genrltxt.5", + "type" : "defeat" + }, + "message" : "core.genrltxt.253" + }, + "specialVictory" : { + "condition" : [ "haveArtifact", { "type" : "artifact.ringOfLife" } ], + "effect" : { + "messageToSend" : "core.genrltxt.281", + "type" : "victory" + }, + "message" : "core.genrltxt.280" + }, + "standardDefeat" : { + "condition" : [ "daysWithoutTown", { "value" : 7 } ], + "effect" : { + "messageToSend" : "core.genrltxt.8", + "type" : "defeat" + }, + "message" : "core.genrltxt.7" + } + }, + "victoryIconIndex" : 0, + "victoryString" : "core.vcdesc.1" + }, + "data/gelu:3" : { // A Thief in the Night + "defeatIconIndex" : 1, + "defeatString" : "core.lcdesc.2", + "triggeredEvents" : { + "heroesMustSurvive" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "noneOf", + [ "control", { "position" : [ 50, 9, 0 ], "type" : "hero" } ] // Gelu + ] + ], + "effect" : { + "messageToSend" : "core.genrltxt.5", + "type" : "defeat" + }, + "message" : "core.genrltxt.253" + }, + "specialVictory" : { + "condition" : [ "haveArtifact", { "type" : "artifact.vialOfLifeblood" } ], + "effect" : { + "messageToSend" : "core.genrltxt.281", + "type" : "victory" + }, + "message" : "core.genrltxt.280" + }, + "standardDefeat" : { + "condition" : [ "daysWithoutTown", { "value" : 7 } ], + "effect" : { + "messageToSend" : "core.genrltxt.8", + "type" : "defeat" + }, + "message" : "core.genrltxt.7" + } + }, + "victoryIconIndex" : 0, + "victoryString" : "core.vcdesc.1" } } From 7823dd394567bee7eb5f86e92d6cf3cbda35b8e8 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 6 Feb 2025 14:18:18 +0000 Subject: [PATCH 03/11] Block pathfinder from allowing to interact with heroes standing on another visitable object --- lib/pathfinder/PathfinderUtil.h | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/pathfinder/PathfinderUtil.h b/lib/pathfinder/PathfinderUtil.h index 3e1f4cbaf..10cfab9c3 100644 --- a/lib/pathfinder/PathfinderUtil.h +++ b/lib/pathfinder/PathfinderUtil.h @@ -40,15 +40,23 @@ namespace PathfinderUtil } else { + bool hasBlockedVisitable = false; + bool hasVisitable = false; + for(const CGObjectInstance * obj : tinfo.visitableObjects) { if(obj->isBlockedVisitable()) - return EPathAccessibility::BLOCKVIS; - else if(obj->passableFor(player)) - return EPathAccessibility::ACCESSIBLE; - else if(obj->ID != Obj::EVENT) - return EPathAccessibility::VISITABLE; + hasBlockedVisitable = true; + else if(!obj->passableFor(player) && obj->ID != Obj::EVENT) + hasVisitable = true; } + + if(hasBlockedVisitable) + return EPathAccessibility::BLOCKVIS; + if(hasVisitable) + return EPathAccessibility::VISITABLE; + + return EPathAccessibility::ACCESSIBLE; } } else if(tinfo.blocked()) From d586d4ea89c8aac87c9aa109905ccf8d302a1b8c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 6 Feb 2025 14:18:56 +0000 Subject: [PATCH 04/11] Do not highlight unavailable columns of hexes on battlefield --- client/battle/BattleFieldController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index 412215deb..bfa1d4c15 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -533,7 +533,7 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas) BattleHexArray hoveredMoveHexes = getHighlightedHexesForMovementTarget(); BattleHex hoveredHex = getHoveredHex(); - BattleHexArray hoveredMouseHex = hoveredHex.isValid() ? BattleHexArray({ hoveredHex }) : BattleHexArray(); + BattleHexArray hoveredMouseHex = hoveredHex.isAvailable() ? BattleHexArray({ hoveredHex }) : BattleHexArray(); const CStack * hoveredStack = getHoveredStack(); if(!hoveredStack && hoveredHex == BattleHex::INVALID) From f4a218285ff3fb5a29a1e3887055f16f04f8ded7 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 6 Feb 2025 14:19:34 +0000 Subject: [PATCH 05/11] Fix display of remaining spell duration for creatures --- client/battle/BattleInterfaceClasses.cpp | 2 +- client/windows/CCreatureWindow.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 1f57228da..5240737d5 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -691,7 +691,7 @@ void StackInfoBasicPanel::initializeData(const CStack * stack) if (spellBonuses->empty()) throw std::runtime_error("Failed to find effects for spell " + effect.toSpell()->getJsonKey()); - int duration = spellBonuses->front()->duration; + int duration = spellBonuses->front()->turnsRemain; icons.push_back(std::make_shared(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); if(settings["general"]["enableUiEnhancements"].Bool()) diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index e86bbf864..8973c19f9 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -238,7 +238,7 @@ CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int if (spellBonuses->empty()) throw std::runtime_error("Failed to find effects for spell " + effect.toSpell()->getJsonKey()); - int duration = spellBonuses->front()->duration; + int duration = spellBonuses->front()->turnsRemain; boost::replace_first(spellText, "%d", std::to_string(duration)); spellIcons.push_back(std::make_shared(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); From 367aa942271391dfe5a3e3c774b085665c87bea5 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 6 Feb 2025 14:20:45 +0000 Subject: [PATCH 06/11] Do not scroll lists in lobby if another item is hovered --- client/globalLobby/GlobalLobbyWidget.cpp | 10 ++++++++++ client/widgets/ObjectLists.cpp | 5 +++++ client/widgets/ObjectLists.h | 2 ++ 3 files changed, 17 insertions(+) diff --git a/client/globalLobby/GlobalLobbyWidget.cpp b/client/globalLobby/GlobalLobbyWidget.cpp index da3adf3be..46ced84d4 100644 --- a/client/globalLobby/GlobalLobbyWidget.cpp +++ b/client/globalLobby/GlobalLobbyWidget.cpp @@ -28,6 +28,7 @@ #include "../widgets/Images.h" #include "../widgets/MiscWidgets.h" #include "../widgets/ObjectLists.h" +#include "../widgets/Slider.h" #include "../widgets/TextControls.h" #include "../../lib/CConfigHandler.h" @@ -126,6 +127,15 @@ std::shared_ptr GlobalLobbyWidget::buildItemList(const JsonNode & co auto result = std::make_shared(callback, position, itemOffset, visibleAmount, totalAmount, initialPos, sliderMode, Rect(sliderPosition, sliderSize)); + if (result->getSlider()) + { + Point scrollBoundsDimensions(sliderPosition.x + result->getSlider()->pos.w, result->getSlider()->pos.h); + Point scrollBoundsOffset = -sliderPosition; + + result->getSlider()->setScrollBounds(Rect(scrollBoundsOffset, scrollBoundsDimensions)); + result->getSlider()->setPanningStep(itemOffset.length()); + } + result->setRedrawParent(true); return result; } diff --git a/client/widgets/ObjectLists.cpp b/client/widgets/ObjectLists.cpp index 6470df4a5..2f4b2d190 100644 --- a/client/widgets/ObjectLists.cpp +++ b/client/widgets/ObjectLists.cpp @@ -168,6 +168,11 @@ std::shared_ptr CListBox::getItem(size_t which) return std::shared_ptr(); } +std::shared_ptr CListBox::getSlider() +{ + return slider; +} + size_t CListBox::getIndexOf(std::shared_ptr item) { size_t i=first; diff --git a/client/widgets/ObjectLists.h b/client/widgets/ObjectLists.h index 11703130c..91b8b99cb 100644 --- a/client/widgets/ObjectLists.h +++ b/client/widgets/ObjectLists.h @@ -91,6 +91,8 @@ public: //return item with index which or null if not present std::shared_ptr getItem(size_t which); + std::shared_ptr getSlider(); + //return currently active items const std::list> & getItems(); From 5a02a4c07f055adbeb732a580c5042eeb3124542 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 6 Feb 2025 14:21:19 +0000 Subject: [PATCH 07/11] Fix path to Chronicles campaigns background, remove old code --- client/render/AssetGenerator.cpp | 2 +- config/campaignSets.json | 2 +- lib/filesystem/Filesystem.cpp | 5 ----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/client/render/AssetGenerator.cpp b/client/render/AssetGenerator.cpp index 6f264b0ac..aacc9105e 100644 --- a/client/render/AssetGenerator.cpp +++ b/client/render/AssetGenerator.cpp @@ -247,7 +247,7 @@ AssetGenerator::CanvasPtr AssetGenerator::createCampaignBackground() AssetGenerator::CanvasPtr AssetGenerator::createChroniclesCampaignImages(int chronicle) { - auto imgPathBg = ImagePath::builtin("data/chronicles_" + std::to_string(chronicle) + "/GamSelBk"); + auto imgPathBg = ImagePath::builtin("chronicles_" + std::to_string(chronicle) + "/GamSelBk"); auto locator = ImageLocator(imgPathBg, EImageBlitMode::OPAQUE); std::shared_ptr img = GH.renderHandler().loadImage(locator); diff --git a/config/campaignSets.json b/config/campaignSets.json index ce118a7fc..607d1be05 100644 --- a/config/campaignSets.json +++ b/config/campaignSets.json @@ -50,7 +50,7 @@ }, "chr": { - "images" : [ {"x": 0, "y": 0, "name":"data/CampaignBackground8"} ], + "images" : [ {"x": 0, "y": 0, "name":"CampaignBackground8"} ], "exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" }, "items": [ diff --git a/lib/filesystem/Filesystem.cpp b/lib/filesystem/Filesystem.cpp index 6987064d9..5ec40b361 100644 --- a/lib/filesystem/Filesystem.cpp +++ b/lib/filesystem/Filesystem.cpp @@ -183,14 +183,9 @@ void CResourceHandler::initialize() knownLoaders["saves"] = new CFilesystemLoader("SAVES/", VCMIDirs::get().userSavePath()); knownLoaders["config"] = new CFilesystemLoader("CONFIG/", VCMIDirs::get().userConfigPath()); - knownLoaders["gen_data"] = new CFilesystemLoader("DATA/", VCMIDirs::get().userDataPath() / "Generated" / "Data"); - knownLoaders["gen_sprites"] = new CFilesystemLoader("SPRITES/", VCMIDirs::get().userDataPath() / "Generated" / "Sprites"); - auto * localFS = new CFilesystemList(); localFS->addLoader(knownLoaders["saves"], true); localFS->addLoader(knownLoaders["config"], true); - localFS->addLoader(knownLoaders["gen_data"], true); - localFS->addLoader(knownLoaders["gen_sprites"], true); addFilesystem("root", "initial", createInitial()); addFilesystem("root", "data", new CFilesystemList()); From 0efa39ec79f9a911aff5a6d0833be5fb1dfb9cd8 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 6 Feb 2025 14:21:32 +0000 Subject: [PATCH 08/11] Fix pack name in logs --- lib/serializer/Connection.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index 823fe721c..527138de0 100644 --- a/lib/serializer/Connection.cpp +++ b/lib/serializer/Connection.cpp @@ -102,7 +102,8 @@ std::unique_ptr CConnection::retrievePack(const std::vector & if (packReader->position != data.size()) throw std::runtime_error("Failed to retrieve pack! Not all data has been read!"); - logNetwork->trace("Received CPack of type %s", typeid(result.get()).name()); + auto packRawPtr = result.get(); + logNetwork->trace("Received CPack of type %s", typeid(*packRawPtr).name()); deserializer->loadedPointers.clear(); deserializer->loadedSharedPointers.clear(); return result; From fff5ac5945f3b20d9ec55a556b0c46abf16f50f8 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 4 Feb 2025 16:41:15 +0000 Subject: [PATCH 09/11] Fix transparency on hero portraits --- client/renderSDL/RenderHandler.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp index aa771c3e9..3e6e01630 100644 --- a/client/renderSDL/RenderHandler.cpp +++ b/client/renderSDL/RenderHandler.cpp @@ -379,7 +379,7 @@ void RenderHandler::addImageListEntries(const EntityService * service) if (imageName.empty()) return; - auto & layout = getAnimationLayout(AnimationPath::builtin("SPRITES/" + listName), 1, EImageBlitMode::SIMPLE); + auto & layout = getAnimationLayout(AnimationPath::builtin("SPRITES/" + listName), 1, EImageBlitMode::COLORKEY); JsonNode entry; entry["file"].String() = imageName; @@ -417,8 +417,8 @@ static void detectOverlappingBuildings(RenderHandler * renderHandler, const Fact if (left->pos.z != right->pos.z) continue; // buildings already have different z-index and have well-defined overlap logic - auto leftImage = renderHandler->loadImage(left->defName, 0, 0, EImageBlitMode::SIMPLE); - auto rightImage = renderHandler->loadImage(right->defName, 0, 0, EImageBlitMode::SIMPLE); + auto leftImage = renderHandler->loadImage(left->defName, 0, 0, EImageBlitMode::COLORKEY); + auto rightImage = renderHandler->loadImage(right->defName, 0, 0, EImageBlitMode::COLORKEY); Rect leftRect( left->pos.x, left->pos.y, leftImage->width(), leftImage->height()); Rect rightRect( right->pos.x, right->pos.y, rightImage->width(), rightImage->height()); From 4e6560c4c9dc6c06410f31b57d50c65cde7acbb9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 6 Feb 2025 19:22:04 +0000 Subject: [PATCH 10/11] Fixes for issues detected by valgrind --- client/render/CanvasImage.cpp | 5 +++++ client/render/CanvasImage.h | 1 + lib/bonuses/CBonusSystemNode.cpp | 2 ++ lib/mapping/CMapHeader.cpp | 13 +++++++++++-- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/client/render/CanvasImage.cpp b/client/render/CanvasImage.cpp index dc6d619bb..3ed438363 100644 --- a/client/render/CanvasImage.cpp +++ b/client/render/CanvasImage.cpp @@ -25,6 +25,11 @@ CanvasImage::CanvasImage(const Point & size, CanvasScalingPolicy scalingPolicy) { } +CanvasImage::~CanvasImage() +{ + SDL_FreeSurface(surface); +} + void CanvasImage::draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const { if(src) diff --git a/client/render/CanvasImage.h b/client/render/CanvasImage.h index 095a4276b..56452c926 100644 --- a/client/render/CanvasImage.h +++ b/client/render/CanvasImage.h @@ -16,6 +16,7 @@ class CanvasImage : public IImage { public: CanvasImage(const Point & size, CanvasScalingPolicy scalingPolicy); + ~CanvasImage(); Canvas getCanvas(); diff --git a/lib/bonuses/CBonusSystemNode.cpp b/lib/bonuses/CBonusSystemNode.cpp index 9f18a1cb5..e1c2d7f46 100644 --- a/lib/bonuses/CBonusSystemNode.cpp +++ b/lib/bonuses/CBonusSystemNode.cpp @@ -188,6 +188,7 @@ std::shared_ptr CBonusSystemNode::getUpdatedBonus(const std::shared_ptrheroh->getDefaultAllowed(); From 8bac023e820e3e9dce314f6f1f94ad8efd29d301 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 6 Feb 2025 19:22:46 +0000 Subject: [PATCH 11/11] Fix internal connection sending queued packages after closing --- lib/network/NetworkConnection.cpp | 12 +++++++----- lib/network/NetworkConnection.h | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/network/NetworkConnection.cpp b/lib/network/NetworkConnection.cpp index e77e683dd..3c8d7934e 100644 --- a/lib/network/NetworkConnection.cpp +++ b/lib/network/NetworkConnection.cpp @@ -216,22 +216,25 @@ InternalConnection::InternalConnection(INetworkConnectionListener & listener, co void InternalConnection::receivePacket(const std::vector & message) { - io->post([self = shared_from_this(), message](){ - self->listener.onPacketReceived(self, message); + io->post([self = std::static_pointer_cast(shared_from_this()), message](){ + if (self->connectionActive) + self->listener.onPacketReceived(self, message); }); } void InternalConnection::disconnect() { - io->post([self = shared_from_this()](){ + io->post([self = std::static_pointer_cast(shared_from_this())](){ self->listener.onDisconnected(self, "Internal connection has been terminated"); self->otherSideWeak.reset(); + self->connectionActive = false; }); } void InternalConnection::connectTo(std::shared_ptr connection) { otherSideWeak = connection; + connectionActive = true; } void InternalConnection::sendPacket(const std::vector & message) @@ -240,8 +243,6 @@ void InternalConnection::sendPacket(const std::vector & message) if (otherSide) otherSide->receivePacket(message); - else - throw std::runtime_error("Failed to send packet! Connection has been deleted!"); } void InternalConnection::setAsyncWritesEnabled(bool on) @@ -257,6 +258,7 @@ void InternalConnection::close() otherSide->disconnect(); otherSideWeak.reset(); + connectionActive = false; } VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkConnection.h b/lib/network/NetworkConnection.h index dc56ffc6a..fe6d34c2b 100644 --- a/lib/network/NetworkConnection.h +++ b/lib/network/NetworkConnection.h @@ -51,6 +51,7 @@ class InternalConnection final : public IInternalConnection, public std::enable_ std::weak_ptr otherSideWeak; std::shared_ptr io; INetworkConnectionListener & listener; + bool connectionActive = false; public: InternalConnection(INetworkConnectionListener & listener, const std::shared_ptr & context);