Browse Source

Enabled new secondary skills to be created (#438)

* Universities, Scholars and Witch Huts may offer new skills
* Moved encode/decodeSkill to CSkillHandler
* Refactored CSkill interface and CSkill::LevelInfo image storage
* Legacy game constants renamed to ORIGINAL_XXX_QUANTITY
Henning Koehler 7 năm trước cách đây
mục cha
commit
6ddcb079a4

+ 1 - 1
ChangeLog

@@ -25,7 +25,7 @@ SPELLS:
 
 MODS:
 * Improve support for WoG commander artifacts and skill descriptions
-* Added basic support for secondary skill modding
+* 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
 

+ 13 - 0
client/Graphics.cpp

@@ -24,6 +24,7 @@
 #include "../lib/CGeneralTextHandler.h"
 #include "../lib/CCreatureHandler.h"
 #include "CBitmapHandler.h"
+#include "../lib/CSkillHandler.h"
 #include "../lib/spells/CSpellHandler.h"
 #include "../lib/CGameState.h"
 #include "../lib/JsonNode.h"
@@ -443,4 +444,16 @@ void Graphics::initializeImageLists()
 		addImageListEntry(spell->id, "SPELLBON", spell->iconScenarioBonus);
 		addImageListEntry(spell->id, "SPELLSCR", spell->iconScroll);
 	}
+
+	for(const CSkill * skill : CGI->skillh->objects)
+	{
+		for(int level = 1; level <= 3; level++)
+		{
+			int frame = 2 + level + 3 * skill->id;
+			const CSkill::LevelInfo & skillAtLevel = skill->at(level);
+			addImageListEntry(frame, "SECSK32", skillAtLevel.iconSmall);
+			addImageListEntry(frame, "SECSKILL", skillAtLevel.iconMedium);
+			addImageListEntry(frame, "SECSK82", skillAtLevel.iconLarge);
+		}
+	}
 }

+ 43 - 0
config/schemas/skill.json

