From dcc39463188fbdf68998aabd84ce0d79788ae4b7 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Fri, 23 Oct 2026 14:46:33 +0100 Subject: [PATCH] Fix imported vanilla ore rules --- CHANGELOG.txt | 2 + docs/CONFIGURATION.md | 6 + docs/PLAYER_GUIDE.md | 3 + gradle/README.md | 2 +- gradle/verification/native-ores.gradle | 24 +- .../orespawn/worldgen/OreSourcePolicies.java | 5 +- .../worldgen/OreSpawnOreGeneration.java | 56 ++- .../worldgen/VanillaOreFeatureGate.java | 2 +- .../testmod/NativeOreProbeBridge.java | 24 + .../testmod/NativeOreProbeTestMod.java | 30 +- .../worldgen/LegacyVanillaOreTest.java | 472 ++++++++++++++++++ 11 files changed, 610 insertions(+), 16 deletions(-) create mode 100644 src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyVanillaOreTest.java diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 67d1b5f2..2be39fec 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ Version 4.1.0.2601021 +* Leave imported vanilla ores inactive while Manage Vanilla Ores is off. +* Use active imported settings instead of adding a second default placement pass when it is on. * Keep biome and ore-list text visible with Minecraft's ARGB renderer. * Move Help & Guide beside Recommended Defaults and fit both main menus above the footer. * Reject invalid geome IDs without changing their spelling. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index c90d0849..1c743997 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -52,6 +52,12 @@ report for the exact decision. | `flat_bedrock` | object | Opt-in flat bedrock controls | | `worldgen_aliases` | ID-to-ID object | Replacement output aliases resolved while baking | +Imported vanilla ore rules obey `manage_vanilla_ores`. With it off, their +settings remain saved but vanilla placement is unchanged. With it on, a valid, +enabled import replaces the matching native default in its eligible dimensions. +Saved Ore Sources placement choices still take precedence. This does not change +custom non-imported ore rules or enable retrogen. + ## Enumerated Values | Setting | Accepted values | diff --git a/docs/PLAYER_GUIDE.md b/docs/PLAYER_GUIDE.md index fa210928..ffed5e31 100644 --- a/docs/PLAYER_GUIDE.md +++ b/docs/PLAYER_GUIDE.md @@ -48,6 +48,9 @@ safely instead of breaking world creation. region. - **Manage Vanilla Ores** lets OreSpawn replace vanilla ore features with the configured OreSpawn rules. Leave it off to keep normal Minecraft placement. + Imported vanilla rules stay saved but inactive while it is off. When it is + on, an active import replaces the matching shipped default rather than + adding another placement pass. Existing chunks are not rewritten. - **Fluid Deposits** appears after strata are enabled or when a mod or pack supplies a rule. Press **Add** to choose water, lava, or a fluid block from an installed mod. These are covered underground deposits, not exposed vanilla diff --git a/gradle/README.md b/gradle/README.md index 7cf87ed0..03f9b111 100644 --- a/gradle/README.md +++ b/gradle/README.md @@ -12,7 +12,7 @@ responsibility so maintainers can find the relevant task quickly. | `verification/ore-sources.gradle` | Test-only future MMD providers and missing-provider reloads. | | `verification/performance.gradle` | Matched world-generation checks with optional sibling mods. | | `verification/biome-override.gradle` | New-terrain biome replacement and no-retrogen probe. | -| `verification/native-ores.gradle` | Managed Nether quartz, tag/block hosts, reloads and wrapper cleanup. | +| `verification/native-ores.gradle` | Managed and imported Nether quartz, vanilla OFF parity, tag/block hosts, reloads and wrapper cleanup. | | `release/artifacts.gradle` | Release jars, bundled guides, artifact audits and checksums. | | `release/publishing.gradle` | Maven coordinates, credentials and opt-in publication. | | `ide/eclipse.gradle` | Generated launches, Buildship setup and production-classpath isolation. | diff --git a/gradle/verification/native-ores.gradle b/gradle/verification/native-ores.gradle index 2dc49919..c8d13917 100644 --- a/gradle/verification/native-ores.gradle +++ b/gradle/verification/native-ores.gradle @@ -22,7 +22,9 @@ def packagedJar = jar def serverRuntime = providers.gradleProperty('packagedForgeServerRuntime') def comparisonJar = providers.gradleProperty('nativeOreSpawnJar') def finalTasks = [] -['Default', 'Explicit'].each { scenario -> +['Default', 'Explicit', 'Imported', 'OffControl', 'OffImported'].each { scenario -> + boolean imported = scenario in ['Imported', 'OffImported'] + boolean managed = !scenario.startsWith('Off') File runDirectory = file("${buildDir}/native-ore-${scenario.toLowerCase()}-run") def prepare = tasks.register("prepareNativeOre${scenario}") { dependsOn shared.releaseJar, packagedJar @@ -71,6 +73,8 @@ max-tick-time=-1 commandLine shared.forge64ServerCommand(file(serverRuntime.get()), runDirectory, ['-XX:ActiveProcessorCount=2', "-Dnativeoreprobe.phase=${phase.toLowerCase()}", + "-Dnativeoreprobe.manage=${managed}", + "-Dnativeoreprobe.imported=${imported}", "-Dnativeoreprobe.host=${scenario.toLowerCase()}"]) } doLast { @@ -97,7 +101,7 @@ max-tick-time=-1 } tasks.register('nativeOreIntegrationTest') { group = 'verification' - description = 'Checks quartz defaults, explicit hosts, exact-save reload and new-terrain-only edits.' + description = 'Checks quartz defaults and imports, vanilla OFF parity, reloads and new-terrain-only edits.' dependsOn finalTasks doLast { ['fresh', 'reload', 'edited'].each { phase -> @@ -114,5 +118,21 @@ tasks.register('nativeOreIntegrationTest') { } } logger.lifecycle('Tag and explicit quartz hosts produce identical terrain, fresh/reload/edited') + [['default', 'imported'], ['offcontrol', 'offimported']].each { pair -> + ['fresh', 'reload', 'edited'].each { phase -> + def audits = pair.collect { scenario -> + Properties result = new Properties() + file("${buildDir}/native-ore-${scenario}-run/audit-logs/${phase}.properties") + .withInputStream { result.load(it) } + result + } + ['quartz', 'blocks_sha256', 'biomes_sha256', 'new_terrain_quartz', 'water', 'new_terrain_water'].each { key -> + if (audits[0].getProperty(key) != audits[1].getProperty(key)) { + throw new GradleException("Imported quartz changed ${pair}, ${phase}: ${key}") + } + } + } + } + logger.lifecycle('Imported quartz: OFF matches native terrain; ON matches one configured pass; reload and no-retrogen verified') } } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java index 9fd7dad1..4a7bd966 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java @@ -196,11 +196,12 @@ private static Map discover(JsonObject root) { for (Entry oreEntry : ores.entrySet()) { if (!oreEntry.getValue().isJsonObject()) continue; JsonObject ore = oreEntry.getValue().getAsJsonObject(); - boolean oreActive = bool(ore, "enabled", true) - && (!bool(ore, "native_generation", false) || manageVanillaOres); Identifier blockId = resource(string(ore, "block", oreEntry.getKey())); Block block = blockId == null ? null : ForgeRegistries.BLOCKS.getValue(blockId); if (block == null || block == Blocks.AIR) continue; + boolean vanillaRule = bool(ore, "native_generation", false) + || OreSpawnOreGeneration.isImportedVanillaOre(resource(oreEntry.getKey()), block, ore); + boolean oreActive = bool(ore, "enabled", true) && (!vanillaRule || manageVanillaOres); int metadata = integer(ore, "metadata", 0); Inference inferred = inferMaterial(root, blockTags(block)); Identifier material = resource(string(ore, "material", "")); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java index 393d2bde..a2636bb6 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java @@ -268,9 +268,12 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) { if (ruleId == null) ruleId = oreId; outputBundles.put(ruleId.toString(), new BakedOutputBundle( output.defaultBlockState(), deepOutput, deepOutputMaxY, outputs)); - if (!oreEnabled || (nativeGeneration && !manageVanillaOres)) continue; + boolean importedVanilla = !nativeGeneration && isImportedVanillaOre(ruleId, output, oreJson); + // Keep imported settings, but let vanilla place its own ores while management is off. + if (!oreEnabled || (!manageVanillaOres && (nativeGeneration || importedVanilla))) continue; Identifier material = OreSourcePolicies.material(profile, oreJson, output); - BakedOreRule rule = new BakedOreRule(output, manageVanillaOres && suppressVanilla); + BakedOreRule rule = new BakedOreRule(ruleId, output, + manageVanillaOres && (importedVanilla || suppressVanilla), nativeGeneration, importedVanilla); for (Entry dimensionEntry : dimensions.entrySet()) { Identifier dimensionId = resource(dimensionEntry.getKey()); @@ -333,10 +336,11 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) { rule.selector, dimension); if (selected != null) { combined.add(selected); - if (rule.suppressVanilla) suppressed.add(rule.output); + if (rule.suppressVanilla) rule.addSuppressedOutputs(suppressed); } } combined = arbitrate(profile, combined, outputBundles); + combined = preferImportedVanilla(rules, combined); result.put(dimension, combined.toArray(new BakedOre[combined.size()])); vanillaOutputs.put(dimension, Collections.unmodifiableSet(suppressed)); } @@ -345,9 +349,10 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) { for (BakedOreRule rule : rules) { if (rule.selector == null) continue; selectorList.add(rule.selector); - if (rule.suppressVanilla) selectorOutputs.add(rule.output); + if (rule.suppressVanilla) rule.addSuppressedOutputs(selectorOutputs); } selectorList = arbitrate(profile, selectorList, outputBundles); + selectorList = preferImportedVanilla(rules, selectorList); BakedOre[] selectorResult = selectorList.toArray(new BakedOre[selectorList.size()]); LOGGER.info("Baked {} OreSpawn-managed ore definitions across {} dimensions", rules.size(), result.size()); @@ -360,6 +365,35 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) { Collections.unmodifiableSet(selectorOutputs)); } + static boolean isImportedVanillaOre(Identifier ruleId, Block output, JsonObject ore) { + return (ruleId != null && ruleId.getPath().startsWith("legacy/") + || "legacy".equals(string(ore, "source_mod", ""))) + && VanillaOreFeatureGate.vanillaOreFamily(output) != null; + } + + private static List preferImportedVanilla(List rules, List selected) { + Map byId = new HashMap<>(); + for (BakedOreRule rule : rules) byId.put(rule.ruleId, rule); + Set importedFamilies = Collections.newSetFromMap(new IdentityHashMap<>()); + for (BakedOre ore : selected) { + BakedOreRule rule = byId.get(ore.ruleId); + if (rule != null && rule.importedVanilla) { + importedFamilies.add(VanillaOreFeatureGate.vanillaOreFamily(rule.output)); + } + } + if (importedFamilies.isEmpty()) return selected; + List result = new ArrayList<>(); + for (BakedOre ore : selected) { + BakedOreRule rule = byId.get(ore.ruleId); + // Apply this after Ore Sources so an explicit saved placement choice wins. + if (rule == null || !rule.nativeGeneration + || !importedFamilies.contains(VanillaOreFeatureGate.vanillaOreFamily(rule.output))) { + result.add(ore); + } + } + return result; + } + private static List arbitrate(JsonObject profile, List original, Map outputBundles) { if (!profile.has(OreSourcePolicies.SECTION) @@ -842,15 +876,27 @@ boolean acceptsBiome(ResourceKey biome) { } private static final class BakedOreRule { + final Identifier ruleId; final Block output; final boolean suppressVanilla; + final boolean nativeGeneration; + final boolean importedVanilla; final Map, BakedOre> explicit = new HashMap<>(); final Set> explicitDimensions = new HashSet<>(); BakedOre selector; - BakedOreRule(Block output, boolean suppressVanilla) { + BakedOreRule(Identifier ruleId, Block output, boolean suppressVanilla, + boolean nativeGeneration, boolean importedVanilla) { + this.ruleId = ruleId; this.output = output; this.suppressVanilla = suppressVanilla; + this.nativeGeneration = nativeGeneration; + this.importedVanilla = importedVanilla; + } + + void addSuppressedOutputs(Set outputs) { + outputs.add(output); + if (importedVanilla) outputs.add(VanillaOreFeatureGate.vanillaOreFamily(output)); } } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/VanillaOreFeatureGate.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/VanillaOreFeatureGate.java index 70eb7c90..f8211e41 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/VanillaOreFeatureGate.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/VanillaOreFeatureGate.java @@ -139,7 +139,7 @@ static Block nativeOutput(ConfiguredFeature feature) { return output; } - private static Block vanillaOreFamily(Block block) { + static Block vanillaOreFamily(Block block) { // Stone and deepslate variants share one managed placement rule. if (block == Blocks.COAL_ORE || block == Blocks.DEEPSLATE_COAL_ORE) return Blocks.COAL_ORE; if (block == Blocks.IRON_ORE || block == Blocks.DEEPSLATE_IRON_ORE) return Blocks.IRON_ORE; diff --git a/src/nativeOreIntegrationTest/java/zone/moddev/mc/orespawn/testmod/NativeOreProbeBridge.java b/src/nativeOreIntegrationTest/java/zone/moddev/mc/orespawn/testmod/NativeOreProbeBridge.java index ac6eea2b..986a4c99 100644 --- a/src/nativeOreIntegrationTest/java/zone/moddev/mc/orespawn/testmod/NativeOreProbeBridge.java +++ b/src/nativeOreIntegrationTest/java/zone/moddev/mc/orespawn/testmod/NativeOreProbeBridge.java @@ -38,6 +38,30 @@ static void verifyStoppedCaches() { } } + static void verifyQuartzPasses(boolean managed, boolean imported) { + try { + Class generation = Class.forName("zone.moddev.mc.orespawn.worldgen.OreSpawnOreGeneration"); + Field cache = generation.getDeclaredField("oresByDimension"); + cache.setAccessible(true); + Object[] ores = (Object[]) ((Map) cache.get(null)).get(Level.NETHER); + int count = ores == null ? 0 : ores.length; + if (count != (managed ? 1 : 0)) { + throw new IllegalStateException("Unexpected managed quartz pass count: " + count); + } + if (managed) { + Field id = ores[0].getClass().getDeclaredField("ruleId"); + id.setAccessible(true); + String expected = imported ? "orespawn:legacy/quartz" : "minecraft:nether_quartz_ore"; + if (!expected.equals(id.get(ores[0]).toString())) { + throw new IllegalStateException("Wrong quartz placement source: " + id.get(ores[0])); + } + } + System.out.println("NATIVE_ORE_PROBE managed=" + managed + " imported=" + imported + " passes=" + count); + } catch (ReflectiveOperationException failure) { + throw new IllegalStateException("Could not inspect imported quartz placement", failure); + } + } + static boolean controlsFluid() { try { Class generation = Class.forName("zone.moddev.mc.orespawn.worldgen.FluidDepositFeature"); diff --git a/src/nativeOreIntegrationTest/java/zone/moddev/mc/orespawn/testmod/NativeOreProbeTestMod.java b/src/nativeOreIntegrationTest/java/zone/moddev/mc/orespawn/testmod/NativeOreProbeTestMod.java index b32cf9f8..f7250f5f 100644 --- a/src/nativeOreIntegrationTest/java/zone/moddev/mc/orespawn/testmod/NativeOreProbeTestMod.java +++ b/src/nativeOreIntegrationTest/java/zone/moddev/mc/orespawn/testmod/NativeOreProbeTestMod.java @@ -71,7 +71,7 @@ private void prepare(ServerAboutToStartEvent event) { JsonObject onlyQuartz = new JsonObject(); onlyQuartz.add(QUARTZ, ores.get(QUARTZ)); root.add("ores", onlyQuartz); - root.addProperty("manage_vanilla_ores", true); + root.addProperty("manage_vanilla_ores", managed()); root.addProperty("place_fluid_deposits", true); root.add("fluid_deposits", fluidProfile()); root.add("terrain_dimensions", new JsonObject()); @@ -82,7 +82,16 @@ private void prepare(ServerAboutToStartEvent event) { retrogen.addProperty("revision", 0); root.add("retrogen", retrogen); } - JsonObject rule = root.getAsJsonObject("ores").getAsJsonObject(QUARTZ) + JsonObject ores = root.getAsJsonObject("ores"); + if (imported() && "fresh".equals(phase)) { + JsonObject legacy = ores.getAsJsonObject(QUARTZ).deepCopy(); + legacy.addProperty("block", QUARTZ); + legacy.addProperty("source_mod", "legacy"); + legacy.addProperty("native_generation", false); + legacy.addProperty("retrogen", false); + ores.add("orespawn:legacy/quartz", legacy); + } + JsonObject rule = ores.getAsJsonObject(imported() ? "orespawn:legacy/quartz" : QUARTZ) .getAsJsonObject("dimensions").getAsJsonObject(NETHER); rule.addProperty("min_y", "edited".equals(phase) ? 100 : 24); rule.addProperty("max_y", "edited".equals(phase) ? 104 : 28); @@ -114,9 +123,10 @@ private void prepare(ServerAboutToStartEvent event) { private void audit(ServerStartedEvent event) { MinecraftServer server = event.getServer(); String phase = System.getProperty("nativeoreprobe.phase", "fresh"); - if (!NativeOreProbeBridge.controlsQuartz()) { - throw new IllegalStateException("Quartz is not controlled by OreSpawn before generation"); + if (NativeOreProbeBridge.controlsQuartz() != managed()) { + throw new IllegalStateException("Quartz takeover disagrees with Manage Vanilla Ores"); } + NativeOreProbeBridge.verifyQuartzPasses(managed(), imported()); if (!NativeOreProbeBridge.controlsFluid()) { throw new IllegalStateException("Tag-only fluid was not baked before generation"); } @@ -183,7 +193,9 @@ private static Properties inspect(ServerLevel world, int origin, int minY, int m blocks.update((byte) id); if (state.getBlock() == Blocks.WATER) water++; if (state.getBlock() == Blocks.NETHER_QUARTZ_ORE) { - if (y < minY || y > maxY) throw new IllegalStateException("Quartz outside managed range at " + cursor); + if (managed() && (y < minY || y > maxY)) { + throw new IllegalStateException("Quartz outside managed range at " + cursor); + } quartz++; } } @@ -201,6 +213,14 @@ private static Properties inspect(ServerLevel world, int origin, int minY, int m return result; } + private static boolean managed() { + return Boolean.parseBoolean(System.getProperty("nativeoreprobe.manage", "true")); + } + + private static boolean imported() { + return Boolean.getBoolean("nativeoreprobe.imported"); + } + private static JsonObject fluidProfile() { JsonObject rule = new JsonObject(); rule.addProperty("enabled", true); diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyVanillaOreTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyVanillaOreTest.java new file mode 100644 index 00000000..19357dcd --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyVanillaOreTest.java @@ -0,0 +1,472 @@ +package zone.moddev.mc.orespawn.worldgen; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Set; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.Level; +import net.minecraftforge.registries.ForgeRegistries; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import zone.moddev.mc.orespawn.test.Forge36TestBootstrap; +import zone.moddev.mc.orespawn.init.OreSpawnPatterns; +import zone.moddev.mc.orespawn.api.OrePatternType; +import zone.moddev.mc.orespawn.api.OreSpawnPatternRegistry; +import net.minecraftforge.registries.RegistryBuilder; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.RegistryObject; + +class LegacyVanillaOreTest { + + @BeforeAll + @SuppressWarnings("unchecked") + static void bootstrap() throws Exception { + Forge36TestBootstrap.registerVanilla(); + if (OreSpawnPatterns.registry() == null) { + // Unit tests have no mod-loading bus. Install the real built-in patterns + // into an isolated JVM registry; packaged tests also exercise normal loading. + var builder = new RegistryBuilder() + .setName(OreSpawnPatternRegistry.REGISTRY_NAME).disableSaving().disableSync(); + Method create = RegistryBuilder.class.getDeclaredMethod("create"); + create.setAccessible(true); + var registry = (net.minecraftforge.registries.IForgeRegistry) create.invoke(builder); + Field types = OreSpawnPatterns.class.getDeclaredField("TYPES"); + types.setAccessible(true); + Field entries = DeferredRegister.class.getDeclaredField("entries"); + entries.setAccessible(true); + var patterns = (Map, java.util.function.Supplier>) + entries.get(types.get(null)); + patterns.forEach((id, factory) -> registry.register(id.getId(), factory.get())); + } + } + + @Test + void importedDiamondStaysInactiveWhileVanillaManagementIsOff() throws Exception { + JsonObject profile = profile(false); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", rule(false, true)); + String original = profile.toString(); + + Object baked = bake(profile); + + assertEquals(0, ores(baked, Level.OVERWORLD).length); + assertFalse(takeover(baked, Level.OVERWORLD, Blocks.DIAMOND_ORE)); + assertFalse(takeover(baked, Level.OVERWORLD, Blocks.IRON_ORE)); + assertFalse(takeover(baked, Level.NETHER, Blocks.DIAMOND_ORE)); + assertEquals(original, profile.toString(), "baking must not rewrite a saved profile"); + } + + @Test + void explicitImportedRulesAlsoRespectTheSwitchInEveryDimension() throws Exception { + JsonObject profile = profile(false); + JsonObject imported = rule(false, true); + JsonObject placement = imported.getAsJsonObject("dimension_selectors") + .getAsJsonObject("orespawn:all_except_nether_end"); + imported.remove("dimension_selectors"); + JsonObject dimensions = new JsonObject(); + for (ResourceKey dimension : java.util.List.of( + Level.OVERWORLD, Level.NETHER, Level.END)) { + dimensions.add(dimension.identifier().toString(), new com.google.gson.JsonParser().parse(placement.toString())); + } + imported.add("dimensions", dimensions); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", imported); + String original = profile.toString(); + + Object baked = bake(profile); + + for (ResourceKey dimension : java.util.List.of( + Level.OVERWORLD, Level.NETHER, Level.END)) { + assertEquals(0, ores(baked, dimension).length); + assertFalse(takeover(baked, dimension, Blocks.DIAMOND_ORE)); + } + assertEquals(original, profile.toString()); + } + + @Test + void switchingOffAndBackOnPreservesTheImportedSettings() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", rule(false, true)); + String original = profile.toString(); + + assertEquals(1, ores(bake(profile), Level.OVERWORLD).length); + profile.addProperty("manage_vanilla_ores", false); + Object disabled = bake(profile); + assertEquals(0, ores(disabled, Level.OVERWORLD).length); + assertFalse(takeover(disabled, Level.OVERWORLD, Blocks.DIAMOND_ORE)); + profile.addProperty("manage_vanilla_ores", true); + Object[] restored = ores(bake(profile), Level.OVERWORLD); + assertEquals(1, restored.length); + assertEquals(Identifier.parse("orespawn:legacy/diamond_ore"), field(restored[0], "ruleId")); + assertEquals(0.375D, field(restored[0], "frequency")); + assertEquals(7, field(restored[0], "minY")); + assertEquals(11, field(restored[0], "maxY")); + assertEquals(false, field(restored[0], "retrogen")); + assertEquals(original, profile.toString()); + } + + @Test + void aMissingManagementSwitchDefaultsToVanillaGeneration() throws Exception { + JsonObject profile = profile(false); + profile.remove("manage_vanilla_ores"); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", rule(false, true)); + + Object baked = bake(profile); + + assertEquals(0, ores(baked, Level.OVERWORLD).length); + assertFalse(takeover(baked, Level.OVERWORLD, Blocks.DIAMOND_ORE)); + assertFalse(profile.has("manage_vanilla_ores")); + } + + @Test + void importedSettingsWinOverModernDefaultsWhenBothAreEnabled() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", rule(false, true)); + + Object[] ores = ores(bake(profile), Level.OVERWORLD); + + assertEquals(1, ores.length, "the native default must not add a second placement pass"); + assertEquals(Identifier.parse("orespawn:legacy/diamond_ore"), field(ores[0], "ruleId")); + assertEquals(0.375D, field(ores[0], "frequency")); + assertEquals(7, field(ores[0], "minY")); + assertEquals(11, field(ores[0], "maxY")); + assertEquals(4, field(ores[0], "minQuantity")); + assertEquals(4, field(ores[0], "maxQuantity")); + assertEquals(false, field(ores[0], "retrogen")); + } + + @Test + void disabledImportedRuleDoesNotSuppressAnOtherwiseEnabledVanillaOre() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", rule(false, false)); + + Object[] ores = ores(bake(profile), Level.OVERWORLD); + + assertEquals(1, ores.length); + assertEquals(Identifier.parse("minecraft:diamond_ore"), field(ores[0], "ruleId")); + profile.addProperty("manage_vanilla_ores", false); + assertFalse(takeover(bake(profile), Level.OVERWORLD, Blocks.DIAMOND_ORE)); + } + + @Test + void anExplicitDimensionLimitDoesNotRemoveTheOverworldDefault() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + JsonObject imported = rule(false, true); + JsonObject placement = imported.getAsJsonObject("dimension_selectors") + .getAsJsonObject("orespawn:all_except_nether_end"); + imported.remove("dimension_selectors"); + JsonObject dimensions = new JsonObject(); + dimensions.add("examplemod:moon", placement); + imported.add("dimensions", dimensions); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", imported); + + Object baked = bake(profile); + + assertEquals(Identifier.parse("minecraft:diamond_ore"), + field(ores(baked, Level.OVERWORLD)[0], "ruleId")); + assertTrue(takeover(baked, ResourceKey.create(Registries.DIMENSION, Identifier.parse("examplemod:moon")), Blocks.DIAMOND_ORE)); + } + + @Test + void aDisabledDimensionOverridesAnImportedSelectorWithoutClaimingVanilla() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + JsonObject imported = rule(false, true); + JsonObject disabled = new JsonObject(); + disabled.addProperty("enabled", false); + JsonObject dimensions = new JsonObject(); + dimensions.add("minecraft:overworld", disabled); + imported.add("dimensions", dimensions); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", imported); + + Object baked = bake(profile); + + assertEquals(1, ores(baked, Level.OVERWORLD).length); + assertEquals(Identifier.parse("minecraft:diamond_ore"), + field(ores(baked, Level.OVERWORLD)[0], "ruleId")); + assertTrue(takeover(baked, ResourceKey.create(Registries.DIMENSION, Identifier.parse("examplemod:moon")), Blocks.DIAMOND_ORE)); + } + + @Test + void anExplicitOreSourceChoiceStillControlsPlacement() throws Exception { + for (String selected : new String[] { "minecraft:diamond_ore", "orespawn:legacy/diamond_ore" }) { + JsonObject profile = profile(true); + JsonObject nativeRule = rule(true, true); + nativeRule.addProperty("material", "minecraft:diamond"); + JsonObject imported = rule(false, true); + imported.addProperty("material", "minecraft:diamond"); + JsonObject dimensions = new JsonObject(); + dimensions.add("minecraft:overworld", imported.getAsJsonObject("dimension_selectors") + .getAsJsonObject("orespawn:all_except_nether_end")); + imported.remove("dimension_selectors"); + imported.add("dimensions", dimensions); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", nativeRule); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", imported); + + JsonObject policy = new JsonObject(); + policy.addProperty("mode", "consolidated"); + JsonObject outputs = new JsonObject(); + outputs.addProperty("minecraft:diamond_ore", 1.0D); + policy.add("outputs", outputs); + JsonObject placements = new JsonObject(); + placements.addProperty("orespawn:standard", selected); + policy.add("placement_sources", placements); + JsonObject policies = new JsonObject(); + policies.add(OreSourcePolicies.key(Identifier.parse("minecraft:diamond"), + Level.OVERWORLD.identifier()), policy); + profile.add(OreSourcePolicies.SECTION, policies); + + Object[] ores = ores(bake(profile), Level.OVERWORLD); + + assertEquals(1, ores.length, selected); + assertEquals(Identifier.parse(selected), field(ores[0], "ruleId")); + profile.addProperty("manage_vanilla_ores", false); + String saved = profile.toString(); + Object disabled = bake(profile); + assertEquals(0, ores(disabled, Level.OVERWORLD).length, selected); + assertFalse(takeover(disabled, Level.OVERWORLD, Blocks.DIAMOND_ORE)); + assertEquals(saved, profile.toString()); + } + } + + @Test + void ordinaryAddOnRulesKeepTheirExistingBehaviour() throws Exception { + JsonObject profile = profile(false); + profile.getAsJsonObject("ores").add("examplemod:diamond_ore", rule(false, true)); + + Object baked = bake(profile); + + assertEquals(1, ores(baked, Level.OVERWORLD).length); + assertFalse(takeover(baked, Level.OVERWORLD, Blocks.DIAMOND_ORE)); + } + + @Test + void providerOwnedImportsRespectTheSameManagementSwitch() throws Exception { + JsonObject profile = profile(false); + JsonObject imported = rule(false, true); + imported.addProperty("source_mod", "basemetals"); + profile.getAsJsonObject("ores").add("basemetals:legacy/diamond_ore", imported); + Object disabled = bake(profile); + assertEquals(0, ores(disabled, Level.OVERWORLD).length); + assertFalse(takeover(disabled, Level.OVERWORLD, Blocks.DIAMOND_ORE)); + profile.addProperty("manage_vanilla_ores", true); + Object enabled = bake(profile); + assertEquals(1, ores(enabled, Level.OVERWORLD).length); + assertTrue(takeover(enabled, Level.OVERWORLD, Blocks.DIAMOND_ORE)); + } + + @Test + void importedNonVanillaBlocksDoNotRequireVanillaManagement() throws Exception { + JsonObject profile = profile(false); + JsonObject imported = rule(false, true); + imported.addProperty("block", "minecraft:gold_block"); + profile.getAsJsonObject("ores").add("orespawn:legacy/gold_block", imported); + Object baked = bake(profile); + assertEquals(1, ores(baked, Level.OVERWORLD).length); + assertFalse(takeover(baked, Level.OVERWORLD, Blocks.GOLD_BLOCK)); + } + + @Test + void anImportMappedToAModernProviderIdStillRespectsTheSwitch() throws Exception { + JsonObject profile = profile(false); + JsonObject imported = rule(false, true); + imported.addProperty("source_mod", "legacy"); + profile.getAsJsonObject("ores").add("basemetals:custom_diamond", imported); + Object disabled = bake(profile); + assertEquals(0, ores(disabled, Level.OVERWORLD).length); + assertFalse(takeover(disabled, Level.OVERWORLD, Blocks.DIAMOND_ORE)); + profile.addProperty("manage_vanilla_ores", true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + Object enabled = bake(profile); + Object[] ores = ores(enabled, Level.OVERWORLD); + assertEquals(1, ores.length); + assertEquals(Identifier.parse("basemetals:custom_diamond"), field(ores[0], "ruleId")); + assertTrue(takeover(enabled, Level.OVERWORLD, Blocks.DIAMOND_ORE)); + } + + @Test + void oreSourcesShowsImportedVanillaPlacementsAsInactiveWhileOff() throws Exception { + for (String id : new String[] { "orespawn:legacy/diamond_ore", "basemetals:legacy/diamond_ore" }) { + JsonObject profile = profile(false); + JsonObject imported = rule(false, true); + imported.addProperty("source_mod", "basemetals"); + imported.addProperty("material", "minecraft:diamond"); + profile.getAsJsonObject("ores").add(id, imported); + OreSourcePolicies.initialize(profile, true); + OreSourcePolicies.Candidate candidate = OreSourcePolicies.snapshot(profile).groups().get(0).candidates.get(0); + assertEquals(id, candidate.sourceId); + assertTrue(candidate.loaded); + assertFalse(candidate.active); + profile.addProperty("manage_vanilla_ores", true); + OreSourcePolicies.initialize(profile, true); + assertTrue(OreSourcePolicies.snapshot(profile).groups().get(0).candidates.get(0).active); + } + } + + @Test + void allImportedVanillaFamiliesRespectOffAndOn() throws Exception { + Block[] blocks = { Blocks.COAL_ORE, Blocks.IRON_ORE, Blocks.GOLD_ORE, Blocks.REDSTONE_ORE, + Blocks.LAPIS_ORE, Blocks.DIAMOND_ORE, Blocks.EMERALD_ORE, Blocks.COPPER_ORE, + Blocks.DEEPSLATE_COAL_ORE, Blocks.DEEPSLATE_IRON_ORE, Blocks.DEEPSLATE_COPPER_ORE, + Blocks.DEEPSLATE_GOLD_ORE, Blocks.DEEPSLATE_REDSTONE_ORE, Blocks.DEEPSLATE_LAPIS_ORE, + Blocks.DEEPSLATE_DIAMOND_ORE, Blocks.DEEPSLATE_EMERALD_ORE, + Blocks.NETHER_QUARTZ_ORE, Blocks.NETHER_GOLD_ORE, Blocks.ANCIENT_DEBRIS }; + for (Block block : blocks) { + JsonObject profile = profile(false); + JsonObject imported = rule(false, true); + imported.addProperty("block", ForgeRegistries.BLOCKS.getKey(block).toString()); + profile.getAsJsonObject("ores").add("orespawn:legacy/" + ForgeRegistries.BLOCKS.getKey(block).getPath(), imported); + Object disabled = bake(profile); + assertEquals(0, ores(disabled, Level.OVERWORLD).length, ForgeRegistries.BLOCKS.getKey(block).toString()); + assertFalse(takeover(disabled, Level.OVERWORLD, block), ForgeRegistries.BLOCKS.getKey(block).toString()); + profile.addProperty("manage_vanilla_ores", true); + Object enabled = bake(profile); + assertEquals(1, ores(enabled, Level.OVERWORLD).length, ForgeRegistries.BLOCKS.getKey(block).toString()); + assertTrue(takeover(enabled, Level.OVERWORLD, block), ForgeRegistries.BLOCKS.getKey(block).toString()); + } + } + + @Test + void invalidImportedRulesDoNotSuppressVanillaWhenManagementIsOn() throws Exception { + for (boolean disabledSelector : new boolean[] { false, true }) { + JsonObject profile = profile(true); + JsonObject imported = rule(false, true); + JsonObject placement = imported.getAsJsonObject("dimension_selectors") + .getAsJsonObject("orespawn:all_except_nether_end"); + if (disabledSelector) placement.addProperty("enabled", false); + else placement.addProperty("frequency", 0.0D); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", imported); + Object baked = bake(profile); + assertEquals(0, ores(baked, Level.OVERWORLD).length); + assertFalse(takeover(baked, Level.OVERWORLD, Blocks.DIAMOND_ORE)); + } + } + + @Test + void importedNetherQuartzDoesNotTakeOverOtherDimensions() throws Exception { + JsonObject profile = profile(true); + JsonObject imported = rule(false, true); + JsonObject placement = imported.getAsJsonObject("dimension_selectors") + .getAsJsonObject("orespawn:all_except_nether_end"); + imported.remove("dimension_selectors"); + imported.addProperty("block", "minecraft:nether_quartz_ore"); + JsonObject dimensions = new JsonObject(); + dimensions.add(Level.NETHER.identifier().toString(), placement); + imported.add("dimensions", dimensions); + profile.getAsJsonObject("ores").add("orespawn:legacy/quartz_ore", imported); + Object baked = bake(profile); + assertTrue(takeover(baked, Level.NETHER, Blocks.NETHER_QUARTZ_ORE)); + assertFalse(takeover(baked, Level.OVERWORLD, Blocks.NETHER_QUARTZ_ORE)); + assertFalse(takeover(baked, Level.END, Blocks.NETHER_QUARTZ_ORE)); + } + + @Test + void importedDiamondDoesNotRemoveTheIndependentIronDefault() throws Exception { + JsonObject profile = profile(true); + JsonObject iron = rule(true, true); + iron.addProperty("block", "minecraft:iron_ore"); + profile.getAsJsonObject("ores").add("minecraft:iron_ore", iron); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", rule(false, true)); + Object[] ores = ores(bake(profile), Level.OVERWORLD); + assertEquals(2, ores.length); + for (Object ore : ores) assertNotEquals(Identifier.parse("minecraft:diamond_ore"), field(ore, "ruleId")); + } + + @Test + void intentionalLegacyDeclarationsAreNotCollapsedIntoOneRule() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + profile.getAsJsonObject("ores").add("orespawn:legacy/a/diamond_ore", rule(false, true)); + profile.getAsJsonObject("ores").add("orespawn:legacy/b/diamond_ore", rule(false, true)); + Object baked = bake(profile); + Object[] ores = ores(baked, Level.OVERWORLD); + assertEquals(2, ores.length); + for (Object ore : ores) assertNotEquals(Identifier.parse("minecraft:diamond_ore"), field(ore, "ruleId")); + assertTrue(takeover(baked, Level.OVERWORLD, Blocks.DIAMOND_ORE)); + } + + @Test + void aDeepslateImportReplacesItsStoneFamilyDefault() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + JsonObject imported = rule(false, true); + imported.addProperty("block", "minecraft:deepslate_diamond_ore"); + profile.getAsJsonObject("ores").add("orespawn:legacy/deepslate_diamond", imported); + Object baked = bake(profile); + assertEquals(1, ores(baked, Level.OVERWORLD).length); + assertEquals(Identifier.parse("orespawn:legacy/deepslate_diamond"), + field(ores(baked, Level.OVERWORLD)[0], "ruleId")); + assertTrue(takeover(baked, Level.OVERWORLD, Blocks.DIAMOND_ORE)); + assertTrue(takeover(baked, Level.OVERWORLD, Blocks.DEEPSLATE_DIAMOND_ORE)); + } + + private static JsonObject profile(boolean manageVanilla) { + JsonObject profile = new JsonObject(); + profile.addProperty("manage_vanilla_ores", manageVanilla); + profile.add("ores", new JsonObject()); + return profile; + } + + private static JsonObject rule(boolean nativeGeneration, boolean enabled) { + JsonObject ore = new JsonObject(); + ore.addProperty("block", "minecraft:diamond_ore"); + ore.addProperty("native_generation", nativeGeneration); + ore.addProperty("enabled", enabled); + ore.addProperty("retrogen", false); + JsonObject placement = new JsonObject(); + placement.addProperty("frequency", nativeGeneration ? 1.0D : 0.375D); + placement.addProperty("min_y", nativeGeneration ? 0 : 7); + placement.addProperty("max_y", nativeGeneration ? 15 : 11); + placement.addProperty("quantity", nativeGeneration ? 8 : 4); + JsonArray hosts = new JsonArray(); + hosts.add(new JsonPrimitive("minecraft:stone")); + placement.add("host_blocks", hosts); + JsonObject dimensions = new JsonObject(); + dimensions.add(nativeGeneration ? "minecraft:overworld" : "orespawn:all_except_nether_end", placement); + ore.add(nativeGeneration ? "dimensions" : "dimension_selectors", dimensions); + return ore; + } + + private static Object bake(JsonObject profile) throws Exception { + Method geology = GeomeConfig.class.getDeclaredMethod("bake", JsonObject.class, Identifier.class); + geology.setAccessible(true); + Object config = geology.invoke(null, new JsonObject(), Level.OVERWORLD.identifier()); + Method bake = OreSpawnOreGeneration.class.getDeclaredMethod("bakeOres", JsonObject.class, BakedGeomeConfig.class); + bake.setAccessible(true); + return bake.invoke(null, profile, config); + } + + @SuppressWarnings("unchecked") + private static Object[] ores(Object baked, ResourceKey dimension) throws Exception { + Object[] explicit = ((Map, Object[]>) field(baked, "byDimension")).get(dimension); + return explicit != null ? explicit : OreSpawnOreGeneration.selectorAllows(dimension) + ? (Object[]) field(baked, "selectorOres") : new Object[0]; + } + + @SuppressWarnings("unchecked") + private static boolean takeover(Object baked, ResourceKey dimension, Block block) throws Exception { + Set explicit = ((Map, Set>) field(baked, "vanillaOutputs")).get(dimension); + return explicit != null ? explicit.contains(block) : OreSpawnOreGeneration.selectorAllows(dimension) + && ((Set) field(baked, "selectorVanillaOutputs")).contains(block); + } + + private static Object field(Object owner, String name) throws Exception { + Field field = owner.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(owner); + } +}