From f5d1657041ca3a2c45c6f5d398edd2a0c963f5ea Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 5 Oct 2025 01:49:00 +0200 Subject: [PATCH 01/26] make town title editable --- client/widgets/CTextInput.cpp | 74 ++++++++++++++++++++++++++ client/widgets/CTextInput.h | 40 ++++++++++---- client/windows/CCastleInterface.cpp | 3 +- client/windows/CCastleInterface.h | 3 +- lib/mapObjects/CGTownInstance.cpp | 7 ++- lib/mapObjects/CGTownInstance.h | 4 ++ lib/serializer/ESerializationVersion.h | 3 +- 7 files changed, 120 insertions(+), 14 deletions(-) diff --git a/client/widgets/CTextInput.cpp b/client/widgets/CTextInput.cpp index 4d1a8d729..700e13854 100644 --- a/client/widgets/CTextInput.cpp +++ b/client/widgets/CTextInput.cpp @@ -27,6 +27,80 @@ std::list CFocusable::focusables; CFocusable * CFocusable::inputWithFocus; +CTextInputWithConfirm::CTextInputWithConfirm(const Rect & Pos, EFonts font, ETextAlignment alignment, std::string text, bool limitToRect, std::function confirmCallback) + : CTextInput(Pos, font, alignment, false), confirmCb(confirmCallback), limitToRect(limitToRect), initialText(text) +{ + setText(text); +} + +bool CTextInputWithConfirm::captureThisKey(EShortcut key) +{ + return hasFocus() && (key == EShortcut::GLOBAL_ACCEPT || key == EShortcut::GLOBAL_CANCEL || key == EShortcut::GLOBAL_BACKSPACE); +} + +void CTextInputWithConfirm::keyPressed(EShortcut key) +{ + if(!hasFocus()) + return; + + if(key == EShortcut::GLOBAL_ACCEPT) + confirm(); + else if(key == EShortcut::GLOBAL_CANCEL) + { + setText(initialText); + removeFocus(); + } + + CTextInput::keyPressed(key); +} + +bool CTextInputWithConfirm::receiveEvent(const Point & position, int eventType) const +{ + return eventType == AEventsReceiver::LCLICK; // capture all left clicks (not only within control) +} + +void CTextInputWithConfirm::clickReleased(const Point & cursorPosition) +{ + if(!pos.isInside(cursorPosition)) // clicked outside + confirm(); +} + +void CTextInputWithConfirm::clickPressed(const Point & cursorPosition) +{ + if(pos.isInside(cursorPosition)) // clickPressed should respect control area (receiveEvent also affects this) + CTextInput::clickPressed(cursorPosition); +} + +void CTextInputWithConfirm::onFocusGot() +{ + initialText = getText(); + + CTextInput::onFocusGot(); +} + +void CTextInputWithConfirm::textInputted(const std::string & enteredText) +{ + if(!hasFocus()) + return; + + CTextInput::textInputted(enteredText); + + std::string visibleText = getVisibleText(); + const auto & font = ENGINE->renderHandler().loadFont(label->font); + while(limitToRect && font->getStringWidth(visibleText) > pos.w) + { + TextOperations::trimRightUnicode(currentText); + visibleText = getVisibleText(); + } +} + +void CTextInputWithConfirm::confirm() +{ + if(confirmCb && initialText != getText()) + confirmCb(); + removeFocus(); +} + CTextInput::CTextInput(const Rect & Pos) :originalAlignment(ETextAlignment::CENTERLEFT) { diff --git a/client/widgets/CTextInput.h b/client/widgets/CTextInput.h index 67639d098..0ab58425d 100644 --- a/client/widgets/CTextInput.h +++ b/client/widgets/CTextInput.h @@ -45,8 +45,9 @@ public: }; /// Text input box where players can enter text -class CTextInput final : public CFocusable +class CTextInput : public CFocusable { +protected: using TextEditedCallback = std::function; using TextFilterCallback = std::function; @@ -71,12 +72,12 @@ class CTextInput final : public CFocusable void createLabel(bool giveFocusToInput); void updateLabel(); - void clickPressed(const Point & cursorPosition) final; - void textInputted(const std::string & enteredText) final; - void textEdited(const std::string & enteredText) final; - void onFocusGot() final; - void onFocusLost() final; - void showPopupWindow(const Point & cursorPosition) final; + void clickPressed(const Point & cursorPosition) override; + void textInputted(const std::string & enteredText) override; + void textEdited(const std::string & enteredText) override; + void onFocusGot() override; + void onFocusLost() override; + void showPopupWindow(const Point & cursorPosition) override; CTextInput(const Rect & Pos); public: @@ -105,7 +106,26 @@ public: void setAlignment(ETextAlignment alignment); // CIntObject interface impl - void keyPressed(EShortcut key) final; - void activate() final; - void deactivate() final; + void keyPressed(EShortcut key) override; + void activate() override; + void deactivate() override; +}; + +class CTextInputWithConfirm final : public CTextInput +{ + std::string initialText; + std::function confirmCb; + bool limitToRect; + + void confirm(); +public: + CTextInputWithConfirm(const Rect & Pos, EFonts font, ETextAlignment alignment, std::string text, bool limitToRect, std::function confirmCallback); + + bool captureThisKey(EShortcut key) override; + void keyPressed(EShortcut key) override; + void clickReleased(const Point & cursorPosition) override; + void clickPressed(const Point & cursorPosition) override; + bool receiveEvent(const Point & position, int eventType) const override; + void onFocusGot() override; + void textInputted(const std::string & enteredText) override; }; diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 10038bf1d..07a623d40 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -30,6 +30,7 @@ #include "../widgets/MiscWidgets.h" #include "../widgets/CComponent.h" #include "../widgets/CGarrisonInt.h" +#include "../widgets/CTextInput.h" #include "../widgets/Buttons.h" #include "../widgets/TextControls.h" #include "../widgets/RadialMenu.h" @@ -1435,7 +1436,7 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst garr->setRedrawParent(true); heroes = std::make_shared(town, Point(241, 387), Point(241, 483), garr, true); - title = std::make_shared(85, 387, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, town->getNameTranslated()); + title = std::make_shared(Rect(83, 386, 140, 20), FONT_MEDIUM, ETextAlignment::TOPLEFT, town->getNameTranslated(), true, [this](){ std::cout << title->getText(); }); income = std::make_shared(195, 443, FONT_SMALL, ETextAlignment::CENTER); icon = std::make_shared(AnimationPath::builtin("ITPT"), 0, 0, 15, 387); diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 46a546b5e..3ff010091 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -37,6 +37,7 @@ class CGarrisonInt; class CComponent; class CComponentBox; class LRClickableArea; +class CTextInputWithConfirm; /// Building "button" class CBuildingRect : public CShowableAnim @@ -225,7 +226,7 @@ public: /// Class which manages the castle window class CCastleInterface final : public CStatusbarWindow, public IGarrisonHolder, public IArtifactsHolder { - std::shared_ptr title; + std::shared_ptr title; std::shared_ptr income; std::shared_ptr icon; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 9f99f74b3..009992aaf 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -852,7 +852,7 @@ CBonusSystemNode & CGTownInstance::whatShouldBeAttached() std::string CGTownInstance::getNameTranslated() const { - return LIBRARY->generaltexth->translate(nameTextId); + return customName.empty() ? LIBRARY->generaltexth->translate(nameTextId) : customName; } std::string CGTownInstance::getNameTextID() const @@ -865,6 +865,11 @@ void CGTownInstance::setNameTextId( const std::string & newName ) nameTextId = newName; } +void CGTownInstance::setCustomName( const std::string & newName ) +{ + customName = newName; +} + const CArmedInstance * CGTownInstance::getUpperArmy() const { if(getGarrisonHero()) diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 132a5f358..4b1c9811b 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -46,6 +46,7 @@ class DLL_LINKAGE CGTownInstance : public CGDwelling, public IShipyard, public I { friend class CTownInstanceConstructor; std::string nameTextId; // name of town + std::string customName; std::map convertOldBuildings(std::vector oldVector); std::set builtBuildings; @@ -75,6 +76,8 @@ public: { h & static_cast(*this); h & nameTextId; + if (h.version >= Handler::Version::CUSTOM_NAMES) + h & customName; h & built; h & destroyed; h & identifier; @@ -128,6 +131,7 @@ public: std::string getNameTranslated() const; std::string getNameTextID() const; void setNameTextId(const std::string & newName); + void setCustomName(const std::string & newName); ////////////////////////////////////////////////////////////////////////// diff --git a/lib/serializer/ESerializationVersion.h b/lib/serializer/ESerializationVersion.h index 45b11eb20..e3f1520db 100644 --- a/lib/serializer/ESerializationVersion.h +++ b/lib/serializer/ESerializationVersion.h @@ -50,8 +50,9 @@ enum class ESerializationVersion : int32_t BONUS_HIDDEN, // hidden bonus MORE_MAP_LAYERS, // more map layers CONFIGURABLE_RESOURCES, // configurable resources + CUSTOM_NAMES, // custom names - CURRENT = CONFIGURABLE_RESOURCES, + CURRENT = CUSTOM_NAMES, }; static_assert(ESerializationVersion::MINIMAL <= ESerializationVersion::CURRENT, "Invalid serialization version definition!"); From e6272fe477a5aa31009d34b379961b31b1e9aad3 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 5 Oct 2025 01:57:01 +0200 Subject: [PATCH 02/26] fix length --- client/widgets/CTextInput.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/client/widgets/CTextInput.cpp b/client/widgets/CTextInput.cpp index 700e13854..e9bac0b06 100644 --- a/client/widgets/CTextInput.cpp +++ b/client/widgets/CTextInput.cpp @@ -83,15 +83,10 @@ void CTextInputWithConfirm::textInputted(const std::string & enteredText) if(!hasFocus()) return; - CTextInput::textInputted(enteredText); - - std::string visibleText = getVisibleText(); + std::string visibleText = getVisibleText() + enteredText; const auto & font = ENGINE->renderHandler().loadFont(label->font); - while(limitToRect && font->getStringWidth(visibleText) > pos.w) - { - TextOperations::trimRightUnicode(currentText); - visibleText = getVisibleText(); - } + if(!limitToRect || font->getStringWidth(visibleText) < pos.w) + CTextInput::textInputted(enteredText); } void CTextInputWithConfirm::confirm() From 115d90cbb3fd215bd13f3a78ecb946f566caeb0f Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 5 Oct 2025 02:03:18 +0200 Subject: [PATCH 03/26] small refactoring --- client/lobby/OptionsTab.cpp | 29 +---------------------------- client/lobby/OptionsTab.h | 7 ++----- 2 files changed, 3 insertions(+), 33 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 5ed1ff996..edf3c7728 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -1035,10 +1035,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con if(s->isControlledByAI() || GAME->server().isGuest()) labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, name, 95); else - { - labelPlayerNameEdit = std::make_shared(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, ETextAlignment::CENTER, false); - labelPlayerNameEdit->setText(name); - } + labelPlayerNameEdit = std::make_shared(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, ETextAlignment::CENTER, name, false, [this](){ updateName(); }); labelWhoCanPlay = std::make_shared(Rect(6, 21, 45, 26), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->arraytxt[206 + whoCanPlay]); @@ -1114,28 +1111,6 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con bonus = std::make_shared(Point(271, 2), *s, BONUS); } -bool OptionsTab::PlayerOptionsEntry::captureThisKey(EShortcut key) -{ - return labelPlayerNameEdit && labelPlayerNameEdit->hasFocus() && key == EShortcut::GLOBAL_ACCEPT; -} - -void OptionsTab::PlayerOptionsEntry::keyPressed(EShortcut key) -{ - if(labelPlayerNameEdit && key == EShortcut::GLOBAL_ACCEPT) - updateName(); -} - -bool OptionsTab::PlayerOptionsEntry::receiveEvent(const Point & position, int eventType) const -{ - return eventType == AEventsReceiver::LCLICK; // capture all left clicks (not only within control) -} - -void OptionsTab::PlayerOptionsEntry::clickReleased(const Point & cursorPosition) -{ - if(labelPlayerNameEdit && !labelPlayerNameEdit->pos.isInside(cursorPosition)) - updateName(); -} - void OptionsTab::PlayerOptionsEntry::updateName() { if(labelPlayerNameEdit->getText() != name) { @@ -1146,8 +1121,6 @@ void OptionsTab::PlayerOptionsEntry::updateName() { set->String() = labelPlayerNameEdit->getText(); } } - - labelPlayerNameEdit->removeFocus(); name = labelPlayerNameEdit->getText(); } diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index e74666e6d..3acaffa22 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -28,6 +28,7 @@ class CTextBox; class CButton; class CSlider; class LRClickableArea; +class CTextInputWithConfirm; class FilledTexturePlayerColored; class TransparentFilledRectangle; @@ -196,7 +197,7 @@ private: std::unique_ptr pi; std::unique_ptr s; std::shared_ptr labelPlayerName; - std::shared_ptr labelPlayerNameEdit; + std::shared_ptr labelPlayerNameEdit; std::shared_ptr labelWhoCanPlay; std::shared_ptr background; std::shared_ptr buttonTownLeft; @@ -215,10 +216,6 @@ private: PlayerOptionsEntry(const PlayerSettings & S, const OptionsTab & parentTab); void hideUnavailableButtons(); - bool captureThisKey(EShortcut key) override; - void keyPressed(EShortcut key) override; - void clickReleased(const Point & cursorPosition) override; - bool receiveEvent(const Point & position, int eventType) const override; private: const OptionsTab & parentTab; From 120213509d10790a27043255481af11a74f9720d Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 5 Oct 2025 02:46:19 +0200 Subject: [PATCH 04/26] netpacks for renaming --- client/netlag/PackRollbackGeneratorVisitor.h | 1 + client/widgets/CTextInput.cpp | 3 +++ client/windows/CCastleInterface.cpp | 5 ++++- lib/callback/CCallback.cpp | 6 ++++++ lib/callback/CCallback.h | 1 + lib/callback/IGameActionCallback.h | 1 + lib/gameState/GameStatePackVisitor.cpp | 5 +++++ lib/gameState/GameStatePackVisitor.h | 1 + lib/networkPacks/NetPackVisitor.h | 2 ++ lib/networkPacks/NetPacksLib.cpp | 10 +++++++++ lib/networkPacks/PacksForClient.h | 14 +++++++++++++ lib/networkPacks/PacksForServer.h | 22 ++++++++++++++++++++ lib/serializer/RegisterTypes.h | 2 ++ server/CGameHandler.cpp | 17 +++++++++++++++ server/CGameHandler.h | 1 + server/NetPacksServer.cpp | 8 +++++++ server/ServerNetPackVisitors.h | 1 + 17 files changed, 99 insertions(+), 1 deletion(-) diff --git a/client/netlag/PackRollbackGeneratorVisitor.h b/client/netlag/PackRollbackGeneratorVisitor.h index affb415f2..a26877591 100644 --- a/client/netlag/PackRollbackGeneratorVisitor.h +++ b/client/netlag/PackRollbackGeneratorVisitor.h @@ -85,6 +85,7 @@ private: //void visitSetCommanderProperty(SetCommanderProperty & pack) override; //void visitAddQuest(AddQuest & pack) override; //void visitChangeFormation(ChangeFormation & pack) override; + //void visitChangeTownName(ChangeTownName & pack) override; //void visitChangeSpells(ChangeSpells & pack) override; //void visitSetAvailableHero(SetAvailableHero & pack) override; //void visitChangeObjectVisitors(ChangeObjectVisitors & pack) override; diff --git a/client/widgets/CTextInput.cpp b/client/widgets/CTextInput.cpp index e9bac0b06..9ad7c8798 100644 --- a/client/widgets/CTextInput.cpp +++ b/client/widgets/CTextInput.cpp @@ -91,6 +91,9 @@ void CTextInputWithConfirm::textInputted(const std::string & enteredText) void CTextInputWithConfirm::confirm() { + if(getText().empty()) + setText(initialText); + if(confirmCb && initialText != getText()) confirmCb(); removeFocus(); diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 07a623d40..6308604ee 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1436,7 +1436,10 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst garr->setRedrawParent(true); heroes = std::make_shared(town, Point(241, 387), Point(241, 483), garr, true); - title = std::make_shared(Rect(83, 386, 140, 20), FONT_MEDIUM, ETextAlignment::TOPLEFT, town->getNameTranslated(), true, [this](){ std::cout << title->getText(); }); + title = std::make_shared(Rect(83, 386, 140, 20), FONT_MEDIUM, ETextAlignment::TOPLEFT, town->getNameTranslated(), true, [this](){ + std::string name = title->getText(); + GAME->interface()->cb->setTownName(town, name); + }); income = std::make_shared(195, 443, FONT_SMALL, ETextAlignment::CENTER); icon = std::make_shared(AnimationPath::builtin("ITPT"), 0, 0, 15, 387); diff --git a/lib/callback/CCallback.cpp b/lib/callback/CCallback.cpp index a826d2950..f94c8cdf1 100644 --- a/lib/callback/CCallback.cpp +++ b/lib/callback/CCallback.cpp @@ -286,6 +286,12 @@ void CCallback::setFormation(const CGHeroInstance * hero, EArmyFormation mode) sendRequest(pack); } +void CCallback::setTownName(const CGTownInstance * town, std::string & name) +{ + SetTownName pack(town->id, name); + sendRequest(pack); +} + void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero) { assert(townOrTavern); diff --git a/lib/callback/CCallback.h b/lib/callback/CCallback.h index cf8484bb9..7eb6a42c0 100644 --- a/lib/callback/CCallback.h +++ b/lib/callback/CCallback.h @@ -76,6 +76,7 @@ public: void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; void trade(const ObjectInstanceID marketId, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; void setFormation(const CGHeroInstance * hero, EArmyFormation mode) override; + void setTownName(const CGTownInstance * town, std::string & name) override; void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero=HeroTypeID::NONE) override; void save(const std::string &fname) override; void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override; diff --git a/lib/callback/IGameActionCallback.h b/lib/callback/IGameActionCallback.h index 946eee67d..8f5a9bcd9 100644 --- a/lib/callback/IGameActionCallback.h +++ b/lib/callback/IGameActionCallback.h @@ -70,6 +70,7 @@ public: virtual void endTurn()=0; virtual void buyArtifact(const CGHeroInstance *hero, ArtifactID aid)=0; //used to buy artifacts in towns (including spell book in the guild and war machines in blacksmith) virtual void setFormation(const CGHeroInstance * hero, EArmyFormation mode)=0; + virtual void setTownName(const CGTownInstance * town, std::string & name)=0; virtual void save(const std::string &fname) = 0; virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0; diff --git a/lib/gameState/GameStatePackVisitor.cpp b/lib/gameState/GameStatePackVisitor.cpp index d527d7555..4fc72af4c 100644 --- a/lib/gameState/GameStatePackVisitor.cpp +++ b/lib/gameState/GameStatePackVisitor.cpp @@ -123,6 +123,11 @@ void GameStatePackVisitor::visitChangeFormation(ChangeFormation & pack) gs.getHero(pack.hid)->setFormation(pack.formation); } +void GameStatePackVisitor::visitChangeTownName(ChangeTownName & pack) +{ + gs.getTown(pack.tid)->setCustomName(pack.name); +} + void GameStatePackVisitor::visitHeroVisitCastle(HeroVisitCastle & pack) { CGHeroInstance *h = gs.getHero(pack.hid); diff --git a/lib/gameState/GameStatePackVisitor.h b/lib/gameState/GameStatePackVisitor.h index fa4421e4e..8cb0642a7 100644 --- a/lib/gameState/GameStatePackVisitor.h +++ b/lib/gameState/GameStatePackVisitor.h @@ -87,6 +87,7 @@ public: void visitSetCommanderProperty(SetCommanderProperty & pack) override; void visitAddQuest(AddQuest & pack) override; void visitChangeFormation(ChangeFormation & pack) override; + void visitChangeTownName(ChangeTownName & pack) override; void visitChangeSpells(ChangeSpells & pack) override; void visitSetAvailableHero(SetAvailableHero & pack) override; void visitChangeObjectVisitors(ChangeObjectVisitors & pack) override; diff --git a/lib/networkPacks/NetPackVisitor.h b/lib/networkPacks/NetPackVisitor.h index eef61f439..d1221172f 100644 --- a/lib/networkPacks/NetPackVisitor.h +++ b/lib/networkPacks/NetPackVisitor.h @@ -58,6 +58,7 @@ public: virtual void visitSetCommanderProperty(SetCommanderProperty & pack) {} virtual void visitAddQuest(AddQuest & pack) {} virtual void visitChangeFormation(ChangeFormation & pack) {} + virtual void visitChangeTownName(ChangeTownName & pack) {} virtual void visitRemoveObject(RemoveObject & pack) {} virtual void visitTryMoveHero(TryMoveHero & pack) {} virtual void visitNewStructures(NewStructures & pack) {} @@ -144,6 +145,7 @@ public: virtual void visitBuyArtifact(BuyArtifact & pack) {} virtual void visitTradeOnMarketplace(TradeOnMarketplace & pack) {} virtual void visitSetFormation(SetFormation & pack) {} + virtual void visitSetTownName(SetTownName & pack) {} virtual void visitHireHero(HireHero & pack) {} virtual void visitBuildBoat(BuildBoat & pack) {} virtual void visitQueryReply(QueryReply & pack) {} diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 5d7c08477..c7df14a35 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -213,6 +213,11 @@ void ChangeFormation::visitTyped(ICPackVisitor & visitor) visitor.visitChangeFormation(*this); } +void ChangeTownName::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitChangeTownName(*this); +} + void RemoveObject::visitTyped(ICPackVisitor & visitor) { visitor.visitRemoveObject(*this); @@ -643,6 +648,11 @@ void SetFormation::visitTyped(ICPackVisitor & visitor) visitor.visitSetFormation(*this); } +void SetTownName::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetTownName(*this); +} + void HireHero::visitTyped(ICPackVisitor & visitor) { visitor.visitHireHero(*this); diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index 0a6bf1051..cd7608954 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -614,6 +614,20 @@ struct DLL_LINKAGE ChangeFormation : public CPackForClient } }; +struct DLL_LINKAGE ChangeTownName : public CPackForClient +{ + ObjectInstanceID tid; + std::string name; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h) + { + h & tid; + h & name; + } +}; + struct DLL_LINKAGE RemoveObject : public CPackForClient { RemoveObject() = default; diff --git a/lib/networkPacks/PacksForServer.h b/lib/networkPacks/PacksForServer.h index 9924d3b4b..f28d64c5f 100644 --- a/lib/networkPacks/PacksForServer.h +++ b/lib/networkPacks/PacksForServer.h @@ -609,6 +609,28 @@ struct DLL_LINKAGE SetFormation : public CPackForServer } }; +struct DLL_LINKAGE SetTownName : public CPackForServer +{ + SetTownName() = default; + ; + SetTownName(const ObjectInstanceID & TID, std::string Name) + : tid(TID) + , name(Name) + { + } + ObjectInstanceID tid; + std::string name; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h) + { + h & static_cast(*this); + h & tid; + h & name; + } +}; + struct DLL_LINKAGE HireHero : public CPackForServer { HireHero() = default; diff --git a/lib/serializer/RegisterTypes.h b/lib/serializer/RegisterTypes.h index 22a4c7508..9116a902d 100644 --- a/lib/serializer/RegisterTypes.h +++ b/lib/serializer/RegisterTypes.h @@ -291,6 +291,8 @@ void registerTypes(Serializer &s) s.template registerType(249); s.template registerType(250); s.template registerType(251); + s.template registerType(252); + s.template registerType(253); } VCMI_LIB_NAMESPACE_END diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 722c601d6..d78ace0cf 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -3219,6 +3219,23 @@ bool CGameHandler::setFormation(ObjectInstanceID hid, EArmyFormation formation) return true; } +bool CGameHandler::setTownName(ObjectInstanceID tid, std::string & name) +{ + const CGTownInstance *t = gameInfo().getTown(tid); + if (!t) + { + logGlobal->error("Town doesn't exist!"); + return false; + } + + ChangeTownName ctn; + ctn.tid = tid; + ctn.name = name; + sendAndApply(ctn); + + return true; +} + bool CGameHandler::queryReply(QueryID qid, std::optional answer, PlayerColor player) { logGlobal->trace("Player %s attempts answering query %d with answer:", player, qid); diff --git a/server/CGameHandler.h b/server/CGameHandler.h index f1661b294..e0e7c791e 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -212,6 +212,7 @@ public: bool queryReply( QueryID qid, std::optional reply, PlayerColor player ); bool buildBoat( ObjectInstanceID objid, PlayerColor player ); bool setFormation( ObjectInstanceID hid, EArmyFormation formation ); + bool setTownName( ObjectInstanceID tid, std::string & name ); bool tradeResources(const IMarket *market, ui32 amountToSell, PlayerColor player, GameResID toSell, GameResID toBuy); bool sacrificeCreatures(const IMarket * market, const CGHeroInstance * hero, const std::vector & slot, const std::vector & count); bool sendResources(ui32 val, PlayerColor player, GameResID r1, PlayerColor r2); diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 47b9c52f0..f02b9975a 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -362,6 +362,14 @@ void ApplyGhNetPackVisitor::visitSetFormation(SetFormation & pack) result = gh.setFormation(pack.hid, pack.formation); } +void ApplyGhNetPackVisitor::visitSetTownName(SetTownName & pack) +{ + gh.throwIfWrongOwner(connection, &pack, pack.tid); + gh.throwIfPlayerNotActive(connection, &pack); + + result = gh.setTownName(pack.tid, pack.name); +} + void ApplyGhNetPackVisitor::visitHireHero(HireHero & pack) { gh.throwIfWrongPlayer(connection, &pack); diff --git a/server/ServerNetPackVisitors.h b/server/ServerNetPackVisitors.h index a6ffffb63..3b971fa7d 100644 --- a/server/ServerNetPackVisitors.h +++ b/server/ServerNetPackVisitors.h @@ -58,6 +58,7 @@ public: void visitBuyArtifact(BuyArtifact & pack) override; void visitTradeOnMarketplace(TradeOnMarketplace & pack) override; void visitSetFormation(SetFormation & pack) override; + void visitSetTownName(SetTownName & pack) override; void visitHireHero(HireHero & pack) override; void visitBuildBoat(BuildBoat & pack) override; void visitQueryReply(QueryReply & pack) override; From 3e2a526140d166f63dccb4b0e6ef9d407333818d Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 5 Oct 2025 03:18:47 +0200 Subject: [PATCH 05/26] correct update --- client/ClientNetPackVisitors.h | 1 + client/NetPacksClient.cpp | 13 +++++++++++++ client/windows/CCastleInterface.cpp | 3 +++ 3 files changed, 17 insertions(+) diff --git a/client/ClientNetPackVisitors.h b/client/ClientNetPackVisitors.h index fc7714b5f..1a2813a02 100644 --- a/client/ClientNetPackVisitors.h +++ b/client/ClientNetPackVisitors.h @@ -104,6 +104,7 @@ public: void visitSetAvailableArtifacts(SetAvailableArtifacts & pack) override; void visitEntitiesChanged(EntitiesChanged & pack) override; void visitPlayerCheated(PlayerCheated & pack) override; + void visitChangeTownName(ChangeTownName & pack) override; }; class ApplyFirstClientNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 29a8558f9..5e74691f0 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -1071,3 +1071,16 @@ void ApplyClientNetPackVisitor::visitPlayerCheated(PlayerCheated & pack) if(pack.colorScheme != ColorScheme::KEEP && vstd::contains(cl.playerint, pack.player)) cl.playerint[pack.player]->setColorScheme(pack.colorScheme); } + +void ApplyClientNetPackVisitor::visitChangeTownName(ChangeTownName & pack) +{ + if(!adventureInt) + return; + + const CGTownInstance *town = gs.getTown(pack.tid); + if(town) + { + adventureInt->onTownChanged(town); + ENGINE->windows().totalRedraw(); + } +} diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 6308604ee..3832c3b0d 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1438,6 +1438,9 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst heroes = std::make_shared(town, Point(241, 387), Point(241, 483), garr, true); title = std::make_shared(Rect(83, 386, 140, 20), FONT_MEDIUM, ETextAlignment::TOPLEFT, town->getNameTranslated(), true, [this](){ std::string name = title->getText(); + std::string originalName = LIBRARY->generaltexth->translate(town->getNameTextID()); + if(name == originalName) + name = ""; // use textID again GAME->interface()->cb->setTownName(town, name); }); income = std::make_shared(195, 443, FONT_SMALL, ETextAlignment::CENTER); From 6c1748e0d6b210346307012a26d1488a56ac8cec Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:24:29 +0200 Subject: [PATCH 06/26] fix allied towns & color text input --- client/widgets/CTextInput.cpp | 11 +++++++++-- client/widgets/CTextInput.h | 1 + client/widgets/TextControls.cpp | 30 +++++++++++++++++++---------- client/widgets/TextControls.h | 2 ++ client/windows/CCastleInterface.cpp | 2 ++ 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/client/widgets/CTextInput.cpp b/client/widgets/CTextInput.cpp index 9ad7c8798..70ab3c343 100644 --- a/client/widgets/CTextInput.cpp +++ b/client/widgets/CTextInput.cpp @@ -85,10 +85,17 @@ void CTextInputWithConfirm::textInputted(const std::string & enteredText) std::string visibleText = getVisibleText() + enteredText; const auto & font = ENGINE->renderHandler().loadFont(label->font); - if(!limitToRect || font->getStringWidth(visibleText) < pos.w) + if(!limitToRect || (font->getStringWidth(visibleText) - CLabel::getDelimitersWidth(label->font, visibleText)) < pos.w) CTextInput::textInputted(enteredText); } +void CTextInputWithConfirm::deactivate() +{ + removeUsedEvents(LCLICK); + + CTextInput::deactivate(); +} + void CTextInputWithConfirm::confirm() { if(getText().empty()) @@ -268,7 +275,7 @@ void CTextInput::updateLabel() label->alignment = originalAlignment; const auto & font = ENGINE->renderHandler().loadFont(label->font); - while (font->getStringWidth(visibleText) > pos.w) + while ((font->getStringWidth(visibleText) - CLabel::getDelimitersWidth(label->font, visibleText)) > pos.w) { label->alignment = ETextAlignment::CENTERRIGHT; visibleText = visibleText.substr(TextOperations::getUnicodeCharacterSize(visibleText[0])); diff --git a/client/widgets/CTextInput.h b/client/widgets/CTextInput.h index 0ab58425d..9edbdde3d 100644 --- a/client/widgets/CTextInput.h +++ b/client/widgets/CTextInput.h @@ -128,4 +128,5 @@ public: bool receiveEvent(const Point & position, int eventType) const override; void onFocusGot() override; void textInputted(const std::string & enteredText) override; + void deactivate() override; }; diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 2cb8a4442..245e02a1c 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -182,29 +182,39 @@ std::vector CMultiLineLabel::getLines() return lines; } -void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) +const std::string delimiters = "{}"; + +int CTextContainer::getDelimitersWidth(EFonts font, std::string text) { const auto f = ENGINE->renderHandler().loadFont(font); - Point where = destRect.topLeft(); - const std::string delimiters = "{}"; - auto delimitersCount = std::count_if(what.cbegin(), what.cend(), [&delimiters](char c) + auto delimitersWidth = std::count_if(text.cbegin(), text.cend(), [](char c) { return delimiters.find(c) != std::string::npos; }); //We should count delimiters length from string to correct centering later. - delimitersCount *= f->getStringWidth(delimiters)/2; + delimitersWidth *= f->getStringWidth(delimiters)/2; std::smatch match; std::regex expr("\\{(.*?)\\|"); - std::string::const_iterator searchStart( what.cbegin() ); - while(std::regex_search(searchStart, what.cend(), match, expr)) + std::string::const_iterator searchStart( text.cbegin() ); + while(std::regex_search(searchStart, text.cend(), match, expr)) { std::string colorText = match[1].str(); if(auto c = Colors::parseColor(colorText)) - delimitersCount += f->getStringWidth(colorText + "|"); + delimitersWidth += f->getStringWidth(colorText + "|"); searchStart = match.suffix().first; } + return delimitersWidth; +} + +void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) +{ + const auto f = ENGINE->renderHandler().loadFont(font); + Point where = destRect.topLeft(); + + int delimitersWidth = getDelimitersWidth(font, what); + // input is rect in which given text should be placed // calculate proper position for top-left corner of the text @@ -212,10 +222,10 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) where.x += getBorderSize().x; if(alignment == ETextAlignment::CENTER || alignment == ETextAlignment::TOPCENTER || alignment == ETextAlignment::BOTTOMCENTER) - where.x += (destRect.w - (static_cast(f->getStringWidth(what)) - delimitersCount)) / 2; + where.x += (destRect.w - (static_cast(f->getStringWidth(what)) - delimitersWidth)) / 2; if(alignment == ETextAlignment::TOPRIGHT || alignment == ETextAlignment::BOTTOMRIGHT || alignment == ETextAlignment::CENTERRIGHT) - where.x += getBorderSize().x + destRect.w - (static_cast(f->getStringWidth(what)) - delimitersCount); + where.x += getBorderSize().x + destRect.w - (static_cast(f->getStringWidth(what)) - delimitersWidth); if(alignment == ETextAlignment::TOPLEFT || alignment == ETextAlignment::TOPCENTER || alignment == ETextAlignment::TOPRIGHT) where.y += getBorderSize().y; diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index 0c99c193b..b190279c2 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -31,6 +31,8 @@ protected: CTextContainer(ETextAlignment alignment, EFonts font, ColorRGBA color); public: + static int getDelimitersWidth(EFonts font, std::string text); + ETextAlignment alignment; EFonts font; ColorRGBA color; // default font color. Can be overridden by placing "{}" into the string diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 3832c3b0d..60eae5819 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1443,6 +1443,8 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst name = ""; // use textID again GAME->interface()->cb->setTownName(town, name); }); + if(town->tempOwner != GAME->interface()->playerID) // disable changing for allied towns + title->deactivate(); income = std::make_shared(195, 443, FONT_SMALL, ETextAlignment::CENTER); icon = std::make_shared(AnimationPath::builtin("ITPT"), 0, 0, 15, 387); From 64c44ce887ee437d498cd85056ca345147c1485b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 8 Oct 2025 01:12:57 +0200 Subject: [PATCH 07/26] template search --- Mods/vcmi/Content/config/english.json | 2 ++ Mods/vcmi/Content/config/german.json | 2 ++ client/lobby/RandomMapTab.cpp | 17 ++++++++++++++++- client/lobby/RandomMapTab.h | 2 ++ client/windows/CWindowObject.cpp | 2 +- 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Mods/vcmi/Content/config/english.json b/Mods/vcmi/Content/config/english.json index d926c7940..668898094 100644 --- a/Mods/vcmi/Content/config/english.json +++ b/Mods/vcmi/Content/config/english.json @@ -138,6 +138,8 @@ "vcmi.lobby.deleteFile" : "Do you want to delete following file?", "vcmi.lobby.deleteFolder" : "Do you want to delete following folder?", "vcmi.lobby.deleteMode" : "Switch to delete mode and back", + "vcmi.lobby.templatesSelect.hover" : "Templates", + "vcmi.lobby.templatesSelect.help" : "Search and select template", "vcmi.broadcast.failedLoadGame" : "Failed to load game", "vcmi.broadcast.command" : "Use '!help' to list available commands", diff --git a/Mods/vcmi/Content/config/german.json b/Mods/vcmi/Content/config/german.json index 6bd8535d4..f6375be29 100644 --- a/Mods/vcmi/Content/config/german.json +++ b/Mods/vcmi/Content/config/german.json @@ -138,6 +138,8 @@ "vcmi.lobby.deleteFile" : "Möchtet Ihr folgende Datei löschen?", "vcmi.lobby.deleteFolder" : "Möchtet Ihr folgenden Ordner löschen?", "vcmi.lobby.deleteMode" : "In den Löschmodus wechseln und zurück", + "vcmi.lobby.templatesSelect.hover" : "Templates", + "vcmi.lobby.templatesSelect.help" : "Suche und wähle Template aus", "vcmi.broadcast.failedLoadGame" : "Spiel konnte nicht geladen werden", "vcmi.broadcast.command" : "Benutze '!help' um alle verfügbaren Befehle aufzulisten", diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 4144bfcaa..5bb343f1d 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -43,7 +43,8 @@ #include "../../lib/serializer/JsonDeserializer.h" RandomMapTab::RandomMapTab(): - InterfaceObjectConfigurable() + InterfaceObjectConfigurable(), + templateIndex(0) { recActions = 0; mapGenOptions = std::make_shared(); @@ -164,6 +165,20 @@ RandomMapTab::RandomMapTab(): return readText(variables["randomTemplate"]); return std::string(""); }; + + w->addCallback([this]() + { + std::vector texts; + texts.push_back(readText(variables["randomTemplate"])); + for(auto & t : LIBRARY->tplh->getTemplates()) + texts.push_back(t->getName()); + + ENGINE->windows().popWindows(1); // no real dropdown... + ENGINE->windows().createAndPushWindow(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.templatesSelect.hover"), LIBRARY->generaltexth->translate("vcmi.lobby.templatesSelect.help"), [this](int index){ + widget("templateList")->setItem(index); + templateIndex = index; + }, templateIndex, std::vector>(), true); + }); } loadOptions(); diff --git a/client/lobby/RandomMapTab.h b/client/lobby/RandomMapTab.h index fa62e151d..4e199e410 100644 --- a/client/lobby/RandomMapTab.h +++ b/client/lobby/RandomMapTab.h @@ -55,6 +55,8 @@ private: std::set playerTeamsAllowed; std::set compCountAllowed; std::set compTeamsAllowed; + + int templateIndex; }; class TeamAlignmentsWidget: public InterfaceObjectConfigurable diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index 9c0c72c6b..e0665ed28 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -86,7 +86,7 @@ std::shared_ptr CWindowObject::createBg(const ImagePath & imageName, b return nullptr; auto image = std::make_shared(imageName, Point(0,0), EImageBlitMode::OPAQUE); - if(playerColored) + if(playerColored && GAME->interface()) image->setPlayerColor(GAME->interface()->playerID); return image; } From 768b233813f8cc195411234097b45d9bbef8cf3b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 9 Oct 2025 00:01:00 +0200 Subject: [PATCH 08/26] fix --- client/lobby/RandomMapTab.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 5bb343f1d..8e9f2d45c 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -141,16 +141,18 @@ RandomMapTab::RandomMapTab(): //set combo box callbacks if(auto w = widget("templateList")) { - w->onConstructItems = [](std::vector & curItems){ + auto getTemplates = [](){ auto templates = LIBRARY->tplh->getTemplates(); - boost::range::sort(templates, [](const CRmgTemplate * a, const CRmgTemplate * b){ return a->getName() < b->getName(); }); + return templates; + }; + w->onConstructItems = [getTemplates](std::vector & curItems){ curItems.push_back(nullptr); //default template - for(auto & t : templates) + for(auto & t : getTemplates()) curItems.push_back(t); }; @@ -166,14 +168,14 @@ RandomMapTab::RandomMapTab(): return std::string(""); }; - w->addCallback([this]() + w->addCallback([this, getTemplates]() // no real dropdown... - instead open dialog { std::vector texts; texts.push_back(readText(variables["randomTemplate"])); - for(auto & t : LIBRARY->tplh->getTemplates()) + for(auto & t : getTemplates()) texts.push_back(t->getName()); - ENGINE->windows().popWindows(1); // no real dropdown... + ENGINE->windows().popWindows(1); ENGINE->windows().createAndPushWindow(texts, nullptr, LIBRARY->generaltexth->translate("vcmi.lobby.templatesSelect.hover"), LIBRARY->generaltexth->translate("vcmi.lobby.templatesSelect.help"), [this](int index){ widget("templateList")->setItem(index); templateIndex = index; From 5a64fbd89c12109d7f79d974f8b32f1310c6ab2a Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 12 Oct 2025 16:26:44 +0200 Subject: [PATCH 09/26] show invisible --- client/mapView/IMapRendererContext.h | 1 + client/mapView/MapRenderer.cpp | 25 +++++++++++++++++++++++-- client/mapView/MapRenderer.h | 7 ++++++- client/mapView/MapRendererContext.cpp | 10 ++++++++++ client/mapView/MapRendererContext.h | 3 +++ client/mapView/MapViewController.cpp | 1 + docs/players/Cheat_Codes.md | 3 ++- lib/mapObjects/ObjectTemplate.cpp | 2 +- 8 files changed, 47 insertions(+), 5 deletions(-) diff --git a/client/mapView/IMapRendererContext.h b/client/mapView/IMapRendererContext.h index c1ff9bfc6..52ee6af51 100644 --- a/client/mapView/IMapRendererContext.h +++ b/client/mapView/IMapRendererContext.h @@ -99,6 +99,7 @@ public: virtual bool showGrid() const = 0; virtual bool showVisitable() const = 0; virtual bool showBlocked() const = 0; + virtual bool showInvisible() const = 0; /// if true, spell range for teleport / scuttle boat will be visible virtual bool showSpellRange(const int3 & position) const = 0; diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index cddaadec7..d85b5ae53 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -14,6 +14,9 @@ #include "IMapRendererContext.h" #include "mapHandler.h" +#include "../CServerHandler.h" +#include "../GameInstance.h" +#include "../Client.h" #include "../GameEngine.h" #include "../render/CAnimation.h" #include "../render/Canvas.h" @@ -23,12 +26,14 @@ #include "../render/Graphics.h" #include "../../lib/CConfigHandler.h" +#include "../../lib/gameState/CGameState.h" #include "../../lib/RiverHandler.h" #include "../../lib/RoadHandler.h" #include "../../lib/TerrainHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/mapObjects/ObjectTemplate.h" +#include "../../lib/mapping/CMap.h" #include "../../lib/mapping/TerrainTile.h" #include "../../lib/pathfinder/CGPathNode.h" @@ -592,8 +597,15 @@ MapRendererOverlay::MapRendererOverlay() , imageBlocked(ENGINE->renderHandler().loadImage(ImagePath::builtin("debug/blocked"), EImageBlitMode::COLORKEY)) , imageVisitable(ENGINE->renderHandler().loadImage(ImagePath::builtin("debug/visitable"), EImageBlitMode::COLORKEY)) , imageSpellRange(ENGINE->renderHandler().loadImage(ImagePath::builtin("debug/spellRange"), EImageBlitMode::COLORKEY)) + , imageEvent(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("AVZevnt0"), EImageBlitMode::COLORKEY)->getImage(0)) + , imageGrail(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("AVZgrail"), EImageBlitMode::COLORKEY)->getImage(0)) + , grailPos(GAME->server().client->gameState().getMap().grailPos) { - + int humanPlayer = 0; + for (const auto & pi : GAME->server().client->gameState().getStartInfo()->playerInfos) + if(pi.second.isControlledByHuman()) + humanPlayer++; + isSinglePlayer = humanPlayer < 2; } void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) @@ -601,7 +613,7 @@ void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & targ if(context.showGrid()) target.draw(imageGrid, Point(0,0)); - if(context.showVisitable() || context.showBlocked()) + if(context.showVisitable() || context.showBlocked() || context.showInvisible()) { bool blocking = false; bool visitable = false; @@ -610,6 +622,12 @@ void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & targ { const auto * object = context.getObject(objectID); + if(object->ID == Obj::EVENT && context.showInvisible() && isSinglePlayer) + target.draw(imageEvent, Point(0,0)); + + if(grailPos == coordinates && context.showInvisible() && isSinglePlayer) + target.draw(imageGrail, Point(0,0)); + if(context.objectTransparency(objectID, coordinates) > 0 && !context.isActiveHero(object)) { visitable |= object->visitableAt(coordinates); @@ -643,6 +661,9 @@ uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 & if (context.showSpellRange(coordinates)) result += 8; + if (context.showInvisible()) + result += 16; + return result; } diff --git a/client/mapView/MapRenderer.h b/client/mapView/MapRenderer.h index e72160a1c..c6e014fc1 100644 --- a/client/mapView/MapRenderer.h +++ b/client/mapView/MapRenderer.h @@ -9,11 +9,11 @@ */ #pragma once +#include "../../lib/int3.h" #include "../../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN -class int3; class ObjectInstanceID; class CGObjectInstance; @@ -139,6 +139,11 @@ class MapRendererOverlay std::shared_ptr imageVisitable; std::shared_ptr imageBlocked; std::shared_ptr imageSpellRange; + std::shared_ptr imageEvent; + std::shared_ptr imageGrail; + + bool isSinglePlayer; + int3 grailPos; public: MapRendererOverlay(); diff --git a/client/mapView/MapRendererContext.cpp b/client/mapView/MapRendererContext.cpp index 5da7cd5c6..7c248a22d 100644 --- a/client/mapView/MapRendererContext.cpp +++ b/client/mapView/MapRendererContext.cpp @@ -232,6 +232,11 @@ bool MapRendererBaseContext::showBlocked() const return false; } +bool MapRendererBaseContext::showInvisible() const +{ + return false; +} + bool MapRendererBaseContext::showSpellRange(const int3 & position) const { return false; @@ -362,6 +367,11 @@ bool MapRendererAdventureContext::showBlocked() const return settingShowBlocked; } +bool MapRendererAdventureContext::showInvisible() const +{ + return settingShowInvisible; +} + bool MapRendererAdventureContext::showTextOverlay() const { return settingTextOverlay; diff --git a/client/mapView/MapRendererContext.h b/client/mapView/MapRendererContext.h index ece349d22..f9054ad42 100644 --- a/client/mapView/MapRendererContext.h +++ b/client/mapView/MapRendererContext.h @@ -62,6 +62,7 @@ public: bool showGrid() const override; bool showVisitable() const override; bool showBlocked() const override; + bool showInvisible() const override; bool showSpellRange(const int3 & position) const override; }; @@ -72,6 +73,7 @@ public: bool settingShowGrid = false; bool settingShowVisitable = false; bool settingShowBlocked = false; + bool settingShowInvisible = false; bool settingTextOverlay = false; bool settingsAdventureObjectAnimation = true; bool settingsAdventureTerrainAnimation = true; @@ -88,6 +90,7 @@ public: bool showGrid() const override; bool showVisitable() const override; bool showBlocked() const override; + bool showInvisible() const override; bool showTextOverlay() const override; bool showSpellRange(const int3 & position) const override; diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index b0ece0265..c5ac99e09 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -233,6 +233,7 @@ void MapViewController::updateState() adventureContext->settingShowGrid = settings["gameTweaks"]["showGrid"].Bool(); adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool(); adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool(); + adventureContext->settingShowInvisible = settings["session"]["showInvisible"].Bool(); adventureContext->settingTextOverlay = (ENGINE->isKeyboardAltDown() || ENGINE->input().getNumTouchFingers() == 2) && settings["general"]["enableOverlay"].Bool(); } } diff --git a/docs/players/Cheat_Codes.md b/docs/players/Cheat_Codes.md index ba65eae71..5b33792f9 100644 --- a/docs/players/Cheat_Codes.md +++ b/docs/players/Cheat_Codes.md @@ -168,7 +168,8 @@ Below a list of supported commands, with their arguments wrapped in `<>` - `headless` - run without GUI, implies `onlyAI` is set - `showGrid` - display a square grid overlay on top of adventure map - `showBlocked` - show blocked tiles on map -- `showVisitable` - show visitable tiles on map +- `showVisitable` - show visitable tiles on map +- `showInvisible` - show invisible tiles (events, grail) on map (only singleplayer) - `hideSystemMessages` - suppress server messages in chat - `antilag` - toggles network lag compensation in multiplayer on or off diff --git a/lib/mapObjects/ObjectTemplate.cpp b/lib/mapObjects/ObjectTemplate.cpp index a14481420..bf15d1ff3 100644 --- a/lib/mapObjects/ObjectTemplate.cpp +++ b/lib/mapObjects/ObjectTemplate.cpp @@ -66,7 +66,7 @@ void ObjectTemplate::afterLoadFixup() if(id == Obj::EVENT) { setSize(1,1); - usedTiles[0][0] = VISITABLE; + usedTiles[0][0] = VISITABLE | VISIBLE; visitDir = 0xFF; } } From 1fbcdb4884d61438607f074061274759f50491b2 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 12 Oct 2025 16:48:54 +0200 Subject: [PATCH 10/26] fix md --- docs/players/Cheat_Codes.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/players/Cheat_Codes.md b/docs/players/Cheat_Codes.md index 5b33792f9..302b1ac8c 100644 --- a/docs/players/Cheat_Codes.md +++ b/docs/players/Cheat_Codes.md @@ -162,15 +162,15 @@ Below a list of supported commands, with their arguments wrapped in `<>` #### Settings -- `set ` - sets special temporary settings that reset on game quit. Below some of the most notable commands: -- `autoskip` - identical to `autoskip` option -- `onlyAI` - run without human player, all players will be *default AI* -- `headless` - run without GUI, implies `onlyAI` is set -- `showGrid` - display a square grid overlay on top of adventure map -- `showBlocked` - show blocked tiles on map -- `showVisitable` - show visitable tiles on map +- `set ` - sets special temporary settings that reset on game quit. Below some of the most notable commands: +- `autoskip` - identical to `autoskip` option +- `onlyAI` - run without human player, all players will be *default AI* +- `headless` - run without GUI, implies `onlyAI` is set +- `showGrid` - display a square grid overlay on top of adventure map +- `showBlocked` - show blocked tiles on map +- `showVisitable` - show visitable tiles on map - `showInvisible` - show invisible tiles (events, grail) on map (only singleplayer) -- `hideSystemMessages` - suppress server messages in chat +- `hideSystemMessages` - suppress server messages in chat - `antilag` - toggles network lag compensation in multiplayer on or off #### Developer Commands From f2755d608956607dc3257013d96d8e48372536a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Zaremba?= Date: Mon, 20 Oct 2025 10:01:09 +0200 Subject: [PATCH 11/26] Fix crash on calling translate command --- client/ClientCommandManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index a28c55f63..e1c361a05 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -239,7 +239,7 @@ void ClientCommandManager::handleTranslateMapsCommand() try { // load and drop loaded map - we only need loader to run over all maps - loadedMaps.push_back(mapService.loadMap(mapName, nullptr)); + loadedMaps.push_back(mapService.loadMap(mapName, GAME->interface()->cb.get())); } catch(std::exception & e) { @@ -260,7 +260,7 @@ void ClientCommandManager::handleTranslateMapsCommand() { loadedCampaigns.push_back(CampaignHandler::getCampaign(campaignName.getName())); for (auto const & part : loadedCampaigns.back()->allScenarios()) - loadedCampaigns.back()->getMap(part, nullptr); + loadedCampaigns.back()->getMap(part, GAME->interface()->cb.get()); } catch(std::exception & e) { From 7a7ba749079518bdc019cdc2409857965220120f Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:30:32 +0200 Subject: [PATCH 12/26] review --- client/mapView/MapRenderer.cpp | 12 ++++-------- client/mapView/MapRenderer.h | 1 - docs/players/Cheat_Codes.md | 4 +++- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index d85b5ae53..3fa453ce3 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -14,6 +14,7 @@ #include "IMapRendererContext.h" #include "mapHandler.h" +#include "../CPlayerInterface.h" #include "../CServerHandler.h" #include "../GameInstance.h" #include "../Client.h" @@ -601,11 +602,6 @@ MapRendererOverlay::MapRendererOverlay() , imageGrail(ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("AVZgrail"), EImageBlitMode::COLORKEY)->getImage(0)) , grailPos(GAME->server().client->gameState().getMap().grailPos) { - int humanPlayer = 0; - for (const auto & pi : GAME->server().client->gameState().getStartInfo()->playerInfos) - if(pi.second.isControlledByHuman()) - humanPlayer++; - isSinglePlayer = humanPlayer < 2; } void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) @@ -613,7 +609,7 @@ void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & targ if(context.showGrid()) target.draw(imageGrid, Point(0,0)); - if(context.showVisitable() || context.showBlocked() || context.showInvisible()) + if(GAME->interface()->cb->getStartInfo()->extraOptionsInfo.cheatsAllowed && (context.showVisitable() || context.showBlocked() || context.showInvisible())) { bool blocking = false; bool visitable = false; @@ -622,10 +618,10 @@ void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & targ { const auto * object = context.getObject(objectID); - if(object->ID == Obj::EVENT && context.showInvisible() && isSinglePlayer) + if(object->ID == Obj::EVENT && context.showInvisible()) target.draw(imageEvent, Point(0,0)); - if(grailPos == coordinates && context.showInvisible() && isSinglePlayer) + if(grailPos == coordinates && context.showInvisible()) target.draw(imageGrail, Point(0,0)); if(context.objectTransparency(objectID, coordinates) > 0 && !context.isActiveHero(object)) diff --git a/client/mapView/MapRenderer.h b/client/mapView/MapRenderer.h index c6e014fc1..880524c48 100644 --- a/client/mapView/MapRenderer.h +++ b/client/mapView/MapRenderer.h @@ -142,7 +142,6 @@ class MapRendererOverlay std::shared_ptr imageEvent; std::shared_ptr imageGrail; - bool isSinglePlayer; int3 grailPos; public: MapRendererOverlay(); diff --git a/docs/players/Cheat_Codes.md b/docs/players/Cheat_Codes.md index 302b1ac8c..42dd2887b 100644 --- a/docs/players/Cheat_Codes.md +++ b/docs/players/Cheat_Codes.md @@ -169,10 +169,12 @@ Below a list of supported commands, with their arguments wrapped in `<>` - `showGrid` - display a square grid overlay on top of adventure map - `showBlocked` - show blocked tiles on map - `showVisitable` - show visitable tiles on map -- `showInvisible` - show invisible tiles (events, grail) on map (only singleplayer) +- `showInvisible` - show invisible tiles (events, grail) on map - `hideSystemMessages` - suppress server messages in chat - `antilag` - toggles network lag compensation in multiplayer on or off +`showBlocked`, `showVisitable` and `showInvisible` only works if cheats are enabled. + #### Developer Commands - `crash` - force a game crash. It is sometimes useful to generate memory dump file in certain situations, for example game freeze From 2bee582cc7be46e4f724537253ad0ec7ce477391 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:40:46 +0200 Subject: [PATCH 13/26] fix --- client/mapView/MapRenderer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index 3fa453ce3..bcbd7cf5d 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -27,6 +27,7 @@ #include "../render/Graphics.h" #include "../../lib/CConfigHandler.h" +#include "../../lib/callback/CCallback.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/RiverHandler.h" #include "../../lib/RoadHandler.h" From 32ad963d1918dbf79574d401cb04cce2f790756d Mon Sep 17 00:00:00 2001 From: George King <98261225+GeorgeK1ng@users.noreply.github.com> Date: Wed, 22 Oct 2025 10:59:48 +0200 Subject: [PATCH 14/26] Correct shortcuts for beta / develop branches --- CI/wininstaller/installer.iss | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/CI/wininstaller/installer.iss b/CI/wininstaller/installer.iss index f74af9881..1b9fba702 100644 --- a/CI/wininstaller/installer.iss +++ b/CI/wininstaller/installer.iss @@ -166,12 +166,12 @@ Source: "{#UCRTFilesPath}\{#InstallerArch}\*"; DestDir: "{app}"; Flags: ignoreve [Icons] -Name: "{group}\{cm:ShortcutLauncher}"; Filename: "{app}\VCMI_launcher.exe"; Comment: "{cm:ShortcutLauncherComment}"; Tasks: startmenu -Name: "{group}\{cm:ShortcutMapEditor}"; Filename: "{app}\VCMI_mapeditor.exe"; Comment: "{cm:ShortcutMapEditorComment}"; Tasks: startmenu +Name: "{group}\{cm:ShortcutLauncher}{code:GetBranchSuffix}"; Filename: "{app}\VCMI_launcher.exe"; Comment: "{cm:ShortcutLauncherComment}{code:GetBranchSuffix}"; Tasks: startmenu +Name: "{group}\{cm:ShortcutMapEditor}{code:GetBranchSuffix}"; Filename: "{app}\VCMI_mapeditor.exe"; Comment: "{cm:ShortcutMapEditorComment}{code:GetBranchSuffix}"; Tasks: startmenu Name: "{group}\{cm:ShortcutWebPage}"; Filename: "{#VCMIHome}"; Comment: "{cm:ShortcutWebPageComment}"; Tasks: startmenu Name: "{group}\{cm:ShortcutDiscord}"; Filename: "{#VCMIContact}"; Comment: "{cm:ShortcutDiscordComment}"; Tasks: startmenu -Name: "{code:GetUserDesktopFolder}\{cm:ShortcutLauncher}"; Filename: "{app}\VCMI_launcher.exe"; Comment: "{cm:ShortcutLauncherComment}"; Tasks: desktop +Name: "{code:GetUserDesktopFolder}\{cm:ShortcutLauncher}{code:GetBranchSuffix}"; Filename: "{app}\VCMI_launcher.exe"; Comment: "{cm:ShortcutLauncherComment}{code:GetBranchSuffix}"; Tasks: desktop [Tasks] @@ -371,6 +371,22 @@ begin end; +function GetBranchSuffix(Param: string): string; +var + Branch: string; +begin + Branch := UpperCase(ExpandConstant('{#VCMIFolder}')); + + if Pos('(BRANCH BETA)', Branch) > 0 then + Result := ' (Beta)' + else + if Pos('(BRANCH DEVELOP)', Branch) > 0 then + Result := ' (Develop)' + else + Result := ''; +end; + + function GetCommonProgramFilesDir: String; begin if IsARM64 then From 3bec32ac140c3c278b08a141937113fab231e09f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 24 Oct 2025 11:13:34 +0300 Subject: [PATCH 15/26] Try to fix tests not running on some presets --- CMakePresets.json | 34 +++++++++++++++++----------------- test/bonus/BonusSystemTest.cpp | 2 ++ 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index c160e6ccc..d1b77b00b 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -518,35 +518,35 @@ } }, { - "name": "linux-clang-release", - "configurePreset": "linux-clang-release", + "name": "linux-clang-debug", + "configurePreset": "linux-clang-debug", "inherits": "default-release" }, + { + "name": "linux-clang-release", + "configurePreset": "linux-clang-release", + "inherits": "default-release" + }, + { + "name": "linux-clang-test", + "configurePreset": "linux-clang-test", + "inherits": "default-release" + }, + { + "name": "linux-gcc-debug", + "configurePreset": "linux-gcc-debug", + "inherits": "default-release" + }, { "name": "linux-gcc-release", "configurePreset": "linux-gcc-release", "inherits": "default-release" }, - { - "name": "linux-clang-debug", - "configurePreset": "linux-clang-test", - "inherits": "default-release" - }, - { - "name": "linux-gcc-debug", - "configurePreset": "linux-gcc-test", - "inherits": "default-release" - }, { "name": "linux-gcc-test", "configurePreset": "linux-gcc-test", "inherits": "default-release" }, - { - "name": "linux-clang-test", - "configurePreset": "linux-clang-test", - "inherits": "default-release" - }, { "name": "macos-xcode-release", "configurePreset": "macos-xcode-release", diff --git a/test/bonus/BonusSystemTest.cpp b/test/bonus/BonusSystemTest.cpp index ed327fffb..b6d19cd3b 100644 --- a/test/bonus/BonusSystemTest.cpp +++ b/test/bonus/BonusSystemTest.cpp @@ -189,6 +189,8 @@ TEST_F(BonusSystemTest, battlewidePropagationToAll) EXPECT_TRUE(heroAine.hasBonusOfType(BonusType::BLOCK_ALL_MAGIC)); EXPECT_TRUE(heroBron.hasBonusOfType(BonusType::BLOCK_ALL_MAGIC)); + + heroAine.detachFromSource(orb); } TEST_F(BonusSystemTest, battlewidePropagationToEnemies) From 90930d90f8fbb8661afd8f9d692d7a9ab85e5370 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 24 Oct 2025 11:32:57 +0300 Subject: [PATCH 16/26] Unify CMakePresets.json formatting - always use 4 spaces --- CMakePresets.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index d1b77b00b..489613626 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -462,19 +462,19 @@ "inherits": "default-release" }, { - "name": "windows-msvc-ninja-release", - "configurePreset": "windows-msvc-ninja-release", - "inherits": "default-release" + "name": "windows-msvc-ninja-release", + "configurePreset": "windows-msvc-ninja-release", + "inherits": "default-release" }, { - "name": "windows-msvc-ninja-release-x86", - "configurePreset": "windows-msvc-ninja-release-x86", - "inherits": "default-release" + "name": "windows-msvc-ninja-release-x86", + "configurePreset": "windows-msvc-ninja-release-x86", + "inherits": "default-release" }, { - "name": "windows-msvc-ninja-release-arm64", - "configurePreset": "windows-msvc-ninja-release-arm64", - "inherits": "default-release" + "name": "windows-msvc-ninja-release-arm64", + "configurePreset": "windows-msvc-ninja-release-arm64", + "inherits": "default-release" }, { "name": "ios-release-conan", @@ -518,9 +518,9 @@ } }, { - "name": "linux-clang-debug", - "configurePreset": "linux-clang-debug", - "inherits": "default-release" + "name": "linux-clang-debug", + "configurePreset": "linux-clang-debug", + "inherits": "default-release" }, { "name": "linux-clang-release", @@ -528,9 +528,9 @@ "inherits": "default-release" }, { - "name": "linux-clang-test", - "configurePreset": "linux-clang-test", - "inherits": "default-release" + "name": "linux-clang-test", + "configurePreset": "linux-clang-test", + "inherits": "default-release" }, { "name": "linux-gcc-debug", @@ -543,9 +543,9 @@ "inherits": "default-release" }, { - "name": "linux-gcc-test", - "configurePreset": "linux-gcc-test", - "inherits": "default-release" + "name": "linux-gcc-test", + "configurePreset": "linux-gcc-test", + "inherits": "default-release" }, { "name": "macos-xcode-release", From 7b6d9bd404c26566acb6ab17f3d343f205b0b3b0 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 24 Oct 2025 11:47:28 +0300 Subject: [PATCH 17/26] Try to fix failing tests in some configurations --- test/bonus/BonusSystemTest.cpp | 2 ++ test/events/ApplyDamageTest.cpp | 10 +++++--- test/events/EventBusTest.cpp | 44 +++++++++++++++++---------------- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/test/bonus/BonusSystemTest.cpp b/test/bonus/BonusSystemTest.cpp index b6d19cd3b..8468763cf 100644 --- a/test/bonus/BonusSystemTest.cpp +++ b/test/bonus/BonusSystemTest.cpp @@ -299,6 +299,8 @@ TEST_F(BonusSystemTest, legionPieces) heroAine.detachFrom(townAndVisitor); EXPECT_EQ(town.valOfBonuses(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(3)), 0); + + heroAine.detachFromSource(legion); } } diff --git a/test/events/ApplyDamageTest.cpp b/test/events/ApplyDamageTest.cpp index b396ccd44..1773133ee 100644 --- a/test/events/ApplyDamageTest.cpp +++ b/test/events/ApplyDamageTest.cpp @@ -22,9 +22,11 @@ namespace test using namespace ::testing; using namespace ::events; -class ListenerMock +class ApplyDamageListenerMock { public: + virtual ~ApplyDamageListenerMock() = default; + MOCK_METHOD1(beforeEvent, void(ApplyDamage &)); MOCK_METHOD1(afterEvent, void(const ApplyDamage &)); }; @@ -33,7 +35,7 @@ class ApplyDamageTest : public Test { public: EventBus eventBus; - ListenerMock listener; + ApplyDamageListenerMock listener; StrictMock environmentMock; std::shared_ptr> targetMock; @@ -47,8 +49,8 @@ protected: //this should be the only subscription test for events, just in case cross-binary subscription breaks TEST_F(ApplyDamageTest, Subscription) { - auto subscription1 = eventBus.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener, _1)); - auto subscription2 = eventBus.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener, _1)); + auto subscription1 = eventBus.subscribeBefore(std::bind(&ApplyDamageListenerMock::beforeEvent, &listener, _1)); + auto subscription2 = eventBus.subscribeAfter(std::bind(&ApplyDamageListenerMock::afterEvent, &listener, _1)); EXPECT_CALL(listener, beforeEvent(_)).Times(1); EXPECT_CALL(listener, afterEvent(_)).Times(1); diff --git a/test/events/EventBusTest.cpp b/test/events/EventBusTest.cpp index 150731960..f8c225be1 100644 --- a/test/events/EventBusTest.cpp +++ b/test/events/EventBusTest.cpp @@ -35,9 +35,11 @@ public: friend class SubscriptionRegistry; }; -class ListenerMock +class EventBusListenerMock { public: + virtual ~EventBusListenerMock() = default; + MOCK_METHOD1(beforeEvent, void(EventExample &)); MOCK_METHOD1(onEvent, void(EventExample &)); MOCK_METHOD1(afterEvent, void(const EventExample &)); @@ -51,9 +53,9 @@ public: EventBus subject1; EventBus subject2; - StrictMock listener; - StrictMock listener1; - StrictMock listener2; + StrictMock listener; + StrictMock listener1; + StrictMock listener2; }; TEST_F(EventBusTest, ExecuteNoListeners) @@ -67,15 +69,15 @@ TEST_F(EventBusTest, ExecuteNoListenersWithHandler) EXPECT_CALL(event1, isEnabled()).WillRepeatedly(Return(true)); EXPECT_CALL(listener, onEvent(Ref(event1))).Times(1); - subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1)); + subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1)); } TEST_F(EventBusTest, ExecuteIgnoredSubscription) { EXPECT_CALL(event1, isEnabled()).WillRepeatedly(Return(true)); - subject1.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener, _1)); - subject1.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener, _1)); + subject1.subscribeBefore(std::bind(&EventBusListenerMock::beforeEvent, &listener, _1)); + subject1.subscribeAfter(std::bind(&EventBusListenerMock::afterEvent, &listener, _1)); EXPECT_CALL(listener, beforeEvent(_)).Times(0); EXPECT_CALL(listener, afterEvent(_)).Times(0); @@ -87,10 +89,10 @@ TEST_F(EventBusTest, ExecuteSequence) { EXPECT_CALL(event1, isEnabled()).WillRepeatedly(Return(true)); - auto subscription1 = subject1.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener1, _1)); - auto subscription2 = subject1.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener1, _1)); - auto subscription3 = subject1.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener2, _1)); - auto subscription4 = subject1.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener2, _1)); + auto subscription1 = subject1.subscribeBefore(std::bind(&EventBusListenerMock::beforeEvent, &listener1, _1)); + auto subscription2 = subject1.subscribeAfter(std::bind(&EventBusListenerMock::afterEvent, &listener1, _1)); + auto subscription3 = subject1.subscribeBefore(std::bind(&EventBusListenerMock::beforeEvent, &listener2, _1)); + auto subscription4 = subject1.subscribeAfter(std::bind(&EventBusListenerMock::afterEvent, &listener2, _1)); { InSequence sequence; @@ -101,17 +103,17 @@ TEST_F(EventBusTest, ExecuteSequence) EXPECT_CALL(listener2, afterEvent(Ref(event1))).Times(1); } - subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1)); + subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1)); } TEST_F(EventBusTest, BusesAreIndependent) { EXPECT_CALL(event1, isEnabled()).WillRepeatedly(Return(true)); - auto subscription1 = subject1.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener1, _1)); - auto subscription2 = subject1.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener1, _1)); - auto subscription3 = subject2.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener2, _1)); - auto subscription4 = subject2.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener2, _1)); + auto subscription1 = subject1.subscribeBefore(std::bind(&EventBusListenerMock::beforeEvent, &listener1, _1)); + auto subscription2 = subject1.subscribeAfter(std::bind(&EventBusListenerMock::afterEvent, &listener1, _1)); + auto subscription3 = subject2.subscribeBefore(std::bind(&EventBusListenerMock::beforeEvent, &listener2, _1)); + auto subscription4 = subject2.subscribeAfter(std::bind(&EventBusListenerMock::afterEvent, &listener2, _1)); EXPECT_CALL(listener1, beforeEvent(_)).Times(1); EXPECT_CALL(listener2, beforeEvent(_)).Times(0); @@ -125,7 +127,7 @@ TEST_F(EventBusTest, DisabledTestDontExecute) { EXPECT_CALL(event1, isEnabled()).Times(AtLeast(1)).WillRepeatedly(Return(false)); EXPECT_CALL(listener, onEvent(Ref(event1))).Times(0); - subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1)); + subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1)); } TEST_F(EventBusTest, DisabledTestDontExecutePostHandler) @@ -134,10 +136,10 @@ TEST_F(EventBusTest, DisabledTestDontExecutePostHandler) EXPECT_CALL(listener, onEvent(Ref(event1))).WillRepeatedly(Return()); EXPECT_CALL(listener1, afterEvent(Ref(event1))).Times(0); - auto subscription1 = subject1.subscribeAfter(std::bind(&ListenerMock::afterEvent, &listener1, _1)); + auto subscription1 = subject1.subscribeAfter(std::bind(&EventBusListenerMock::afterEvent, &listener1, _1)); - subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1)); + subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1)); } TEST_F(EventBusTest, DisabledTestExecutePreHandler) @@ -146,9 +148,9 @@ TEST_F(EventBusTest, DisabledTestExecutePreHandler) EXPECT_CALL(listener, onEvent(Ref(event1))).WillRepeatedly(Return()); EXPECT_CALL(listener1, beforeEvent(Ref(event1))).Times(1); - auto subscription1 = subject1.subscribeBefore(std::bind(&ListenerMock::beforeEvent, &listener1, _1)); + auto subscription1 = subject1.subscribeBefore(std::bind(&EventBusListenerMock::beforeEvent, &listener1, _1)); - subject1.executeEvent(event1, std::bind(&ListenerMock::onEvent, &listener, _1)); + subject1.executeEvent(event1, std::bind(&EventBusListenerMock::onEvent, &listener, _1)); } From cf5133f2858c93e772dc61e568398e826779e597 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 24 Oct 2025 17:55:47 +0300 Subject: [PATCH 18/26] Add missing hero specialty fields to docs These properties are already supported in 1.7, but undocumented --- docs/modders/Entities_Format/Hero_Type_Format.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/modders/Entities_Format/Hero_Type_Format.md b/docs/modders/Entities_Format/Hero_Type_Format.md index c8dc305f9..e96dd93b6 100644 --- a/docs/modders/Entities_Format/Hero_Type_Format.md +++ b/docs/modders/Entities_Format/Hero_Type_Format.md @@ -130,10 +130,21 @@ In order to make functional hero you also need: "anotherOne" : {Bonus Format} }, // Shortcut for defining creature specialty, using standard H3 rules + // Can be combined with bonuses-based specialty if desired "creature" : "griffin", // Shortcut for defining specialty in secondary skill, using standard H3 rules - "secondary" : "offence" + // Can be combined with bonuses-based specialty if desired + "secondary" : "offence", + + // Optional, only applicable to creature specialties + // Overrides creature level to specific value for purposes of computing growth of h3-like creature specialty + "creatureLevel" : 5 + + // Optional, only applicable to creature and secondary skill specialties + // Overrides default (5% for vanilla H3 specialties) growth of specialties per level to a specified value + // Default value can be modified globally using specialtySecondarySkillGrowth and specialtyCreatureGrowth game settings + "stepSize" : 5 } } ``` From 9a21b73a81befafae1b16bb0e5f9ea2c29869205 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 04:16:14 +0000 Subject: [PATCH 19/26] Bump actions/download-artifact from 5 to 6 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/github.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 214987da1..e8629dbb2 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -615,7 +615,7 @@ jobs: PULL_REQUEST: ${{ github.event.pull_request.number }} - name: Download Artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} path: ${{github.workspace}}/artifact @@ -720,7 +720,7 @@ jobs: - name: Download all partial JSON artifacts continue-on-error: true - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: pattern: partial-json-* merge-multiple: true From ac5e3fc456939f640ee729eb53e788e6f6d4014e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 04:17:11 +0000 Subject: [PATCH 20/26] Bump actions/upload-artifact from 4 to 5 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/aab-from-build.yml | 2 +- .github/workflows/github.yml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/aab-from-build.yml b/.github/workflows/aab-from-build.yml index 57ce6b12a..6392388ed 100644 --- a/.github/workflows/aab-from-build.yml +++ b/.github/workflows/aab-from-build.yml @@ -42,7 +42,7 @@ jobs: echo "ANDROID_AAB_PATH=$ANDROID_AAB_PATH" >> $GITHUB_ENV - name: Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: aab compression-level: 0 diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 214987da1..966689057 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -332,7 +332,7 @@ jobs: - name: Upload Artifact id: upload_artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} compression-level: 9 @@ -342,7 +342,7 @@ jobs: - name: Upload AAB Artifact id: upload_aab if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - aab compression-level: 9 @@ -352,7 +352,7 @@ jobs: - name: Upload debug symbols id: upload_symbols if: ${{ startsWith(matrix.platform, 'msvc') }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols compression-level: 9 @@ -381,7 +381,7 @@ jobs: python3 CI/emit_partial.py - name: Upload partial JSON with build informations - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: partial-json-${{ matrix.platform }} path: .summary/${{ matrix.platform }}.json @@ -412,7 +412,7 @@ jobs: - name: Upload source code archive id: upload_source - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} compression-level: 9 @@ -428,7 +428,7 @@ jobs: JSON - name: Upload partial JSON with source informations - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: partial-json-source path: .summary/source.json @@ -645,7 +645,7 @@ jobs: - name: Upload VCMI Installer Artifacts id: upload_installer - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - installer compression-level: 9 @@ -673,7 +673,7 @@ jobs: JSON - name: Upload partial JSON with installer informations - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: partial-json-${{ matrix.platform }}-installer path: .summary/installer-${{ matrix.platform }}.json From 304ef80d0b82799b463f529590ee67208876afbb Mon Sep 17 00:00:00 2001 From: Opuszek Date: Mon, 27 Oct 2025 12:14:01 +0100 Subject: [PATCH 21/26] Fixes a bug that changes rives directions when terrain type is changed in mapeditor --- lib/mapping/CMapOperation.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mapping/CMapOperation.cpp b/lib/mapping/CMapOperation.cpp index 92d985bd6..2cf46f200 100644 --- a/lib/mapping/CMapOperation.cpp +++ b/lib/mapping/CMapOperation.cpp @@ -311,7 +311,7 @@ void CDrawTerrainOperation::updateTerrainViews() if(!pattern.diffImages) { tile.terView = gen->nextInt(mapping.first, mapping.second); - tile.extTileFlags = valRslt.flip; + tile.extTileFlags = (tile.extTileFlags & 0b11111100) | valRslt.flip; } else { From 483e170f6b8f0340c3de7a0cc2afbd62280a916e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Oct 2025 19:33:48 +0200 Subject: [PATCH 22/26] Fix text trimming in object window - Do not attempt to preserve object count in string when string does not contains object count - Correctly look up last bracket, to correctly handle object names that contain brackets --- client/windows/GUIClasses.cpp | 24 ++++++++++++++++-------- client/windows/GUIClasses.h | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 1d0f4de49..6c6275b0e 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -1596,7 +1596,7 @@ CObjectListWindow::CObjectListWindow(const std::vector & _items, std::share for(int id : _items) { std::string objectName = GAME->interface()->cb->getObjInstance(ObjectInstanceID(id))->getObjectName(); - trimTextIfTooWide(objectName); + trimTextIfTooWide(objectName, false); items.emplace_back(id, objectName); } itemsVisible = items; @@ -1620,7 +1620,7 @@ CObjectListWindow::CObjectListWindow(const std::vector & _items, st for(size_t i = 0; i < _items.size(); i++) { std::string objectName = _items[i]; - trimTextIfTooWide(objectName); + trimTextIfTooWide(objectName, true); items.emplace_back(static_cast(i), objectName); } itemsVisible = items; @@ -1664,16 +1664,24 @@ void CObjectListWindow::init(std::shared_ptr titleWidget_, std::stri searchBox->setCallback(std::bind(&CObjectListWindow::itemsSearchCallback, this, std::placeholders::_1)); } -void CObjectListWindow::trimTextIfTooWide(std::string & text) const +void CObjectListWindow::trimTextIfTooWide(std::string & text, bool preserveCountSuffix) const { + std::string suffix = "..."; int maxWidth = pos.w - 60; // 60 px for scrollbar and borders - auto posBrace = text.find('('); - auto posClosing = text.find(')'); - std::string objCount = text.substr(posBrace, posClosing - posBrace) + ')'; + if(text[0] == '{') - objCount = '}' + objCount; + suffix += "}"; + + if (preserveCountSuffix) + { + auto posBrace = text.find_last_of("("); + auto posClosing = text.find_last_of(")"); + std::string objCount = text.substr(posBrace, posClosing - posBrace) + ')'; + suffix += " "; + suffix += objCount; + } + const auto & font = ENGINE->renderHandler().loadFont(FONT_SMALL); - std::string suffix = "... " + objCount; if(font->getStringWidth(text) >= maxWidth) { diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 6eae917e0..6a59a8c1b 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -206,7 +206,7 @@ class CObjectListWindow : public CWindowObject std::vector< std::pair > itemsVisible; //visible items present in list void init(std::shared_ptr titleWidget_, std::string _title, std::string _descr, bool searchBoxEnabled); - void trimTextIfTooWide(std::string & text) const; // trim item's text to fit within window's width + void trimTextIfTooWide(std::string & text, bool preserveCountSuffix) const; // trim item's text to fit within window's width void itemsSearchCallback(const std::string & text); void exitPressed(); public: From b3943b917bd5765a39d4f4c87628929381fb2c5f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Oct 2025 19:34:22 +0200 Subject: [PATCH 23/26] Use town portal configuration instead of direct spell level check --- lib/spells/adventure/TownPortalEffect.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/spells/adventure/TownPortalEffect.cpp b/lib/spells/adventure/TownPortalEffect.cpp index f4f985a74..49861c5f6 100644 --- a/lib/spells/adventure/TownPortalEffect.cpp +++ b/lib/spells/adventure/TownPortalEffect.cpp @@ -140,7 +140,7 @@ void TownPortalEffect::endCast(SpellCastEnvironment * env, const AdventureSpellC { const CGTownInstance * destination = nullptr; - if(parameters.caster->getSpellSchoolLevel(owner) < 2) + if(!allowTownSelection) { std::vector pool = getPossibleTowns(env, parameters); destination = findNearestTown(env, parameters, pool); @@ -194,7 +194,7 @@ ESpellCastResult TownPortalEffect::beginCast(SpellCastEnvironment * env, const A return ESpellCastResult::CANCEL; } - if(!parameters.pos.isValid() && parameters.caster->getSpellSchoolLevel(owner) >= 2) + if(!parameters.pos.isValid() && allowTownSelection) { auto queryCallback = [&mechanics, env, parameters](std::optional reply) -> void { From f9f4a8e9afa2322f96208910bec4531cd927b012 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Oct 2025 19:40:24 +0200 Subject: [PATCH 24/26] Add workaround for crash on attempt to use town portal map object --- server/CGameHandler.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 722c601d6..79cdad53f 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -3881,8 +3881,14 @@ void CGameHandler::castSpell(const spells::Caster * caster, SpellID spellID, con const CSpell * s = spellID.toSpell(); s->adventureCast(spellEnv.get(), p); - if(const auto * hero = caster->getHeroCaster()) - useChargeBasedSpell(hero->id, spellID); + // FIXME: hack to avoid attempts to use charges when spell is casted externally + // For example, town gates map object in hota/wog + // Proper fix would be to instead spend charges similar to existing caster::spendMana call + if (dynamic_cast(caster) == nullptr) + { + if(const auto * hero = caster->getHeroCaster()) + useChargeBasedSpell(hero->id, spellID); + } } bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & sl2) From 21d3f690b876f4becc83597efb50e6139626ec4a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Oct 2025 21:18:41 +0200 Subject: [PATCH 25/26] Fix handling of artifacts in Seer Huts on HotA maps Fixed bug that caused artifacts in Pandora Boxes, Seer Huts/Border guards quests, and Seer Hut rewards fail to load, resulting in potentially broken map objects --- lib/mapping/MapFormatH3M.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 29940945f..79d1e427e 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1100,6 +1100,8 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi SpellID scrollSpell = reader->readSpell16(); if (grantedArtifact == ArtifactID::SPELL_SCROLL) reward.grantedScrolls.push_back(scrollSpell); + else + reward.grantedArtifacts.push_back(grantedArtifact); } else reward.grantedArtifacts.push_back(grantedArtifact); @@ -2306,6 +2308,8 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con SpellID scrollSpell = reader->readSpell16(); if (grantedArtifact == ArtifactID::SPELL_SCROLL) reward.grantedScrolls.push_back(scrollSpell); + else + reward.grantedArtifacts.push_back(grantedArtifact); } else reward.grantedArtifacts.push_back(grantedArtifact); @@ -2381,6 +2385,8 @@ EQuestMission CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & positi SpellID scrollSpell = reader->readSpell16(); if (requiredArtifact == ArtifactID::SPELL_SCROLL) guard->getQuest().mission.scrolls.push_back(scrollSpell); + else + guard->getQuest().mission.artifacts.push_back(requiredArtifact); } else guard->getQuest().mission.artifacts.push_back(requiredArtifact); From a4d840f60fd483060f7e5f0c9ab4cdde706e6439 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:30:51 +0100 Subject: [PATCH 26/26] code review --- server/NetPacksServer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index f02b9975a..e7cde39c3 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -365,7 +365,6 @@ void ApplyGhNetPackVisitor::visitSetFormation(SetFormation & pack) void ApplyGhNetPackVisitor::visitSetTownName(SetTownName & pack) { gh.throwIfWrongOwner(connection, &pack, pack.tid); - gh.throwIfPlayerNotActive(connection, &pack); result = gh.setTownName(pack.tid, pack.name); }