@@ -13,6 +13,27 @@
 			"description" : "Set of bonuses provided by skill at given level",
 			"required" : ["description", "effects"],
 			"properties" : {
+				"images" : {
+					"type" : "object",
+					"description" : "skill icons of varying size",
+					"properties" : {
+						"small" : {
+							"type" : "string",
+							"description" : "32x32 skill icon",
+							"format" : "imageFile"
+						},
+						"medium" : {
+							"type" : "string",
+							"description" : "44x44 skill icon",
+							"format" : "imageFile"
+						},
+						"large" : {
+							"type" : "string",
+							"description" : "82x93 skill icon",
+							"format" : "imageFile"
+						}
+					}
+				},
 				"description" : {
 					"type" : "string",
 					"description" : "localizable description"
@@ -39,6 +60,28 @@
 			"type": "string",
 			"description": "localizable skill name"
 		},
+		"gainChance" : {
+			"description" : "Chance for the skill to be offered on level-up (heroClass may override)",
+			"anyOf" : [
+				{
+					"type" : "number"
+				},
+				{
+					"type" : "object",
+					"required" : ["might", "magic"],
+					"properties" : {
+						"might" : {
+							"type" : "number",
+							"description" : "Chance for hero classes with might affinity"
+						},
+						"magic" : {
+							"type" : "number",
+							"description" : "Chance for hero classes with magic affinity"
+						}
+					}
+				}
+			]
+		},
 		"base" : {
 			"type" : "object",
 			"description" : "will be merged with all levels",

+ 20 - 28
lib/CHeroHandler.cpp

@@ -20,6 +20,7 @@
 #include "CModHandler.h"
 #include "CTownHandler.h"
 #include "mapObjects/CObjectHandler.h" //for hero specialty
+#include "CSkillHandler.h"
 #include <math.h>
 
 #include "mapObjects/CObjectClassesHandler.h"
@@ -117,9 +118,15 @@ CHeroClass * CHeroClassHandler::loadFromJson(const JsonNode & node, const std::s
 		heroClass->primarySkillHighLevel.push_back(node["highLevelChance"][pSkill].Float());
 	}
 
-	for(const std::string & secSkill : NSecondarySkill::names)
+	for(auto skillPair : node["secondarySkills"].Struct())
 	{
-		heroClass->secSkillProbability.push_back(node["secondarySkills"][secSkill].Float());
+		int probability = skillPair.second.Integer();
+		VLC->modh->identifiers.requestIdentifier(skillPair.second.meta, "skill", skillPair.first, [heroClass, probability](si32 skillID)
+		{
+			if(heroClass->secSkillProbability.size() <= skillID)
+				heroClass->secSkillProbability.resize(skillID + 1, -1); // -1 = override with default later
+			heroClass->secSkillProbability[skillID] = probability;
+		});
 	}
 
 	VLC->modh->identifiers.requestIdentifier ("creature", node["commander"],
@@ -241,6 +248,17 @@ void CHeroClassHandler::afterLoadFinalization()
 			float chance = heroClass->defaultTavernChance * faction->town->defaultTavernChance;
 			heroClass->selectionProbability[faction->index] = static_cast<int>(sqrt(chance) + 0.5); //FIXME: replace with std::round once MVS supports it
 		}
+		// set default probabilities for gaining secondary skills where not loaded previously
+		heroClass->secSkillProbability.resize(VLC->skillh->size(), -1);
+		for(int skillID = 0; skillID < VLC->skillh->size(); skillID++)
+		{
+			if(heroClass->secSkillProbability[skillID] < 0)
+			{
+				const CSkill * skill = (*VLC->skillh)[SecondarySkill(skillID)];
+				logMod->trace("%s: no probability for %s, using default", heroClass->identifier, skill->identifier);
+				heroClass->secSkillProbability[skillID] = skill->gainChance[heroClass->affinity];
+			}
+		}
 	}
 
 	for (CHeroClass * hc : heroClasses)
@@ -277,11 +295,6 @@ CHeroHandler::CHeroHandler()
 {
 	VLC->heroh = this;
 
-	for (int i = 0; i < GameConstants::SKILL_QUANTITY; ++i)
-	{
-		VLC->modh->identifiers.registerObject("core", "skill", NSecondarySkill::names[i], i);
-		VLC->modh->identifiers.registerObject("core", "secondarySkill", NSecondarySkill::names[i], i);
-	}
 	loadObstacles();
 	loadTerrains();
 	for (int i = 0; i < GameConstants::TERRAIN_TYPES; ++i)
@@ -943,13 +956,6 @@ std::vector<bool> CHeroHandler::getDefaultAllowed() const
 	return allowedHeroes;
 }
 
-std::vector<bool> CHeroHandler::getDefaultAllowedAbilities() const
-{
-	std::vector<bool> allowedAbilities;
-	allowedAbilities.resize(GameConstants::SKILL_QUANTITY, true);
-	return allowedAbilities;
-}
-
 si32 CHeroHandler::decodeHero(const std::string & identifier)
 {
 	auto rawId = VLC->modh->identifiers.getIdentifier("core", "hero", identifier);
@@ -963,17 +969,3 @@ std::string CHeroHandler::encodeHero(const si32 index)
 {
 	return VLC->heroh->heroes.at(index)->identifier;
 }
-
-si32 CHeroHandler::decodeSkill(const std::string & identifier)
-{
-	auto rawId = VLC->modh->identifiers.getIdentifier("core", "skill", identifier);
-	if(rawId)
-		return rawId.get();
-	else
-		return -1;
-}
-
-std::string CHeroHandler::encodeSkill(const si32 index)
-{
-	return NSecondarySkill::names[index];
-}

+ 0 - 13
lib/CHeroHandler.h

@@ -311,25 +311,12 @@ public:
 
 	std::vector<bool> getDefaultAllowed() const override;
 
-	/**
-	 * Gets a list of default allowed abilities. OH3 abilities/skills are all allowed by default.
-	 *
-	 * @return a list of allowed abilities, the index is the ability id
-	 */
-	std::vector<bool> getDefaultAllowedAbilities() const;
-
 	///json serialization helper
 	static si32 decodeHero(const std::string & identifier);
 
 	///json serialization helper
 	static std::string encodeHero(const si32 index);
 
-	///json serialization helper
-	static si32 decodeSkill(const std::string & identifier);
-
-	///json serialization helper
-	static std::string encodeSkill(const si32 index);
-
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & classes;

+ 1 - 1
lib/CModHandler.cpp

@@ -240,7 +240,7 @@ std::vector<CIdentifierStorage::ObjectData> CIdentifierStorage::getPossibleIdent
 		{
 			// allow only available to all core mod or dependencies
 			auto myDeps = VLC->modh->getModData(request.localScope).dependencies;
-			if (request.remoteScope == "core" || myDeps.count(request.remoteScope))
+			if(request.remoteScope == "core" || request.remoteScope == request.localScope || myDeps.count(request.remoteScope))
 				allowedScopes.insert(request.remoteScope);
 		}
 	}

+ 50 - 38
lib/CSkillHandler.cpp

@@ -31,16 +31,10 @@ CSkill::LevelInfo::~LevelInfo()
 {
 }
 
-CSkill::CSkill(SecondarySkill id) : id(id)
+CSkill::CSkill(SecondarySkill id, std::string identifier)
+	: id(id), identifier(identifier)
 {
-	if(id == SecondarySkill::DEFAULT)
-		identifier = "default";
-	else
-		identifier = NSecondarySkill::names[id];
-	// init levels
-	LevelInfo emptyLevel;
-	for(int level = 1; level < NSecondarySkill::levels.size(); level++)
-		levels.push_back(emptyLevel);
+	levels.resize(NSecondarySkill::levels.size() - 1);
 }
 
 CSkill::~CSkill()
@@ -56,19 +50,16 @@ void CSkill::addNewBonus(const std::shared_ptr<Bonus> & b, int level)
 	levels[level-1].effects.push_back(b);
 }
 
