Переглянути джерело

Merge pull request #3820 from IvanSavenko/bugfixing

Bugfixing
Ivan Savenko 1 рік тому
батько
коміт
62d15ed23c

+ 1 - 1
AI/Nullkiller/AIGateway.cpp

@@ -619,7 +619,7 @@ void AIGateway::commanderGotLevel(const CCommanderInstance * commander, std::vec
 
 void AIGateway::showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel, bool safeToAutoaccept)
 {
-	LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i'", text % askID % soundID % selection % cancel % safeToAutoaccept);
+	LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i', autoaccept '%i'", text % askID % soundID % selection % cancel % safeToAutoaccept);
 	NET_EVENT_HANDLER;
 	status.addQuery(askID, boost::str(boost::format("Blocking dialog query with %d components - %s")
 									  % components.size() % text));

+ 1 - 1
AI/Nullkiller/Goals/CompleteQuest.cpp

@@ -94,7 +94,7 @@ std::string CompleteQuest::questToString() const
 		return "find " + VLC->generaltexth->tentColors[q.obj->subID] + " keymaster tent";
 	}
 
-	if(q.quest->questName == CQuest::missionName(0))
+	if(q.quest->questName == CQuest::missionName(EQuestMission::NONE))
 		return "inactive quest";
 
 	MetaString ms;

+ 4 - 1
AI/VCAI/Goals/CollectRes.cpp

