From 56ba2e51b7bc8062ba619e982b16fe8d4972c143 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 14 Oct 2026 21:00:02 -0400 Subject: [PATCH] Preserved merged tag order - Merged datapack tags keep first-seen value order with duplicates removed instead of re-sorting, so world preset tag order is preserved - NeoForge remove lists are kept as the deduplicated union of both tags instead of being dropped --- .../iris/modded/ModdedDatapackTagMerge.java | 41 ++++++++------ .../iris/modded/ModdedForcedDatapackTest.java | 55 +++++++++++++++++++ 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDatapackTagMerge.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDatapackTagMerge.java index 241efeb91..07ed671f0 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDatapackTagMerge.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedDatapackTagMerge.java @@ -25,12 +25,13 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.LinkedHashMap; import java.util.Map; -import java.util.TreeMap; final class ModdedDatapackTagMerge { private static final String REPLACE = "replace"; private static final String VALUES = "values"; + private static final String REMOVE = "remove"; private ModdedDatapackTagMerge() { } @@ -45,19 +46,27 @@ final class ModdedDatapackTagMerge { static void merge(Path source, Path destination) throws IOException { JSONObject incoming = read(source); JSONObject existing = read(destination); - Map values = new TreeMap<>(); - collect(existing, values, destination); - collect(incoming, values, source); - JSONArray merged = new JSONArray(); - for (Object value : values.values()) { - merged.put(value); - } JSONObject tag = new JSONObject(); tag.put(REPLACE, existing.optBoolean(REPLACE, false) || incoming.optBoolean(REPLACE, false)); - tag.put(VALUES, merged); + tag.put(VALUES, union(existing, incoming, VALUES, source, destination)); + if (existing.has(REMOVE) || incoming.has(REMOVE)) { + tag.put(REMOVE, union(existing, incoming, REMOVE, source, destination)); + } Files.writeString(destination, tag.toString(4), StandardCharsets.UTF_8); } + private static JSONArray union(JSONObject existing, JSONObject incoming, String field, Path source, Path destination) + throws IOException { + Map entries = new LinkedHashMap<>(); + collect(existing, field, entries, destination); + collect(incoming, field, entries, source); + JSONArray merged = new JSONArray(); + for (Object entry : entries.values()) { + merged.put(entry); + } + return merged; + } + private static JSONObject read(Path tag) throws IOException { try { return new JSONObject(Files.readString(tag, StandardCharsets.UTF_8)); @@ -66,18 +75,18 @@ final class ModdedDatapackTagMerge { } } - private static void collect(JSONObject tag, Map values, Path origin) throws IOException { - JSONArray entries = tag.optJSONArray(VALUES); - if (entries == null) { + private static void collect(JSONObject tag, String field, Map entries, Path origin) throws IOException { + JSONArray values = tag.optJSONArray(field); + if (values == null) { return; } - for (int index = 0; index < entries.length(); index++) { - Object entry = entries.get(index); + for (int index = 0; index < values.length(); index++) { + Object entry = values.get(index); String key = entry instanceof JSONObject object ? object.optString("id", "") : entry.toString(); if (key.isBlank()) { - throw new IOException("Iris cannot merge datapack tag " + origin + ": entry " + index + " has no id."); + throw new IOException("Iris cannot merge datapack tag " + origin + ": " + field + " entry " + index + " has no id."); } - values.putIfAbsent(key, entry); + entries.putIfAbsent(key, entry); } } } diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedForcedDatapackTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedForcedDatapackTest.java index 9a4f04f16..a455f20fb 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedForcedDatapackTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/ModdedForcedDatapackTest.java @@ -19,6 +19,7 @@ package art.arcane.iris.modded; import art.arcane.volmlib.util.collection.KSet; +import art.arcane.volmlib.util.json.JSONArray; import art.arcane.volmlib.util.json.JSONObject; import art.arcane.iris.pack.datapack.DataVersion; import art.arcane.iris.pack.datapack.v1217.DataFixerV1217; @@ -291,6 +292,60 @@ public class ModdedForcedDatapackTest { } } + @Test + public void tagMergeKeepsFirstSeenOrderAndUnionsRemoveLists() throws IOException { + Path root = Files.createTempDirectory("iris-forced-pack-tag-order"); + try { + Path tag = Path.of("data/minecraft/tags/worldgen/world_preset/normal.json"); + Path source = root.resolve("source"); + Path destination = root.resolve("destination"); + Files.createDirectories(source.resolve(tag).getParent()); + Files.createDirectories(destination.resolve(tag).getParent()); + Files.writeString(destination.resolve(tag), + "{\"replace\":false,\"values\":[\"irisworldgen:z\",\"irisworldgen:a\"],\"remove\":[\"minecraft:large_biomes\"]}", + StandardCharsets.UTF_8); + Files.writeString(source.resolve(tag), + "{\"replace\":false,\"values\":[\"irisworldgen:m\",\"irisworldgen:a\"],\"remove\":[\"minecraft:amplified\",\"minecraft:large_biomes\"]}", + StandardCharsets.UTF_8); + + ModdedForcedDatapack.mergeDirectory(source, destination); + + JSONObject merged = new JSONObject(Files.readString(destination.resolve(tag), StandardCharsets.UTF_8)); + assertEquals(List.of("irisworldgen:z", "irisworldgen:a", "irisworldgen:m"), strings(merged.getJSONArray("values"))); + assertEquals(List.of("minecraft:large_biomes", "minecraft:amplified"), strings(merged.getJSONArray("remove"))); + } finally { + deleteTree(root); + } + } + + @Test + public void tagMergeWithoutRemoveListsWritesNone() throws IOException { + Path root = Files.createTempDirectory("iris-forced-pack-tag-no-remove"); + try { + Path tag = Path.of("data/minecraft/tags/worldgen/biome/is_overworld.json"); + for (String side : List.of("source", "destination")) { + Files.createDirectories(root.resolve(side).resolve(tag).getParent()); + Files.writeString(root.resolve(side).resolve(tag), + "{\"replace\":false,\"values\":[\"iris:biomes/" + side + "\"]}", StandardCharsets.UTF_8); + } + + ModdedForcedDatapack.mergeDirectory(root.resolve("source"), root.resolve("destination")); + + JSONObject merged = new JSONObject(Files.readString(root.resolve("destination").resolve(tag), StandardCharsets.UTF_8)); + assertFalse(merged.has("remove")); + } finally { + deleteTree(root); + } + } + + private static List strings(JSONArray array) { + List values = new ArrayList<>(); + for (int index = 0; index < array.length(); index++) { + values.add(array.get(index).toString()); + } + return values; + } + @Test public void tagMergeKeepsStrictConflictsForNonTagFilesInTheSameMerge() throws IOException { Path root = Files.createTempDirectory("iris-forced-pack-tag-strict");