-void CSkill::setDescription(const std::string & desc, int level)
-{
-	levels[level-1].description = desc;
-}
-
-const std::vector<std::shared_ptr<Bonus>> & CSkill::getBonus(int level) const
+const CSkill::LevelInfo & CSkill::at(int level) const
 {
-	return levels[level-1].effects;
+	assert(1 <= level && level < NSecondarySkill::levels.size());
+	return levels[level - 1];
 }
 
-const std::string & CSkill::getDescription(int level) const
+CSkill::LevelInfo & CSkill::at(int level)
 {
-	return levels[level-1].description;
+	assert(1 <= level && level < NSecondarySkill::levels.size());
+	return levels[level - 1];
 }
 
 DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInfo & info)
@@ -139,14 +130,15 @@ std::vector<JsonNode> CSkillHandler::loadLegacyData(size_t dataSize)
 	return legacyData;
 }
 
-const std::string CSkillHandler::getTypeName() const
+const std::vector<std::string> & CSkillHandler::getTypeNames() const
 {
-	return "skill";
+	static const std::vector<std::string> typeNames = { "skill", "secondarySkill" };
+	return typeNames;
 }
 
 const std::string & CSkillHandler::skillInfo(int skill, int level) const
 {
-	return objects[skill]->getDescription(level);
+	return objects[skill]->at(level).description;
 }
 
 const std::string & CSkillHandler::skillName(int skill) const
