浏览代码

Implemented option to emulate H3 seer hut full-army quest bug

Ivan Savenko 5 月之前
父节点
当前提交
0e2ea99283

+ 1 - 1
client/Client.h

@@ -188,7 +188,7 @@ public:
 	void giveResources(PlayerColor player, TResources resources) override {};
 
 	void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet & creatures, bool remove) override {};
-	void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> & creatures) override {};
+	void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> & creatures, bool forceRemoval) override {};
 	bool changeStackType(const StackLocation & sl, const CCreature * c) override {return false;};
 	bool changeStackCount(const StackLocation & sl, TQuantity count, bool absoluteValue = false) override {return false;};
 	bool insertNewStack(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;};

+ 8 - 0
config/gameConfig.json

@@ -463,6 +463,14 @@
 			"mergeOnRecruit" : true
 		},
 		
+		"mapObjects" : 
+		{
+			// Allow behavior that emulates h3 bug where quest from Seer Hut or Border Guard can take entire army from hero
+			// WARNING: handling of heroes without armies is not tested and may lead to bugs or crashes! Use at own risk!
+			// If this option is off, quests will only allow taking entire army if quest reward also gives creatures
+			"h3BugQuestTakesEntireArmy" : false
+		},
+
 		"markets" : 
 		{
 			// period between restocking of "Black Market" object found on adventure map

+ 1 - 0
lib/GameSettings.cpp

@@ -90,6 +90,7 @@ const std::vector<GameSettings::SettingOption> GameSettings::settingProperties =
 		{EGameSettings::MAP_FORMAT_JSON_VCMI,                             "mapFormat", "jsonVCMI"                             },
 		{EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA,                "mapFormat", "restorationOfErathia"                 },
 		{EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH,                       "mapFormat", "shadowOfDeath"                        },
+		{EGameSettings::MAP_OBJECTS_H3_BUG_QUEST_TAKES_ENTIRE_ARMY,       "mapObjects","h3BugQuestTakesEntireArmy"            },
 		{EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD,              "markets",   "blackMarketRestockPeriod"             },
 		{EGameSettings::MODULE_COMMANDERS,                                "modules",   "commanders"                           },
 		{EGameSettings::MODULE_STACK_ARTIFACT,                            "modules",   "stackArtifact"                        },

+ 1 - 1
lib/IGameCallback.h

@@ -103,7 +103,7 @@ public:
 	virtual void giveResources(PlayerColor player, TResources resources)=0;
 
 	virtual void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) =0;
-	virtual void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> &creatures) =0;
+	virtual void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> &creatures, bool forceRemoval = false) =0;
 	virtual bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) =0;
 	virtual bool changeStackType(const StackLocation &sl, const CCreature *c) =0;
 	virtual bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count = -1) =0; //count -1 => moves whole stack

+ 1 - 0
lib/IGameSettings.h

@@ -63,6 +63,7 @@ enum class EGameSettings
 	MAP_FORMAT_JSON_VCMI,
 	MAP_FORMAT_RESTORATION_OF_ERATHIA,
 	MAP_FORMAT_SHADOW_OF_DEATH,
+	MAP_OBJECTS_H3_BUG_QUEST_TAKES_ENTIRE_ARMY,
 	MARKETS_BLACK_MARKET_RESTOCK_PERIOD,
 	MODULE_COMMANDERS,
 	MODULE_STACK_ARTIFACT,

+ 15 - 5
lib/mapObjects/CQuest.cpp

@@ -17,6 +17,7 @@
 #include "../texts/CGeneralTextHandler.h"
 #include "CGCreature.h"
 #include "../IGameCallback.h"
+#include "../IGameSettings.h"
 #include "../entities/artifact/CArtifact.h"
 #include "../entities/hero/CHeroHandler.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
@@ -118,7 +119,7 @@ bool CQuest::checkQuest(const CGHeroInstance * h) const
 	return true;
 }
 
