Преглед изворни кода

Merge pull request #5707 from IvanSavenko/dwelling_fix

Fixes for handling of oversized map dwellings
Ivan Savenko пре 5 месеци
родитељ
комит
a13ab8f013

+ 1 - 0
config/objects/dwellings.json

@@ -377,6 +377,7 @@
 			},
 			"unicornGladeBig": {
 				"index": 51,
+				"bannedForRandomDwelling" : true,
 				"creatures": [["unicorn"]],
 				"sounds": {
 					"ambient": ["LOOPUNIC"]

+ 46 - 0
docs/modders/Map_Objects/Dwelling.md

@@ -9,6 +9,11 @@
 		[ "airElemental", "stormElemental" ],
 		[ "waterElemental" ]
 	],
+	
+	/// If set to true, this dwelling will not be selected as a replacement for random dwelling on map
+	/// Such dwellings have no restrictions on which tiles are visitable or blocked
+	/// For dwelling to be usable as a replacement, it must follow some additional restrictions (see below)
+	"bannedForRandomDwelling" : true,
 
 	/// List of guards for this dwelling. Can have two possible values:
 	/// Boolean true/false - If set to "true", guards will be generated using H3 formula:
@@ -20,3 +25,44 @@
 	]
 }
 ```
+
+## Replacement of random dwellings
+
+Existing maps may contain random dwellings that will be replaced with concrete dwellings on map loading.
+
+For dwelling to be a valid replacement for such random dwelling it must be:
+
+- block at most 2x2 tile square
+- one tile in bottom row must be visitable, and another - blocked
+
+Visible tiles (`V` in map object template mask) don't have any restrictions and can have any layout
+
+It is possible to make dwellings that don't fulfill this requirements, however such dwellings should only be used for custom maps or random maps. Mod that adds a new faction need to also provide a set of valid dwellings that can be used for replacement of random dwellings.
+
+Examples of valid dwellings:
+
+- minimal - bottom row contains one blocked and one visitable tile, second row fully passable
+
+  ```json
+  "mask":[
+      "AB"
+  ],
+  ```
+
+- maximal - bottom row contains one blocked and one visitable tile, both tiles on second row are blocked
+
+  ```json
+  "mask":[
+      "BB"
+      "BA"
+  ],
+  ```
+
+- extended visual - similar to maximal, but right-most column is fully passable. Note that blocked tiles still fit into 2x2 square
+
+  ```json
+  "mask":[
+      "BBV"
+      "BAV"
+  ],
+  ```

+ 1 - 0
lib/mapObjectConstructors/AObjectTypeHandler.cpp

@@ -175,6 +175,7 @@ void AObjectTypeHandler::clearTemplates()
 void AObjectTypeHandler::addTemplate(const std::shared_ptr<const ObjectTemplate> & templ)
 {
 	templates.push_back(templ);
+	onTemplateAdded(templ);
 }
 
 void AObjectTypeHandler::addTemplate(JsonNode config)

+ 2 - 0
lib/mapObjectConstructors/AObjectTypeHandler.h

@@ -57,6 +57,8 @@ protected:
 
 	/// initialization for classes that inherit this one
 	virtual void initTypeData(const JsonNode & input);
+
+	virtual void onTemplateAdded(const std::shared_ptr<const ObjectTemplate>) {}
 public:
 
 	AObjectTypeHandler();

+ 26 - 1
lib/mapObjectConstructors/DwellingInstanceConstructor.cpp

@@ -15,7 +15,9 @@
 #include "../json/JsonRandom.h"
 #include "../GameLibrary.h"
 #include "../mapObjects/CGDwelling.h"
+#include "../mapObjects/ObjectTemplate.h"
 #include "../modding/IdentifierStorage.h"
+#include "../CConfigHandler.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -52,6 +54,30 @@ void DwellingInstanceConstructor::initTypeData(const JsonNode & input)
 	}
 	guards = input["guards"];
 	bannedForRandomDwelling = input["bannedForRandomDwelling"].Bool();
+
+	for (const auto & mapTemplate : getTemplates())
+		onTemplateAdded(mapTemplate);
+}
+
+void DwellingInstanceConstructor::onTemplateAdded(const std::shared_ptr<const ObjectTemplate> mapTemplate)
+{
+	if (bannedForRandomDwelling || settings["mods"]["validation"].String() == "off")
+		return;
+
+	bool invalidForRandomDwelling = false;
+	int3 corner = mapTemplate->getCornerOffset();
+
+	for (const auto & tile : mapTemplate->getBlockedOffsets())
+		invalidForRandomDwelling |= (tile.x != -corner.x && tile.x != -corner.x-1) || (tile.y != -corner.y && tile.y != -corner.y-1);
+
+	for (const auto & tile : {mapTemplate->getVisitableOffset()})
+		invalidForRandomDwelling |= (tile.x != corner.x && tile.x != corner.x+1) || tile.y != corner.y;
+
+	invalidForRandomDwelling |= !mapTemplate->isBlockedAt(corner.x+0, corner.y) && !mapTemplate->isVisibleAt(corner.x+0, corner.y);
+	invalidForRandomDwelling |= !mapTemplate->isBlockedAt(corner.x+1, corner.y) && !mapTemplate->isVisibleAt(corner.x+1, corner.y);
+
+	if (invalidForRandomDwelling)
+		logMod->warn("Dwelling %s has template %s which is not valid for a random dwelling! Dwellings must not block tiles outside 2x2 range and must be visitable in bottom row. Change dwelling mask or mark dwelling as 'bannedForRandomDwelling'", getJsonKey(), mapTemplate->animationFile.getOriginalName());
 }
 
 bool DwellingInstanceConstructor::isBannedForRandomDwelling() const
@@ -152,5 +178,4 @@ std::vector<const CCreature *> DwellingInstanceConstructor::getProducedCreatures
 	return creatures;
 }
 
-
 VCMI_LIB_NAMESPACE_END

+ 1 - 0
lib/mapObjectConstructors/DwellingInstanceConstructor.h

@@ -28,6 +28,7 @@ class DwellingInstanceConstructor : public CDefaultObjectTypeHandler<CGDwelling>
 protected:
 	bool objectFilter(const CGObjectInstance * obj, std::shared_ptr<const ObjectTemplate> tmpl) const override;
 	void initTypeData(const JsonNode & input) override;
+	void onTemplateAdded(const std::shared_ptr<const ObjectTemplate>) override;
 
 public:
 	bool hasNameTextID() const override;

+ 3 - 2
lib/mapObjects/CGObjectInstance.cpp

@@ -121,7 +121,7 @@ const std::set<int3> & CGObjectInstance::getBlockedOffsets() const
 void CGObjectInstance::setType(MapObjectID newID, MapObjectSubID newSubID)
 {
 	auto position = visitablePos();
-	auto oldOffset = getVisitableOffset();
+	auto oldOffset = appearance->getCornerOffset();
 	auto &tile = cb->gameState().getMap().getTile(position);
 
 	//recalculate blockvis tiles - new appearance might have different blockmap than before
@@ -144,11 +144,12 @@ void CGObjectInstance::setType(MapObjectID newID, MapObjectSubID newSubID)
 	// instead, appearance update & pos adjustment occurs in GiveHero::applyGs
 	needToAdjustOffset |= this->ID == Obj::PRISON && newID == Obj::HERO;
 	needToAdjustOffset |= newID == Obj::MONSTER;
+	needToAdjustOffset |= newID == Obj::CREATURE_GENERATOR1 || newID == Obj::CREATURE_GENERATOR2 || newID == Obj::CREATURE_GENERATOR3 || newID == Obj::CREATURE_GENERATOR4;
 
 	if(needToAdjustOffset)
 	{
 		// adjust position since object visitable offset might have changed
-		auto newOffset = getVisitableOffset();
+		auto newOffset = appearance->getCornerOffset();
 		pos = pos - oldOffset + newOffset;
 	}
 

+ 17 - 0
lib/mapObjects/ObjectTemplate.cpp

@@ -500,6 +500,23 @@ void ObjectTemplate::calculateVisitableOffset()
 	visitableOffset = int3(0, 0, 0);
 }
 
+int3 ObjectTemplate::getCornerOffset() const
+{
+	assert(isVisitable());
+
+	int3 ret = visitableOffset;
+	for (const auto & tile : blockedOffsets)
+	{
+		ret = {
+			std::min(-tile.x, ret.x),
+			std::min(-tile.y, ret.y),
+			ret.z
+		};
+	}
+
+	return ret;
+}
+
 bool ObjectTemplate::canBePlacedAt(TerrainId terrainID) const
 {
 	if (anyLandTerrain)

+ 5 - 0
lib/mapObjects/ObjectTemplate.h

@@ -124,6 +124,11 @@ public:
 	// Checks if object can be placed on specific terrain
 	bool canBePlacedAt(TerrainId terrain) const;
 
+	/// Returns number of completely empty rows & columns in template
+	/// Such as shifted wandering monster def's from hota, or Portal of Glory dwelling from H3
+	/// object must be visitable
+	int3 getCornerOffset() const;
+
 	CompoundMapObjectID getCompoundID() const;
 
 	ObjectTemplate();