@@ -156,24 +148,22 @@ const std::string & CSkillHandler::skillName(int skill) const
 
 CSkill * CSkillHandler::loadFromJson(const JsonNode & json, const std::string & identifier, size_t index)
 {
-	CSkill * skill = nullptr;
-
-	for(int id = 0; id < GameConstants::SKILL_QUANTITY; id++)
-	{
-		if(NSecondarySkill::names[id].compare(identifier) == 0)
-		{
-			skill = new CSkill(SecondarySkill(id));
-			break;
-		}
-	}
+	CSkill * skill = new CSkill(SecondarySkill(index), identifier);
 
-	if(!skill)
+	skill->name = json["name"].String();
+	switch(json["gainChance"].getType())
 	{
-		logMod->error("unknown secondary skill %s", identifier);
-		throw std::runtime_error("invalid skill");
+	case JsonNode::JsonType::DATA_INTEGER:
+		skill->gainChance[0] = json["gainChance"].Integer();
+		skill->gainChance[1] = json["gainChance"].Integer();
+		break;
+	case JsonNode::JsonType::DATA_STRUCT:
+		skill->gainChance[0] = json["gainChance"]["might"].Integer();
+		skill->gainChance[1] = json["gainChance"]["magic"].Integer();
+		break;
+	default:
+		break;
 	}
-
-	skill->name = json["name"].String();
 	for(int level = 1; level < NSecondarySkill::levels.size(); level++)
 	{
 		const std::string & levelName = NSecondarySkill::levels[level]; // basic, advanced, expert
@@ -182,10 +172,13 @@ CSkill * CSkillHandler::loadFromJson(const JsonNode & json, const std::string &
 		for(auto b : levelNode["effects"].Struct())
 		{
 			auto bonus = JsonUtils::parseBonus(b.second);
-			bonus->sid = skill->id;
 			skill->addNewBonus(bonus, level);
 		}
-		skill->setDescription(levelNode["description"].String(), level);
+		CSkill::LevelInfo & skillAtLevel = skill->at(level);
+		skillAtLevel.description = levelNode["description"].String();
+		skillAtLevel.iconSmall = levelNode["images"]["small"].String();
+		skillAtLevel.iconMedium = levelNode["images"]["medium"].String();
+		skillAtLevel.iconLarge = levelNode["images"]["large"].String();
 	}
 	logMod->debug("loaded secondary skill %s(%d)", identifier, (int)skill->id);
 	logMod->trace("%s", skill->toString());
@@ -220,3 +213,22 @@ std::vector<bool> CSkillHandler::getDefaultAllowed() const
 	std::vector<bool> allowedSkills(objects.size(), true);
 	return allowedSkills;
 }
+
+si32 CSkillHandler::decodeSkill(const std::string & identifier)
+{
+	auto rawId = VLC->modh->identifiers.getIdentifier("core", "skill", identifier);
+	if(rawId)
+		return rawId.get();
+	else
+		return -1;
+}
+
+std::string CSkillHandler::encodeSkill(const si32 index)
+{
+	return (*VLC->skillh)[SecondarySkill(index)]->identifier;
+}
+
+std::string CSkillHandler::encodeSkillWithType(const si32 index)
+{
+	return CModHandler::makeFullIdentifier("", "skill", encodeSkill(index));
+}

+ 27 - 7
lib/CSkillHandler.h

@@ -15,10 +15,13 @@
 
 class DLL_LINKAGE CSkill // secondary skill
 {
-protected:
+public:
 	struct LevelInfo
 	{
 		std::string description; //descriptions of spell for skill level
+		std::string iconSmall;
+		std::string iconMedium;
+		std::string iconLarge;
 		std::vector<std::shared_ptr<Bonus>> effects;
 
 		LevelInfo();
@@ -27,31 +30,43 @@ protected:
 		template <typename Handler> void serialize(Handler & h, const int version)
 		{
 			h & description;
+			if(version >= 785)
+			{
+				h & iconSmall;
+				h & iconMedium;
+				h & iconLarge;
+			}
 			h & effects;
 		}
 	};
 
+private:
 	std::vector<LevelInfo> levels; // bonuses provided by basic, advanced and expert level
+	void addNewBonus(const std::shared_ptr<Bonus> & b, int level);
 
 public:
-	CSkill(SecondarySkill id = SecondarySkill::DEFAULT);
+	CSkill(SecondarySkill id = SecondarySkill::DEFAULT, std::string identifier = "default");
 	~CSkill();
 
-	void addNewBonus(const std::shared_ptr<Bonus> & b, int level);
-	void setDescription(const std::string & desc, int level);
-	const std::vector<std::shared_ptr<Bonus>> & getBonus(int level) const;
-	const std::string & getDescription(int level) const;
+	const LevelInfo & at(int level) const;
+	LevelInfo & at(int level);
+
 	std::string toString() const;
 
 	SecondarySkill id;
 	std::string identifier;
 	std::string name; //as displayed in GUI
+	std::array<si32, 2> gainChance; // gainChance[0/1] = default gain chance on level-up for might/magic heroes
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
 		h & id;
 		h & identifier;
 		h & name;
+		if(version >= 785)
+		{
+			h & gainChance;
+		}
 		h & levels;
 	}
 
@@ -72,11 +87,16 @@ public:
 	void beforeValidate(JsonNode & object) override;
 
 	std::vector<bool> getDefaultAllowed() const override;
-	const std::string getTypeName() const override;
+	const std::vector<std::string> & getTypeNames() const override;
 
 	const std::string & skillInfo(int skill, int level) const;
 	const std::string & skillName(int skill) const;
 
+	///json serialization helpers
+	static si32 decodeSkill(const std::string & identifier);
+	static std::string encodeSkill(const si32 index);
+	static std::string encodeSkillWithType(const si32 index);
+
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
 		h & objects;

+ 2 - 2
lib/HeroBonus.cpp

@@ -1274,7 +1274,7 @@ JsonNode subtypeToJson(Bonus::BonusType type, int subtype)
 	case Bonus::PRIMARY_SKILL:
 		return JsonUtils::stringNode("primSkill." + PrimarySkill::names[subtype]);
 	case Bonus::SECONDARY_SKILL_PREMY:
-		return JsonUtils::stringNode("skill." + NSecondarySkill::names[subtype]);
+		return JsonUtils::stringNode(CSkillHandler::encodeSkillWithType(subtype));
 	case Bonus::SPECIAL_SPELL_LEV:
 	case Bonus::SPECIFIC_SPELL_DAMAGE:
 	case Bonus::SPECIAL_BLESS_DAMAGE:
@@ -1362,7 +1362,7 @@ std::string Bonus::nameForBonus() const
 	case Bonus::PRIMARY_SKILL:
 		return PrimarySkill::names[subtype];
 	case Bonus::SECONDARY_SKILL_PREMY:
-		return NSecondarySkill::names[subtype];
+		return CSkillHandler::encodeSkill(subtype);
 	case Bonus::SPECIAL_SPELL_LEV:
 	case Bonus::SPECIFIC_SPELL_DAMAGE:
 	case Bonus::SPECIAL_BLESS_DAMAGE:

+ 10 - 6
lib/IHandlerBase.h

@@ -67,22 +67,22 @@ public:
 	}
 	void loadObject(std::string scope, std::string name, const JsonNode & data) override
 	{
-		auto type_name = getTypeName();
 		auto object = loadFromJson(data, normalizeIdentifier(scope, "core", name), objects.size());
 
 		objects.push_back(object);
 
-		registerObject(scope, type_name, name, object->id);
+		for(auto type_name : getTypeNames())
+			registerObject(scope, type_name, name, object->id);
 	}
 	void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override
 	{
-		auto type_name = getTypeName();
 		auto object = loadFromJson(data, normalizeIdentifier(scope, "core", name), index);
 
 		assert(objects[index] == nullptr); // ensure that this id was not loaded before
 		objects[index] = object;
 
-		registerObject(scope,type_name, name, object->id);
+		for(auto type_name : getTypeNames())
+			registerObject(scope, type_name, name, object->id);
 	}
 
 	ConstTransitivePtr<_Object> operator[] (const _ObjectID id) const