-void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const
+void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h, bool allowFullArmyRemoval) const
 {
 	// FIXME: this should be part of 'reward', and not hacking into limiter state that should only limit access to such reward
 
@@ -152,7 +153,7 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const
 		logGlobal->error("Failed to find artifact %s in inventory of hero %s", elem.toEntity(LIBRARY)->getJsonKey(), h->getHeroTypeID());
 	}
 
-	cb->takeCreatures(h->id, mission.creatures);
+	cb->takeCreatures(h->id, mission.creatures, allowFullArmyRemoval);
 	cb->giveResources(h->getOwner(), -mission.resources);
 }
 
@@ -435,7 +436,8 @@ void CGSeerHut::init(vstd::RNG & rand)
 	seerName = LIBRARY->generaltexth->translate(seerNameID);
 	getQuest().textOption = rand.nextInt(2);
 	getQuest().completedOption = rand.nextInt(1, 3);
-	
+	getQuest().mission.hasExtraCreatures = !allowsFullArmyRemoval();
+
 	configuration.canRefuse = true;
 	configuration.visitMode = Rewardable::EVisitMode::VISIT_ONCE;
 	configuration.selectMode = Rewardable::ESelectMode::SELECT_PLAYER;
@@ -645,14 +647,21 @@ const CGCreature * CGSeerHut::getCreatureToKill(bool allowNull) const
 	return dynamic_cast<const CGCreature *>(o);
 }
 
+bool CGSeerHut::allowsFullArmyRemoval() const
+{
+	bool seerGivesUnits = !configuration.info.empty() && !configuration.info.back().reward.creatures.empty();
+	bool h3BugSettingEnabled = cb->getSettings().getBoolean(EGameSettings::MAP_OBJECTS_H3_BUG_QUEST_TAKES_ENTIRE_ARMY);
+	return seerGivesUnits || h3BugSettingEnabled;
+}
+
 void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, int32_t answer) const
 {
-	CRewardableObject::blockingDialogAnswered(hero, answer);
 	if(answer)
 	{
-		getQuest().completeQuest(cb, hero);
+		getQuest().completeQuest(cb, hero, allowsFullArmyRemoval());
 		cb->setObjPropertyValue(id, ObjProperty::SEERHUT_COMPLETE, !getQuest().repeatedQuest); //mission complete
 	}
+	CRewardableObject::blockingDialogAnswered(hero, answer);
 }
 
 void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler)
@@ -736,6 +745,7 @@ void CGQuestGuard::init(vstd::RNG & rand)
 	blockVisit = true;
 	getQuest().textOption = rand.nextInt(3, 5);
 	getQuest().completedOption = rand.nextInt(4, 5);
+	getQuest().mission.hasExtraCreatures = !allowsFullArmyRemoval();
 	
 	configuration.info.push_back({});
 	configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT;

+ 2 - 1
lib/mapObjects/CQuest.h

@@ -80,7 +80,7 @@ public:
 	void getVisitText(const CGameInfoCallback * cb, MetaString &text, std::vector<Component> & components, bool FirstVisit, const CGHeroInstance * h = nullptr) const;
 	void getCompletionText(const CGameInfoCallback * cb, MetaString &text) const;
 	void getRolloverText (const CGameInfoCallback * cb, MetaString &text, bool onHover) const; //hover or quest log entry
-	void completeQuest(IGameCallback *, const CGHeroInstance * h) const;
+	void completeQuest(IGameCallback *, const CGHeroInstance * h, bool allowFullArmyRemoval) const;
 	void addTextReplacements(const CGameInfoCallback * cb, MetaString &out, std::vector<Component> & components) const;
 	void addKillTargetReplacements(MetaString &out) const;
 	void defineQuestName();
@@ -166,6 +166,7 @@ public:
 		h & seerName;
 	}
 protected:
+	bool allowsFullArmyRemoval() const;
 	void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;
 
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;

+ 0 - 1
lib/mapping/MapFormatH3M.cpp

