浏览代码

Merge branch 'vcmi/master' into 'vcmi/develop'

Ivan Savenko 1 年之前
父节点
当前提交
b7391f49f6

+ 29 - 0
ChangeLog.md

@@ -31,6 +31,35 @@
 * Added support for multiple music tracks for terrains on adventure map
 * Fixed several cases where vcmi will report errors in json without specifying filename of invalid file
 
+# 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
+* Fixed non-functioning slider in invite to game room dialog
+
+### 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.

+ 27 - 27
client/CPlayerInterface.cpp

@@ -171,40 +171,39 @@ void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::
 	adventureInt.reset(new AdventureMapInterface());
 }
 
-void CPlayerInterface::playerEndsTurn(PlayerColor player)
+void CPlayerInterface::closeAllDialogs()
 {
-	EVENT_HANDLER_CALLED_BY_CLIENT;
-	if (player == playerID)
+	// remove all active dialogs that do not expect query answer
+	for (;;)
 	{
-		makingTurn = false;
-
-		// remove all active dialogs that do not expect query answer
-		for (;;)
-		{
-			auto adventureWindow = GH.windows().topWindow<AdventureMapInterface>();
-			auto infoWindow = GH.windows().topWindow<CInfoWindow>();
+		auto adventureWindow = GH.windows().topWindow<AdventureMapInterface>();
+		auto infoWindow = GH.windows().topWindow<CInfoWindow>();
 
-			if(adventureWindow != nullptr)
-				break;
+		if(adventureWindow != nullptr)
+			break;
 
-			if(infoWindow && infoWindow->ID != QueryID::NONE)
-				break;
+		if(infoWindow && infoWindow->ID != QueryID::NONE)
+			break;
 
-			if (infoWindow)
-				infoWindow->close();
-			else
-				GH.windows().popWindows(1);
-		}
+		if (infoWindow)
+			infoWindow->close();
+		else
+			GH.windows().popWindows(1);
+	}
 
-		if(castleInt)
-			castleInt->close();
+	if(castleInt)
+		castleInt->close();
 
-		castleInt = nullptr;
+	castleInt = nullptr;
+}
 
-		// remove all pending dialogs that do not expect query answer
-		vstd::erase_if(dialogs, [](const std::shared_ptr<CInfoWindow> & window){
-			return window->ID == QueryID::NONE;
-		});
+void CPlayerInterface::playerEndsTurn(PlayerColor player)
+{
+	EVENT_HANDLER_CALLED_BY_CLIENT;
+	if (player == playerID)
+	{
+		makingTurn = false;
+		closeAllDialogs();
 	}
 }
 
@@ -286,6 +285,7 @@ void CPlayerInterface::gamePause(bool pause)
 
 void CPlayerInterface::yourTurn(QueryID queryID)
 {
+	closeAllDialogs();
 	CTutorialWindow::openWindowFirstTime(TutorialMode::TOUCH_ADVENTUREMAP);
 
 	EVENT_HANDLER_CALLED_BY_CLIENT;
@@ -1477,7 +1477,7 @@ void CPlayerInterface::update()
 		return;
 
 	//if there are any waiting dialogs, show them
-	if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->isBusy())
+	if (makingTurn && !dialogs.empty() && !showingDialog->isBusy())
 	{
 		showingDialog->setBusy();
 		GH.windows().pushWindow(dialogs.front());

+ 1 - 0
client/CPlayerInterface.h

@@ -197,6 +197,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();

+ 1 - 1
client/adventureMap/AdventureMapInterface.cpp

@@ -375,7 +375,7 @@ void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID, bool isHuma
 	mapAudio->onEnemyTurnStarted();
 	widget->getMinimap()->setAIRadar(!isHuman);
 	widget->getInfoBar()->startEnemyTurn(playerID);
-	setState(isHuman ? EAdventureState::OTHER_HUMAN_PLAYER_TURN : EAdventureState::AI_PLAYER_TURN);
+	setState(isHuman ? EAdventureState::MAKING_TURN : EAdventureState::AI_PLAYER_TURN);
 }
 
 void AdventureMapInterface::setState(EAdventureState state)

