Просмотр исходного кода

Merge pull request #1466 from IvanSavenko/translate_adventure_map

Translate adventure map objects
Ivan Savenko 2 лет назад
Родитель
Сommit
aef4cb3c71

+ 0 - 4
AI/VCAI/MapObjectsEvaluator.cpp

@@ -32,10 +32,6 @@ MapObjectsEvaluator::MapObjectsEvaluator()
 				{
 					objectDatabase[CompoundMapObjectID(primaryID, secondaryID)] = handler->getAiValue().get();
 				}
-				else if(VLC->objtypeh->getObjGroupAiValue(primaryID) != boost::none) //if value is not initialized - fallback to default value for this object family if it exists
-				{
-					objectDatabase[CompoundMapObjectID(primaryID, secondaryID)] = VLC->objtypeh->getObjGroupAiValue(primaryID).get();
-				}
 				else //some default handling when aiValue not found, objects that require advanced properties (unavailable from handler) get their value calculated in getObjectValue
 				{
 					objectDatabase[CompoundMapObjectID(primaryID, secondaryID)] = 0;

+ 1 - 1
client/mapHandler.cpp

@@ -1371,7 +1371,7 @@ void CMapHandler::getTerrainDescr(const int3 & pos, std::string & out, bool isRM
 
 	if(t.hasFavorableWinds())
 	{
-		out = CGI->objtypeh->getObjectName(Obj::FAVORABLE_WINDS);
+		out = CGI->objtypeh->getObjectName(Obj::FAVORABLE_WINDS, 0);
 		return;
 	}
 	const TerrainTile2 & tt = ttiles[pos.z][pos.x][pos.y];

+ 1 - 0
config/objects/creatureBanks.json

@@ -2,6 +2,7 @@
 	"creatureBank" : {
 		"index" :16,
 		"handler": "bank",
+		"lastReservedIndex" : 6,
 		"base" : {
 			"sounds" : {
 				"visit" : ["ROGUE"]

+ 7 - 0
config/objects/dwellings.json

@@ -2,6 +2,7 @@
 	"creatureGeneratorCommon" : {
 		"index" :17,
 		"handler": "dwelling",
+		"lastReservedIndex" : 79,
 		"base" : {
 			"base" : {
 				"visitableFrom" : [ "---", "+++", "+++" ],
@@ -528,6 +529,12 @@
 	"creatureGeneratorSpecial" : {
 		"index" :20,
 		"handler": "dwelling",
+		"lastReservedIndex" : 1,
+		"base" : {
+			"base" : {
+				"visitableFrom" : [ "---", "+++", "+++" ]
+			}
+		},
 		"types" : {
 			"elementalConflux" : {
 				"index" : 0,

+ 3 - 14
config/objects/generic.json

@@ -238,8 +238,8 @@
 		},
 		"types" : {
 			"object" : {
-				"index" : 0 },
-			"objectWoG" : { "index" : 1 } // WoG object? Present on VCMI Test 2011b
+				"index" : 0 
+			}
 		}
 	},
 
@@ -341,9 +341,6 @@
 					"value"		: 3000,
 					"rarity"	: 100
 				}
-			},
-			"object1":{ //WoG?
-				"index" :1
 			}
 		}
 	},
@@ -573,9 +570,6 @@
 					"value"		: 1500,
 					"rarity"	: 80
 				}
-			},
-			"object1" : { //WoG?
-				"index" : 1
 			}
 		}
 	},
@@ -1042,10 +1036,5 @@
 	"grassHills"					: { "index" :208, "handler": "static", "types" : { "object" : { "index" : 0} } },
 	"roughHills"					: { "index" :209, "handler": "static", "types" : { "object" : { "index" : 0} } },
 	"subterraneanRocks"				: { "index" :210, "handler": "static", "types" : { "object" : { "index" : 0} } },
-	"swampFoliage" 					: { "index" :211, "handler": "static", "types" : { "object" : { "index" : 0} } },
-
-	//These are WoG objects? They are not available in H3
-	"frozenLakeDUPLICATE"			: { "index" :172, "handler": "static", "types" : { "object" : { "index" : 0} } },
-	"oakTreesDUPLICATE"				: { "index" :186, "handler": "static", "types" : { "object" : { "index" : 0} } },
-	"plant"							: { "index" :189, "handler": "static", "types" : { "object" : { "index" : 0} } }
+	"swampFoliage" 					: { "index" :211, "handler": "static", "types" : { "object" : { "index" : 0} } }
 }

+ 34 - 23
config/objects/moddables.json