@@ -91,15 +91,19 @@ public:
 
 		if (raw_id < 0 || raw_id >= objects.size())
 		{
-			logMod->error("%s id %d is invalid", getTypeName(), static_cast<si64>(raw_id));
+			logMod->error("%s id %d is invalid", getTypeNames()[0], static_cast<si64>(raw_id));
 			throw std::runtime_error("internal error");
 		}
 
 		return objects[raw_id];
 	}
+	size_t size() const
+	{
+		return objects.size();
+	}
 protected:
 	virtual _Object * loadFromJson(const JsonNode & json, const std::string & identifier, size_t index) = 0;
-	virtual const std::string getTypeName() const = 0;
+	virtual const std::vector<std::string> & getTypeNames() const = 0;
 public: //todo: make private
 	std::vector<ConstTransitivePtr<_Object>> objects;
 };

+ 5 - 5
lib/mapObjects/CGHeroInstance.cpp

@@ -560,7 +560,7 @@ void CGHeroInstance::recreateSpecialtyBonuses(std::vector<HeroSpecial *> & speci
 void CGHeroInstance::updateSkillBonus(SecondarySkill which, int val)
 {
 	removeBonuses(Selector::source(Bonus::SECONDARY_SKILL, which));
-	auto skillBonus = (*VLC->skillh)[which]->getBonus(val);
+	auto skillBonus = (*VLC->skillh)[which]->at(val).effects;
 	for (auto b : skillBonus)
 		addNewBonus(std::make_shared<Bonus>(*b));
 }
@@ -1107,7 +1107,7 @@ std::vector<SecondarySkill> CGHeroInstance::getLevelUpProposedSecondarySkills()
 	std::vector<SecondarySkill> skills;
 	//picking sec. skills for choice
 	std::set<SecondarySkill> basicAndAdv, expert, none;
-	for(int i=0;i<GameConstants::SKILL_QUANTITY;i++)
+	for(int i = 0; i < VLC->skillh->size(); i++)
 		if (cb->isAllowed(2,i))
 			none.insert(SecondarySkill(i));
 
@@ -1450,10 +1450,10 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler)
 			{
 				const si32 rawId = p.first.num;
 
-				if(rawId < 0 || rawId >= GameConstants::SKILL_QUANTITY)
+				if(rawId < 0 || rawId >= VLC->skillh->size())
 					logGlobal->error("Invalid secondary skill %d", rawId);
 
-				handler.serializeEnum(NSecondarySkill::names[rawId], p.second, 0, NSecondarySkill::levels);
+				handler.serializeEnum((*VLC->skillh)[SecondarySkill(rawId)]->identifier, p.second, 0, NSecondarySkill::levels);
 			}
 		}
 	}