+ 4 - 5
client/adventureMap/AdventureMapShortcuts.cpp

@@ -532,7 +532,7 @@ bool AdventureMapShortcuts::optionCanVisitObject()
 	auto * hero = LOCPLINT->localState->getCurrentHero();
 	auto objects = LOCPLINT->cb->getVisitableObjs(hero->visitablePos());
 
-	assert(vstd::contains(objects,hero));
+	//assert(vstd::contains(objects,hero));
 	return objects.size() > 1; // there is object other than our hero
 }
 
@@ -577,16 +577,15 @@ bool AdventureMapShortcuts::optionInWorldView()
 
 bool AdventureMapShortcuts::optionSidePanelActive()
 {
-return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
+return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW;
 }
 
 bool AdventureMapShortcuts::optionMapScrollingActive()
 {
-	return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
+	return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW;
 }
 
 bool AdventureMapShortcuts::optionMapViewActive()
 {
-	return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL
-		|| state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
+	return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL;
 }

+ 0 - 1
client/adventureMap/AdventureState.h

@@ -15,7 +15,6 @@ enum class EAdventureState
 	HOTSEAT_WAIT,
 	MAKING_TURN,
 	AI_PLAYER_TURN,
-	OTHER_HUMAN_PLAYER_TURN,
 	CASTING_SPELL,
 	WORLD_VIEW
 };

+ 6 - 2
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<int64_t>(estimation.kills.max, targetStack->getCount());
 				estimation.kills.min = std::min<int64_t>(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<int64_t>(estimation.kills.max, targetStack->getCount());
 			estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
 			return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available());

+ 1 - 1
client/globalLobby/GlobalLobbyInviteWindow.cpp

@@ -94,7 +94,7 @@ GlobalLobbyInviteWindow::GlobalLobbyInviteWindow()
 	};
 
 	listBackground = std::make_shared<TransparentFilledRectangle>(Rect(8, 48, 220, 324), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
-	accountList = std::make_shared<CListBox>(createAccountCardCallback, Point(10, 50), Point(0, 40), 8, 0, 0, 1 | 4, Rect(200, 0, 320, 320));
+	accountList = std::make_shared<CListBox>(createAccountCardCallback, Point(10, 50), Point(0, 40), 8, CSH->getGlobalLobby().getActiveAccounts().size(), 0, 1 | 4, Rect(200, 0, 320, 320));
 
 	buttonClose = std::make_shared<CButton>(Point(86, 384), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this]() { close(); }, EShortcut::GLOBAL_RETURN );
 

+ 1 - 1
client/globalLobby/GlobalLobbyLoginWindow.cpp

@@ -43,7 +43,7 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
 	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
 	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.login.title"));
 	labelUsernameTitle = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username"));
-	labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString());
+	labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString(), 265);
 	backgroundUsername = std::make_shared<TransparentFilledRectangle>(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
 	inputUsername = std::make_shared<CTextInput>(Rect(15, 93, 260, 16), FONT_SMALL, ETextAlignment::CENTERLEFT, true);
 	buttonLogin = std::make_shared<CButton>(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); }, EShortcut::GLOBAL_ACCEPT);

+ 16 - 4
client/globalLobby/GlobalLobbyRoomWindow.cpp

@@ -37,7 +37,7 @@ GlobalLobbyRoomAccountCard::GlobalLobbyRoomAccountCard(const GlobalLobbyAccount
 	pos.w = 130;
 	pos.h = 40;
 	backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
-	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName);
+	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName, 120);
 	labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status);
 }
 
@@ -56,9 +56,14 @@ GlobalLobbyRoomModCard::GlobalLobbyRoomModCard(const GlobalLobbyRoomModInfo & mo
 	pos.h = 40;
 	backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
 
-	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, modInfo.modName);
+	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, modInfo.modName, 190);
 	labelVersion = std::make_shared<CLabel>(195, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, modInfo.version);
