Browse Source

Merge pull request #6364 from IvanSavenko/bugfixing

Fixes for multiple reported issues
Ivan Savenko 2 weeks ago
parent
commit
0d6e5f4d69

+ 1 - 1
config/schemas/spell.json

@@ -284,7 +284,7 @@
 							"removeOnTrigger" : { "type" : "boolean" },
 							"dispellable" : { "type" : "boolean" },
 							"moatDamage" : { "type" : "number" },
-							"moatHexes" : {  },
+							"moatHexes" : { "type" : "array", "items" : { "type" : "number" } },
 							"triggerAbility" : { "type" : "string" },
 							"defender" : {  },
 							"bonus" : { "additionalProperties" : { "$ref" : "bonusInstance.json" } }

+ 20 - 51
docs/modders/Campaign_Format.md

@@ -89,10 +89,6 @@ Scenario description looks like follow:
 - `"heroKeeps"` defines what hero will carry to the next scenario. Can be specified one or several attributes from list `"experience", "primarySkills", "secondarySkills", "spells", "artifacts"`
 - `"keepCreatures"` array of creature types which hero will carry to the next scenario. Game identifiers are used to specify creature type.
 - `"startOptions"` defines what type of bonuses player may have. Possible values are `"none", "bonus", "crossover", "hero"`
