Browse Source

Enable Limiter nesting with AllOf, AnyOf, NoneOf (#439)

* Renamed LimiterList to AllOfLimiter and added AnyOfLimiter, NoneOfLimiter
* Updated bonus schema to new limiter format
Henning Koehler 7 years ago
parent
commit
82f334b503
8 changed files with 252 additions and 95 deletions
  1. 1 0
      ChangeLog
  2. 37 25
      config/schemas/bonus.json
  3. 73 7
      lib/HeroBonus.cpp
  4. 43 6
      lib/HeroBonus.h
  5. 91 55
      lib/JsonNode.cpp
  6. 2 0
      lib/JsonNode.h
  7. 4 1
      lib/registerTypes/RegisterTypes.h
  8. 1 1
      lib/serializer/CSerializer.h

+ 1 - 0
ChangeLog

@@ -28,6 +28,7 @@ MODS:
 * Added support for modding of original secondary skills and creation of new ones.
 * Map object sounds can now be configured via json
 * Added bonus updaters for hero specialties
+* Added allOf, anyOf and noneOf qualifiers for bonus limiters
 
 SOUND:
 * Fixed many mising or wrong pickup and visit sounds for map objects

+ 37 - 25
config/schemas/bonus.json

@@ -5,6 +5,40 @@
 	"description" : "Subsection of several formats, used to add generic bonuses to objects",
 	"required": ["type"],
 
+	"definitions" :
+	{
+		"nestedLimiter" : {
+			"anyOf" : [
+				{ 
+					"type" : "string",
+					"description" : "parameterless limiter or boolean operator at start of array"
+				},
+				{
+					"type" : "object",
+					"additionalProperties" : false,
+					"properties" : {
+						"type" : {
+							"type" : "string",
+							"description" : "type"
+						},
+						"parameters" : {
+							"type" : "array",
+							"description" : "parameters",
+							"additionalItems" : true
+						},
+					}
+				},
+				{
+					"type" : "array",
+					"additionalItems" : {
+						"$ref" : "#/definitions/nestedLimiter",
+						"description" : "nested limiters optionally prefixed with boolean operator"
+					}
+				}
+			]
+		}
+	},
+
 	"additionalProperties" : false,
 	"properties":{
 		"addInfo": {
@@ -38,31 +72,9 @@
 			"type":"string",
 			"description": "effectRange"
 		},
-		"limiters": {
-			"type":"array",
-			"description": "limiters",
-			"items": {
-				"oneOf" : [
-					{
-						"type":"object",
-						"additionalProperties" : false,
-						"properties" : {
-							"parameters": {
-								"type":"array",
-								"description" : "parameters",
-								"additionalItems": true
-							},
-							"type": {
-								"type":"string",
-								"description": "type"
-							}
-						}
-					},
-					{
-						"type" : "string"
-					}
-				]
-			}
+		"limiters" : {
+			"$ref" : "#/definitions/nestedLimiter",
+			"description" : "limiter"
 		},
 		"propagator": {
 			"description": "propagator",

+ 73 - 7
lib/HeroBonus.cpp

@@ -1171,7 +1171,7 @@ void CBonusSystemNode::limitBonuses(const BonusList &allBonuses, BonusList &out)
 		for(int i = 0; i < undecided.size(); i++)
 		{
 			auto b = undecided[i];
-			BonusLimitationContext context = {b, *this, out};
+			BonusLimitationContext context = {b, *this, out, undecided};
 			int decision = b->limiter ? b->limiter->limit(context) : ILimiter::ACCEPT; //bonuses without limiters will be accepted by default
 			if(decision == ILimiter::DISCARD)
 			{
@@ -1347,7 +1347,7 @@ JsonNode Bonus::toJsonNode() const
 	if(turnsRemain)
 		root["turns"].Integer() = turnsRemain;
 	if(limiter)
-		root["limiters"].Vector().push_back(limiter->toJsonNode());
+		root["limiters"] = limiter->toJsonNode();
 	if(updater)
 		root["updater"] = updater->toJsonNode();
 	if(propagator)
@@ -1557,11 +1557,11 @@ std::shared_ptr<Bonus> Bonus::addLimiter(TLimiterPtr Limiter)
 	if (limiter)
 	{
 		//If we already have limiter list, retrieve it
-		auto limiterList = std::dynamic_pointer_cast<LimiterList>(limiter);
+		auto limiterList = std::dynamic_pointer_cast<AllOfLimiter>(limiter);
 		if(!limiterList)
 		{
 			//Create a new limiter list with old limiter and the new one will be pushed later
-			limiterList = std::make_shared<LimiterList>();
+			limiterList = std::make_shared<AllOfLimiter>();
 			limiterList->add(limiter);
 			limiter = limiterList;
 		}
@@ -1661,6 +1661,10 @@ int HasAnotherBonusLimiter::limit(const BonusLimitationContext &context) const
 	if(context.alreadyAccepted.getFirst(mySelector))
 		return ACCEPT;
 
+	//if there are no matching bonuses pending, we can (and must) reject right away
+	if(!context.stillUndecided.getFirst(mySelector))
+		return DISCARD;
+
 	//do not accept for now but it may change if more bonuses gets included
 	return NOT_SURE;
 }
@@ -1824,7 +1828,30 @@ StackOwnerLimiter::StackOwnerLimiter(PlayerColor Owner)
 {
 }
 
-int LimiterList::limit( const BonusLimitationContext &context ) const
+// Aggregate/Boolean Limiters
+
+void AggregateLimiter::add(TLimiterPtr limiter)
+{
+	if(limiter)
+		limiters.push_back(limiter);
+}
+
+JsonNode AggregateLimiter::toJsonNode() const
+{
+	JsonNode result(JsonNode::JsonType::DATA_VECTOR);
+	result.Vector().push_back(JsonUtils::stringNode(getAggregator()));
+	for(auto l : limiters)
+		result.Vector().push_back(l->toJsonNode());
+	return result;
+}
+
+const std::string AllOfLimiter::aggregator = "allOf";
+const std::string & AllOfLimiter::getAggregator() const
+{
+	return aggregator;
+}
+
+int AllOfLimiter::limit(const BonusLimitationContext & context) const
 {
 	bool wasntSure = false;
 
@@ -1840,9 +1867,48 @@ int LimiterList::limit( const BonusLimitationContext &context ) const
 	return wasntSure ? ILimiter::NOT_SURE : ILimiter::ACCEPT;
 }
 
-void LimiterList::add( TLimiterPtr limiter )
+const std::string AnyOfLimiter::aggregator = "anyOf";
+const std::string & AnyOfLimiter::getAggregator() const
 {
-	limiters.push_back(limiter);
+	return aggregator;
+}
+
+int AnyOfLimiter::limit(const BonusLimitationContext & context) const
+{
+	bool wasntSure = false;
+
+	for(auto limiter : limiters)
+	{
+		auto result = limiter->limit(context);
+		if(result == ILimiter::ACCEPT)
+			return result;
+		if(result == ILimiter::NOT_SURE)
+			wasntSure = true;
+	}
+
+	return wasntSure ? ILimiter::NOT_SURE : ILimiter::DISCARD;
+}
+
+const std::string NoneOfLimiter::aggregator = "noneOf";
+const std::string & NoneOfLimiter::getAggregator() const
+{
+	return aggregator;
+}
+
+int NoneOfLimiter::limit(const BonusLimitationContext & context) const
+{
+	bool wasntSure = false;
+
+	for(auto limiter : limiters)
+	{
+		auto result = limiter->limit(context);
+		if(result == ILimiter::ACCEPT)
+			return ILimiter::DISCARD;
+		if(result == ILimiter::NOT_SURE)
+			wasntSure = true;
+	}
+
+	return wasntSure ? ILimiter::NOT_SURE : ILimiter::ACCEPT;
 }
 
 // Updaters

+ 43 - 6
lib/HeroBonus.h

@@ -615,8 +615,9 @@ public:
 struct BonusLimitationContext
 {
 	const std::shared_ptr<Bonus> b;
-	const CBonusSystemNode &node;
-	const BonusList &alreadyAccepted;
+	const CBonusSystemNode & node;
+	const BonusList & alreadyAccepted;
+	const BonusList & stillUndecided;
 };
 
 class DLL_LINKAGE ILimiter
@@ -873,14 +874,50 @@ public:
 	}
 };
 
-//Stores multiple limiters. If any of them fails -> bonus is dropped.
-class DLL_LINKAGE LimiterList : public ILimiter
+class DLL_LINKAGE AggregateLimiter : public ILimiter
 {
+protected:
 	std::vector<TLimiterPtr> limiters;
-
+	virtual const std::string & getAggregator() const = 0;
 public:
-	int limit(const BonusLimitationContext &context) const override;
 	void add(TLimiterPtr limiter);
+	JsonNode toJsonNode() const override;
+
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & static_cast<ILimiter&>(*this);
+		if(version >= 786)
+		{
+			h & limiters;
+		}
+	}
+};
+
+class DLL_LINKAGE AllOfLimiter : public AggregateLimiter
+{
+protected:
+	const std::string & getAggregator() const override;
+public:
+	static const std::string aggregator;
+	int limit(const BonusLimitationContext & context) const override;
+};
+
+class DLL_LINKAGE AnyOfLimiter : public AggregateLimiter
+{
+protected:
+	const std::string & getAggregator() const override;
+public:
+	static const std::string aggregator;
+	int limit(const BonusLimitationContext & context) const override;
+};
+
+class DLL_LINKAGE NoneOfLimiter : public AggregateLimiter
+{
+protected:
+	const std::string & getAggregator() const override;
+public:
+	static const std::string aggregator;
+	int limit(const BonusLimitationContext & context) const override;
 };
 
 class DLL_LINKAGE CCreatureTypeLimiter : public ILimiter //affect only stacks of given creature (and optionally it's upgrades)

+ 91 - 55
lib/JsonNode.cpp

@@ -567,6 +567,96 @@ void JsonUtils::resolveIdentifier(const JsonNode &node, si32 &var)
 	}
 }
 
+std::shared_ptr<ILimiter> JsonUtils::parseLimiter(const JsonNode & limiter)
+{
+	switch(limiter.getType())
+	{
+	case JsonNode::JsonType::DATA_VECTOR:
+		{
+			const JsonVector & subLimiters = limiter.Vector();
+			if(subLimiters.size() == 0)
+			{
+				logMod->warn("Warning: empty limiter list");
+				return std::make_shared<AllOfLimiter>();
+			}
+			std::shared_ptr<AggregateLimiter> result;
+			int offset = 1;
+			// determine limiter type and offset for sub-limiters
+			if(subLimiters[0].getType() == JsonNode::JsonType::DATA_STRING)
+			{
+				const std::string & aggregator = subLimiters[0].String();
+				if(aggregator == AllOfLimiter::aggregator)
+					result = std::make_shared<AllOfLimiter>();
+				else if(aggregator == AnyOfLimiter::aggregator)
+					result = std::make_shared<AnyOfLimiter>();
+				else if(aggregator == NoneOfLimiter::aggregator)
+					result = std::make_shared<NoneOfLimiter>();
+			}
+			if(!result)
+			{
+				// collapse for single limiter without explicit aggregate operator
+				if(subLimiters.size() == 1)
+					return parseLimiter(subLimiters[0]);
+				// implicit aggregator must be allOf
+				result = std::make_shared<AllOfLimiter>();
+				offset = 0;
+			}
+			if(subLimiters.size() == offset)
+				logMod->warn("Warning: empty sub-limiter list");
+			for(int sl = offset; sl < subLimiters.size(); ++sl)
+				result->add(parseLimiter(subLimiters[sl]));
+			return result;
+		}
+		break;
+	case JsonNode::JsonType::DATA_STRING: //pre-defined limiters
+		return parseByMap(bonusLimiterMap, &limiter, "limiter type ");
+		break;
+	case JsonNode::JsonType::DATA_STRUCT: //customizable limiters
+		{
+			std::string limiterType = limiter["type"].String();
+			const JsonVector & parameters = limiter["parameters"].Vector();
+			if(limiterType == "CREATURE_TYPE_LIMITER")
+			{
+				std::shared_ptr<CCreatureTypeLimiter> creatureLimiter = std::make_shared<CCreatureTypeLimiter>();
+				VLC->modh->identifiers.requestIdentifier("creature", parameters[0], [=](si32 creature)
+				{
+					creatureLimiter->setCreature(CreatureID(creature));
+				});
+				creatureLimiter->includeUpgrades = parameters.size() > 1 ? parameters[1].Bool() : false;
+				return creatureLimiter;
+			}
+			else if(limiterType == "HAS_ANOTHER_BONUS_LIMITER")
+			{
+				std::string anotherBonusType = parameters[0].String();
+				auto it = bonusNameMap.find(anotherBonusType);
+				if(it == bonusNameMap.end())
+				{
+					logMod->error("Error: invalid ability type %s.", anotherBonusType);
+				}
+				else
+				{
+					std::shared_ptr<HasAnotherBonusLimiter> bonusLimiter = std::make_shared<HasAnotherBonusLimiter>();
+					bonusLimiter->type = it->second;
+					if(parameters.size() > 1)
+					{
+						resolveIdentifier(parameters[1], bonusLimiter->subtype);
+						bonusLimiter->isSubtypeRelevant = true;
+					}
+					return bonusLimiter;
+				}
+			}
+			else
+			{
+				logMod->error("Error: invalid customizable limiter type %s.", limiterType);
+			}
+		}
+		break;
+	default:
+		break;
+	}
+	return nullptr;
+}
+
 std::shared_ptr<Bonus> JsonUtils::parseBonus(const JsonNode &ability)
 {
 	auto b = std::make_shared<Bonus>();
@@ -641,61 +731,7 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b)
 
 	value = &ability["limiters"];
 	if (!value->isNull())
-	{
-		for (const JsonNode & limiter : value->Vector())
-		{
-			switch (limiter.getType())
-			{
-				case JsonNode::JsonType::DATA_STRING: //pre-defined limiters
-					b->limiter = parseByMap(bonusLimiterMap, &limiter, "limiter type ");
-					break;
-				case JsonNode::JsonType::DATA_STRUCT: //customizable limiters
-					{
-						std::shared_ptr<ILimiter> l;
-						if (limiter["type"].String() == "CREATURE_TYPE_LIMITER")
-						{
-							std::shared_ptr<CCreatureTypeLimiter> l2 = std::make_shared<CCreatureTypeLimiter>(); //TODO: How the hell resolve pointer to creature?
-							const JsonVector vec = limiter["parameters"].Vector();
-							VLC->modh->identifiers.requestIdentifier("creature", vec[0], [=](si32 creature)
-							{
-								l2->setCreature(CreatureID(creature));
-							});
-							if (vec.size() > 1)
-							{
-								l2->includeUpgrades = vec[1].Bool();
-							}
-							else
-								l2->includeUpgrades = false;
-
-							l = l2;
-						}
-						if (limiter["type"].String() == "HAS_ANOTHER_BONUS_LIMITER")
-						{
-							std::shared_ptr<HasAnotherBonusLimiter> l2 = std::make_shared<HasAnotherBonusLimiter>();
-							const JsonVector vec = limiter["parameters"].Vector();
-							std::string anotherBonusType = vec[0].String();
-
-							auto it = bonusNameMap.find(anotherBonusType);
-							if (it == bonusNameMap.end())
-							{
-								logMod->error("Error: invalid ability type %s.", anotherBonusType);
-								continue;
-							}
-							l2->type = it->second;
-
-							if (vec.size() > 1 )
-							{
-								resolveIdentifier(vec[1], l2->subtype);
-								l2->isSubtypeRelevant = true;
-							}
-							l = l2;
-						}
-						b->addLimiter(l);
-					}
-					break;
-			}
-		}
-	}
+		b->limiter = parseLimiter(*value);
 
 	value = &ability["propagator"];
 	if (!value->isNull())

+ 2 - 0
lib/JsonNode.h

@@ -16,6 +16,7 @@ typedef std::vector <JsonNode> JsonVector;
 struct Bonus;
 class ResourceID;
 class CAddInfo;
+class ILimiter;
 
 class DLL_LINKAGE JsonNode
 {
@@ -168,6 +169,7 @@ namespace JsonUtils
 	DLL_LINKAGE std::shared_ptr<Bonus> parseBonus(const JsonVector &ability_vec);
 	DLL_LINKAGE std::shared_ptr<Bonus> parseBonus(const JsonNode &ability);
 	DLL_LINKAGE bool parseBonus(const JsonNode &ability, Bonus *placement);
+	DLL_LINKAGE std::shared_ptr<ILimiter> parseLimiter(const JsonNode & limiter);
 	DLL_LINKAGE void resolveIdentifier(si32 &var, const JsonNode &node, std::string name);
 	DLL_LINKAGE void resolveIdentifier(const JsonNode &node, si32 &var);
 	DLL_LINKAGE void resolveAddInfo(CAddInfo & var, const JsonNode & node);

+ 4 - 1
lib/registerTypes/RegisterTypes.h

@@ -139,6 +139,9 @@ void registerTypesMapObjectTypes(Serializer &s)
 	s.template registerType<IUpdater, GrowsWithLevelUpdater>();
 	s.template registerType<IUpdater, TimesHeroLevelUpdater>();
 	s.template registerType<IUpdater, TimesStackLevelUpdater>();
+
+	s.template registerType<ILimiter, AnyOfLimiter>();
+	s.template registerType<ILimiter, NoneOfLimiter>();
 	//new types (other than netpacks) must register here
 	//order of type registration is critical for loading old savegames
 }
@@ -178,7 +181,7 @@ void registerTypesMapObjects2(Serializer &s)
 
 	// Limiters
 	//s.template registerType<ILimiter>();
-	s.template registerType<ILimiter, LimiterList>();
+	s.template registerType<ILimiter, AllOfLimiter>();
 	s.template registerType<ILimiter, CCreatureTypeLimiter>();
 	s.template registerType<ILimiter, HasAnotherBonusLimiter>();
 	s.template registerType<ILimiter, CreatureNativeTerrainLimiter>();

+ 1 - 1
lib/serializer/CSerializer.h

@@ -12,7 +12,7 @@
 #include "../ConstTransitivePtr.h"
 #include "../GameConstants.h"
 
-const ui32 SERIALIZATION_VERSION = 785;
+const ui32 SERIALIZATION_VERSION = 786;
 const ui32 MINIMAL_SERIALIZATION_VERSION = 753;
 const std::string SAVEGAME_MAGIC = "VCMISVG";