@@ -23,8 +23,8 @@
 	"hero" : {
 		"index" :34,
 		"handler": "hero",
-		"defaultAiValue" : 7500,
 		"base" : {
+			"aiValue" : 7500,
 			"base" : {
 				"visitableFrom" : [ "+++", "+-+", "+++" ],
 				"mask" : [ "VVV", "VAV"]
@@ -54,6 +54,7 @@
 	"resource" : {
 		"index" :79,
 		"handler": "resource",
+		"lastReservedIndex" : 6,
 		"base" : {
 			"base" : {
 				"visitableFrom" : [ "+++", "+-+", "+++" ],
@@ -67,8 +68,7 @@
 			"sulfur" :  { "index" : 3, "aiValue" : 2000, "rmg" : { "value" : 2000, "rarity" : 300 }, "templates" : { "res" : { "animation" : "AVTsulf0.def" } } },
 			"crystal" : { "index" : 4, "aiValue" : 2000, "rmg" : { "value" : 2000, "rarity" : 300 }, "templates" : { "res" : { "animation" : "AVTcrys0.def" } } },
 			"gems" :    { "index" : 5, "aiValue" : 2000, "rmg" : { "value" : 2000, "rarity" : 300 }, "templates" : { "res" : { "animation" : "AVTgems0.def" } } },
-			"gold" :    { "index" : 6, "aiValue" : 750, "rmg" : { "value" : 750,  "rarity" : 300 }, "templates" : { "res" : { "animation" : "AVTgold0.def" } } },
-			"mithril" : { "index" : 7, "aiValue" : 3500 } // TODO: move to WoG?
+			"gold" :    { "index" : 6, "aiValue" : 750, "rmg" : { "value" : 750,  "rarity" : 300 }, "templates" : { "res" : { "animation" : "AVTgold0.def" } } }
 		}
 	},
 	
@@ -76,8 +76,8 @@
 	"town" : {
 		"index" :98,
 		"handler": "town",
-		"defaultAiValue" : 20000,
 		"base" : {
+			"aiValue" : 20000,
 			"filters" : {
 				// village image - fort not present
 				"village" : [ "noneOf", [ "fort" ] ],
@@ -106,8 +106,9 @@
 	"boat" : {
 		"index" :8,
 		"handler": "boat",
-		"defaultAiValue" : 0,
+		"lastReservedIndex" : 2,
 		"base" : {
+			"aiValue" : 0,
 			"base" : {
 				"visitableFrom" : [ "+++", "+-+", "+++" ],
 				"mask" : [ "VVV", "VAV" ]
@@ -124,8 +125,9 @@
 	"borderGuard" : {
 		"index" :9,
 		"handler": "borderGuard",
-		"defaultAiValue" : 0,
+		"lastReservedIndex" : 7,
 		"base" : {
+			"aiValue" : 0,
 			"sounds" : {
 				"visit" : ["CAVEHEAD"],
 				"removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ]
@@ -145,8 +147,9 @@
 	"borderGate" : {
 		"index" :212,
 		"handler": "borderGate",
-		"defaultAiValue" : 0,
+		"lastReservedIndex" : 7,
 		"base" : {
+			"aiValue" : 0,
 			"sounds" : {
 				"visit" : ["CAVEHEAD"]
 			}
@@ -165,8 +168,9 @@
 	"keymasterTent" : {
 		"index" :10,
 		"handler": "keymaster",
-		"defaultAiValue" : 10000,
+		"lastReservedIndex" : 7,
 		"base" : {
+			"aiValue" : 10000,
 			"sounds" : {
 				"visit" : ["CAVEHEAD"]
 			}
@@ -186,8 +190,9 @@
 	"seerHut" : {
 		"index" :83,
 		"handler": "seerHut",
-		"defaultAiValue" : 10000,
+		"lastReservedIndex" : 2,
 		"base" : {
+			"aiValue" : 10000,
 			"base" : {
 				"visitableFrom" : [ "---", "+++", "+++" ],
 				"mask" : [ "VVV", "VVV", "VAV" ]
@@ -207,6 +212,7 @@
 	"cartographer" : {
 		"index" :13,
 		"handler": "cartographer",
+		"lastReservedIndex" : 2,
 		"base" : {
 			"sounds" : {
 				"visit" : ["LIGHTHOUSE"]
@@ -223,6 +229,7 @@
 	"mine" : {
 		"index" :53,
 		"handler": "mine",
+		"lastReservedIndex" : 7,
 		"base" : {
 			"sounds" : {
 				"visit" : ["FLAGMINE"]
@@ -237,8 +244,7 @@
 				},
 				"sounds" : {
 					"ambient" : ["LOOPLUMB"]
-				},
-				"battleground": "subterranean"
+				}
 			},
 			"alchemistLab" : {
 				"index" : 1,
@@ -248,8 +254,7 @@
 				},
 				"sounds" : {
 					"ambient" : ["LOOPSTAR"]
-				},
-				"battleground": "subterranean"
+				}
 			},
 			"orePit" : {
 				"index" : 2,
@@ -259,8 +264,7 @@
 				},
 				"sounds" : {
 					"ambient" : ["LOOPSULF"]
-				},
-				"battleground": "subterranean"
+				}
 			},
 			"sulfurDune" : {
 				"index" : 3,
@@ -270,8 +274,7 @@
 				},
 				"sounds" : {
 					"ambient" : ["LOOPSULF"]
-				},
-				"battleground": "subterranean"
+				}
 			},
 			"crystalCavern" : {
 				"index" : 4,
@@ -292,8 +295,7 @@
 				},
 				"sounds" : {
 					"ambient" : ["LOOPGEMP"]
-				},
-				"battleground": "subterranean"
+				}
 			},
 			"goldMine" : {
 				"index" : 6,
@@ -320,8 +322,9 @@
 	"abandonedMine" : {
 		"index" :220,
 		"handler": "mine",
-		"defaultAiValue" : 3500,
+		"lastReservedIndex" : 7,
 		"base" : {
+			"aiValue" : 3500,
 			"sounds" : {
 				"ambient" : ["LOOPCAVE"]
 			}
@@ -334,8 +337,9 @@
 	"garrisonHorizontal": {
 		"index" :33,
 		"handler": "garrison",
-		"defaultAiValue" : 0,
+		"lastReservedIndex" : 1,
 		"base" : {
+			"aiValue" : 0,
 			"sounds" : {
 				"visit" : ["MILITARY"]
 			}
@@ -358,8 +362,9 @@
 	"garrisonVertical" : {
 		"index" :219,
 		"handler": "garrison",
-		"defaultAiValue" : 0,
+		"lastReservedIndex" : 1,
 		"base" : {
+			"aiValue" : 0,
 			"sounds" : {
 				"visit" : ["MILITARY"]
 			}
@@ -384,6 +389,7 @@
 	"monolithOneWayEntrance" : {
 		"index" :43,
 		"handler": "monolith",
+		"lastReservedIndex" : 7,
 		"base" : {
 			"sounds" : {
 				"ambient" : ["LOOPMON1"],
@@ -404,6 +410,7 @@
 	"monolithOneWayExit" : {
 		"index" :44,
 		"handler": "monolith",
+		"lastReservedIndex" : 7,
 		"base" : {
 			"sounds" : { "ambient" : ["LOOPMON1"] }
 		},
@@ -421,6 +428,7 @@
 	"monolithTwoWay" : {
 		"index" :45,
 		"handler": "monolith",
+		"lastReservedIndex" : 7,
 		"base" : {
 			"sounds" : {
 				"ambient" : ["LOOPMON2"],
@@ -441,7 +449,9 @@
 
 	// subtype: level
 	"randomDwellingLvl" : {
-		"index" :217, "handler": "randomDwelling",
+		"index" :217, 
+		"handler": "randomDwelling",
+		"lastReservedIndex" : 6,
 			"types" : {
 				"objectLvl1" : { "index" : 0},
 				"objectLvl2" : { "index" : 1},
@@ -457,6 +467,7 @@
 	"randomDwellingFaction" : {
 		"index" :218,
 		"handler": "randomDwelling",
+		"lastReservedIndex" : 8,
 			"types" : {
 				"objectCastle"     : { "index" : 0},
 				"objectRampart"    : { "index" : 1},

+ 4 - 35
config/schemas/object.json

@@ -10,49 +10,18 @@
 		"index": {
 			"type":"number"
 		},
-		"name": {
-			"type":"string"
-		},
-		"defaultAiValue": {
+		"lastReservedIndex" : {
 			"type":"number"
 		},
-
 		"handler": {
 			"type":"string"
 		},
-
-		"sounds": {
-			"type":"object",
-			"additionalProperties" : false,
-			"description": "Sounds used by this object",
-			"properties" : {
-				"ambient": {
-					"type":"array",
-					"description": "Background sound of an object",
-					"items": {
-						"type": "string"
-					}
-				},
-				"visit": {
-					"type":"array",
-					"description": "Sound that played on object visit",
-					"items": {
-						"type": "string"
-					}
-				},
-				"removal": {
-					"type":"array",
-					"description": "Sound that played on object removal",
-					"items": {
-						"type": "string"
-					}
-				}
-			}
-		},
-
 		"base": {
 			"type" : "object"
 		},
+		"name": {
+			"type":"string"
+		},
 		"types": {
 			"type":"object",
 			"additionalProperties": {

+ 0 - 4
config/schemas/objectType.json

@@ -10,13 +10,9 @@
 		"index": {
 			"type":"number"
 		},
-		"name": {
-			"type":"string"
-		},
 		"aiValue": {
 			"type":"number"
 		},
-
 		"sounds": {
 			"type":"object",
 			"additionalProperties" : false,

+ 1 - 1
lib/CArtHandler.cpp

@@ -390,7 +390,7 @@ CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode
 		JsonNode conf;
 		conf.setMeta(scope);
 
-		VLC->objtypeh->loadSubObject(art->getJsonKey(), conf, Obj::ARTIFACT, art->getIndex());
+		VLC->objtypeh->loadSubObject(art->identifier, conf, Obj::ARTIFACT, art->getIndex());
 
 		if(!art->advMapDef.empty())
 		{

+ 3 - 3
lib/CGameState.cpp

@@ -129,7 +129,7 @@ void MetaString::getLocalString(const std::pair<ui8,ui32> &txt, std::string &dst
 	}
 	else if(type == OBJ_NAMES)
 	{
-		dst = VLC->objtypeh->getObjectName(ser);
+		dst = VLC->objtypeh->getObjectName(ser, 0);
 	}
 	else if(type == SEC_SKILL_NAME)
 	{
@@ -149,10 +149,10 @@ void MetaString::getLocalString(const std::pair<ui8,ui32> &txt, std::string &dst
 			dst = VLC->generaltexth->translate("core.arraytxt", ser);
 			break;
 		case CREGENS:
-			dst = VLC->generaltexth->translate("core.crgen1", ser);
+			dst = VLC->objtypeh->getObjectName(Obj::CREATURE_GENERATOR1, ser);
 			break;
 		case CREGENS4:
-			dst = VLC->generaltexth->translate("core.crgen4", ser);
+			dst = VLC->objtypeh->getObjectName(Obj::CREATURE_GENERATOR4, ser);
 			break;
 		case ADVOB_TXT:
 			dst = VLC->generaltexth->translate("core.advevent", ser);

+ 0 - 2
lib/CGeneralTextHandler.cpp

@@ -440,8 +440,6 @@ CGeneralTextHandler::CGeneralTextHandler():
 	readToVector("core.advevent", "DATA/ADVEVENT.TXT" );
 	readToVector("core.restypes", "DATA/RESTYPES.TXT" );
 	readToVector("core.randsign", "DATA/RANDSIGN.TXT" );
-	readToVector("core.crgen1",   "DATA/CRGEN1.TXT"   );
-	readToVector("core.crgen4",   "DATA/CRGEN4.TXT"   );
 	readToVector("core.overview", "DATA/OVERVIEW.TXT" );
 	readToVector("core.arraytxt", "DATA/ARRAYTXT.TXT" );
 	readToVector("core.priskill", "DATA/PRISKILL.TXT" );

+ 1 - 0
lib/CGeneralTextHandler.h

@@ -160,6 +160,7 @@ class DLL_LINKAGE CGeneralTextHandler
 	/// Attempts to detect encoding & language of H3 files
 	void detectInstallParameters() const;
 public:
+
 	/// Loads translation from provided json
 	/// Any entries loaded by this will have priority over texts registered normally
 	void loadTranslationOverrides(JsonNode const & file);

+ 201 - 181
lib/mapObjects/CObjectClassesHandler.cpp

@@ -92,7 +92,7 @@ CObjectClassesHandler::CObjectClassesHandler()
 CObjectClassesHandler::~CObjectClassesHandler()
 {
 	for(auto p : objects)
-		delete p.second;
+		delete p;
 }
 
 std::vector<JsonNode> CObjectClassesHandler::loadLegacyData(size_t dataSize)
@@ -112,6 +112,8 @@ std::vector<JsonNode> CObjectClassesHandler::loadLegacyData(size_t dataSize)
 		legacyTemplates.insert(std::make_pair(key, std::shared_ptr<const ObjectTemplate>(tmpl)));
 	}
 
+	objects.resize(256);
+
 	std::vector<JsonNode> ret(dataSize);// create storage for 256 objects
 	assert(dataSize == 256);
 
@@ -122,171 +124,170 @@ std::vector<JsonNode> CObjectClassesHandler::loadLegacyData(size_t dataSize)
 		namesParser.endLine();
 	}
 
+	JsonNode cregen1;
+	JsonNode cregen4;
+
 	CLegacyConfigParser cregen1Parser("data/crgen1");
 	do
-		customNames[Obj::CREATURE_GENERATOR1].push_back(cregen1Parser.readString());
+	{
+		JsonNode subObject;
+		subObject["name"].String() = cregen1Parser.readString();
+		cregen1.Vector().push_back(subObject);
+	}
 	while(cregen1Parser.endLine());
 
 	CLegacyConfigParser cregen4Parser("data/crgen4");
 	do
-		customNames[Obj::CREATURE_GENERATOR4].push_back(cregen4Parser.readString());
+	{
+		JsonNode subObject;
+		subObject["name"].String() = cregen4Parser.readString();
+		cregen4.Vector().push_back(subObject);
+	}
 	while(cregen4Parser.endLine());
 
+	ret[Obj::CREATURE_GENERATOR1]["subObjects"] = cregen1;
+	ret[Obj::CREATURE_GENERATOR4]["subObjects"] = cregen4;
+
+	ret[Obj::REFUGEE_CAMP]["subObjects"].Vector().push_back(ret[Obj::REFUGEE_CAMP]);
+	ret[Obj::WAR_MACHINE_FACTORY]["subObjects"].Vector().push_back(ret[Obj::WAR_MACHINE_FACTORY]);
+
 	return ret;
 }
 
-/// selects preferred ID (or subID) for new object
-template<typename Map>
-si32 selectNextID(const JsonNode & fixedID, const Map & map, si32 fixedObjectsBound)
+void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj)
 {
-	assert(fixedObjectsBound > 0);
-	if(fixedID.isNull())
-	{
-		auto lastID = map.empty() ? 0 : map.rbegin()->first;
-		return lastID < fixedObjectsBound ? fixedObjectsBound : lastID + 1;
-	}
-	auto id = static_cast<si32>(fixedID.Float());
-	if(id >= fixedObjectsBound)
-		logGlobal->error("Getting next ID overflowed: %d >= %d", id, fixedObjectsBound);
+	auto object = loadSubObjectFromJson(scope, identifier, entry, obj, obj->objects.size());
+
+	assert(object);
+	obj->objects.push_back(object);
+
+	registerObject(scope, obj->getJsonKey(), object->getSubTypeName(), object->subtype);
+}
 
-	return id;
+void CObjectClassesHandler::loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj, size_t index)
+{
+	auto object = loadSubObjectFromJson(scope, identifier, entry, obj, index);
+
+	assert(object);
+	assert(obj->objects[index] == nullptr); // ensure that this id was not loaded before
+	obj->objects[index] = object;
+
+	registerObject(scope, obj->getJsonKey(), object->getSubTypeName(), object->subtype);
 }
 
-void CObjectClassesHandler::loadObjectEntry(const std::string & identifier, const JsonNode & entry, ObjectContainter * obj, bool isSubobject)
+TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj, size_t index)
 {
-	static const si32 fixedObjectsBound = 1000; // legacy value for backward compatibilitty
-	static const si32 fixedSubobjectsBound = 10000000; // large enough arbitrary value to avoid ID-collisions
-	si32 usedBound = fixedObjectsBound;
+	assert(identifier.find(':') == std::string::npos);
+	assert(!scope.empty());
 
 	if(!handlerConstructors.count(obj->handlerName))
 	{
 		logGlobal->error("Handler with name %s was not found!", obj->handlerName);
-		return;
+		return nullptr;
 	}
-	const auto convertedId = VLC->modh->normalizeIdentifier(entry.meta, CModHandler::scopeBuiltin(), identifier);
-	const auto & entryIndex = entry["index"];
-	bool useSelectNextID = !isSubobject || entryIndex.isNull();
 
-	if(useSelectNextID && isSubobject)
-	{
-		usedBound = fixedSubobjectsBound;
-		logGlobal->error("Subobject index is Null. convertedId = '%s' obj->id = %d", convertedId, obj->id);
-	}
-	si32 id = useSelectNextID ? selectNextID(entryIndex, obj->subObjects, usedBound) 
-		: (si32)entryIndex.Float();
+	auto createdObject = handlerConstructors.at(obj->handlerName)();
 
-	auto handler = handlerConstructors.at(obj->handlerName)();
-	handler->setType(obj->id, id);
-	handler->setTypeName(obj->identifier, convertedId);
+	createdObject->modScope = scope;
+	createdObject->typeName = obj->identifier;;
+	createdObject->subTypeName = identifier;
 
-	if (customNames.count(obj->id) && customNames.at(obj->id).size() > id)
-		handler->init(entry, customNames.at(obj->id).at(id));
-	else
-		handler->init(entry);
+	createdObject->type = obj->id;
+	createdObject->subtype = index;
+	createdObject->init(entry);
 
-	//if (handler->getTemplates().empty())
+	auto range = legacyTemplates.equal_range(std::make_pair(obj->id, index));
+	for (auto & templ : boost::make_iterator_range(range.first, range.second))
 	{
-		auto range = legacyTemplates.equal_range(std::make_pair(obj->id, id));
-		for (auto & templ : boost::make_iterator_range(range.first, range.second))
-		{
-			handler->addTemplate(templ.second);
-		}
-		legacyTemplates.erase(range.first, range.second);
+		createdObject->addTemplate(templ.second);
 	}
+	legacyTemplates.erase(range.first, range.second);
 
-	logGlobal->debug("Loaded object %s(%d)::%s(%d)", obj->identifier, obj->id, convertedId, id);
+	logGlobal->debug("Loaded object %s(%d)::%s(%d)", obj->getJsonKey(), obj->id, identifier, index);
 
-	//some mods redefine content handlers in the decoration.json in such way:
-	//"core:sign" : { "types" : { "forgeSign" : { ...
-	static const std::vector<std::string> breakersRMG
-	{
-		"hota.hota decorations:hotaPandoraBox"
-		, "hota.hota decorations:hotaSubterreanGate"
-	};
-	const bool isExistingKey = obj->subObjects.count(id) > 0;
-	const bool isBreaker = std::any_of(breakersRMG.begin(), breakersRMG.end(),
-		[&handler](const std::string & str)
-		{
-			return str.compare(handler->subTypeName) == 0;
-		});
-	const bool passedHandler = !isExistingKey && !isBreaker;
+	return createdObject;
+}
 
-	if(passedHandler)
-	{
-		obj->subObjects[id] = handler;
-		obj->subIds[convertedId] = id;
-	}
-	else if(isExistingKey) //It's supposed that fan mods handlers are not overridden by default handlers
-	{
-		logGlobal->trace("Handler '%s' has not been overridden with handler '%s' in object %s(%d)::%s(%d)",
-			obj->subObjects[id]->subTypeName, obj->handlerName, obj->identifier, obj->id, convertedId, id);
-	}
-	else
-	{
-		logGlobal->warn("Handler '%s' for object %s(%d)::%s(%d) has not been activated as RMG breaker",
-			obj->handlerName, obj->identifier, obj->id, convertedId, id);
-	}
+std::string ObjectClass::getJsonKey() const
+{
+	return modScope + ':' + identifier;
 }
 
-CObjectClassesHandler::ObjectContainter * CObjectClassesHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name)
+std::string ObjectClass::getNameTextID() const
 {
-	auto obj = new ObjectContainter();
-	static const si32 fixedObjectsBound = 256; //Legacy value for backward compatibility
+	return TextIdentifier("object", modScope, identifier, "name").get();
+}
 
+std::string ObjectClass::getNameTranslated() const
+{
+	return VLC->generaltexth->translate(getNameTextID());
+}
+
+ObjectClass * CObjectClassesHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index)
+{
+	auto obj = new ObjectClass();
+
+	obj->modScope = scope;
 	obj->identifier = name;
-	obj->name = json["name"].String();
 	obj->handlerName = json["handler"].String();
 	obj->base = json["base"];
-	obj->id = selectNextID(json["index"], objects, fixedObjectsBound);
+	obj->id = index;
 
-	if(json["defaultAiValue"].isNull())
-		obj->groupDefaultAiValue = boost::none;
-	else
-		obj->groupDefaultAiValue = static_cast<boost::optional<si32>>(json["defaultAiValue"].Integer());
+	VLC->generaltexth->registerString(obj->getNameTextID(), json["name"].String());
 
-	for (auto entry : json["types"].Struct())
-		loadObjectEntry(entry.first, entry.second, obj);
+	obj->objects.resize(json["lastReservedIndex"].Float() + 1);
 
+	for (auto subData : json["types"].Struct())
+	{
+		if (!subData.second["index"].isNull())
+		{
+			std::string const & subMeta = subData.second["index"].meta;
+
+			if ( subMeta != "core")
+				logMod->warn("Object %s:%s.%s - attempt to load object with preset index! This option is reserved for built-in mod", subMeta, name, subData.first );
+			size_t subIndex = subData.second["index"].Integer();
+			loadSubObject(subData.second.meta, subData.first, subData.second, obj, subIndex);
+		}
+		else
+			loadSubObject(subData.second.meta, subData.first, subData.second, obj);
+	}
 	return obj;
 }
 
 void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data)
 {
-	auto object = loadFromJson(scope, data, VLC->modh->normalizeIdentifier(scope, CModHandler::scopeBuiltin(), name));
-	objects[object->id] = object;
+	auto object = loadFromJson(scope, data, name, objects.size());
+	objects.push_back(object);
 	VLC->modh->identifiers.registerObject(scope, "object", name, object->id);
 }
 
 void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index)
 {
-	auto object = loadFromJson(scope, data, VLC->modh->normalizeIdentifier(scope, CModHandler::scopeBuiltin(), name));
+	auto object = loadFromJson(scope, data, name, index);
 	assert(objects[(si32)index] == nullptr); // ensure that this id was not loaded before
 	objects[(si32)index] = object;
 	VLC->modh->identifiers.registerObject(scope, "object", name, object->id);
 }
 
-void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, si32 ID, boost::optional<si32> subID)
+void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, si32 ID, si32 subID)
 {
-	static const bool isSubObject = true;
-
 	config.setType(JsonNode::JsonType::DATA_STRUCT); // ensure that input is not NULL
-	assert(objects.count(ID));
-	if (subID)
-	{
-		assert(objects.at(ID)->subObjects.count(subID.get()) == 0);
-		assert(config["index"].isNull());
-		config["index"].Float() = subID.get();
-		config["index"].setMeta(config.meta);
-	}
+	assert(ID < objects.size());
+	assert(objects[ID]);
+
+	if ( subID >= objects[ID]->objects.size())
+		objects[ID]->objects.resize(subID+1);
+
 	JsonUtils::inherit(config, objects.at(ID)->base);
-	loadObjectEntry(identifier, config, objects[ID], isSubObject);
+	loadSubObject(config.meta, identifier, config, objects[ID], subID);
 }
 
 void CObjectClassesHandler::removeSubObject(si32 ID, si32 subID)
 {
-	assert(objects.count(ID));
-	assert(objects.at(ID)->subObjects.count(subID));
-	objects.at(ID)->subObjects.erase(subID); //TODO: cleanup string id map
+	assert(ID < objects.size());
+	assert(objects[ID]);
+	assert(subID < objects[ID]->objects.size());
+	objects[ID]->objects[subID] = nullptr;
 }
 
 std::vector<bool> CObjectClassesHandler::getDefaultAllowed() const
@@ -296,29 +297,25 @@ std::vector<bool> CObjectClassesHandler::getDefaultAllowed() const
 
 TObjectTypeHandler CObjectClassesHandler::getHandlerFor(si32 type, si32 subtype) const
 {
-	if (objects.count(type))
-	{
-		if (objects.at(type)->subObjects.count(subtype))
-			return objects.at(type)->subObjects.at(subtype);
-	}
-	std::string errorString = "Failed to find object of type " + std::to_string(type) + "::" + std::to_string(subtype);
-	logGlobal->error(errorString);
-	throw std::runtime_error(errorString);
+	assert(type < objects.size());
+	assert(objects[type]);
+	assert(subtype < objects[type]->objects.size());
+
+	return objects.at(type)->objects.at(subtype);
 }
 
 TObjectTypeHandler CObjectClassesHandler::getHandlerFor(std::string scope, std::string type, std::string subtype) const
 {
-	boost::optional<si32> id = VLC->modh->identifiers.getIdentifier(scope, "object", type, false);
+	boost::optional<si32> id = VLC->modh->identifiers.getIdentifier(scope, "object", type);
 	if(id)
 	{
-		auto object = objects.at(id.get());
-		if(object->subIds.count(subtype))
-		{
-			si32 subId = object->subIds.at(subtype);
+		auto object = objects[id.get()];
+		boost::optional<si32> subID = VLC->modh->identifiers.getIdentifier(scope, object->getJsonKey(), subtype);
 
-			return object->subObjects.at(subId);
-		}
+		if (subID)
+			return object->objects[subID.get()];
 	}
+
 	std::string errorString = "Failed to find object of type " + type + "::" + subtype;
 	logGlobal->error(errorString);
 	throw std::runtime_error(errorString);
@@ -334,20 +331,23 @@ std::set<si32> CObjectClassesHandler::knownObjects() const
 	std::set<si32> ret;
 
 	for (auto entry : objects)
-		ret.insert(entry.first);
+		if (entry)
+			ret.insert(entry->id);
 
 	return ret;
 }
 
 std::set<si32> CObjectClassesHandler::knownSubObjects(si32 primaryID) const
 {
+	assert(primaryID < objects.size());
+	assert(objects[primaryID]);
+
 	std::set<si32> ret;
 
-	if (objects.count(primaryID))
-	{
-		for (auto entry : objects.at(primaryID)->subObjects)
-			ret.insert(entry.first);
-	}
+	for (auto entry : objects.at(primaryID)->objects)
+		if (entry)
+			ret.insert(entry->subtype);
+
 	return ret;
 }
 
@@ -355,68 +355,75 @@ void CObjectClassesHandler::beforeValidate(JsonNode & object)
 {
 	for (auto & entry : object["types"].Struct())
 	{
+		if (object.Struct().count("subObjects"))
+		{
+			auto const & vector = object["subObjects"].Vector();
+
+			if (entry.second.Struct().count("index"))
+			{
+				size_t index = entry.second["index"].Integer();
+
+				if (index < vector.size())
+					JsonUtils::inherit(entry.second, vector[index]);
+			}
+		}
+
 		JsonUtils::inherit(entry.second, object["base"]);
 		for (auto & templ : entry.second["templates"].Struct())
 			JsonUtils::inherit(templ.second, entry.second["base"]);
 	}
+	object.Struct().erase("subObjects");
 }
 
 void CObjectClassesHandler::afterLoadFinalization()
 {
 	for(auto entry : objects)
 	{
-		for(auto obj : entry.second->subObjects)
+		if (!entry)
+			continue;
+
+		for(auto obj : entry->objects)
 		{
-			obj.second->afterLoadFinalization();
-			if(obj.second->getTemplates().empty())
-				logGlobal->warn("No templates found for %d:%d", entry.first, obj.first);
+			if (!obj)
+				continue;
+
+			obj->afterLoadFinalization();
+			if(obj->getTemplates().empty())
+				logGlobal->warn("No templates found for %s:%s", entry->getJsonKey(), obj->getJsonKey());
 		}
 	}
 
 	//duplicate existing two-way portals to make reserve for RMG
-	auto& portalVec = objects[Obj::MONOLITH_TWO_WAY]->subObjects;
+	auto& portalVec = objects[Obj::MONOLITH_TWO_WAY]->objects;
 	size_t portalCount = portalVec.size();
-	size_t currentIndex = portalCount;
-	while(portalVec.size() < 100)
-	{
-		portalVec[(si32)currentIndex] = portalVec[static_cast<si32>(currentIndex % portalCount)];
-		currentIndex++;
-	}
-}
 
-std::string CObjectClassesHandler::getObjectName(si32 type) const
-{
-	if (objects.count(type))
-		return objects.at(type)->name;
-	logGlobal->error("Access to non existing object of type %d", type);
-	return "";
+	for (size_t i = portalCount; i < 100; ++i)
+		portalVec.push_back(portalVec[static_cast<si32>(i % portalCount)]);
 }
 
 std::string CObjectClassesHandler::getObjectName(si32 type, si32 subtype) const
 {
-	if (knownSubObjects(type).count(subtype))
-	{
-		auto name = getHandlerFor(type, subtype)->getCustomName();
-		if (name)
-			return name.get();
-	}
-	return getObjectName(type);
-}
-
-SObjectSounds CObjectClassesHandler::getObjectSounds(si32 type) const
-{
-	if(objects.count(type))
-		return objects.at(type)->sounds;
-	logGlobal->error("Access to non existing object of type %d", type);
-	return SObjectSounds();
+	auto const handler = getHandlerFor(type, subtype);
+	if (handler->hasNameTextID())
+		return handler->getNameTranslated();
+	else
+		return objects[type]->getNameTranslated();
 }
 
 SObjectSounds CObjectClassesHandler::getObjectSounds(si32 type, si32 subtype) const
 {
-	if(knownSubObjects(type).count(subtype))
-		return getHandlerFor(type, subtype)->getSounds();
-	else
-		return getObjectSounds(type);
+	// TODO: these objects may have subID's that does not have associated handler:
+	// Prison: uses hero type as subID
+	// Hero: uses hero type as subID, but registers hero classes as subtypes
+	// Spell scroll: uses spell ID as subID
+	if(type == Obj::PRISON || type == Obj::HERO || type == Obj::SPELL_SCROLL)
+		subtype = 0;
+
+	assert(type < objects.size());
+	assert(objects[type]);
+	assert(subtype < objects[type]->objects.size());
+
+	return getHandlerFor(type, subtype)->getSounds();
 }
 
 std::string CObjectClassesHandler::getObjectHandlerName(si32 type) const
@@ -424,11 +431,6 @@ std::string CObjectClassesHandler::getObjectHandlerName(si32 type) const
 	return objects.at(type)->handlerName;
 }
 
-boost::optional<si32> CObjectClassesHandler::getObjGroupAiValue(si32 primaryID) const
-{
-	return objects.at(primaryID)->groupDefaultAiValue;
-}
-
 AObjectTypeHandler::AObjectTypeHandler():
 	type(-1), subtype(-1)
 {
@@ -439,16 +441,29 @@ AObjectTypeHandler::~AObjectTypeHandler()
 {
 }
 
-void AObjectTypeHandler::setType(si32 type, si32 subtype)
+std::string AObjectTypeHandler::getJsonKey() const
+{
+	return modScope + ':' + subTypeName;
+}
+
+si32 AObjectTypeHandler::getIndex() const
 {
-	this->type = type;
-	this->subtype = subtype;
+	return type;
 }
 
-void AObjectTypeHandler::setTypeName(std::string type, std::string subtype)
+si32 AObjectTypeHandler::getSubIndex() const
 {
-	this->typeName = type;
-	this->subTypeName = subtype;
+	return subtype;
+}
+
+std::string AObjectTypeHandler::getTypeName() const
+{
+	return typeName;
+}
+
+std::string AObjectTypeHandler::getSubTypeName() const
+{
+	return subTypeName;
 }
 
 static ui32 loadJsonOrMax(const JsonNode & input)
@@ -459,7 +474,7 @@ static ui32 loadJsonOrMax(const JsonNode & input)
 		return static_cast<ui32>(input.Float());
 }
 
-void AObjectTypeHandler::init(const JsonNode & input, boost::optional<std::string> name)
+void AObjectTypeHandler::init(const JsonNode & input)
 {
 	base = input["base"];
 
@@ -491,11 +506,6 @@ void AObjectTypeHandler::init(const JsonNode & input, boost::optional<std::strin
 		}
 	}
 
-	if (input["name"].isNull())
-		objectName = name;
-	else
-		objectName.reset(input["name"].String());
-
 	for(const JsonNode & node : input["sounds"]["ambient"].Vector())
 		sounds.ambient.push_back(node.String());
 
@@ -536,9 +546,19 @@ void AObjectTypeHandler::initTypeData(const JsonNode & input)
 	// empty implementation for overrides
 }
 
-boost::optional<std::string> AObjectTypeHandler::getCustomName() const
+bool AObjectTypeHandler::hasNameTextID() const
+{
+	return false;
+}
+
+std::string AObjectTypeHandler::getNameTextID() const
+{
+	return TextIdentifier("mapObject", modScope, typeName, subTypeName, "name").get();
+}
+
+std::string AObjectTypeHandler::getNameTranslated() const
 {
-	return objectName;
+	return VLC->generaltexth->translate(getNameTextID());
 }
 
 SObjectSounds AObjectTypeHandler::getSounds() const

+ 62 - 53
lib/mapObjects/CObjectClassesHandler.h

@@ -136,12 +136,12 @@ public:
 
 class CGObjectInstance;
 
+/// Class responsible for creation of objects of specific type & subtype
 class DLL_LINKAGE AObjectTypeHandler : public boost::noncopyable
 {
-	RandomMapInfo rmgInfo;
+	friend class CObjectClassesHandler;
 
-	/// Human-readable name of this object, used for objects like banks and dwellings, if set
-	boost::optional<std::string> objectName;
+	RandomMapInfo rmgInfo;
 
 	JsonNode base; /// describes base template
 
@@ -150,9 +150,15 @@ class DLL_LINKAGE AObjectTypeHandler : public boost::noncopyable
 	SObjectSounds sounds;
 
 	boost::optional<si32> aiValue;
-
 	boost::optional<std::string> battlefield;
 
+	std::string modScope;
+	std::string typeName;
+	std::string subTypeName;
+
+	si32 type;
+	si32 subtype;
+
 protected:
 	void preInitObject(CGObjectInstance * obj) const;
 	virtual bool objectFilter(const CGObjectInstance *, std::shared_ptr<const ObjectTemplate>) const;
@@ -160,22 +166,23 @@ protected:
 	/// initialization for classes that inherit this one
 	virtual void initTypeData(const JsonNode & input);
 public:
-	std::string typeName;
-	std::string subTypeName;
 
-	si32 type;
-	si32 subtype;
 	AObjectTypeHandler();
 	virtual ~AObjectTypeHandler();
 
-	void setType(si32 type, si32 subtype);
-	void setTypeName(std::string type, std::string subtype);
+	si32 getIndex() const;
+	si32 getSubIndex() const;
+
+	std::string getTypeName() const;
+	std::string getSubTypeName() const;
 
 	/// loads generic data from Json structure and passes it towards type-specific constructors
-	void init(const JsonNode & input, boost::optional<std::string> name = boost::optional<std::string>());
+	void init(const JsonNode & input);
+
+	/// returns full form of identifier of this object in form of modName:objectName
+	std::string getJsonKey() const;
 
 	/// Returns object-specific name, if set
-	boost::optional<std::string> getCustomName() const;
 	SObjectSounds getSounds() const;
 
 	void addTemplate(std::shared_ptr<const ObjectTemplate> templ);
@@ -191,13 +198,19 @@ public:
 
 	BattleField getBattlefield() const;
 
-	/// returns preferred template for this object, if present (e.g. one of 3 possible templates for town - village, fort and castle)
-	/// note that appearance will not be changed - this must be done separately (either by assignment or via pack from server)
-
 	const RandomMapInfo & getRMGInfo();
 
 	boost::optional<si32> getAiValue() const;
 
+	/// returns true if this class provides custom text ID's instead of generic per-object name
+	virtual bool hasNameTextID() const;
+
+	/// returns object's name in form of translatable text ID
+	virtual std::string getNameTextID() const;
+
+	/// returns object's name in form of human-readable text
+	std::string getNameTranslated() const;
+
 	virtual bool isStaticObject();
 
 	virtual void afterLoadFinalization();
@@ -219,7 +232,7 @@ public:
 		h & subtype;
 		h & templates;
 		h & rmgInfo;
-		h & objectName;
+		h & modScope;
 		h & typeName;
 		h & subTypeName;
 		h & sounds;
@@ -230,40 +243,40 @@ public:
 
 typedef std::shared_ptr<AObjectTypeHandler> TObjectTypeHandler;
 
-class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase
+/// Class responsible for creation of adventure map objects of specific type
+class DLL_LINKAGE ObjectClass
 {
-	/// Small internal structure that contains information on specific group of objects
-	/// (creating separate entity is overcomplicating at least at this point)
-	struct ObjectContainter
-	{
-		si32 id;
-		std::string identifier;
-		std::string name; // human-readable name
-		std::string handlerName; // ID of handler that controls this object, should be determined using handlerConstructor map
+public:
+	std::string modScope;
+	std::string identifier;
 
-		JsonNode base;
-		std::map<si32, TObjectTypeHandler> subObjects;
-		std::map<std::string, si32> subIds;//full id from core scope -> subtype
+	si32 id;
+	std::string handlerName; // ID of handler that controls this object, should be determined using handlerConstructor map
 
-		SObjectSounds sounds;
+	JsonNode base;
+	std::vector<TObjectTypeHandler> objects;
 
-		boost::optional<si32> groupDefaultAiValue;
+	ObjectClass() = default;
 
-		template <typename Handler> void serialize(Handler &h, const int version)
-		{
-			h & name;
-			h & handlerName;
-			h & base;
-			h & subObjects;
-			h & identifier;
-			h & subIds;
-			h & sounds;
-			h & groupDefaultAiValue;
-		}
-	};
+	std::string getJsonKey() const;
+	std::string getNameTextID() const;
+	std::string getNameTranslated() const;
+
+	template <typename Handler> void serialize(Handler &h, const int version)
+	{
+		h & handlerName;
+		h & base;
+		h & objects;
+		h & identifier;
+		h & modScope;
+	}
+};
 
+/// Main class responsible for creation of all adventure map objects
+class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase
+{
 	/// list of object handlers, each of them handles only one type
-	std::map<si32, ObjectContainter * > objects;
+	std::vector<ObjectClass * > objects;
 
 	/// map that is filled during contruction with all known handlers. Not serializeable due to usage of std::function
 	std::map<std::string, std::function<TObjectTypeHandler()> > handlerConstructors;
@@ -272,12 +285,12 @@ class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase
 	typedef std::multimap<std::pair<si32, si32>, std::shared_ptr<const ObjectTemplate>> TTemplatesContainer;
 	TTemplatesContainer legacyTemplates;
 
-	/// contains list of custom names for H3 objects (e.g. Dwellings), used to load H3 data
-	/// format: customNames[primaryID][secondaryID] -> name
-	std::map<si32, std::vector<std::string>> customNames;
+	TObjectTypeHandler loadSubObjectFromJson(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj, size_t index);
 
-	void loadObjectEntry(const std::string & identifier, const JsonNode & entry, ObjectContainter * obj, bool isSubobject = false);
-	ObjectContainter * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name);
+	void loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj);
+	void loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj, size_t index);
+
+	ObjectClass * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index);
 
 public:
 	CObjectClassesHandler();
@@ -288,7 +301,7 @@ public:
 	void loadObject(std::string scope, std::string name, const JsonNode & data) override;
 	void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override;
 
-	void loadSubObject(const std::string & identifier, JsonNode config, si32 ID, boost::optional<si32> subID = boost::optional<si32>());
+	void loadSubObject(const std::string & identifier, JsonNode config, si32 ID, si32 subID);
 	void removeSubObject(si32 ID, si32 subID);
 
 	void beforeValidate(JsonNode & object) override;
@@ -305,17 +318,13 @@ public:
 	TObjectTypeHandler getHandlerFor(std::string scope, std::string type, std::string subtype) const;
 	TObjectTypeHandler getHandlerFor(CompoundMapObjectID compoundIdentifier) const;
 
-	std::string getObjectName(si32 type) const;
 	std::string getObjectName(si32 type, si32 subtype) const;
 
-	SObjectSounds getObjectSounds(si32 type) const;
 	SObjectSounds getObjectSounds(si32 type, si32 subtype) const;
 
 	/// Returns handler string describing the handler (for use in client)
 	std::string getObjectHandlerName(si32 type) const;
 
-	boost::optional<si32> getObjGroupAiValue(si32 primaryID) const; //default AI value of objects belonging to particular primaryID
-
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & objects;

+ 1 - 1
lib/mapObjects/CQuest.cpp

@@ -1171,7 +1171,7 @@ void CGBorderGuard::getVisitText (MetaString &text, std::vector<Component> &comp
 void CGBorderGuard::getRolloverText (MetaString &text, bool onHover) const
 {
 	if (!onHover)
-		text << VLC->generaltexth->tentColors[subID] << " " << VLC->objtypeh->getObjectName(Obj::KEYMASTER);
+		text << VLC->generaltexth->tentColors[subID] << " " << VLC->objtypeh->getObjectName(Obj::KEYMASTER, subID);
 }
 
 bool CGBorderGuard::checkQuest(const CGHeroInstance * h) const

+ 21 - 1
lib/mapObjects/CommonConstructors.cpp

@@ -16,6 +16,7 @@
 #include "../TerrainHandler.h"
 #include "../mapping/CMap.h"
 #include "../CHeroHandler.h"
+#include "../CGeneralTextHandler.h"
 #include "../CCreatureHandler.h"
 #include "JsonRandom.h"
 #include "../CModHandler.h"
@@ -150,8 +151,18 @@ CDwellingInstanceConstructor::CDwellingInstanceConstructor()
 
 }
 
+bool CDwellingInstanceConstructor::hasNameTextID() const
+{
+	return true;
+}
+
 void CDwellingInstanceConstructor::initTypeData(const JsonNode & input)
 {
+	if (input.Struct().count("name") == 0)
+		logMod->warn("Dwelling %s missing name!", getJsonKey());
+
+	VLC->generaltexth->registerString(getNameTextID(), input["name"].String());
+
 	const JsonVector & levels = input["creatures"].Vector();
 	const auto totalLevels = levels.size();
 
@@ -272,9 +283,18 @@ CBankInstanceConstructor::CBankInstanceConstructor()
 
 }
 
+bool CBankInstanceConstructor::hasNameTextID() const
+{
+	return true;
+}
+
 void CBankInstanceConstructor::initTypeData(const JsonNode & input)
 {
-	//TODO: name = input["name"].String();
+	if (input.Struct().count("name") == 0)
+		logMod->warn("Bank %s missing name!", getJsonKey());
+
+	VLC->generaltexth->registerString(getNameTextID(), input["name"].String());
+
 	levels = input["levels"].Vector();
 	bankResetDuration = static_cast<si32>(input["resetDuration"].Float());
 }

+ 3 - 0
lib/mapObjects/CommonConstructors.h

@@ -127,6 +127,7 @@ protected:
 	void initTypeData(const JsonNode & input) override;
 
 public:
+	bool hasNameTextID() const override;
 
 	CDwellingInstanceConstructor();
 	CGObjectInstance * create(std::shared_ptr<const ObjectTemplate> tmpl = nullptr) const override;
@@ -218,6 +219,8 @@ public:
 	CGObjectInstance * create(std::shared_ptr<const ObjectTemplate> tmpl = nullptr) const override;
 	void configureObject(CGObjectInstance * object, CRandomGenerator & rng) const override;
 
+	bool hasNameTextID() const override;
+
 	std::unique_ptr<IObjectInfo> getObjectInfo(std::shared_ptr<const ObjectTemplate> tmpl) const override;
 
 	template <typename Handler> void serialize(Handler &h, const int version)

+ 3 - 0
lib/mapObjects/ObjectTemplate.cpp

@@ -559,6 +559,9 @@ void ObjectTemplate::recalculate()
 	calculateBlockedOffsets();
 	calculateBlockMapOffset();
 	calculateVisitableOffset();
+
+	if (visitable && visitDir == 0)
+		logMod->warn("Template for %s is visitable but has no visitable directions!", animationFile);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 4 - 4
lib/mapping/MapFormatJson.cpp

@@ -172,7 +172,7 @@ namespace TriggeredEventsDetail
 				{
 					si32 subtype = *subtypes.begin();
 					auto handler = VLC->objtypeh->getHandlerFor(type, subtype);
-					identifier = handler->typeName;
+					identifier = handler->getTypeName();
 				}
 			}
 			break;
@@ -1141,7 +1141,7 @@ void CMapLoaderJson::MapObjectLoader::construct()
 	pos.z = static_cast<si32>(configuration["l"].Float());
 
 	//special case for grail
-    if(typeName == "grail")
+	if(typeName == "grail")
 	{
 		owner->map->grailPos = pos;
 
@@ -1159,8 +1159,8 @@ void CMapLoaderJson::MapObjectLoader::construct()
 
 	auto appearance = new ObjectTemplate;
 
-	appearance->id = Obj(handler->type);
-	appearance->subid = handler->subtype;
+	appearance->id = Obj(handler->getIndex());
+	appearance->subid = handler->getSubIndex();
 	appearance->readJson(configuration["template"], false);
 
 	// Will be destroyed soon and replaced with shared template

+ 4 - 4
mapeditor/mainwindow.cpp

@@ -468,6 +468,9 @@ void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool useCust
 		itemGroup = itms.front();
 	}
 
+	if (VLC->objtypeh->knownObjects().count(ID) == 0)
+		return;
+
 	auto knownSubObjects = VLC->objtypeh->knownSubObjects(ID);
 	for(auto secondaryID : knownSubObjects)
 	{
@@ -477,10 +480,7 @@ void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool useCust
 		if(staticOnly && !factory->isStaticObject())
 			continue;
 
-		auto subGroupName = QString::fromStdString(factory->subTypeName);
-		auto customName = factory->getCustomName();
-		if(customName)
-			subGroupName = tr(customName->c_str());
+		auto subGroupName = QString::fromStdString(factory->getNameTranslated());
 		
 		auto * itemType = new QStandardItem(subGroupName);
 		for(int templateId = 0; templateId < templates.size(); ++templateId)

+ 2 - 2
mapeditor/mapcontroller.cpp

@@ -106,8 +106,8 @@ void MapController::repairMap()
 		if(obj->ID != Obj::HERO && obj->ID != Obj::PRISON && (obj->typeName.empty() || obj->subTypeName.empty()))
 		{
 			auto handler = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID);
-			obj->typeName = handler->typeName;
-			obj->subTypeName = handler->subTypeName;
+			obj->typeName = handler->getTypeName();
+			obj->subTypeName = handler->getSubTypeName();
 		}
 		//fix flags
 		if(obj->getOwner() == PlayerColor::UNFLAGGABLE)