-	labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, 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<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, statusColor, CGI->generaltexth->translate("vcmi.lobby.mod.state." + statusToString.at(modInfo.status)));
 }
 
 static std::string getJoinRoomErrorMessage(const GlobalLobbyRoom & roomDescription, const std::vector<GlobalLobbyRoomModInfo> & modVerificationList)
@@ -134,6 +139,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");
@@ -142,7 +154,7 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s
 
 	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
 	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.title").toString());
-	labelSubtitle = std::make_shared<CLabel>( pos.w / 2, 40, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, subtitleText.toString());
+	labelSubtitle = std::make_shared<CLabel>( pos.w / 2, 40, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, subtitleText.toString(), 400);
 
 	labelVersionTitle = std::make_shared<CLabel>( 10, 60, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.version").toString());
 	labelVersionValue = std::make_shared<CLabel>( 10, 80, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.gameVersion);

+ 3 - 3
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<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName);
+	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName, 120);
 	labelStatus = std::make_shared<CLabel>(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<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
 
-	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName);
-	labelDescription = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, roomDescription.description);
+	labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName, 180);
+	labelDescription = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, roomDescription.description, 160);
 	labelRoomSize = std::make_shared<CLabel>(212, 10, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomSizeText.toString());
 	labelRoomStatus = std::make_shared<CLabel>(225, 30, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::YELLOW, roomStatusText.toString());
 	iconRoomSize = std::make_shared<CPicture>(ImagePath::builtin("lobby/iconPlayer"), Point(214, 5));

+ 8 - 1
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()
 {

+ 2 - 1
client/gui/InterfaceObjectConfigurable.cpp

@@ -345,7 +345,8 @@ std::shared_ptr<CLabel> InterfaceObjectConfigurable::buildLabel(const JsonNode &
 	auto color = readColor(config["color"]);
 	auto text = readText(config["text"]);
 	auto position = readPosition(config["position"]);
-	return std::make_shared<CLabel>(position.x, position.y, font, alignment, color, text);
+	auto maxWidth = config["maxWidth"].Integer();
+	return std::make_shared<CLabel>(position.x, position.y, font, alignment, color, text, maxWidth);
 }
 
 std::shared_ptr<CMultiLineLabel> InterfaceObjectConfigurable::buildMultiLineLabel(const JsonNode & config) const

+ 3 - 3
client/lobby/OptionsTabBase.cpp

@@ -340,10 +340,10 @@ void OptionsTabBase::recreate(bool campaign)
 
 	//Simultaneous turns
 	if(auto turnSlider = widget<CSlider>("simturnsDurationMin"))
-		turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.requiredTurns);
+		turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.requiredTurns, false);
 
 	if(auto turnSlider = widget<CSlider>("simturnsDurationMax"))
-		turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.optionalTurns);
+		turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.optionalTurns, false);
 
 	if(auto w = widget<CLabel>("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<CLabel>("labelTurnDurationValue"))
 						w->setText(CGI->generaltexth->turnDurations[idx]);
 				}

+ 4 - 1
client/widgets/CComponent.cpp

@@ -293,7 +293,10 @@ std::string CComponent::getSubtitle() const
 			return CGI->artifacts()->getById(data.subType.as<ArtifactID>())->getNameTranslated();
 		case ComponentType::SPELL_SCROLL:
 		case ComponentType::SPELL:
-			return CGI->spells()->getById(data.subType.as<SpellID>())->getNameTranslated();
+			if (data.value < 0)
+				return "{#A9A9A9|" + CGI->spells()->getById(data.subType.as<SpellID>())->getNameTranslated() + "}";
+			else
+				return CGI->spells()->getById(data.subType.as<SpellID>())->getNameTranslated();
 		case ComponentType::NONE:
 		case ComponentType::MORALE:
 		case ComponentType::LUCK:

+ 6 - 5
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);
 }

