From b6c22b2053f977b6dc869bc4c7179e787a18f36d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 27 Jul 2024 16:46:46 +0000 Subject: [PATCH 01/20] Fix typo --- lib/mapObjects/CGDwelling.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 5375a39ec..058139e39 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -424,7 +424,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const if(count) //there are available creatures { - if (VLC->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED)) + if (VLC->settings()->getBoolean(EGameSettings::DWELLINGS_MERGE_ON_RECRUIT)) { SlotID testSlot = h->getSlotFor(crid); if(!testSlot.validSlot()) //no available slot - try merging army of visiting hero From f9348fc84a2c3d8a101f797b7f350f75b708285a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 27 Jul 2024 16:58:22 +0000 Subject: [PATCH 02/20] Do not hide spells from reward if hero can't learn them --- lib/rewardable/Reward.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index 0bdd5fb5d..11830b3c0 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -115,8 +115,7 @@ void Rewardable::Reward::loadComponents(std::vector & comps, const CG comps.emplace_back(ComponentType::ARTIFACT, entry); for(const auto & entry : spells) - if (!h || h->canLearnSpell(entry.toEntity(VLC), true)) - comps.emplace_back(ComponentType::SPELL, entry); + comps.emplace_back(ComponentType::SPELL, entry); for(const auto & entry : creatures) comps.emplace_back(ComponentType::CREATURE, entry.type->getId(), entry.count); From 74fea5109ba818d30a882f0fe3344a24e6bd9d9e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 27 Jul 2024 18:09:12 +0000 Subject: [PATCH 03/20] Show non-learnable spells from rewardable object as greyed-out --- client/widgets/CComponent.cpp | 5 ++++- lib/rewardable/Reward.cpp | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index 043dde6fa..0325e9ca9 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -290,7 +290,10 @@ std::string CComponent::getSubtitle() const return CGI->artifacts()->getById(data.subType.as())->getNameTranslated(); case ComponentType::SPELL_SCROLL: case ComponentType::SPELL: - return CGI->spells()->getById(data.subType.as())->getNameTranslated(); + if (data.value < 0) + return "{#A9A9A9|" + CGI->spells()->getById(data.subType.as())->getNameTranslated() + "}"; + else + return CGI->spells()->getById(data.subType.as())->getNameTranslated(); case ComponentType::NONE: case ComponentType::MORALE: case ComponentType::LUCK: diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index 11830b3c0..c52501a6b 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -115,7 +115,10 @@ void Rewardable::Reward::loadComponents(std::vector & comps, const CG comps.emplace_back(ComponentType::ARTIFACT, entry); for(const auto & entry : spells) - comps.emplace_back(ComponentType::SPELL, entry); + { + bool learnable = !h || h->canLearnSpell(entry.toEntity(VLC), true); + comps.emplace_back(ComponentType::SPELL, entry, learnable ? 0 : -1); + } for(const auto & entry : creatures) comps.emplace_back(ComponentType::CREATURE, entry.type->getId(), entry.count); From 87b5f955d1a8efc8208d04f4fa10569a0c2fdabe Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 27 Jul 2024 18:12:08 +0000 Subject: [PATCH 04/20] Fix some shortcuts not active during enemy turn in multiplayer --- client/adventureMap/AdventureMapShortcuts.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/adventureMap/AdventureMapShortcuts.cpp b/client/adventureMap/AdventureMapShortcuts.cpp index 5cc33fc28..269183d0e 100644 --- a/client/adventureMap/AdventureMapShortcuts.cpp +++ b/client/adventureMap/AdventureMapShortcuts.cpp @@ -553,12 +553,12 @@ bool AdventureMapShortcuts::optionSpellcasting() bool AdventureMapShortcuts::optionInMapView() { - return state == EAdventureState::MAKING_TURN; + return state == EAdventureState::MAKING_TURN || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN; } bool AdventureMapShortcuts::optionInWorldView() { - return state == EAdventureState::WORLD_VIEW; + return state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN; } bool AdventureMapShortcuts::optionSidePanelActive() From 434a2fb0fb143cc4f0e28948bf0b8b0ea4832478 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 27 Jul 2024 18:45:27 +0000 Subject: [PATCH 05/20] Explicitly specify to use ranged or melee attack for damage estimation --- client/battle/BattleActionsController.cpp | 8 ++++++-- lib/battle/CBattleInfoCallback.cpp | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index a4fe689a1..b66eea8f2 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -499,9 +499,12 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle case PossiblePlayerBattleAction::WALK_AND_ATTACK: case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return { + const auto * attacker = owner.stacksController->getActiveStack(); BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); + int distance = attacker->position.isValid() ? owner.getBattle()->battleGetDistances(attacker, attacker->getPosition())[attackFromHex] : 0; DamageEstimation retaliation; - DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex, &retaliation); + BattleAttackInfo attackInfo(attacker, targetStack, distance, false ); + DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(attackInfo, &retaliation); estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); bool enemyMayBeKilled = estimation.kills.max == targetStack->getCount(); @@ -514,7 +517,8 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle const auto * shooter = owner.stacksController->getActiveStack(); DamageEstimation retaliation; - DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition(), &retaliation); + BattleAttackInfo attackInfo(shooter, targetStack, 0, true ); + DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(attackInfo, &retaliation); estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available()); diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index c5f578412..914c20e0e 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -742,15 +742,15 @@ DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * { RETURN_IF_NOT_BATTLE({}); auto reachability = battleGetDistances(attacker, attacker->getPosition()); - int getMovementRange = attackerPosition.isValid() ? reachability[attackerPosition] : 0; - return battleEstimateDamage(attacker, defender, getMovementRange, retaliationDmg); + int movementRange = attackerPosition.isValid() ? reachability[attackerPosition] : 0; + return battleEstimateDamage(attacker, defender, movementRange, retaliationDmg); } -DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int getMovementRange, DamageEstimation * retaliationDmg) const +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementRange, DamageEstimation * retaliationDmg) const { RETURN_IF_NOT_BATTLE({}); const bool shooting = battleCanShoot(attacker, defender->getPosition()); - const BattleAttackInfo bai(attacker, defender, getMovementRange, shooting); + const BattleAttackInfo bai(attacker, defender, movementRange, shooting); return battleEstimateDamage(bai, retaliationDmg); } From 5f0e6f7ce18fa6de10eb8cecf3096ac73ba4cfa6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 29 Jul 2024 15:57:49 +0000 Subject: [PATCH 06/20] Close all dialogs on start of new turn in MP --- client/CPlayerInterface.cpp | 61 ++++++++++++++++++++----------------- client/CPlayerInterface.h | 1 + 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 0129cf9c6..ad44404ce 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -169,40 +169,44 @@ void CPlayerInterface::initGameInterface(std::shared_ptr ENV, std:: adventureInt.reset(new AdventureMapInterface()); } +void CPlayerInterface::closeAllDialogs() +{ + // remove all active dialogs that do not expect query answer + for (;;) + { + auto adventureWindow = GH.windows().topWindow(); + auto infoWindow = GH.windows().topWindow(); + + if(adventureWindow != nullptr) + break; + + if(infoWindow && infoWindow->ID != QueryID::NONE) + break; + + if (infoWindow) + infoWindow->close(); + else + GH.windows().popWindows(1); + } + + if(castleInt) + castleInt->close(); + + castleInt = nullptr; + + // remove all pending dialogs that do not expect query answer + vstd::erase_if(dialogs, [](const std::shared_ptr & window){ + return window->ID == QueryID::NONE; + }); +} + void CPlayerInterface::playerEndsTurn(PlayerColor player) { EVENT_HANDLER_CALLED_BY_CLIENT; if (player == playerID) { makingTurn = false; - - // remove all active dialogs that do not expect query answer - for (;;) - { - auto adventureWindow = GH.windows().topWindow(); - auto infoWindow = GH.windows().topWindow(); - - if(adventureWindow != nullptr) - break; - - if(infoWindow && infoWindow->ID != QueryID::NONE) - break; - - if (infoWindow) - infoWindow->close(); - else - GH.windows().popWindows(1); - } - - if(castleInt) - castleInt->close(); - - castleInt = nullptr; - - // remove all pending dialogs that do not expect query answer - vstd::erase_if(dialogs, [](const std::shared_ptr & window){ - return window->ID == QueryID::NONE; - }); + closeAllDialogs(); } } @@ -284,6 +288,7 @@ void CPlayerInterface::gamePause(bool pause) void CPlayerInterface::yourTurn(QueryID queryID) { + closeAllDialogs(); CTutorialWindow::openWindowFirstTime(TutorialMode::TOUCH_ADVENTUREMAP); EVENT_HANDLER_CALLED_BY_CLIENT; diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index fd03f76ed..1d1dcd01f 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -203,6 +203,7 @@ public: // public interface for use by client via LOCPLINT access void performAutosave(); void gamePause(bool pause); void endNetwork(); + void closeAllDialogs(); ///returns true if all events are processed internally bool capturedAllEvents(); From 15f37f8c4b55639c89ab1be99072ec8791b9aa18 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 29 Jul 2024 15:58:07 +0000 Subject: [PATCH 07/20] Fix possible crash on invalid SPELL_LIKE_ATTACK ability --- lib/constants/VariantIdentifier.h | 11 +++++++---- server/battles/BattleActionProcessor.cpp | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/constants/VariantIdentifier.h b/lib/constants/VariantIdentifier.h index 5e3002391..e1b9f83b2 100644 --- a/lib/constants/VariantIdentifier.h +++ b/lib/constants/VariantIdentifier.h @@ -32,18 +32,14 @@ public: int32_t getNum() const { int32_t result; - std::visit([&result] (const auto& v) { result = v.getNum(); }, value); - return result; } std::string toString() const { std::string result; - std::visit([&result] (const auto& v) { result = v.encode(v.getNum()); }, value); - return result; } @@ -58,6 +54,13 @@ public: return IdentifierType(); } + bool hasValue() const + { + bool result = false; + std::visit([&result] (const auto& v) { result = v.hasValue(); }, value); + return result; + } + template void serialize(Handler &h) { h & value; diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 44157097e..2867c5bed 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -494,7 +494,7 @@ bool BattleActionProcessor::doHealAction(const CBattleInfoCallback & battle, con else destStack = battle.battleGetUnitByPos(target.at(0).hexValue); - if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype == BonusSubtypeID()) + if(stack == nullptr || destStack == nullptr || !healerAbility || !healerAbility->subtype.hasValue()) { gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P"); } @@ -971,7 +971,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const } std::shared_ptr bonus = attacker->getFirstBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); - if(bonus && ranged) //TODO: make it work in melee? + if(bonus && ranged && bonus->subtype.hasValue()) //TODO: make it work in melee? { //this is need for displaying hit animation bat.flags |= BattleAttack::SPELL_LIKE; From 789e370950847f8c4e0e08650f8e5e308bfe8a55 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 29 Jul 2024 16:24:58 +0000 Subject: [PATCH 08/20] Add compatibility for old vcmi maps --- config/objects/moddables.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/objects/moddables.json b/config/objects/moddables.json index b97606eb9..422fe3532 100644 --- a/config/objects/moddables.json +++ b/config/objects/moddables.json @@ -155,18 +155,21 @@ "types" : { "boatNecropolis" : { "index" : 0, + "compatibilityIdentifiers" : [ "evil" ], "actualAnimation" : "AB01_.def", "overlayAnimation" : "ABM01_.def", "flagAnimations" : ["ABF01L", "ABF01G", "ABF01R", "ABF01D", "ABF01B", "ABF01P", "ABF01W", "ABF01K"] }, "boatCastle" : { "index" : 1, + "compatibilityIdentifiers" : [ "good" ], "actualAnimation" : "AB02_.def", "overlayAnimation" : "ABM02_.def", "flagAnimations" : ["ABF02L", "ABF02G", "ABF02R", "ABF02D", "ABF02B", "ABF02P", "ABF02W", "ABF02K"] }, "boatFortress" : { "index" : 2, + "compatibilityIdentifiers" : [ "neutral" ], "actualAnimation" : "AB03_.def", "overlayAnimation" : "ABM03_.def", "flagAnimations" : ["ABF03L", "ABF03G", "ABF03R", "ABF03D", "ABF03B", "ABF03P", "ABF03W", "ABF03K"] From 0d1f744ba474ce0ff1d9bc57d99dc09204ebfdd5 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 31 Jul 2024 21:40:49 +0200 Subject: [PATCH 09/20] limit text sizes --- client/globalLobby/GlobalLobbyLoginWindow.cpp | 2 +- client/globalLobby/GlobalLobbyRoomWindow.cpp | 6 +++--- client/globalLobby/GlobalLobbyWidget.cpp | 6 +++--- client/gui/InterfaceObjectConfigurable.cpp | 3 ++- config/widgets/lobbyWindow.json | 3 ++- docs/modders/Configurable_Widgets.md | 4 +++- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/client/globalLobby/GlobalLobbyLoginWindow.cpp b/client/globalLobby/GlobalLobbyLoginWindow.cpp index 1c8f98e85..b34c4d587 100644 --- a/client/globalLobby/GlobalLobbyLoginWindow.cpp +++ b/client/globalLobby/GlobalLobbyLoginWindow.cpp @@ -43,7 +43,7 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow() filledBackground = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); labelTitle = std::make_shared( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.login.title")); labelUsernameTitle = std::make_shared( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username")); - labelUsername = std::make_shared( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString()); + labelUsername = std::make_shared( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString(), 265); backgroundUsername = std::make_shared(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64)); inputUsername = std::make_shared(Rect(15, 93, 260, 16), FONT_SMALL, ETextAlignment::CENTERLEFT, true); buttonLogin = std::make_shared(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); }, EShortcut::GLOBAL_ACCEPT); diff --git a/client/globalLobby/GlobalLobbyRoomWindow.cpp b/client/globalLobby/GlobalLobbyRoomWindow.cpp index 65ec73857..8dc7988be 100644 --- a/client/globalLobby/GlobalLobbyRoomWindow.cpp +++ b/client/globalLobby/GlobalLobbyRoomWindow.cpp @@ -37,7 +37,7 @@ GlobalLobbyRoomAccountCard::GlobalLobbyRoomAccountCard(const GlobalLobbyAccount pos.w = 130; pos.h = 40; backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1); - labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName); + labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName, 120); labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status); } @@ -56,7 +56,7 @@ GlobalLobbyRoomModCard::GlobalLobbyRoomModCard(const GlobalLobbyRoomModInfo & mo pos.h = 40; backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1); - labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, modInfo.modName); + labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, modInfo.modName, 190); labelVersion = std::make_shared(195, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, modInfo.version); labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.mod.state." + statusToString.at(modInfo.status))); } @@ -142,7 +142,7 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s filledBackground = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); labelTitle = std::make_shared( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.title").toString()); - labelSubtitle = std::make_shared( pos.w / 2, 40, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, subtitleText.toString()); + labelSubtitle = std::make_shared( pos.w / 2, 40, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, subtitleText.toString(), 400); labelVersionTitle = std::make_shared( 10, 60, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.version").toString()); labelVersionValue = std::make_shared( 10, 80, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.gameVersion); diff --git a/client/globalLobby/GlobalLobbyWidget.cpp b/client/globalLobby/GlobalLobbyWidget.cpp index a6af670db..7b71937b6 100644 --- a/client/globalLobby/GlobalLobbyWidget.cpp +++ b/client/globalLobby/GlobalLobbyWidget.cpp @@ -207,7 +207,7 @@ GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const : GlobalLobbyChannelCardBase(window, Point(130, 40), "player", accountDescription.accountID, accountDescription.displayName) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName); + labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName, 120); labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status); } @@ -238,8 +238,8 @@ GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const Globa else backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1); - labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName); - labelDescription = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, roomDescription.description); + labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName, 180); + labelDescription = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, roomDescription.description, 160); labelRoomSize = std::make_shared(212, 10, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomSizeText.toString()); labelRoomStatus = std::make_shared(225, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomStatusText.toString()); iconRoomSize = std::make_shared(ImagePath::builtin("lobby/iconPlayer"), Point(214, 5)); diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 5bcf45e17..11a2e4ade 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -345,7 +345,8 @@ std::shared_ptr InterfaceObjectConfigurable::buildLabel(const JsonNode & auto color = readColor(config["color"]); auto text = readText(config["text"]); auto position = readPosition(config["position"]); - return std::make_shared(position.x, position.y, font, alignment, color, text); + auto maxWidth = config["maxWidth"].Integer(); + return std::make_shared(position.x, position.y, font, alignment, color, text, maxWidth); } std::shared_ptr InterfaceObjectConfigurable::buildMultiLineLabel(const JsonNode & config) const diff --git a/config/widgets/lobbyWindow.json b/config/widgets/lobbyWindow.json index 529d26ce5..118a60317 100644 --- a/config/widgets/lobbyWindow.json +++ b/config/widgets/lobbyWindow.json @@ -43,7 +43,8 @@ { "name" : "accountNameLabel", "type": "labelTitleMain", - "position": {"x": 15, "y": 10} + "position": {"x": 15, "y": 10}, + "maxWidth": 230 }, { diff --git a/docs/modders/Configurable_Widgets.md b/docs/modders/Configurable_Widgets.md index 0649b866b..a1d40f76a 100644 --- a/docs/modders/Configurable_Widgets.md +++ b/docs/modders/Configurable_Widgets.md @@ -461,7 +461,9 @@ Configurable object has following structure: `"text"`: [text](#text), -`"position"`: [position](#position) +`"position"`: [position](#position), + +`"maxWidth"`: int` optional, trim longer text ### [VCMI-1.4] Multi-line label From 0cc743f7caa366eb3e76a0cdb46aa7a16f5a2129 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 31 Jul 2024 22:10:37 +0200 Subject: [PATCH 10/20] mod compatibility improvements --- client/globalLobby/GlobalLobbyRoomWindow.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/globalLobby/GlobalLobbyRoomWindow.cpp b/client/globalLobby/GlobalLobbyRoomWindow.cpp index 65ec73857..282b1a2f2 100644 --- a/client/globalLobby/GlobalLobbyRoomWindow.cpp +++ b/client/globalLobby/GlobalLobbyRoomWindow.cpp @@ -58,7 +58,7 @@ GlobalLobbyRoomModCard::GlobalLobbyRoomModCard(const GlobalLobbyRoomModInfo & mo labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, modInfo.modName); labelVersion = std::make_shared(195, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, modInfo.version); - labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.mod.state." + statusToString.at(modInfo.status))); + labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, modInfo.status == ModVerificationStatus::FULL_MATCH ? Colors::YELLOW : Colors::RED, CGI->generaltexth->translate("vcmi.lobby.mod.state." + statusToString.at(modInfo.status))); } static const std::string getJoinRoomErrorMessage(const GlobalLobbyRoom & roomDescription, const std::vector & modVerificationList) @@ -134,6 +134,13 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s modVerificationList.push_back(modInfo); } + std::sort(modVerificationList.begin(), modVerificationList.end(), [](const GlobalLobbyRoomModInfo &a, const GlobalLobbyRoomModInfo &b) + { + if(a.status == b.status) + return a.modName < b.modName; + + return a.status < b.status; + }); MetaString subtitleText; subtitleText.appendTextID("vcmi.lobby.preview.subtitle"); From 12af016eaca4dbdbe9770d6df28e73b20424d1ae Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 31 Jul 2024 22:25:05 +0200 Subject: [PATCH 11/20] more colors for status --- client/globalLobby/GlobalLobbyRoomWindow.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/globalLobby/GlobalLobbyRoomWindow.cpp b/client/globalLobby/GlobalLobbyRoomWindow.cpp index 282b1a2f2..20cde882c 100644 --- a/client/globalLobby/GlobalLobbyRoomWindow.cpp +++ b/client/globalLobby/GlobalLobbyRoomWindow.cpp @@ -58,7 +58,12 @@ GlobalLobbyRoomModCard::GlobalLobbyRoomModCard(const GlobalLobbyRoomModInfo & mo labelName = std::make_shared(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, modInfo.modName); labelVersion = std::make_shared(195, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, modInfo.version); - labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, modInfo.status == ModVerificationStatus::FULL_MATCH ? Colors::YELLOW : Colors::RED, CGI->generaltexth->translate("vcmi.lobby.mod.state." + statusToString.at(modInfo.status))); + auto statusColor = Colors::RED; + if(modInfo.status == ModVerificationStatus::FULL_MATCH) + statusColor = ColorRGBA(128, 128, 128); + else if(modInfo.status == ModVerificationStatus::VERSION_MISMATCH) + statusColor = Colors::YELLOW; + labelStatus = std::make_shared(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, statusColor, CGI->generaltexth->translate("vcmi.lobby.mod.state." + statusToString.at(modInfo.status))); } static const std::string getJoinRoomErrorMessage(const GlobalLobbyRoom & roomDescription, const std::vector & modVerificationList) From 2c42737b28f6970520647aae8d1a5c6db96afbaf Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 31 Jul 2024 21:03:24 +0000 Subject: [PATCH 12/20] Fix infinitely updating simultaneous turns slider --- client/lobby/OptionsTabBase.cpp | 6 +++--- client/widgets/Slider.cpp | 11 ++++++----- client/widgets/Slider.h | 6 +++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index c4d728d00..17936a6d0 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/client/lobby/OptionsTabBase.cpp @@ -340,10 +340,10 @@ void OptionsTabBase::recreate(bool campaign) //Simultaneous turns if(auto turnSlider = widget("simturnsDurationMin")) - turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.requiredTurns); + turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.requiredTurns, false); if(auto turnSlider = widget("simturnsDurationMax")) - turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.optionalTurns); + turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.optionalTurns, false); if(auto w = widget("labelSimturnsDurationValueMin")) w->setText(generateSimturnsDurationText(SEL->getStartInfo()->simturnsInfo.requiredTurns)); @@ -388,7 +388,7 @@ void OptionsTabBase::recreate(bool campaign) auto & tpreset = variables["timerPresets"].Vector()[idx]; if(tpreset.Vector().at(1).Integer() == turnTimerRemote.turnTimer / 1000) { - turnSlider->scrollTo(idx); + turnSlider->scrollTo(idx, false); if(auto w = widget("labelTurnDurationValue")) w->setText(CGI->generaltexth->turnDurations[idx]); } diff --git a/client/widgets/Slider.cpp b/client/widgets/Slider.cpp index 2c866ff47..e51896cdc 100644 --- a/client/widgets/Slider.cpp +++ b/client/widgets/Slider.cpp @@ -70,7 +70,7 @@ int CSlider::getValue() const return value; } -void CSlider::setValue(int to) +void CSlider::setValue(int to, bool callCallbacks) { scrollTo(value); } @@ -113,7 +113,7 @@ void CSlider::updateSliderPos() } } -void CSlider::scrollTo(int to) +void CSlider::scrollTo(int to, bool callCallbacks) { vstd::amax(to, 0); vstd::amin(to, positions); @@ -125,7 +125,8 @@ void CSlider::scrollTo(int to) updateSliderPos(); - moved(getValue()); + if (callCallbacks) + moved(getValue()); } void CSlider::clickPressed(const Point & cursorPosition) @@ -321,7 +322,7 @@ int SliderNonlinear::getValue() const return scaledValues.at(CSlider::getValue()); } -void SliderNonlinear::setValue(int to) +void SliderNonlinear::setValue(int to, bool callCallbacks) { size_t nearest = 0; @@ -334,5 +335,5 @@ void SliderNonlinear::setValue(int to) nearest = i; } - scrollTo(nearest); + scrollTo(nearest, callCallbacks); } diff --git a/client/widgets/Slider.h b/client/widgets/Slider.h index 88a580187..6cbba6823 100644 --- a/client/widgets/Slider.h +++ b/client/widgets/Slider.h @@ -52,14 +52,14 @@ public: void clearScrollBounds(); /// Value modifiers - void scrollTo(int value); + void scrollTo(int value, bool callCallbacks = true); void scrollBy(int amount) override; void scrollToMin(); void scrollToMax(); /// Amount modifier void setAmount(int to); - virtual void setValue(int to); + virtual void setValue(int to, bool callCallbacks = true); /// Accessors int getAmount() const; @@ -95,7 +95,7 @@ class SliderNonlinear : public CSlider using CSlider::setAmount; // make private public: - void setValue(int to) override; + void setValue(int to, bool callCallbacks) override; int getValue() const override; SliderNonlinear(Point position, int length, const std::function & Moved, const std::vector & values, int Value, Orientation orientation, EStyle style); From 16b28c28e47dac5741e3c1480e3fc983f76bd477 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 1 Aug 2024 19:15:17 +0000 Subject: [PATCH 13/20] Version bump to 1.5.6 --- android/vcmi-app/build.gradle | 4 ++-- cmake_modules/VersionDefinition.cmake | 2 +- debian/changelog | 6 ++++++ launcher/eu.vcmi.VCMI.metainfo.xml | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index f8f7596b8..c6429c6cb 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -26,8 +26,8 @@ android { minSdk = qtMinSdkVersion as Integer targetSdk = qtTargetSdkVersion as Integer // ANDROID_TARGET_SDK_VERSION in the CMake project - versionCode 1550 - versionName "1.5.5" + versionCode 1560 + versionName "1.5.6" setProperty("archivesBaseName", "vcmi") } diff --git a/cmake_modules/VersionDefinition.cmake b/cmake_modules/VersionDefinition.cmake index 67002ded8..959837e8b 100644 --- a/cmake_modules/VersionDefinition.cmake +++ b/cmake_modules/VersionDefinition.cmake @@ -1,6 +1,6 @@ set(VCMI_VERSION_MAJOR 1) set(VCMI_VERSION_MINOR 5) -set(VCMI_VERSION_PATCH 5) +set(VCMI_VERSION_PATCH 6) add_definitions( -DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR} -DVCMI_VERSION_MINOR=${VCMI_VERSION_MINOR} diff --git a/debian/changelog b/debian/changelog index 592ea716b..52247d334 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +vcmi (1.5.6) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Sun, 4 Aug 2024 12:00:00 +0200 + vcmi (1.5.5) jammy; urgency=medium * New upstream release diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 94e15aeb3..5e54c6620 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -90,6 +90,7 @@ vcmilauncher.desktop + From 46669e78e8206b7f3668ee97f28e7a5d30e6e335 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 2 Aug 2024 15:02:37 +0000 Subject: [PATCH 14/20] Fix possible crash on deletion of adventureInt after GH on shutdown --- client/gui/CGuiHandler.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index 675130e46..f5dd29eab 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -19,6 +19,7 @@ #include "../eventsSDL/InputHandler.h" #include "../CGameInfo.h" +#include "../adventureMap/AdventureMapInterface.h" #include "../render/Colors.h" #include "../render/Graphics.h" #include "../render/IFont.h" @@ -145,7 +146,13 @@ CGuiHandler::CGuiHandler() { } -CGuiHandler::~CGuiHandler() = default; +CGuiHandler::~CGuiHandler() +{ + // enforce deletion order on shutdown + // all UI elements including adventure map must be destroyed before Gui Handler + // proper solution would be removal of adventureInt global + adventureInt.reset(); +} ShortcutHandler & CGuiHandler::shortcuts() { From 8ce6bcdf3b8c9c3353130ad971297276624fed8b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 2 Aug 2024 15:03:25 +0000 Subject: [PATCH 15/20] Right-clicking dwellings will now show recruitable creatures (but not their number) for all players --- lib/mapObjects/CGDwelling.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 058139e39..023abd77e 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -346,15 +346,19 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const std::vector CGDwelling::getPopupComponents(PlayerColor player) const { - if (getOwner() != player) - return {}; + bool visitedByOwner = getOwner() == player; std::vector result; if (ID == Obj::CREATURE_GENERATOR1 && !creatures.empty()) { for (auto const & creature : creatures.front().second) - result.emplace_back(ComponentType::CREATURE, creature, creatures.front().first); + { + if (visitedByOwner) + result.emplace_back(ComponentType::CREATURE, creature, creatures.front().first); + else + result.emplace_back(ComponentType::CREATURE, creature); + } } if (ID == Obj::CREATURE_GENERATOR4) @@ -362,7 +366,12 @@ std::vector CGDwelling::getPopupComponents(PlayerColor player) const for (auto const & creatureLevel : creatures) { if (!creatureLevel.second.empty()) - result.emplace_back(ComponentType::CREATURE, creatureLevel.second.back(), creatureLevel.first); + { + if (visitedByOwner) + result.emplace_back(ComponentType::CREATURE, creatureLevel.second.back(), creatureLevel.first); + else + result.emplace_back(ComponentType::CREATURE, creatureLevel.second.back()); + } } } return result; From 0ac1ef077dcdb56d1fede9f69daf121915aca9d9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 2 Aug 2024 15:03:48 +0000 Subject: [PATCH 16/20] Workaround for hota witch hut preview --- lib/mapObjects/CRewardableObject.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 0e671b028..c1f9d546c 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -181,7 +181,26 @@ void CRewardableObject::heroLevelUpDone(const CGHeroInstance *hero) const void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const { if(answer == 0) + { + switch (configuration.visitMode) + { + case Rewardable::VISIT_UNLIMITED: + case Rewardable::VISIT_BONUS: + case Rewardable::VISIT_HERO: + case Rewardable::VISIT_LIMITER: + { + // workaround for object with refusable reward not getting marked as visited + // TODO: better solution that would also work for player-visitable objects + if (!wasScouted(hero->getOwner())) + { + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, hero->id); + cb->sendAndApply(&cov); + } + } + } + return; // player refused + } if(answer > 0 && answer-1 < configuration.info.size()) { From c9b6b17422831f97ca4e400b1872e9df9dee1de8 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 2 Aug 2024 15:04:07 +0000 Subject: [PATCH 17/20] Add more logging for weird crash on game start --- server/CGameHandler.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 2b8ec21da..18b831759 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -682,6 +682,15 @@ void CGameHandler::onNewTurn() } } + for (auto & player : gs->players) + { + if (player.second.status != EPlayerStatus::INGAME) + continue; + + if (player.second.heroes.empty() && player.second.towns.empty()) + throw std::runtime_error("Invalid player in player state! Player " + std::to_string(player.first.getNum()) + ", map name: " + gs->map->name.toString() + ", map description: " + gs->map->description.toString()); + } + if (newWeek && !firstTurn) { n.specialWeek = NewTurn::NORMAL; From 13108849caf50f9addc8dd7b7cc2ff6613b45dcf Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 2 Aug 2024 15:56:05 +0000 Subject: [PATCH 18/20] Try to fix crash on transferring component of a composite artifact --- lib/gameState/CGameStateCampaign.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index a016cc6ee..7f11c92fb 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -130,13 +130,15 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(const CampaignTravel & tr if(!art) return false; - bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId()); + ArtifactLocation al(hero.hero->id, artifactPosition); - if (takeable) + bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId()); + bool locked = hero.hero->getSlot(al.slot)->locked; + + if (!locked && takeable) hero.transferrableArtifacts.push_back(artifactPosition); - ArtifactLocation al(hero.hero->id, artifactPosition); - if(!takeable && !hero.hero->getSlot(al.slot)->locked) //don't try removing locked artifacts -> it crashes #1719 + if (!locked && !takeable) { hero.hero->getArt(al.slot)->removeFrom(*hero.hero, al.slot); return true; From 5023e08ae8a3f72d911e22aaa4c8710da428fdec Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 2 Aug 2024 15:56:34 +0000 Subject: [PATCH 19/20] Fix crash on testing for hero faction before deserializing hero type --- lib/mapObjects/CGHeroInstance.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index c99667f58..0a9a127c7 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1709,6 +1709,16 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler) setHeroTypeName(typeName); } + if(!handler.saving) + { + if(!appearance) + { + // crossoverDeserialize + type = getHeroType().toHeroType(); + appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); + } + } + CArmedInstance::serializeJsonOptions(handler); { @@ -1724,13 +1734,6 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler) if(!handler.saving) { - if(!appearance) - { - // crossoverDeserialize - type = getHeroType().toHeroType(); - appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); - } - patrol.patrolling = (rawPatrolRadius > NO_PATROLING); patrol.initialPos = visitablePos(); patrol.patrolRadius = (rawPatrolRadius > NO_PATROLING) ? rawPatrolRadius : 0; From a4e4ec565b765c7eacc73f6ad32d9cc302f1480d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 1 Aug 2024 19:27:35 +0000 Subject: [PATCH 20/20] Changelog for 1.5.6 --- ChangeLog.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 4124de750..2932b8fc4 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,31 @@ +# 1.5.5 -> 1.5.6 + +# Stability +* Fixed possible crash on transferring hero to next campaign scenario if hero has combined artifact some components of which can be transferred +* Fixed possible crash on transferring hero to next campaign scenario that has creature with faction limiter in his army +* Fixed possible crash on application shutdown due to incorrect destruction order of UI entities + +# Multiplayer +* Mod compatibility issues when joining a lobby room now use color coding to make them less easy to miss. +* Incompatible mods are now placed before compatible mods when joining lobby room. +* Fixed text overflow in online lobby interface +* Fixed jittering simultaneous turns slider after moving it twice over short period + +# Interface +* Fixed some shortcuts that were not active during the enemy's turn, such as Thieves' Guild. +* Game now correctly uses melee damage calculation when forcing a melee attack with a shooter. +* Game will now close all open dialogs on start of our turn, to avoid bugs like locked right-click popups + +# Map Objects +* Spells the hero can't learn are no longer hidden when received from a rewardable object, such as the Pandora Box +* Spells that cannot be learned are now displayed with gray text in the name of the spell. +* Configurable objects with scouted state such as Witch Hut in HotA now correctly show their reward on right click after vising them but refusing to accept reward +* Right-click tooltip on map dwelling now always shows produced creatures. Player that owns the dwelling can also see number of creatures available for recruit + +# Modding +* Fixed possible crash on invalid SPELL_LIKE_ATTACK bonus +* Added compatibility check when loading maps with old names for boats + # 1.5.4 -> 1.5.5 * Fixed crash when advancing to the next scenario in campaigns when the hero not transferring has a combination artefact that can be transferred to the next scenario.