@@ -146,7 +146,10 @@ TSubgoal CollectRes::whatToDoToTrade()
 	markets.erase(boost::remove_if(markets, [](const IMarket * market) -> bool
 	{
 		auto * o = dynamic_cast<const CGObjectInstance *>(market);
-		if(o && !(o->ID == Obj::TOWN && o->tempOwner == ai->playerID))
+		// FIXME: disabled broken visitation of external markets
+		//if(o && !(o->ID == Obj::TOWN && o->tempOwner == ai->playerID))
+
+		if(o && o->ID == Obj::TOWN)
 		{
 			if(!ai->isAccessible(o->visitablePos()))
 				return true;

+ 1 - 1
AI/VCAI/Goals/CompleteQuest.cpp

@@ -99,7 +99,7 @@ std::string CompleteQuest::completeMessage() const
 
 std::string CompleteQuest::questToString() const
 {
-	if(q.quest->questName == CQuest::missionName(0))
+	if(q.quest->questName == CQuest::missionName(EQuestMission::NONE))
 		return "inactive quest";
 
 	MetaString ms;

+ 1 - 1
AI/VCAI/VCAI.cpp

@@ -657,7 +657,7 @@ void VCAI::commanderGotLevel(const CCommanderInstance * commander, std::vector<u
 
 void VCAI::showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel, bool safeToAutoaccept)
 {
-	LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i'", text % askID % soundID % selection % cancel % safeToAutoaccept);
+	LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i', autoaccept '%i'", text % askID % soundID % selection % cancel % safeToAutoaccept);
 	NET_EVENT_HANDLER;
 	int sel = 0;
 	status.addQuery(askID, boost::str(boost::format("Blocking dialog query with %d components - %s")

+ 63 - 0
Mods/vcmi/config/vcmi/english.json

@@ -386,6 +386,69 @@
 	"vcmi.stackExperience.rank.8" : "Elite",
 	"vcmi.stackExperience.rank.9" : "Master",
 	"vcmi.stackExperience.rank.10" : "Ace",
+	
+	// Strings for HotA Seer Hut / Quest Guards
+	"core.seerhut.quest.heroClass.complete.0" : "Ah, you are %s.  Here's a gift for you.  Do you accept?",
+	"core.seerhut.quest.heroClass.complete.1" : "Ah, you are %s.  Here's a gift for you.  Do you accept?",
+	"core.seerhut.quest.heroClass.complete.2" : "Ah, you are %s.  Here's a gift for you.  Do you accept?",
+	"core.seerhut.quest.heroClass.complete.3" : "The guards note that you are %s and offer to let you pass.  Do you accept?",
+	"core.seerhut.quest.heroClass.complete.4" : "The guards note that you are %s and offer to let you pass.  Do you accept?",
+	"core.seerhut.quest.heroClass.complete.5" : "The guards note that you are %s and offer to let you pass.  Do you accept?",
+	"core.seerhut.quest.heroClass.description.0" : "Send %s to %s",
+	"core.seerhut.quest.heroClass.description.1" : "Send %s to %s",
+	"core.seerhut.quest.heroClass.description.2" : "Send %s to %s",
+	"core.seerhut.quest.heroClass.description.3" : "Send %s to open gate",
+	"core.seerhut.quest.heroClass.description.4" : "Send %s to open gate",
+	"core.seerhut.quest.heroClass.description.5" : "Send %s to open gate",
+	"core.seerhut.quest.heroClass.hover.0" : "(seeks hero of %s class)",
+	"core.seerhut.quest.heroClass.hover.1" : "(seeks hero of %s class)",
+	"core.seerhut.quest.heroClass.hover.2" : "(seeks hero of %s class)",
+	"core.seerhut.quest.heroClass.hover.3" : "(seeks hero of %s class)",
+	"core.seerhut.quest.heroClass.hover.4" : "(seeks hero of %s class)",
+	"core.seerhut.quest.heroClass.hover.5" : "(seeks hero of %s class)",
+	"core.seerhut.quest.heroClass.receive.0" : "I've got a gift for %s.",
+	"core.seerhut.quest.heroClass.receive.1" : "I've got a gift for %s.",
+	"core.seerhut.quest.heroClass.receive.2" : "I've got a gift for %s.",
+	"core.seerhut.quest.heroClass.receive.3" : "The guards here say they will only let %s pass.",
+	"core.seerhut.quest.heroClass.receive.4" : "The guards here say they will only let %s pass.",
+	"core.seerhut.quest.heroClass.receive.5" : "The guards here say they will only let %s pass.",
+	"core.seerhut.quest.heroClass.visit.0" : "You are not %s.  I've got nothing for you. Begone!",
+	"core.seerhut.quest.heroClass.visit.1" : "You are not %s.  I've got nothing for you. Begone!",
+	"core.seerhut.quest.heroClass.visit.2" : "You are not %s.  I've got nothing for you. Begone!",
+	"core.seerhut.quest.heroClass.visit.3" : "The guards here will only let %s pass.",
+	"core.seerhut.quest.heroClass.visit.4" : "The guards here will only let %s pass.",
+	"core.seerhut.quest.heroClass.visit.5" : "The guards here will only let %s pass.",
+	
+	"core.seerhut.quest.reachDate.complete.0" : "I'm free now.  Here's what I've got for you.  Do you accept?",
+	"core.seerhut.quest.reachDate.complete.1" : "I'm free now.  Here's what I've got for you.  Do you accept?",
+	"core.seerhut.quest.reachDate.complete.2" : "I'm free now.  Here's what I've got for you.  Do you accept?",
+	"core.seerhut.quest.reachDate.complete.3" : "You are free to go through now.  Do you wish to pass?",
+	"core.seerhut.quest.reachDate.complete.4" : "You are free to go through now.  Do you wish to pass?",
+	"core.seerhut.quest.reachDate.complete.5" : "You are free to go through now.  Do you wish to pass?",
+	"core.seerhut.quest.reachDate.description.0" : "Wait till %s for %s",
+	"core.seerhut.quest.reachDate.description.1" : "Wait till %s for %s",
+	"core.seerhut.quest.reachDate.description.2" : "Wait till %s for %s",
+	"core.seerhut.quest.reachDate.description.3" : "Wait till %s to open gate",
+	"core.seerhut.quest.reachDate.description.4" : "Wait till %s to open gate",
+	"core.seerhut.quest.reachDate.description.5" : "Wait till %s to open gate",
+	"core.seerhut.quest.reachDate.hover.0" : "(Return not before %s)",
+	"core.seerhut.quest.reachDate.hover.1" : "(Return not before %s)",
+	"core.seerhut.quest.reachDate.hover.2" : "(Return not before %s)",
+	"core.seerhut.quest.reachDate.hover.3" : "(Return not before %s)",
+	"core.seerhut.quest.reachDate.hover.4" : "(Return not before %s)",
+	"core.seerhut.quest.reachDate.hover.5" : "(Return not before %s)",
+	"core.seerhut.quest.reachDate.receive.0" : "I'm busy.  Come back not before %s",
+	"core.seerhut.quest.reachDate.receive.1" : "I'm busy.  Come back not before %s",
+	"core.seerhut.quest.reachDate.receive.2" : "I'm busy.  Come back not before %s",
+	"core.seerhut.quest.reachDate.receive.3" : "Closed till %s.",
+	"core.seerhut.quest.reachDate.receive.4" : "Closed till %s.",
+	"core.seerhut.quest.reachDate.receive.5" : "Closed till %s.",
+	"core.seerhut.quest.reachDate.visit.0" : "I'm busy.  Come back not before %s.",
+	"core.seerhut.quest.reachDate.visit.1" : "I'm busy.  Come back not before %s.",
+	"core.seerhut.quest.reachDate.visit.2" : "I'm busy.  Come back not before %s.",
+	"core.seerhut.quest.reachDate.visit.3" : "Closed till %s.",
+	"core.seerhut.quest.reachDate.visit.4" : "Closed till %s.",
+	"core.seerhut.quest.reachDate.visit.5" : "Closed till %s.",
 
 	"core.bonus.ADDITIONAL_ATTACK.name": "Double Strike",
 	"core.bonus.ADDITIONAL_ATTACK.description": "Attacks twice",

+ 1 - 1
client/lobby/CBonusSelection.cpp

@@ -240,7 +240,7 @@ void CBonusSelection::createBonusesIcons()
 		}
 		case CampaignBonusType::SECONDARY_SKILL:
 			desc.appendLocalString(EMetaText::GENERAL_TXT, 718);
-			desc.replaceTextID(TextIdentifier("core", "genrltxt", "levels", bonDescs[i].info3 - 1).get());
+			desc.replaceTextID(TextIdentifier("core", "skilllev", bonDescs[i].info3 - 1).get());
 			desc.replaceName(SecondarySkill(bonDescs[i].info2));
 			picNumber = bonDescs[i].info2 * 3 + bonDescs[i].info3 - 1;
 

+ 5 - 0
config/objects/dwellings.json

@@ -65,6 +65,7 @@
 			},
 			"airConflux": {
 				"index": 7,
+				"bannedForRandomDwelling" : true,
 				"creatures": [["airElemental"]],
 				"sounds": {
 					"ambient": ["LOOPAIR"]
@@ -107,6 +108,7 @@
 			},
 			"earthConflux": {
 				"index": 13,
+				"bannedForRandomDwelling" : true,
 				"creatures": [["earthElemental"]],
 				"sounds": {
 					"ambient": ["LOOPEART"]
@@ -128,6 +130,7 @@
 			},
 			"fireConflux": {
 				"index": 16,
+				"bannedForRandomDwelling" : true,
 				"creatures": [["fireElemental"]],
 				"sounds": {
 					"ambient": ["LOOPFIRE"]
@@ -345,6 +348,7 @@
 			},
 			"waterConflux": {
 				"index": 47,
+				"bannedForRandomDwelling" : true,
 				"creatures": [["waterElemental"]],
 				"sounds": {
 					"ambient": ["LOOPFOUN"]
@@ -538,6 +542,7 @@
 		"types" : {
 			"elementalConflux" : {
 				"index" : 0,
+				"bannedForRandomDwelling" : true,
 				"creatures" : [ // 4 separate "levels" to give them separate growth
 					[ "airElemental" ],
 					[ "waterElemental" ],

+ 2 - 1
launcher/modManager/cmodlistview_moc.cpp

@@ -810,7 +810,8 @@ void CModListView::installFiles(QStringList files)
 			images.push_back(filename);
 	}
 
-	manager->loadRepositories(repositories);
+	if (!repositories.empty())
+		manager->loadRepositories(repositories);
 
 	if(!mods.empty())
 		installMods(mods);

+ 3 - 1
lib/CGeneralTextHandler.cpp

@@ -569,7 +569,9 @@ CGeneralTextHandler::CGeneralTextHandler():
 
 		for (size_t i = 0; i < 9; ++i) //9 types of quests
 		{
-			std::string questName = CQuest::missionName(1+i);
+			EQuestMission missionID = static_cast<EQuestMission>(i+1);
+
+			std::string questName = CQuest::missionName(missionID);
 
 			for (size_t j = 0; j < 5; ++j)
 			{

+ 2 - 2
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -351,12 +351,12 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObj
 	}
 	catch (std::out_of_range & e)
 	{
-		// Leave catch block silently
+		// Leave catch block silently and handle error in block outside of try ... catch - all valid values should use 'return' in try block
 	}
 
 	std::string errorString = "Failed to find object of type " + std::to_string(type.getNum()) + "::" + std::to_string(subtype.getNum());
 	logGlobal->error(errorString);
-	throw std::runtime_error(errorString);
+	throw std::out_of_range(errorString);
 }
 
 TObjectTypeHandler CObjectClassesHandler::getHandlerFor(const std::string & scope, const std::string & type, const std::string & subtype) const

+ 6 - 0
lib/mapObjectConstructors/DwellingInstanceConstructor.cpp

@@ -51,6 +51,12 @@ void DwellingInstanceConstructor::initTypeData(const JsonNode & input)
 		assert(!availableCreatures[currentLevel].empty());
 	}
 	guards = input["guards"];
+	bannedForRandomDwelling = input["bannedForRandomDwelling"].Bool();
+}
+
+bool DwellingInstanceConstructor::isBannedForRandomDwelling() const
+{
+	return bannedForRandomDwelling;
 }
 
 bool DwellingInstanceConstructor::objectFilter(const CGObjectInstance * obj, std::shared_ptr<const ObjectTemplate> tmpl) const

+ 2 - 0
lib/mapObjectConstructors/DwellingInstanceConstructor.h

@@ -23,6 +23,7 @@ class DwellingInstanceConstructor : public CDefaultObjectTypeHandler<CGDwelling>
 	std::vector<std::vector<const CCreature *>> availableCreatures;
 
 	JsonNode guards;
+	bool bannedForRandomDwelling = false;
 
 protected:
 	bool objectFilter(const CGObjectInstance * obj, std::shared_ptr<const ObjectTemplate> tmpl) const override;
@@ -34,6 +35,7 @@ public:
 	void initializeObject(CGDwelling * object) const override;
 	void randomizeObject(CGDwelling * object, CRandomGenerator & rng) const override;
 
+	bool isBannedForRandomDwelling() const;
 	bool producesCreature(const CCreature * crea) const;
 	std::vector<const CCreature *> getProducedCreatures() const;
 };

+ 13 - 0
lib/mapObjects/CGCreature.cpp

@@ -16,6 +16,7 @@
 #include "../CConfigHandler.h"
 #include "../GameSettings.h"
 #include "../IGameCallback.h"
+#include "../mapObjectConstructors/CObjectClassesHandler.h"
 #include "../networkPacks/PacksForClient.h"
 #include "../networkPacks/PacksForClientBattle.h"
 #include "../networkPacks/StackLocation.h"
@@ -217,6 +218,18 @@ void CGCreature::pickRandomObject(CRandomGenerator & rand)
 			subID = VLC->creh->pickRandomMonster(rand, 7);
 			break;
 	}
+
+	try {
+		// sanity check
+		VLC->objtypeh->getHandlerFor(ID, subID);
+	}
+	catch (const std::out_of_range & )
+	{
+		// Try to generate some debug information if sanity check failed
+		CreatureID creatureID(subID.getNum());
+		throw std::out_of_range("Failed to find handler for creature " + std::to_string(creatureID.getNum()) + ", identifer:" + creatureID.toEntity(VLC)->getJsonKey());
+	}
+
 	ID = MapObjectID::MONSTER;
 	setType(ID, subID);
 }

+ 1 - 1
lib/mapObjects/CGDwelling.cpp

@@ -146,7 +146,7 @@ void CGDwelling::pickRandomObject(CRandomGenerator & rand)
 			{
 				const auto * handler = dynamic_cast<const DwellingInstanceConstructor *>(VLC->objtypeh->getHandlerFor(primaryID, entry).get());
 
-				if (handler->producesCreature(cid.toCreature()))
+				if (!handler->isBannedForRandomDwelling() && handler->producesCreature(cid.toCreature()))
 					return MapObjectSubID(entry);
 			}
 			return MapObjectSubID();

+ 1 - 5
lib/mapObjects/CGObjectInstance.cpp

@@ -133,11 +133,7 @@ void CGObjectInstance::setType(MapObjectID newID, MapObjectSubID newSubID)
 	//recalculate blockvis tiles - new appearance might have different blockmap than before
 	cb->gameState()->map->removeBlockVisTiles(this, true);
 	auto handler = VLC->objtypeh->getHandlerFor(newID, newSubID);
-	if(!handler)
-	{
-		logGlobal->error("Unknown object type %d:%d at %s", newID, newSubID, visitablePos().toString());
-		return;
-	}
+
 	if(!handler->getTemplates(tile.terType->getId()).empty())
 	{
 		appearance = handler->getTemplates(tile.terType->getId())[0];

+ 19 - 20
lib/mapObjects/CQuest.cpp

@@ -49,7 +49,7 @@ CQuest::CQuest():
 	isCustomNext(false),
 	isCustomComplete(false),
 	repeatedQuest(false),
-	questName(CQuest::missionName(0))
+	questName(CQuest::missionName(EQuestMission::NONE))
 {
 }
 
@@ -59,9 +59,9 @@ static std::string visitedTxt(const bool visited)
 	return VLC->generaltexth->allTexts[id];
 }
 
-const std::string & CQuest::missionName(int mission)
+const std::string & CQuest::missionName(EQuestMission mission)
 {
-	static const std::array<std::string, 13> names = {
+	static const std::array<std::string, 14> names = {
 		"empty",
 		"heroLevel",
 		"primarySkill",
@@ -72,9 +72,10 @@ const std::string & CQuest::missionName(int mission)
 		"bringResources",
 		"bringHero",
 		"bringPlayer",
+		"hotaINVALID", // only used for h3m parsing
 		"keymaster",
-		"hota",
-		"other"
+		"heroClass",
+		"reachDate"
 	};
 
 	if(static_cast<size_t>(mission) < names.size())
@@ -307,20 +308,18 @@ void CQuest::getCompletionText(IGameCallback * cb, MetaString &iwText) const
 void CQuest::defineQuestName()
 {
 	//standard quests
-	questName = CQuest::missionName(0);
-	if(mission != Rewardable::Limiter{}) questName = CQuest::missionName(12);
-	if(mission.heroLevel > 0) questName = CQuest::missionName(1);
-	for(auto & s : mission.primary) if(s) questName = CQuest::missionName(2);
-	if(!mission.spells.empty()) questName = CQuest::missionName(2);
-	if(!mission.secondary.empty()) questName = CQuest::missionName(2);
-	if(killTarget != ObjectInstanceID::NONE && !heroName.empty()) questName = CQuest::missionName(3);
-	if(killTarget != ObjectInstanceID::NONE && stackToKill != CreatureID::NONE) questName = CQuest::missionName(4);
-	if(!mission.artifacts.empty()) questName = CQuest::missionName(5);
-	if(!mission.creatures.empty()) questName = CQuest::missionName(6);
-	if(mission.resources.nonZero()) questName = CQuest::missionName(7);
-	if(!mission.heroes.empty()) questName = CQuest::missionName(8);
-	if(!mission.players.empty()) questName = CQuest::missionName(9);
-	if(mission.daysPassed > 0 || !mission.heroClasses.empty()) questName = CQuest::missionName(11);
+	questName = CQuest::missionName(EQuestMission::NONE);
+	if(mission.heroLevel > 0) questName = CQuest::missionName(EQuestMission::LEVEL);
+	for(auto & s : mission.primary) if(s) questName = CQuest::missionName(EQuestMission::PRIMARY_SKILL);
+	if(killTarget != ObjectInstanceID::NONE && !heroName.empty()) questName = CQuest::missionName(EQuestMission::KILL_HERO);
+	if(killTarget != ObjectInstanceID::NONE && stackToKill != CreatureID::NONE) questName = CQuest::missionName(EQuestMission::KILL_CREATURE);
+	if(!mission.artifacts.empty()) questName = CQuest::missionName(EQuestMission::ARTIFACT);
+	if(!mission.creatures.empty()) questName = CQuest::missionName(EQuestMission::ARMY);
+	if(mission.resources.nonZero()) questName = CQuest::missionName(EQuestMission::RESOURCES);
+	if(!mission.heroes.empty()) questName = CQuest::missionName(EQuestMission::HERO);
+	if(!mission.players.empty()) questName = CQuest::missionName(EQuestMission::PLAYER);
+	if(mission.daysPassed > 0) questName = CQuest::missionName(EQuestMission::HOTA_REACH_DATE);
+	if(!mission.heroClasses.empty()) questName = CQuest::missionName(EQuestMission::HOTA_HERO_CLASS);
 }
 
 void CQuest::addKillTargetReplacements(MetaString &out) const
@@ -466,7 +465,7 @@ void CGSeerHut::initObj(CRandomGenerator & rand)
 	if(quest->mission == Rewardable::Limiter{} && quest->killTarget == ObjectInstanceID::NONE)
 		quest->isCompleted = true;
 	
-	if(quest->questName == quest->missionName(0))
+	if(quest->questName == quest->missionName(EQuestMission::NONE))
 	{
 		quest->firstVisitText.appendTextID(TextIdentifier("core", "seehut", "empty", quest->completedOption).get());
 	}

+ 20 - 1
lib/mapObjects/CQuest.h

@@ -17,11 +17,30 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 class CGCreature;
 
+enum class EQuestMission {
+	NONE = 0,
+	LEVEL = 1,
+	PRIMARY_SKILL = 2,
+	KILL_HERO = 3,
+	KILL_CREATURE = 4,
+	ARTIFACT = 5,
+	ARMY = 6,
+	RESOURCES = 7,
+	HERO = 8,
+	PLAYER = 9,
+	HOTA_MULTI = 10,
+	// end of H3 missions
+
+	KEYMASTER = 11,
+	HOTA_HERO_CLASS = 12,
+	HOTA_REACH_DATE = 13,
+};
+
 class DLL_LINKAGE CQuest final
 {
 public:
 
-	static const std::string & missionName(int index);
+	static const std::string & missionName(EQuestMission index);
 	static const std::string & missionState(int index);
 	
 	std::string questName;

+ 1 - 1
lib/mapObjects/MiscObjects.cpp

@@ -1257,7 +1257,7 @@ void CGObelisk::setPropertyDer(ObjProperty what, ObjPropertyID identifier)
 				if(progress > cb->gameState()->map->obeliskCount)
 				{
 					logGlobal->error("Visited %d of %d", static_cast<int>(progress), cb->gameState()->map->obeliskCount);
-					throw std::runtime_error("Player visited more obelisks than present on map!");
+					throw std::runtime_error("Player visited " + std::to_string(progress) + " obelisks out of " + std::to_string(cb->gameState()->map->obeliskCount) + " present on map!");
 				}
 
 				break;

+ 1 - 17
lib/mapping/MapFormatH3M.h

@@ -35,23 +35,7 @@ class SpellID;
 class PlayerColor;
 class int3;
 
-enum class EQuestMission {
-	NONE = 0,
-	LEVEL = 1,
-	PRIMARY_SKILL = 2,
-	KILL_HERO = 3,
-	KILL_CREATURE = 4,
-	ARTIFACT = 5,
-	ARMY = 6,
-	RESOURCES = 7,
-	HERO = 8,
-	PLAYER = 9,
-	HOTA_MULTI = 10,
-	// end of H3 missions
-	KEYMASTER = 100,
-	HOTA_HERO_CLASS = 101,
-	HOTA_REACH_DATE = 102
-};
+enum class EQuestMission;
 
 enum class EVictoryConditionType : int8_t
 {

+ 1 - 1
server/battles/BattleActionProcessor.cpp

@@ -760,7 +760,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta
 
 					//TODO we need find batter way to handle double-wide stacks
 					//currently if only second occupied stack part is standing on gate / bridge hex then stack will start to wait for bridge to lower before it's needed. Though this is just a visual bug.
-					if (curStack->doubleWide())
+					if (curStack->doubleWide() && i + 2 < path.first.size())
 					{
 						BattleHex otherHex = curStack->occupiedHex(hex);
 						if (otherHex.isValid() && needOpenGates(otherHex))