浏览代码

Merge pull request #5591 from IvanSavenko/vector_replace

Support for replacement of individual entries in json lists
Ivan Savenko 6 月之前
父节点
当前提交
549a729e20
共有 2 个文件被更改,包括 122 次插入3 次删除
  1. 64 0
      docs/modders/Readme.md
  2. 58 3
      lib/json/JsonUtils.cpp

+ 64 - 0
docs/modders/Readme.md

@@ -134,6 +134,70 @@ Note that modification of existing objects does not requires a dependency on edi
 
 This allows using objects editing not just for rebalancing mods but also to provide compatibility between two different mods or to add interaction between two mods.
 
+### Modifying properties of existing objects
+
+As mentioned above, you can change any properties of existing objects using this form:
+
+```json
+"core:archer" : {
+	"hitPoints" : 10
+},
+```
+
+Note that you only need to specify changed properties. This will make your mod smaller and easier to read or maintain, and will reduce potential conflicts with other mods.
+
+When replacing booleans, numbers, or strings in this way, you only need to specify desired value. When replacing list of values, you can use the same approach, but if you only need to modify some values in the list, in order to reduce potential conflicts with other mods, you may want to consider different approach. Consider this:
+
+```json
+"core:archer" : {
+	"upgrades": ["marksman", "crossbowman" ],
+}
+```
+
+Such form will allow you to make an alternative upgrade of Archer, so it may be upgraded to either Marksman or to your new unit, Crossbowman. This will work, however if there is another mod that also attempts to add upgrade to the same unit, this would result in a mod conflict, and only one value will be used.
+
+To avoid such conflict, you can use following form:
+
+```json
+"core:archer" : {
+	"upgrades": {
+		"append" : "crossbowman"
+	}
+}
+```
+
+This form will preserve all existing upgrades for such unit, and will append new upgrade to Crossbowman to list of potential upgrades. And if there is another mod that also adds new upgrades, both mods will work as intended, without conflict.
+
+More complete description of such syntax:
+
+```json
+"core:archer" : {
+	"upgrades": {
+		// appends a single item to the end of list
+		"append" : "crossbowman"
+
+		// appends multiple items from the provided list to the end of list
+		"appendItems" : [ "crossbowman", "arbalist" ]
+		
+		// insert new item before specified position
+		// NOTE: VCMI assume 1-based indexation, the very first item has index '1'
+		// Following example will insert new item before 0th item - at the very beginning of the list
+		// Item with provided index must exist in the list
+		"insert@0" : "crossbowman"
+		
+		// modify existing item at specified position
+		// NOTE: VCMI assume 1-based indexation, the very first item has index '1'
+		// Following example will modify 0th item
+		// If item is a json object with multiple properties, e.g. { "key" : "value" } 
+		// you only need to provide changed properites, as usually
+		// Item with provided index must exist in the list
+		"modify@0" : "crossbowman"
+	}
+}
+```
+
+Such formatting is not unique to creature upgrades, and can be used in any place that uses json lists (`[ valueA, valueB ]`)
+
 ## Overriding graphical files from Heroes III
 
 Any graphical replacer mods fall under this category. In VCMI directory **<mod name>/Content** acts as mod-specific game root directory. So for example file **<mod name>/Content/Data/AISHIELD.PNG** will replace file with same name from **H3Bitmap.lod** game archive.

+ 58 - 3
lib/json/JsonUtils.cpp

@@ -209,9 +209,64 @@ void JsonUtils::merge(JsonNode & dest, JsonNode & source, bool ignoreOverride, b
 				if (copyMeta)
 					dest.setModScope(source.getModScope(), false);
 
-				//recursively merge all entries from struct
-				for(auto & node : source.Struct())
-					merge(dest[node.first], node.second, ignoreOverride);
+				if (dest.isStruct())
+				{
+					//recursively merge all entries from struct
+					for(auto & node : source.Struct())
+						merge(dest[node.first], node.second, ignoreOverride);
+					break;
+				}
+				if (dest.isVector())
+				{
+					auto getIndexSafe = [&dest](const std::string & keyName) -> std::optional<int>
+					{
+						try {
+							int index = std::stoi(keyName);
+							if (index <= 0 || index > dest.Vector().size())
+								throw std::out_of_range("dummy");
+							return index - 1; // 1-based index -> 0-based index
+						}
+						catch(const std::invalid_argument &)
+						{
+							logMod->warn("Failed to interpret key '%s' when replacing individual items in array. Expected 'appendItem', 'appendItems', 'modify@NUM' or 'insert@NUM", keyName);
+							return std::nullopt;
+						}
+						catch(const std::out_of_range & )
+						{
+							logMod->warn("Failed to replace index when replacing individual items in array. Value '%s' does not exists in targeted array of %d items", keyName, dest.Vector().size());
+							return std::nullopt;
+						}
+					};
+
+					for(auto & node : source.Struct())
+					{
+						if (node.first == "append")
+						{
+							dest.Vector().push_back(std::move(node.second));
+						}
+						else if (node.first == "appendItems")
+						{
+							assert(node.second.isVector());
+							std::move(dest.Vector().begin(), dest.Vector().end(), std::back_inserter(dest.Vector()));
+						}
+						else if (boost::algorithm::starts_with(node.first, "insert@"))
+						{
+							constexpr int numberPosition = std::char_traits<char>::length("insert@");
+							auto index = getIndexSafe(node.first.substr(numberPosition));
+							if (index)
+								dest.Vector().insert(dest.Vector().begin() + index.value(), std::move(node.second));
+						}
+						else if (boost::algorithm::starts_with(node.first, "modify@"))
+						{
+							constexpr int numberPosition = std::char_traits<char>::length("modify@");
+							auto index = getIndexSafe(node.first.substr(numberPosition));
+							if (index)
+								merge(dest.Vector().at(index.value()), node.second, ignoreOverride);
+						}
+					}
+					break;
+				}
+				assert(false);
 			}
 		}
 	}