@@ -1474,7 +1474,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler)
 				const std::string skillId = p.first;
 				const std::string levelId =  p.second.String();
 
-				const int rawId = vstd::find_pos(NSecondarySkill::names, skillId);
+				const int rawId = CSkillHandler::decodeSkill(skillId);
 				if(rawId < 0)
 				{
 					logGlobal->error("Invalid secondary skill %s", skillId);

+ 2 - 1
lib/mapObjects/CGMarket.cpp

@@ -18,6 +18,7 @@
 #include "../CGameState.h"
 #include "CGTownInstance.h"
 #include "../CModHandler.h"
+#include "../CSkillHandler.h"
 
 ///helpers
 static void openWindow(const OpenWindow::EWindow type, const int id1, const int id2 = -1)
@@ -299,7 +300,7 @@ void CGBlackMarket::newTurn(CRandomGenerator & rand) const
 void CGUniversity::initObj(CRandomGenerator & rand)
 {
 	std::vector<int> toChoose;
-	for(int i = 0; i < GameConstants::SKILL_QUANTITY; ++i)
+	for(int i = 0; i < VLC->skillh->size(); ++i)
 	{
 		if(cb->isAllowed(2, i))
 		{

+ 3 - 2
lib/mapObjects/CGPandoraBox.cpp

@@ -15,6 +15,7 @@
 #include "../CSoundBase.h"
 
 #include "../spells/CSpellHandler.h"
+#include "../CSkillHandler.h"
 #include "../StartInfo.h"
 #include "../IGameCallback.h"
 #include "../StringConstants.h"
@@ -419,7 +420,7 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler)
 
 			for(size_t idx = 0; idx < abilities.size(); idx++)
 			{
-				handler.serializeEnum(NSecondarySkill::names[abilities[idx]], abilityLevels[idx], NSecondarySkill::levels);
+				handler.serializeEnum(CSkillHandler::encodeSkill(abilities[idx]), abilityLevels[idx], NSecondarySkill::levels);
 			}
 		}
 	}
@@ -437,7 +438,7 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler)
 			const std::string skillName = p.first;
 			const std::string levelId = p.second.String();
 