@@ -2429,7 +2429,6 @@ EQuestMission CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & positi
 		{
 			size_t typeNumber = reader->readUInt8();
 			guard->getQuest().mission.creatures.resize(typeNumber);
-			guard->getQuest().mission.hasExtraCreatures = true;
 			for(size_t hh = 0; hh < typeNumber; ++hh)
 			{
 				guard->getQuest().mission.creatures[hh].setType(reader->readCreature().toCreature());

+ 1 - 1
lib/rewardable/Interface.cpp

@@ -218,7 +218,7 @@ void Rewardable::Interface::grantRewardAfterLevelup(const Rewardable::VisitInfo
 
 	if (!info.reward.takenCreatures.empty())
 	{
-		cb->takeCreatures(hero->id, info.reward.takenCreatures);
+		cb->takeCreatures(hero->id, info.reward.takenCreatures, !info.reward.creatures.empty());
 	}
 
 	if(!info.reward.creaturesChange.empty())

+ 21 - 11
server/CGameHandler.cpp

@@ -1157,26 +1157,36 @@ void CGameHandler::giveCreatures(const CArmedInstance *obj, const CGHeroInstance
 	tryJoiningArmy(obj, h, remove, true);
 }
 
-void CGameHandler::takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> &creatures)
+void CGameHandler::takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> &creatures, bool forceRemoval)
 {
-	std::vector<CStackBasicDescriptor> cres = creatures;
-	if (cres.size() <= 0)
+	std::vector<CStackBasicDescriptor> remainerForTaking = creatures;
+	if (remainerForTaking.empty())
 		return;
-	const CArmedInstance* obj = static_cast<const CArmedInstance*>(getObj(objid));
 
-	for (CStackBasicDescriptor &sbd : cres)
+	const CArmedInstance* army = static_cast<const CArmedInstance*>(getObj(objid));
+
+	for (const CStackBasicDescriptor &stackToTake : remainerForTaking)
 	{
 		TQuantity collected = 0;
-		while(collected < sbd.getCount())
+		while(collected < stackToTake.getCount())
 		{
 			bool foundSth = false;
-			for (auto i = obj->Slots().begin(); i != obj->Slots().end(); i++)
+			for (const auto & armySlot : army->Slots())
 			{
-				if (i->second->getType() == sbd.getType())
+				if (armySlot.second->getType() == stackToTake.getType())
 				{
-					TQuantity take = std::min(sbd.getCount() - collected, i->second->getCount()); //collect as much cres as we can
-					changeStackCount(StackLocation(obj->id, i->first), -take, false);
-					collected += take;
+					if (stackToTake.getCount() - collected >= armySlot.second->getCount())
+					{
+						// take entire stack
+						collected += armySlot.second->getCount();
+						eraseStack(StackLocation(army->id, armySlot.first), forceRemoval);
+					}
+					else
+					{
+						// take part of the stack
+						collected = stackToTake.getCount();
+						changeStackCount(StackLocation(army->id, armySlot.first), collected - stackToTake.getCount(), false);
+					}
 					foundSth = true;
 					break;
 				}

+ 1 - 1
server/CGameHandler.h

@@ -125,7 +125,7 @@ public:
 	void giveResources(PlayerColor player, TResources resources) override;
 
 	void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) override;
-	void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> &creatures) override;
+	void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> &creatures, bool forceRemoval) override;
 	bool changeStackType(const StackLocation &sl, const CCreature *c) override;
 	bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) override;
 	bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) override;

+ 1 - 1
test/mock/mock_IGameCallback.h

@@ -62,7 +62,7 @@ public:
 	void giveResources(PlayerColor player, TResources resources) override {}
 
 	void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) override {}
-	void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> &creatures) override {}
+	void takeCreatures(ObjectInstanceID objid, const std::vector<CStackBasicDescriptor> &creatures, bool forceRemoval) override {}
 	bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) override {return false;}
 	bool changeStackType(const StackLocation &sl, const CCreature *c) override {return false;}
 	bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count = -1) override {return false;} //count -1 => moves whole stack