+ 3 - 3
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<void(int)> & Moved, const std::vector<int> & values, int Value, Orientation orientation, EStyle style);

+ 3 - 0
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"]

+ 2 - 1
config/widgets/lobbyWindow.json

@@ -43,7 +43,8 @@
 		{
 			"name" : "accountNameLabel",
 			"type": "labelTitleMain",
-			"position": {"x": 15, "y": 10}
+			"position": {"x": 15, "y": 10},
+			"maxWidth": 230
 		},
 
 		{

+ 6 - 0
debian/changelog

@@ -4,6 +4,12 @@ vcmi (1.6.0) jammy; urgency=medium
 
  -- Ivan Savenko <[email protected]>  Fri, 30 Aug 2024 12:00:00 +0200
 
+vcmi (1.5.6) jammy; urgency=medium
+
+  * New upstream release
+
+ -- Ivan Savenko <[email protected]>  Sun, 4 Aug 2024 12:00:00 +0200
+
 vcmi (1.5.5) jammy; urgency=medium
 
   * New upstream release

+ 2 - 2
docs/Readme.md

@@ -1,7 +1,7 @@
 [![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.0)
-[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.4/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.4)
-[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.5/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.5)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.4/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.5)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.5/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.6)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 
 # VCMI Project

+ 3 - 1
docs/modders/Configurable_Widgets.md

@@ -463,7 +463,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
 

+ 1 - 0
launcher/eu.vcmi.VCMI.metainfo.xml

@@ -91,6 +91,7 @@
 	<launchable type="desktop-id">vcmilauncher.desktop</launchable>
 	<releases>
 		<release version="1.6.0" date="2024-08-30" type="development"/>
+		<release version="1.5.6" date="2024-08-04" type="stable"/>
 		<release version="1.5.5" date="2024-07-17" type="stable"/>
 		<release version="1.5.4" date="2024-07-12" type="stable"/>
 		<release version="1.5.3" date="2024-06-21" type="stable"/>

+ 4 - 4
lib/battle/CBattleInfoCallback.cpp

@@ -755,15 +755,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);
 }
 

+ 7 - 4
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 <typename Handler> void serialize(Handler &h)
 	{
 		h & value;

+ 5 - 3
lib/gameState/CGameStateCampaign.cpp

@@ -133,13 +133,15 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(const CampaignTravel & tr
 				if(!art)
 					return false;
 
+				ArtifactLocation al(hero.hero->id, artifactPosition);
+
 				bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId());
+				bool locked = hero.hero->getSlot(al.slot)->locked;
 
-				if (takeable)
+				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;

+ 14 - 5
lib/mapObjects/CGDwelling.cpp

@@ -348,15 +348,19 @@ void CGDwelling::newTurn(vstd::RNG & rand) const
 
 std::vector<Component> CGDwelling::getPopupComponents(PlayerColor player) const
 {
-	if (getOwner() != player)
-		return {};
+	bool visitedByOwner = getOwner() == player;
 
 	std::vector<Component> 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)
@@ -364,7 +368,12 @@ std::vector<Component> 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;
@@ -426,7 +435,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

+ 10 - 7
lib/mapObjects/CGHeroInstance.cpp

@@ -1704,6 +1704,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);
 
 	{
@@ -1719,13 +1729,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;

+ 19 - 0
lib/mapObjects/CRewardableObject.cpp

@@ -184,7 +184,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())
 	{

+ 4 - 2
lib/rewardable/Reward.cpp

@@ -115,8 +115,10 @@ void Rewardable::Reward::loadComponents(std::vector<Component> & 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);
+	{
+		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);

+ 9 - 0
server/CGameHandler.cpp

@@ -694,6 +694,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;

+ 2 - 2
server/battles/BattleActionProcessor.cpp

@@ -496,7 +496,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");
 	}
@@ -973,7 +973,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const
 	}
 
 	std::shared_ptr<const Bonus> 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;