-			const int rawId = vstd::find_pos(NSecondarySkill::names, skillName);
+			const int rawId = CSkillHandler::decodeSkill(skillName);
 			if(rawId < 0)
 			{
 				logGlobal->error("Invalid secondary skill %s", skillName);

+ 2 - 1
lib/mapObjects/CQuest.cpp

@@ -24,6 +24,7 @@
 #include "../GameConstants.h"
 #include "../StringConstants.h"
 #include "../spells/CSpellHandler.h"
+#include "../CSkillHandler.h"
 #include "../mapping/CMap.h"
 
 
@@ -920,7 +921,7 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler)
 			identifier = PrimarySkill::names[rID];
 			break;
 		case SECONDARY_SKILL:
-			identifier = NSecondarySkill::names[rID];
+			identifier = CSkillHandler::encodeSkill(rID);
 			break;
 		case ARTIFACT:
 			identifier = ArtifactID(rID).toArtifact()->identifier;

+ 9 - 8
lib/mapObjects/MiscObjects.cpp

@@ -1439,7 +1439,7 @@ void CGWitchHut::initObj(CRandomGenerator & rand)
 {
 	if (allowedAbilities.empty()) //this can happen for RMG. regular maps load abilities from map file
 	{
-		for (int i = 0; i < GameConstants::SKILL_QUANTITY; i++)
+		for(int i = 0; i < VLC->skillh->size(); i++)
 			allowedAbilities.push_back(i);
 	}
 	ability = *RandomGeneratorUtil::nextItem(allowedAbilities, rand);
@@ -1496,23 +1496,24 @@ void CGWitchHut::serializeJsonOptions(JsonSerializeFormat & handler)
 	//TODO: unify allowed abilities with others - make them std::vector<bool>
 
 	std::vector<bool> temp;
-	temp.resize(GameConstants::SKILL_QUANTITY, false);
+	size_t skillCount = VLC->skillh->size();
+	temp.resize(skillCount, false);
 
-	auto standard = VLC->heroh->getDefaultAllowedAbilities(); //todo: for WitchHut default is all except Leadership and Necromancy
+	auto standard = VLC->skillh->getDefaultAllowed(); //todo: for WitchHut default is all except Leadership and Necromancy
 
 	if(handler.saving)
 	{
-		for(si32 i = 0; i < GameConstants::SKILL_QUANTITY; ++i)
+		for(si32 i = 0; i < skillCount; ++i)
 			if(vstd::contains(allowedAbilities, i))
 				temp[i] = true;
 	}
 
-	handler.serializeLIC("allowedSkills", &CHeroHandler::decodeSkill, &CHeroHandler::encodeSkill, standard, temp);
+	handler.serializeLIC("allowedSkills", &CSkillHandler::decodeSkill, &CSkillHandler::encodeSkill, standard, temp);
 
 	if(!handler.saving)
 	{
 		allowedAbilities.clear();
-		for (si32 i=0; i<temp.size(); i++)
+		for(si32 i = 0; i < skillCount; ++i)
 			if(temp[i])
 				allowedAbilities.push_back(i);
 	}
@@ -1746,7 +1747,7 @@ void CGScholar::initObj(CRandomGenerator & rand)
 			bonusID = rand.nextInt(GameConstants::PRIMARY_SKILLS -1);
 			break;
 		case SECONDARY_SKILL:
-			bonusID = rand.nextInt(GameConstants::SKILL_QUANTITY -1);
+			bonusID = rand.nextInt(VLC->skillh->size() - 1);
 			break;
 		case SPELL:
 			std::vector<SpellID> possibilities;
@@ -1770,7 +1771,7 @@ void CGScholar::serializeJsonOptions(JsonSerializeFormat & handler)
 			handler.serializeString("rewardPrimSkill", value);
 			break;
 		case SECONDARY_SKILL:
-			value = NSecondarySkill::names[bonusID];
+			value = CSkillHandler::encodeSkill(bonusID);
 			handler.serializeString("rewardSkill", value);
 			break;
 		case SPELL:

+ 2 - 1
lib/mapping/CMap.cpp

@@ -19,6 +19,7 @@
 #include "../mapObjects/CGHeroInstance.h"
 #include "../CGeneralTextHandler.h"
 #include "../spells/CSpellHandler.h"
+#include "../CSkillHandler.h"
 #include "CMapEditManager.h"
 #include "../serializer/JsonSerializeFormat.h"
 
@@ -240,7 +241,7 @@ CMap::CMap()
 	guardingCreaturePositions(nullptr)
 {
 	allHeroes.resize(allowedHeroes.size());
-	allowedAbilities = VLC->heroh->getDefaultAllowedAbilities();
+	allowedAbilities = VLC->skillh->getDefaultAllowed();
 	allowedArtifact = VLC->arth->getDefaultAllowed();
 	allowedSpell = VLC->spellh->getDefaultAllowed();
 }

+ 9 - 4
lib/mapping/MapFormatH3M.cpp

@@ -17,6 +17,7 @@
 #include "../CStopWatch.h"
 #include "../filesystem/Filesystem.h"
 #include "../spells/CSpellHandler.h"
+#include "../CSkillHandler.h"
 #include "../CCreatureHandler.h"
 #include "../CGeneralTextHandler.h"
 #include "../CHeroHandler.h"
@@ -1144,14 +1145,18 @@ void CMapLoaderH3M::readObjects()
 							}
 						}
 					}