-  - `none`: player starts scenario without bonuses. [Description](#none-start-option)
-  - `bonus`: player chooses one of the predefined bonuses. [Description](#bonus-start-option)
-  - `crossover`: player will start with hero from previous scenario. [Description](#crossover-start-option)
-  - `hero` : player will start scenario with specified hero. [Description](#hero-start-option)
 - `"playerColor"` defines color id of flag which player will play for. Possible values are `0: red, 1: blue, tan: 2, green: 3, orange: 4, purple: 5, teal: 6, pink: 7`
 - "bonuses" array of possible bonus objects, format depends on `"startOptions"` parameter
 
@@ -109,13 +105,7 @@ Prolog and epilog properties are optional
 }
 ```
 
-### Start options and bonuses
-
-#### None start option
-
-If `startOptions` is `none`, `bonuses` field will be ignored
-
-#### Bonus start option
+### Bonus start option
 
 If `startOptions` is `bonus`, bonus format may vary depending on its type.
 
@@ -127,22 +117,22 @@ If `startOptions` is `bonus`, bonus format may vary depending on its type.
 },
 ```
 
-- `"what"` field defines bonus type. Possible values are: `spell, creature, building, artifact, scroll, primarySkill, secondarySkill, resource`
+- `"what"` field defines bonus type. Possible values are: `spell, creature, building, artifact, scroll, primarySkill, secondarySkill, resource, prevHero, hero`
   - `"spell"` has following attributes (fields):
     - `"hero"`: hero who will get spell (see below)
-    - `"type"`: spell type, string, e.g. "firewall"
+    - `"spellType"`: spell type, string, e.g. "firewall"
   - `"creature"` has following attributes (fields):
     - `"hero"`: hero who will get spell (see below)
-    - `"type"`: creature type, string, e.g. "pikeman"
-    - `"amount"`: amount of creatures
+    - `"creatureType"`: creature type, string, e.g. "pikeman"
+    - `"creatureAmount"`: amount of creatures
   - `"building"` has following attributes (fields):
-    - `"type"`: building type (string), e.g. "citadel" or "dwellingLvl1"
+    - `"buildingType"`: building type (string), e.g. "citadel" or "dwellingLvl1"
   - `"artifact"` has following attributes (fields):
     - `"hero"`: hero who will get spell (see below)
-    - `"type"`: artifact type, string, e.g. "spellBook"
+    - `"artifactType"`: artifact type, string, e.g. "spellBook"
   - `"scroll"` has following attributes (fields):
     - `"hero"`: hero who will get spell (see below)
-    - `"type"`: spell type in the scroll, string, e.g. "firewall"
+    - `"spellType"`: spell type in the scroll, string, e.g. "firewall"
   - `"primarySkill"` has following attributes (fields):
     - `"hero"`: hero who will get spell (see below)
     - `"attack"`: amount of attack gained
@@ -151,40 +141,19 @@ If `startOptions` is `bonus`, bonus format may vary depending on its type.
     - `"knowledge"`: amount of knowledge gained
   - `"secondarySkill"` has following attributes (fields):
     - `"hero"`: hero who will get spell (see below)
-    - `"type"`: skill type, string, e.g. "logistics"
-    - `"amount"`: skill level, `1: beginner, 2: advanced, 3: expert`
+    - `"skillType"`: skill type, string, e.g. "logistics"
+    - `"skillMastery"`: skill level, `1: beginner, 2: advanced, 3: expert`
   - `"resource"` has following attributes (fields):
-    - `"type"`: resource type, one of `wood, ore, mercury, sulfur, crystal, gems, gold, common, rare`, where `common` is both wood and ore, `rare` means that bonus gives each rare resource
-    - `"amount"`: amount of resources
-- `"hero"` can be specified as explicit hero name and as one of keywords: `strongest`, `generated`
-
-#### Crossover start option
-
-If `startOptions` is `crossover`, heroes from specific scenario will be moved to this scenario. Bonus format is following
-
-```json
-{
-    "playerColor": 0,
-    "scenario": 0
-},
-```
-
-- `"playerColor"` from what player color heroes shall be taken. Possible values are `0: red, 1: blue, tan: 2, green: 3, orange: 4, purple: 5, teal: 6, pink: 7`
-- `"scenario"` from which scenario heroes shall be taken. 0 means first scenario
-
-#### Hero start option
-
-If `startOptions` is `hero`, hero can be chosen as a starting bonus. Bonus format is following
-
-```json
-{
-    "playerColor": 0,
-    "hero": "random"
-}
-```
-
-- `"playerColor"` from what player color heroes shall be taken. Possible values are `0: red, 1: blue, tan: 2, green: 3, orange: 4, purple: 5, teal: 6, pink: 7`
-- `"hero"` can be specified as explicit hero name and as one of keywords: `random`
+    - `"resourceType"`: resource type, one of `wood, ore, mercury, sulfur, crystal, gems, gold, common, rare`, where `common` is both wood and ore, `rare` means that bonus gives each rare resource
+    - `"resourceAmount"`: amount of resources
+  - `"prevHero"` has following attributes (fields):
+    - `"scenario"`: scenario from which to pick heroes from
+    - `"playerColor"`: color which player will play as in current scenario
+  - `"hero"` has following attributes (fields):
+    - `"hero"`: hero that player would receive on start, or `random`
+    - `"playerColor"`: color which player will play as in current scenario
+  
+`hero` field in all bonus types can be specified as explicit hero name and as one of keywords: `strongest`, `generated`
 
 ### Regions description
 

+ 7 - 1
docs/modders/Entities_Format/Battle_Obstacle_Format.md

@@ -4,7 +4,13 @@
 
 ```json
 	// List of terrains on which this obstacle can be used
-	"allowedTerrains" : []
+	
+	"allowedTerrains" : [
+		"dirt",
+		"sand",
+		"asphalt", // terrain from mod, such form can only be used if mod has dependency on mod with such terrain
+		"hota:wasteland", // terrain from mod, such form can be used even without dependency, entry will be ignored if such mod does not exists
+	]
 	
 	// List of special battlefields on which this obstacle can be used
 	"specialBattlefields" : []

+ 4 - 2
docs/modders/Entities_Format/Hero_Class_Format.md

@@ -28,9 +28,11 @@ In order to make functional hero class you also need:
 
 	// Description of map object representing this hero class.
 	"mapObject" : {
-		// Optional, hero ID-base filter, using same rules as building requirements
+		// Optional, filter to apply specific template to specific heroes
 		"filters" : {
-			"mutare" : [ "anyOf", [ "mutare" ], [ "mutareDrake" ]]
+			"maleOnly" : { "male" : true },
+			"femaleOnly" : { "female" : true },
+			"mutareOnly" : { "hero" : "mutare" }
 		},
 
 		// List of templates used for this object, normally - only one is needed. See map template format for details

+ 7 - 2
docs/modders/Map_Object_Format.md

@@ -194,8 +194,13 @@ These are internal types that are generally not available for modding and are ha
 		// optional; default or if explicitly set to null: all "land" terrains (e.g. not rock and not water)
 		// allowed terrain types to place object to. Affects also RMG.
 		// Note that map editor will still allow to place object on other terrains
-		// allowed terrain types: "dirt", "sand", "grass", "snow", "swamp", "rough", "subterra", "lava", "water", "rock"
-		"allowedTerrains":["dirt", "sand"],
+		// h3 terrain types: "dirt", "sand", "grass", "snow", "swamp", "rough", "subterra", "lava", "water", "rock"
+		"allowedTerrains":[
+			"dirt",
+			"sand",
+			"asphalt", // terrain from mod, such form can only be used if mod has dependency on mod with such terrain
+			"hota:wasteland", // terrain from mod, such form can be used even without dependency, entry will be ignored if such mod does not exists
+		],
 
 		//zindex, defines order in which objects on same tile will be blit. optional, default is 0 
 		//NOTE: legacy overlay objects has zindex = 100

+ 1 - 1
lib/ObstacleHandler.cpp

@@ -101,7 +101,7 @@ std::shared_ptr<ObstacleInfo> ObstacleHandler::loadFromJson(const std::string &
 	info->height = json["height"].Integer();
 	for(const auto & t : json["allowedTerrains"].Vector())
 	{
-		LIBRARY->identifiers()->requestIdentifier("terrain", t, [info](int32_t identifier){
+		LIBRARY->identifiers()->requestIdentifierIfFound("terrain", t, [info](int32_t identifier){
 			info->allowedTerrains.emplace_back(identifier);
 		});
 	}

+ 3 - 3
lib/campaign/CampaignBonus.cpp

@@ -30,13 +30,13 @@ static const std::map<std::string, CampaignBonusType> bonusTypeMap = {
 	{"hero", CampaignBonusType::HERO},
 };
 
-static const std::map<std::string, ui16> heroSpecialMap = {
+static const std::map<std::string, HeroTypeID> heroSpecialMap = {
 	{"strongest", HeroTypeID::CAMP_STRONGEST},
 	{"generated", HeroTypeID::CAMP_GENERATED},
 	{"random", HeroTypeID::CAMP_RANDOM}
 };
 
-static const std::map<std::string, ui8> resourceTypeMap = {
+static const std::map<std::string, EGameResID> resourceTypeMap = {
 	{"wood", EGameResID::WOOD},
 	{"mercury", EGameResID::MERCURY},
 	{"ore", EGameResID::ORE},
@@ -172,7 +172,7 @@ CampaignBonus::CampaignBonus(const JsonNode & bjson, CampaignStartOptions mode)
 	const auto & decodeHeroTypeID = [](JsonNode heroType) -> HeroTypeID
 	{
 		if(vstd::contains(heroSpecialMap, heroType.String()))
-			return HeroTypeID(heroSpecialMap.at(heroType.String()));
+			return heroSpecialMap.at(heroType.String());
 		else
 			return HeroTypeID(HeroTypeID::decode(heroType.String()));
 	};

+ 11 - 10
lib/mapObjects/ObjectTemplate.cpp

@@ -220,19 +220,20 @@ void ObjectTemplate::readJson(const JsonNode &node, const bool withTerrain)
 	else
 		visitDir = 0x00;
 
-	if(withTerrain && !node["allowedTerrains"].isNull())
+	anyLandTerrain = true;
+	if(withTerrain)
 	{
-		for(const auto & entry : node["allowedTerrains"].Vector())
+		if (!node["allowedTerrains"].isNull())
 		{
-			LIBRARY->identifiers()->requestIdentifier("terrain", entry, [this](int32_t identifier){
-				allowedTerrains.insert(TerrainId(identifier));
-			});
+			anyLandTerrain = false;
+
+			for(const auto & entry : node["allowedTerrains"].Vector())
+			{
+				LIBRARY->identifiers()->requestIdentifierIfFound("terrain", entry, [this](int32_t identifier){
+					allowedTerrains.insert(TerrainId(identifier));
+				});
+			}
 		}
-		anyLandTerrain = false;
-	}
-	else
-	{
-		anyLandTerrain = true;
 	}
 
 	auto charToTile = [&](const char & ch) -> ui8

+ 32 - 12
lib/modding/ModManager.cpp

@@ -767,35 +767,45 @@ void ModDependenciesResolver::tryAddMods(TModList modsToResolve, const ModsStora
 	std::set<TModID> resolvedModIDs(activeMods.begin(), activeMods.end()); // Use a set for validation for performance reason, but set does not keep order of elements
 	std::set<TModID> notResolvedModIDs(modsToResolve.begin(), modsToResolve.end()); // Use a set for validation for performance reason
 
+	enum class ModResolveStatus {
+		RESOLVED, // ok - mod can be added to load order
+		WAITING, // maybe - wait for more iterations before deciding
+		BROKEN // fail - this mod definitely can't be loaded
+	};
+
 	// Mod is resolved if it has no dependencies or all its dependencies are already resolved
-	auto isResolved = [&](const ModDescription & mod) -> bool
+	auto isResolved = [&](const ModDescription & mod) -> ModResolveStatus
 	{
 		if (mod.isTranslation() && CGeneralTextHandler::getPreferredLanguage() != mod.getBaseLanguage())
-			return false;
+			return ModResolveStatus::BROKEN;
 
 		if(!mod.isCompatible())
-			return false;
-
-		if(mod.getDependencies().size() > resolvedModIDs.size())
-			return false;
+			return ModResolveStatus::BROKEN;
 
 		for(const TModID & dependency : mod.getDependencies())
+		{
 			if(!vstd::contains(resolvedModIDs, dependency))
-				return false;
+			{
+				if (vstd::contains(notResolvedModIDs, dependency))
+					return ModResolveStatus::WAITING;
+				else
+					return ModResolveStatus::BROKEN;
+			}
+		}
 
 		for(const TModID & softDependency : mod.getSoftDependencies())
 			if(vstd::contains(notResolvedModIDs, softDependency))
-				return false;
+				return ModResolveStatus::WAITING;
 
 		for(const TModID & conflict : mod.getConflicts())
 			if(vstd::contains(resolvedModIDs, conflict))
-				return false;
+				return ModResolveStatus::BROKEN;
 
 		for(const TModID & reverseConflict : resolvedModIDs)
 			if(vstd::contains(storage.getMod(reverseConflict).getConflicts(), mod.getID()))
-				return false;
+				return ModResolveStatus::BROKEN;
 
-		return true;
+		return ModResolveStatus::RESOLVED;
 	};
 
 	while(true)
@@ -803,7 +813,9 @@ void ModDependenciesResolver::tryAddMods(TModList modsToResolve, const ModsStora
 		std::set<TModID> resolvedOnCurrentTreeLevel;
 		for(auto it = modsToResolve.begin(); it != modsToResolve.end();) // One iteration - one level of mods tree
 		{
-			if(isResolved(storage.getMod(*it)))
+			ModResolveStatus status = isResolved(storage.getMod(*it));
+
+			if (status == ModResolveStatus::RESOLVED)
 			{
 				resolvedOnCurrentTreeLevel.insert(*it); // Not to the resolvedModIDs, so current node children will be resolved on the next iteration
 				assert(!vstd::contains(sortedValidMods, *it));
@@ -811,6 +823,14 @@ void ModDependenciesResolver::tryAddMods(TModList modsToResolve, const ModsStora
 				it = modsToResolve.erase(it);
 				continue;
 			}
+			if (status == ModResolveStatus::BROKEN)
+			{
+				resolvedOnCurrentTreeLevel.insert(*it);
+				brokenMods.push_back(*it);
+				it = modsToResolve.erase(it);
+				continue;
+			}
+
 			it++;
 		}
 		if(!resolvedOnCurrentTreeLevel.empty())

+ 2 - 2
server/battles/BattleActionProcessor.cpp

@@ -274,7 +274,7 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c
 	}
 
 	static const auto firstStrikeSelector = Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeAll).Or(Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeMelee));
-	const bool firstStrike = destinationStack->hasBonus(firstStrikeSelector);
+	const bool firstStrike = destinationStack->hasBonus(firstStrikeSelector) && !destinationStack->hasBonusOfType(BonusType::NOT_ACTIVE);
 
 	const bool retaliation = destinationStack->ableToRetaliate();
 	bool ferocityApplied = false;
@@ -367,7 +367,7 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co
 	if(!emptyTileAreaAttack)
 	{
 		static const auto firstStrikeSelector = Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeAll).Or(Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeRanged));
-		firstStrike = destinationStack->hasBonus(firstStrikeSelector);
+		firstStrike = destinationStack->hasBonus(firstStrikeSelector) && !destinationStack->hasBonusOfType(BonusType::NOT_ACTIVE);
 	}
 
 	if (!firstStrike)