+					// enable new (modded) skills
+					if(wh->allowedAbilities.size() != 1)
+					{
+						for(int skillID = GameConstants::SKILL_QUANTITY; skillID < VLC->skillh->size(); ++skillID)
+							wh->allowedAbilities.push_back(skillID);
+					}
 				}
 				else
 				{
 					// RoE map
-					for(int gg = 0; gg < GameConstants::SKILL_QUANTITY; ++gg)
-					{
-						wh->allowedAbilities.push_back(gg);
-					}
+					for(int skillID = 0; skillID < VLC->skillh->size(); ++skillID)
+						wh->allowedAbilities.push_back(skillID);
 				}
 				break;
 			}

+ 2 - 1
lib/mapping/MapFormatJson.cpp

@@ -25,6 +25,7 @@
 #include "../mapObjects/CGHeroInstance.h"
 #include "../mapObjects/CGTownInstance.h"
 #include "../spells/CSpellHandler.h"
+#include "../CSkillHandler.h"
 #include "../StringConstants.h"
 #include "../serializer/JsonDeserializer.h"
 #include "../serializer/JsonSerializer.h"
@@ -807,7 +808,7 @@ void CMapFormatJson::serializeOptions(JsonSerializeFormat & handler)
 
 	serializePredefinedHeroes(handler);
 
-	handler.serializeLIC("allowedAbilities", &CHeroHandler::decodeSkill, &CHeroHandler::encodeSkill, VLC->heroh->getDefaultAllowedAbilities(), map->allowedAbilities);
+	handler.serializeLIC("allowedAbilities", &CSkillHandler::decodeSkill, &CSkillHandler::encodeSkill, VLC->skillh->getDefaultAllowed(), map->allowedAbilities);
 
 	handler.serializeLIC("allowedArtifacts",  &ArtifactID::decode, &ArtifactID::encode, VLC->arth->getDefaultAllowed(), map->allowedArtifact);
 

+ 1 - 1
lib/serializer/CSerializer.h

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

+ 3 - 2
lib/spells/CSpellHandler.cpp

@@ -707,9 +707,10 @@ std::vector<JsonNode> CSpellHandler::loadLegacyData(size_t dataSize)
 	return legacyData;
 }
 
-const std::string CSpellHandler::getTypeName() const
+const std::vector<std::string> & CSpellHandler::getTypeNames() const
 {
-	return "spell";
+	static const std::vector<std::string> typeNames = { "spell" };
+	return typeNames;
 }
 
 CSpell * CSpellHandler::loadFromJson(const JsonNode & json, const std::string & identifier, size_t index)

+ 1 - 1
lib/spells/CSpellHandler.h

@@ -441,7 +441,7 @@ public:
 	 */
 	std::vector<bool> getDefaultAllowed() const override;
 
-	const std::string getTypeName() const override;
+	const std::vector<std::string> & getTypeNames() const override;
 
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{