From b66b502a9b824d61dc0a0000e5b9155e36ae9761 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Sun, 1 Nov 2026 17:01:09 +0000 Subject: [PATCH 1/2] Fix Mesa gold generation controls --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 5 + README.md | 2 +- docs/CONFIGURATION.md | 7 + docs/VERSIONS.md | 2 +- gradle.properties | 2 +- gradle/release/artifacts.gradle | 6 +- .../orespawn/compat/LegacyOs3Bridge.java | 2 +- src/main/java/cyano/orespawn/OreSpawn.java | 2 +- .../zone/moddev/mc/orespawn/OreSpawn.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../worldgen/LegacyOs3ProfileMigration.java | 2 +- .../worldgen/OreSpawnWorldGenerator.java | 17 ++- .../VanillaMesaGoldCompatibility.java | 87 +++++++++++ .../BiomeSurfaceFeatureOrderTest.java | 6 + .../VanillaMesaGoldCompatibilityTest.java | 138 ++++++++++++++++++ .../worldgen/WorldgenBenchmarkTest.java | 37 +++-- 17 files changed, 295 insertions(+), 30 deletions(-) create mode 100644 src/main/java/zone/moddev/mc/orespawn/worldgen/VanillaMesaGoldCompatibility.java create mode 100644 src/test/java/zone/moddev/mc/orespawn/worldgen/VanillaMesaGoldCompatibilityTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df76b628..36022ff7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,9 +126,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.1.4.110021.jar - build/libs/OreSpawn-4.1.4.110021-sources.jar - build/libs/OreSpawn-4.1.4.110021-javadoc.jar + build/libs/OreSpawn-4.1.5.110021.jar + build/libs/OreSpawn-4.1.5.110021-sources.jar + build/libs/OreSpawn-4.1.5.110021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 057a2c6f..6fd2b880 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +Version 4.1.5.110021 + +* Apply the normal gold-generation controls to Mesa's extra gold pass too. + Keep custom channels, imported settings and existing chunks unchanged. + Version 4.1.4.110021 * Let Java providers select an exact legacy host variant, such as plain stone diff --git a/README.md b/README.md index f3b71f3b..eea6f9fb 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It gives mods and modpacks one place to configure ores, deposit shapes, optional rock strata and geomes, provider-owned underground fluid deposits, biome palettes and world materials, flat bedrock, and bounded ore retrogen. -This branch builds target-qualified version `4.1.4.110021`: the OreSpawn 4.1.4 +This branch builds target-qualified version `4.1.5.110021`: the OreSpawn 4.1.5 feature set for Minecraft 1.10.2 and Forge. It adds stable generation identity, non-loading geology sampling, per-resource background control, existing-world provider merge policy, an Ore Sources arbiter for equivalent provider ores and diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index e13af100..fd9e4a7e 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -323,6 +323,13 @@ Settings refuses to move a native vanilla ore's Ore Dictionary alias into another material group and explains that vanilla management must be enabled first. +On Minecraft 1.10.2, Mesa's extra gold follows the same decision as ordinary +gold. With vanilla management off, imported vanilla rules stay saved but +inactive. A child's hybrid or replacement policy can still limit native gold, +including Mesa gold. With management on, the selected ordinary rule supplies +gold without a second native Mesa pass. Explicit custom channels remain +independent, and none of these controls rewrites already generated chunks. + Consolidation runs the chosen placement rule's pattern, hosts, filters, height and frequency once; selected candidates contribute output bundles only. An inactive managed provider may therefore supply an output block without adding diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 9880738d..4c3f70c3 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -51,7 +51,7 @@ Examples: | Minecraft | Loader | Target | Example full version | | --- | --- | ---: | --- | -| 1.10.2 | Forge | `110021` | `4.1.4.110021` | +| 1.10.2 | Forge | `110021` | `4.1.5.110021` | | 1.12.2 | Forge | `112021` | `4.0.6.112021` | | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | diff --git a/gradle.properties b/gradle.properties index c17aaac7..febbf260 100644 --- a/gradle.properties +++ b/gradle.properties @@ -30,7 +30,7 @@ mod_vendor=Minecraft Mod Development mod_license=LGPL-2.1 mod_license_name=GNU Lesser General Public License, Version 2.1 mod_license_url=https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -mod_version=4.1.4.110021 +mod_version=4.1.5.110021 mod_group=zone.moddev.mc.orespawn mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/gradle/release/artifacts.gradle b/gradle/release/artifacts.gradle index 6b0d5b7e..746ae826 100644 --- a/gradle/release/artifacts.gradle +++ b/gradle/release/artifacts.gradle @@ -77,7 +77,7 @@ tasks.register('verifyReleaseConfiguration') { description = 'Validates the target-qualified release, API, schemas, reports, and publishing identity.' doLast { - if (project.mod_version != '4.1.4.110021' + if (project.mod_version != '4.1.5.110021' || project.mod_group != expectedMavenGroup) { throw new GradleException("Unexpected OreSpawn release version: ${project.mod_version}") } @@ -106,8 +106,8 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.1.4.110021')) { - throw new GradleException("Authoritative release location does not contain 4.1.4.110021: ${path}") + if (!file(path).getText('UTF-8').contains('4.1.5.110021')) { + throw new GradleException("Authoritative release location does not contain 4.1.5.110021: ${path}") } } if (!file('docs/API.md').getText('UTF-8').contains('orespawn@[4.0.6,5.0.0)')) { diff --git a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java index 5af35159..d43e6931 100644 --- a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java +++ b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java @@ -1027,7 +1027,7 @@ private static void writeHumanUpgradeReport(Path destination) throws IOException } } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.1.4.110021 Upgrade Report"); + lines.add("OreSpawn 4.1.5.110021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn configuration was consumed and translated for OS4."); diff --git a/src/main/java/cyano/orespawn/OreSpawn.java b/src/main/java/cyano/orespawn/OreSpawn.java index fc86bf41..cdbfd49b 100644 --- a/src/main/java/cyano/orespawn/OreSpawn.java +++ b/src/main/java/cyano/orespawn/OreSpawn.java @@ -19,7 +19,7 @@ public class OreSpawn { public static OreSpawn INSTANCE; public static final String MODID = "orespawn"; public static final String NAME = "OreSpawn"; - public static final String VERSION = "4.1.4.110021"; + public static final String VERSION = "4.1.5.110021"; public static final List oreSpawnConfigFiles = new ArrayList<>(); public static final List additionalStoneBlocks = new ArrayList<>(); public static boolean disableVanillaOreGen; diff --git a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java index cb9eb94e..aa8e913d 100644 --- a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java +++ b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java @@ -52,7 +52,7 @@ public class OreSpawn { public static final String MODID = "orespawn"; public static final String NAME = "OreSpawn"; - public static final String VERSION = "4.1.4.110021"; + public static final String VERSION = "4.1.5.110021"; private static final Logger LOGGER = LogManager.getLogger(); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index e8bafced..b94ae147 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -125,7 +125,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configDirectory, } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.1.4.110021 Upgrade Report"); + lines.add("OreSpawn 4.1.5.110021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + mineralogyVersion diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java index cae11bdf..232c72b0 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java @@ -65,7 +65,7 @@ private static void writeInitialUpgradeReport(Path configDirectory, boolean forceRetrogen, boolean flatBedrock, boolean retrogenBedrock, int bedrockLayers) throws IOException { String newline = System.lineSeparator(); - String text = "OreSpawn 4.1.4.110021 Upgrade Report" + newline + String text = "OreSpawn 4.1.5.110021 Upgrade Report" + newline + "================================" + newline + newline + "RESULT: Legacy OreSpawn settings were imported into the OS4 profile." + newline + "- Manage vanilla ores: " + manageVanilla + newline diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnWorldGenerator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnWorldGenerator.java index bb99cba1..26384b65 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnWorldGenerator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnWorldGenerator.java @@ -66,18 +66,26 @@ public void beforeVanillaOres(OreGenEvent.Pre event) { public void filterVanillaOre(OreGenEvent.GenerateMinable event) { if (WorldgenBenchmark.isVanillaBaseline()) return; ResourceLocation dimension = WorldIds.dimension(event.getWorld()); + Block output = vanillaOutput(event.getType()); if (WorldGeologyProfileManager.activeProfile().suppressAllOreFeatures()) { event.setResult(Event.Result.DENY); - return; - } - Block output = vanillaOutput(event.getType()); - if (output != null) { + } else if (output != null) { ChunkPos chunk = new ChunkPos(event.getPos()); if (!OreSpawnOreGeneration.allowsVanillaOre(dimension, output, event.getWorld().getSeed(), chunk.chunkXPos, chunk.chunkZPos)) { event.setResult(Event.Result.DENY); } } + + if (output == Blocks.GOLD_ORE && event.getResult() == Event.Result.DENY) { + VanillaMesaGoldCompatibility.suppress(event.getGenerator(), event.getWorld(), event.getPos()); + } + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void afterBiomeDecoration(DecorateBiomeEvent.Post event) { + if (WorldgenBenchmark.isVanillaBaseline()) return; + VanillaMesaGoldCompatibility.restore(event.getWorld(), event.getPos()); } @Override @@ -114,6 +122,7 @@ private void generateOres(World world, int chunkX, int chunkZ, Random random) { } public void clear() { + VanillaMesaGoldCompatibility.clear(); AquiferMaterialSubstitution.clear(); earlyComplete.clear(); oreComplete.clear(); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/VanillaMesaGoldCompatibility.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/VanillaMesaGoldCompatibility.java new file mode 100644 index 00000000..a1dfe8db --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/VanillaMesaGoldCompatibility.java @@ -0,0 +1,87 @@ +package zone.moddev.mc.orespawn.worldgen; + +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Random; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.biome.BiomeDecorator; +import net.minecraft.world.biome.BiomeMesa; +import net.minecraft.world.gen.feature.WorldGenerator; + +/** Applies the normal gold decision to Mesa's extra, unhooked gold pass. */ +final class VanillaMesaGoldCompatibility { + + private static final WorldGenerator SKIPPED = new WorldGenerator() { + @Override + public boolean generate(World world, Random random, BlockPos pos) { + return false; + } + }; + + private static final Map suppressed = new IdentityHashMap<>(); + + private VanillaMesaGoldCompatibility() { + } + + static void suppress(WorldGenerator generator, World world, BlockPos origin) { + if (generator == null || generator == SKIPPED) return; + + // Mesa calls the same generator again after Forge's gold event. Match it + // by identity, not by a nearby biome, so chunk borders cannot pick the + // wrong decorator. No other ore generators or biome settings are changed. + for (Biome biome : Biome.REGISTRY) { + if (!(biome instanceof BiomeMesa)) continue; + + BiomeDecorator decorator = biome.theBiomeDecorator; + if (decorator.goldGen == generator) { + suppressed.put(decorator, new SuppressedPass(generator, world, origin)); + decorator.goldGen = SKIPPED; + } + } + } + + static void restore(World world, BlockPos origin) { + Iterator> iterator = suppressed.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + SuppressedPass pass = entry.getValue(); + + if (pass.world == world && pass.origin.equals(origin)) { + restore(entry.getKey(), pass); + iterator.remove(); + } + } + } + + static void clear() { + for (Map.Entry entry : suppressed.entrySet()) { + restore(entry.getKey(), entry.getValue()); + } + + suppressed.clear(); + } + + private static void restore(BiomeDecorator decorator, SuppressedPass pass) { + // Leave a replacement installed by another mod alone. + if (decorator.goldGen == SKIPPED) { + decorator.goldGen = pass.generator; + } + } + + private static final class SuppressedPass { + final WorldGenerator generator; + final World world; + final BlockPos origin; + + SuppressedPass(WorldGenerator generator, World world, BlockPos origin) { + this.generator = generator; + this.world = world; + this.origin = origin.toImmutable(); + } + } +} diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeatureOrderTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeatureOrderTest.java index 7a82fa46..1246249d 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeatureOrderTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeatureOrderTest.java @@ -28,6 +28,12 @@ void coordinatorRunsSurfacesBeforeManagedOresAndLeavesStaticBiomeListsAlone() th "vanilla-ore suppression must make its decision at Forge's final standard event priority"); assertTrue(source.contains("OreSpawnOreGeneration.allowsVanillaOre(dimension, output")); assertTrue(source.contains("event.setResult(Event.Result.DENY)")); + assertTrue(source.contains("output == Blocks.GOLD_ORE && event.getResult() == Event.Result.DENY")); + assertTrue(source.contains("VanillaMesaGoldCompatibility.suppress(event.getGenerator()"), + "Mesa's extra pass must use the same gold decision, not a second scaling roll"); + assertTrue(source.contains("afterBiomeDecoration(DecorateBiomeEvent.Post event)")); + assertTrue(source.contains("VanillaMesaGoldCompatibility.restore(event.getWorld(), event.getPos())")); + assertTrue(source.contains("VanillaMesaGoldCompatibility.clear()")); assertTrue(source.contains("earlyComplete.add(key)") && source.contains("oreComplete.add(key)"), "terrain and ore callbacks must be independently deduplicated"); assertTrue(source("BiomeFeatureInstaller.java").contains("ordered terrain-event"), diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/VanillaMesaGoldCompatibilityTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/VanillaMesaGoldCompatibilityTest.java new file mode 100644 index 00000000..b5cce959 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/VanillaMesaGoldCompatibilityTest.java @@ -0,0 +1,138 @@ +package zone.moddev.mc.orespawn.worldgen; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Random; + +import net.minecraft.init.Biomes; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.biome.BiomeDecorator; +import net.minecraft.world.biome.BiomeMesa; +import net.minecraft.world.gen.feature.WorldGenerator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import zone.moddev.mc.orespawn.test.Forge12TestBootstrap; + +class VanillaMesaGoldCompatibilityTest { + + private static final BlockPos ORIGIN = new BlockPos(-32, 0, 48); + + @BeforeAll + static void bootstrap() { + Forge12TestBootstrap.registerVanilla(); + } + + @AfterEach + void restoreDecorators() { + VanillaMesaGoldCompatibility.clear(); + } + + @Test + void deniedGoldSkipsTheExtraPassForEveryMesaVariant() { + int variants = 0; + + for (Biome biome : Biome.REGISTRY) { + if (!(biome instanceof BiomeMesa)) continue; + + BiomeDecorator decorator = biome.theBiomeDecorator; + WorldGenerator previous = decorator.goldGen; + CountingGenerator gold = new CountingGenerator(); + + try { + decorator.goldGen = gold; + VanillaMesaGoldCompatibility.suppress(gold, null, ORIGIN); + + assertFalse(decorator.goldGen.generate(null, new Random(1), ORIGIN)); + assertEquals(0, gold.calls); + + VanillaMesaGoldCompatibility.restore(null, ORIGIN); + assertSame(gold, decorator.goldGen); + assertTrue(decorator.goldGen.generate(null, new Random(1), ORIGIN)); + assertEquals(1, gold.calls); + variants++; + } finally { + VanillaMesaGoldCompatibility.clear(); + decorator.goldGen = previous; + } + } + + assertEquals(6, variants); + } + + @Test + void unrelatedGeneratorsAndNonMesaDecoratorsAreUntouched() { + BiomeDecorator mesa = Biomes.MESA.theBiomeDecorator; + BiomeDecorator plains = Biomes.PLAINS.theBiomeDecorator; + WorldGenerator previousMesa = mesa.goldGen; + WorldGenerator previousPlains = plains.goldGen; + CountingGenerator gold = new CountingGenerator(); + + try { + mesa.goldGen = gold; + plains.goldGen = gold; + VanillaMesaGoldCompatibility.suppress(new CountingGenerator(), null, ORIGIN); + assertSame(gold, mesa.goldGen); + + VanillaMesaGoldCompatibility.suppress(gold, null, ORIGIN); + assertSame(gold, plains.goldGen); + } finally { + VanillaMesaGoldCompatibility.clear(); + mesa.goldGen = previousMesa; + plains.goldGen = previousPlains; + } + } + + @Test + void restorationIsScopedAndDoesNotOverwriteAnotherModsReplacement() { + BiomeDecorator mesa = Biomes.MESA.theBiomeDecorator; + WorldGenerator previous = mesa.goldGen; + CountingGenerator gold = new CountingGenerator(); + CountingGenerator replacement = new CountingGenerator(); + + try { + mesa.goldGen = gold; + BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos(ORIGIN.getX(), 0, ORIGIN.getZ()); + VanillaMesaGoldCompatibility.suppress(gold, null, mutable); + mutable.setPos(100, 0, 100); + + VanillaMesaGoldCompatibility.restore(null, mutable); + assertNotSame(gold, mesa.goldGen); + + mesa.goldGen = replacement; + VanillaMesaGoldCompatibility.restore(null, ORIGIN); + assertSame(replacement, mesa.goldGen); + } finally { + VanillaMesaGoldCompatibility.clear(); + mesa.goldGen = previous; + } + } + + @Test + void serverStopRestoresAnInterruptedDecoration() { + BiomeDecorator mesa = Biomes.MESA.theBiomeDecorator; + WorldGenerator previous = mesa.goldGen; + CountingGenerator gold = new CountingGenerator(); + + try { + mesa.goldGen = gold; + VanillaMesaGoldCompatibility.suppress(gold, null, ORIGIN); + OreSpawnWorldGenerator.INSTANCE.clear(); + assertSame(gold, mesa.goldGen); + } finally { + mesa.goldGen = previous; + } + } + + private static final class CountingGenerator extends WorldGenerator { + int calls; + + @Override + public boolean generate(World world, Random random, BlockPos pos) { + calls++; + return true; + } + } +} diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmarkTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmarkTest.java index e933d46c..ff1e27bc 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmarkTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/WorldgenBenchmarkTest.java @@ -25,12 +25,17 @@ void rejectsInvalidCustomDimensionIds() { } @Test - void vanillaControlBypassesEveryForge112GenerationEntryPoint() throws Exception { - String source = new String(Files.readAllBytes(Paths.get("src", "main", "java", "zone", - "moddev", "mc", "orespawn", "worldgen", "OreSpawnWorldGenerator.java")), - StandardCharsets.UTF_8); - assertEquals(5, occurrences(source, "if (WorldgenBenchmark.isVanillaBaseline()) return;"), - "terrain, springs, ores, ore filtering, and IWorldGenerator must all be bypassed"); + void vanillaControlBypassesTheExistingGenerationEntryPoints() throws Exception { + String source = coordinatorSource(); + for (String method : new String[] { "beforeBiomeDecoration", "replaceVanillaSprings", + "beforeVanillaOres", "filterVanillaOre", "generate" }) { + assertBaselineGuard(source, method); + } + } + + @Test + void vanillaControlBypassesMesaRestorationBeforeReadingTheEvent() throws Exception { + assertBaselineGuard(coordinatorSource(), "afterBiomeDecoration"); } @Test @@ -47,11 +52,19 @@ void integrationBenchmarkCanProveNormalizedBlockAndBiomeParity() throws Exceptio "hashing must happen outside the measured generation interval"); } - private static int occurrences(String value, String needle) { - int result = 0; - for (int index = 0; (index = value.indexOf(needle, index)) >= 0; index += needle.length()) { - result++; - } - return result; + private static String coordinatorSource() throws Exception { + return new String(Files.readAllBytes(Paths.get("src", "main", "java", "zone", + "moddev", "mc", "orespawn", "worldgen", "OreSpawnWorldGenerator.java")), + StandardCharsets.UTF_8); + } + + private static void assertBaselineGuard(String source, String method) { + int declaration = source.indexOf("public void " + method + "("); + assertTrue(declaration >= 0, "Missing generation entry point: " + method); + int body = source.indexOf('{', declaration); + assertTrue(body >= 0, "Missing method body: " + method); + assertTrue(source.substring(body + 1).trim() + .startsWith("if (WorldgenBenchmark.isVanillaBaseline()) return;"), + method + " must bypass generation work before reading its arguments"); } } From 5f2f429b6b77f15516112921836ebc6655035525 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Sun, 8 Nov 2026 21:44:48 +0000 Subject: [PATCH 2/2] Fix ore source defaults and old world upgrades --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 9 + README.md | 2 +- docs/MIGRATION.md | 9 + docs/PROVIDERS.md | 11 +- docs/VERSIONS.md | 2 +- gradle.properties | 2 +- gradle/release/artifacts.gradle | 6 +- .../orespawn/compat/LegacyOs3Bridge.java | 4 +- src/main/java/cyano/orespawn/OreSpawn.java | 2 +- .../zone/moddev/mc/orespawn/OreSpawn.java | 6 +- .../integration/LegacyOreReplacements.java | 203 ++++++++++++++++++ .../WorldgenIntegrationManager.java | 24 ++- .../mc/orespawn/worldgen/GeomeConfig.java | 24 ++- .../LegacyMineralogyProfileMigration.java | 2 +- .../worldgen/LegacyOs3ProfileMigration.java | 2 +- .../orespawn/worldgen/OreSourcePolicies.java | 4 +- .../worldgen/WorldGeologyProfile.java | 7 +- .../worldgen/WorldGeologyProfileManager.java | 22 +- .../LegacyOreReplacementsTest.java | 155 +++++++++++++ .../worldgen/OreSourcePoliciesTest.java | 20 ++ 21 files changed, 487 insertions(+), 35 deletions(-) create mode 100644 src/main/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacements.java create mode 100644 src/test/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacementsTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36022ff7..e64d8113 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,9 +126,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.1.5.110021.jar - build/libs/OreSpawn-4.1.5.110021-sources.jar - build/libs/OreSpawn-4.1.5.110021-javadoc.jar + build/libs/OreSpawn-4.1.6.110021.jar + build/libs/OreSpawn-4.1.6.110021-sources.jar + build/libs/OreSpawn-4.1.6.110021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 6fd2b880..489a0829 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Version 4.1.6.110021 + +* Fix duplicate ore generation when an OreSpawn 1 world is moved without its + converted provider file. Keep imported rules and saved choices, and back + up the profile before removing a duplicate. Leave edited rules for review. +* Pick the default sulfur and lithium sources after all mods have loaded. + New setups can share those ores with Base Electric without adding extra + veins. Existing worlds keep their choices and existing chunks stay as they are. + Version 4.1.5.110021 * Apply the normal gold-generation controls to Mesa's extra gold pass too. diff --git a/README.md b/README.md index eea6f9fb..4e3071ab 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It gives mods and modpacks one place to configure ores, deposit shapes, optional rock strata and geomes, provider-owned underground fluid deposits, biome palettes and world materials, flat bedrock, and bounded ore retrogen. -This branch builds target-qualified version `4.1.5.110021`: the OreSpawn 4.1.5 +This branch builds target-qualified version `4.1.6.110021`: the OreSpawn 4.1.6 feature set for Minecraft 1.10.2 and Forge. It adds stable generation identity, non-loading geology sampling, per-resource background control, existing-world provider merge policy, an Ore Sources arbiter for equivalent provider ores and diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index d6ac17d9..878b9df2 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -77,6 +77,15 @@ remain unchanged. ## Provider-Aware OS3 Imports +An OreSpawn 1 import keeps its saved rule ID and settings even if you move +the world without its converted provider file. If an older build added a +second rule, OreSpawn removes it only when the saved rule still matches the +provider default and no custom ore-source selection depends on it. The +original profile is backed up before that repair. Edited or ambiguous +overlaps are left in place and listed under `legacy_ore_review` in the +provider's saved manifest and reported in the log. This does not change +ore blocks in chunks already generated. + When an OreSpawn 2/3 file is named for an installed provider, the migrator now compares each converted primary output with that provider's ore declarations. A unique output match is written under the provider's stable rule ID, allowing diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 46ee066c..ace4314d 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -120,11 +120,12 @@ unambiguous. NetherMetals and EndMetals stay in their respective dimension domains. DenseMetals is enrichment, not an interchangeable output, and BaseSciences has no ordinary ore-source role. -Reviewed fresh-world conflicts start Balanced with all eligible output blocks. -Mineralogy, then BaseMinerals, then ElectricAdvantage is the initial placement -and Single-mode priority for sulfur; BaseMinerals, then ElectricAdvantage is the -corresponding lithium priority. No other conflict is automatically consolidated -without an explicit catalog row. Exact Ore Dictionary `oreX` inference recognizes the curated +Once mods have finished registering their ores, reviewed fresh-world conflicts +start Balanced with all eligible output blocks. Sulfur prefers Mineralogy, +then Base Minerals, then Base Electric (`baseelectric` or the older +`electricadvantage` ID) for placement. Lithium prefers Base Minerals, then +Base Electric. Existing choices are kept. Other conflicts need an explicit +catalog entry for automatic consolidation. Exact Ore Dictionary `oreX` inference recognizes the curated sulfur/sulphur and aluminum/aluminium aliases but deliberately keeps Niter and Saltpeter distinct. Multiple unrelated entries are marked for review rather than fuzzy-matched from block names. diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 4c3f70c3..e3fa6b79 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -51,7 +51,7 @@ Examples: | Minecraft | Loader | Target | Example full version | | --- | --- | ---: | --- | -| 1.10.2 | Forge | `110021` | `4.1.5.110021` | +| 1.10.2 | Forge | `110021` | `4.1.6.110021` | | 1.12.2 | Forge | `112021` | `4.0.6.112021` | | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | diff --git a/gradle.properties b/gradle.properties index febbf260..58fe7950 100644 --- a/gradle.properties +++ b/gradle.properties @@ -30,7 +30,7 @@ mod_vendor=Minecraft Mod Development mod_license=LGPL-2.1 mod_license_name=GNU Lesser General Public License, Version 2.1 mod_license_url=https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -mod_version=4.1.5.110021 +mod_version=4.1.6.110021 mod_group=zone.moddev.mc.orespawn mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/gradle/release/artifacts.gradle b/gradle/release/artifacts.gradle index 746ae826..9f61621d 100644 --- a/gradle/release/artifacts.gradle +++ b/gradle/release/artifacts.gradle @@ -77,7 +77,7 @@ tasks.register('verifyReleaseConfiguration') { description = 'Validates the target-qualified release, API, schemas, reports, and publishing identity.' doLast { - if (project.mod_version != '4.1.5.110021' + if (project.mod_version != '4.1.6.110021' || project.mod_group != expectedMavenGroup) { throw new GradleException("Unexpected OreSpawn release version: ${project.mod_version}") } @@ -106,8 +106,8 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.1.5.110021')) { - throw new GradleException("Authoritative release location does not contain 4.1.5.110021: ${path}") + if (!file(path).getText('UTF-8').contains('4.1.6.110021')) { + throw new GradleException("Authoritative release location does not contain 4.1.6.110021: ${path}") } } if (!file('docs/API.md').getText('UTF-8').contains('orespawn@[4.0.6,5.0.0)')) { diff --git a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java index d43e6931..29c0d244 100644 --- a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java +++ b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java @@ -488,6 +488,7 @@ private static JsonObject convertOs1Source(String owner, JsonObject source) { continue; } JsonObject spawn = new JsonObject(); spawn.addProperty("enabled", true); + spawn.addProperty("legacy_import", "os1"); spawn.addProperty("feature", "default"); spawn.addProperty("replaces", "default"); JsonArray blocks = new JsonArray(); JsonObject block = new JsonObject(); block.addProperty("name", output); block.addProperty("chance", 100); @@ -620,6 +621,7 @@ private static JsonObject translateSpawn(String modId, String name, JsonObject s ore.addProperty("block", output); ore.addProperty("source_mod", modId); ore.addProperty("native_generation", false); ore.addProperty("retrogen", bool(spawn, "retrogen", false)); + if ("os1".equals(text(spawn, "legacy_import", ""))) ore.addProperty("legacy_import", "os1"); copyMetadata(first, ore); if (flags.replaceVanilla && "minecraft".equals(new ResourceLocation(output).getResourceDomain())) { ore.addProperty("suppress_vanilla", true); @@ -1027,7 +1029,7 @@ private static void writeHumanUpgradeReport(Path destination) throws IOException } } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.1.5.110021 Upgrade Report"); + lines.add("OreSpawn 4.1.6.110021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn configuration was consumed and translated for OS4."); diff --git a/src/main/java/cyano/orespawn/OreSpawn.java b/src/main/java/cyano/orespawn/OreSpawn.java index cdbfd49b..69af7ac5 100644 --- a/src/main/java/cyano/orespawn/OreSpawn.java +++ b/src/main/java/cyano/orespawn/OreSpawn.java @@ -19,7 +19,7 @@ public class OreSpawn { public static OreSpawn INSTANCE; public static final String MODID = "orespawn"; public static final String NAME = "OreSpawn"; - public static final String VERSION = "4.1.5.110021"; + public static final String VERSION = "4.1.6.110021"; public static final List oreSpawnConfigFiles = new ArrayList<>(); public static final List additionalStoneBlocks = new ArrayList<>(); public static boolean disableVanillaOreGen; diff --git a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java index aa8e913d..e9a0f50b 100644 --- a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java +++ b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java @@ -52,7 +52,7 @@ public class OreSpawn { public static final String MODID = "orespawn"; public static final String NAME = "OreSpawn"; - public static final String VERSION = "4.1.5.110021"; + public static final String VERSION = "4.1.6.110021"; private static final Logger LOGGER = LogManager.getLogger(); @@ -90,7 +90,7 @@ public void preInit(FMLPreInitializationEvent event) { public void init(FMLInitializationEvent event) { LegacyOs3Bridge.completeInitialization(); WorldgenIntegrationManager.initialize(); - GeomeConfig.bake(); + GeomeConfig.bakeBeforeLoadComplete(); logGeomeSampler(); DocumentationExporter.exportBundledGuide(); StoneReplacer.registerConfiguredFeature(); @@ -109,7 +109,7 @@ public void processInterMod(FMLInterModComms.IMCEvent event) { public void postInit(FMLPostInitializationEvent event) { WorldgenIntegrationManager.processImcMessages(); WorldgenIntegrationManager.freeze(); - GeomeConfig.bake(); + GeomeConfig.bakeBeforeLoadComplete(); refreshGenerationConfig(); WorldgenIntegrationManager.markFeatureReady(); } diff --git a/src/main/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacements.java b/src/main/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacements.java new file mode 100644 index 00000000..eea84399 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacements.java @@ -0,0 +1,203 @@ +package zone.moddev.mc.orespawn.integration; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import zone.moddev.mc.orespawn.util.JsonCopies; + +/** Keeps a saved OS1 rule in charge when its provider starts supplying a native default. */ +final class LegacyOreReplacements { + private static final Logger LOGGER = LogManager.getLogger(); + private static final String REPLACEMENTS = "legacy_ore_replacements"; + private static final String REVIEW = "legacy_ore_review"; + + static final class Result { + final Set suppressed = new HashSet<>(); + boolean changed; + } + + private LegacyOreReplacements() {} + + static Result reconcile(JsonObject profile, String provider, JsonObject ores, JsonObject defaults, + JsonObject active, JsonObject manifest, Set known) { + Result result = new Result(); + JsonObject previous = object(manifest, REPLACEMENTS); + JsonObject replacements = JsonCopies.copy(previous); + JsonObject review = new JsonObject(); + Map> imports = new HashMap<>(); + for (String id : known) { + JsonObject ore = object(ores, id); + String output = output(ore); + if (output != null && isOs1Import(provider, id, ore)) { + imports.computeIfAbsent(output, ignored -> new ArrayList<>()).add(id); + } + } + if (imports.isEmpty() && previous.entrySet().isEmpty() && !manifest.has(REVIEW)) return result; + + Map countByOutput = new HashMap<>(); + for (Map.Entry entry : defaults.entrySet()) { + JsonObject ore = object(defaults, entry.getKey()); + if (!isOs1Import(provider, entry.getKey(), ore)) { + String output = output(ore); + if (output != null) countByOutput.merge(output, 1, Integer::sum); + } + } + for (Map.Entry entry : defaults.entrySet()) { + String id = entry.getKey(); + JsonObject nativeOre = object(defaults, id); + if (isOs1Import(provider, id, nativeOre)) continue; + String remembered = text(previous, id); + String output = output(nativeOre); + List matches = imports.getOrDefault(output, Collections.emptyList()); + if (remembered.isEmpty() && matches.isEmpty()) continue; + + if (remembered.isEmpty() && (matches.size() != 1 || countByOutput.getOrDefault(output, 0) != 1)) { + flag(review, manifest, provider, id, "ambiguous_import"); + result.suppressed.add(id); + continue; + } + String legacyId = remembered.isEmpty() ? matches.get(0) : remembered; + if (id.equals(legacyId) || !known.contains(legacyId)) continue; + if (active.has(id) && !entry.getValue().equals(active.get(id))) { + flag(review, manifest, provider, id, "provider_override_differs"); + continue; + } + if (ores.has(id)) { + // Never undo an edited rule or a selection that depends on it. + JsonObject expected = JsonCopies.copy(nativeOre); + expected.addProperty("source_provider", provider); + JsonObject saved = JsonCopies.copy(object(ores, id)); + saved.remove("orphaned_provider"); + List disposablePolicies = stockPolicyReferences(profile, id, legacyId); + if (!known.contains(id) || !ores.has(legacyId) || !ores.get(id).isJsonObject() + || !expected.equals(saved) || disposablePolicies == null) { + flag(review, manifest, provider, id, "saved_rule_or_policy_differs"); + continue; + } + ores.remove(id); + JsonObject policies = object(profile, "ore_source_policies"); + for (String key : disposablePolicies) policies.remove(key); + result.changed = true; + LOGGER.info("Kept imported ore rule {} and removed untouched duplicate {}", legacyId, id); + } + replacements.addProperty(id, legacyId); + result.suppressed.add(id); + // The provider still supplies a counterpart for the imported rule. + JsonObject imported = object(ores, legacyId); + if (imported.has("orphaned_provider")) { + imported.remove("orphaned_provider"); + result.changed = true; + } + } + result.changed |= update(manifest, REPLACEMENTS, replacements); + result.changed |= update(manifest, REVIEW, review); + return result; + } + + static boolean retains(JsonObject manifest, String id, Set currentDefaults) { + JsonObject replacements = object(manifest, REPLACEMENTS); + for (Map.Entry entry : replacements.entrySet()) { + if (currentDefaults.contains(entry.getKey()) && id.equals(text(replacements, entry.getKey()))) { + return true; + } + } + return false; + } + + private static boolean isOs1Import(String provider, String id, JsonObject ore) { + String block = text(ore, "block"); + if (!block.startsWith(provider + ":") || !provider.equals(text(ore, "source_mod")) + || !"false".equals(text(ore, "native_generation"))) return false; + if ("os1".equals(text(ore, "legacy_import"))) return true; + // Older OS1 imports used the namespaced output plus an ordinal. OS3 and + // authored rules do not qualify by block name alone. + String prefix = provider + ":legacy/" + block.replace(':', '_'); + return provider.equals(text(ore, "source_provider")) && id.startsWith(prefix) + && id.substring(prefix.length()).matches("_[0-9]+"); + } + + private static String output(JsonObject ore) { + String block = text(ore, "block"); + String variant = metadata(ore); + if (block.isEmpty() || variant == null) return null; + if (ore.has("outputs")) { + JsonElement value = ore.get("outputs"); + if (!value.isJsonArray()) return null; + JsonArray outputs = value.getAsJsonArray(); + if (outputs.size() != 1 || !outputs.get(0).isJsonObject()) return null; + JsonObject only = outputs.get(0).getAsJsonObject(); + if (!block.equals(text(only, "block")) || !variant.equals(metadata(only))) return null; + } + return block + "#" + variant; + } + + private static String metadata(JsonObject ore) { + if (!ore.has("metadata")) return "0"; + String meta = text(ore, "metadata"); + return meta.matches("[0-9]|1[0-5]") ? meta : null; + } + + /** Only remove the fallback policy generated for a lone native rule. */ + private static List stockPolicyReferences(JsonObject profile, String nativeId, String legacyId) { + List disposable = new ArrayList<>(); + JsonObject imported = object(object(profile, "ores"), legacyId); + for (Map.Entry entry : object(profile, "ore_source_policies").entrySet()) { + if (!entry.getValue().isJsonObject()) continue; + JsonObject policy = entry.getValue().getAsJsonObject(); + JsonObject outputs = object(policy, "outputs"); + JsonObject placements = object(policy, "placement_sources"); + if (!outputs.has(nativeId) && !placements.entrySet().stream() + .anyMatch(place -> nativeId.equals(text(placements, place.getKey())))) continue; + String domain = text(policy, "domain"); + if (!entry.getKey().equals(text(policy, "material") + "|" + domain) || policy.has("dormant") + || object(imported, "dimensions").has(domain) || object(imported, "dimension_selectors").has(domain) + || !"keep_separate".equals(text(policy, "mode")) || !"custom".equals(text(policy, "output_mode")) + || !"separate".equals(text(policy, "status")) || !"false".equals(text(policy, "review_required")) + || outputs.entrySet().size() != 1 || !unitWeight(outputs.get(nativeId)) + || placements.entrySet().size() != 1 + || !nativeId.equals(text(placements, "orespawn:standard"))) return null; + disposable.add(entry.getKey()); + } + return disposable; + } + + private static boolean unitWeight(JsonElement value) { + return value != null && value.isJsonPrimitive() && value.getAsJsonPrimitive().isNumber() + && value.getAsDouble() == 1.0D; + } + + private static void flag(JsonObject review, JsonObject manifest, String provider, String id, String reason) { + review.addProperty(id, reason); + if (!reason.equals(text(object(manifest, REVIEW), id))) { + LOGGER.warn("Review legacy ore overlap for provider {} and rule {} ({}); saved settings were kept", + provider, id, reason); + } + } + + private static boolean update(JsonObject manifest, String key, JsonObject value) { + if (value.equals(object(manifest, key))) return false; + if (value.entrySet().isEmpty()) manifest.remove(key); + else manifest.add(key, value); + return true; + } + + private static JsonObject object(JsonObject root, String key) { + JsonElement value = root.get(key); + return value != null && value.isJsonObject() ? value.getAsJsonObject() : new JsonObject(); + } + + private static String text(JsonObject root, String key) { + JsonElement value = root.get(key); + return value != null && value.isJsonPrimitive() ? value.getAsString() : ""; + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/integration/WorldgenIntegrationManager.java b/src/main/java/zone/moddev/mc/orespawn/integration/WorldgenIntegrationManager.java index 82cfb426..07d83bb2 100644 --- a/src/main/java/zone/moddev/mc/orespawn/integration/WorldgenIntegrationManager.java +++ b/src/main/java/zone/moddev/mc/orespawn/integration/WorldgenIntegrationManager.java @@ -280,9 +280,15 @@ private static boolean mergeProviderDefinitions(JsonObject target, boolean exist String knownKey = "known_" + sectionName; Set known = stringSet(manifest.get(knownKey)); Set current = JsonCopies.keys(providerSection); + LegacyOreReplacements.Result replacements = existingWorld && "ores".equals(sectionName) + ? LegacyOreReplacements.reconcile(target, provider.modId, targetSection, + nativeOreDefaults(provider.modId, providerSection), providerSection, manifest, known) + : new LegacyOreReplacements.Result(); + changed |= replacements.changed; for (Entry entry : providerSection.entrySet()) { String id = entry.getKey(); + if (replacements.suppressed.contains(id)) continue; if (mergeNewEntries && !targetSection.has(id) && !known.contains(id)) { JsonObject value = JsonCopies.copy(entry.getValue().getAsJsonObject()); if (!"biome_rules".equals(sectionName)) { @@ -301,7 +307,9 @@ private static boolean mergeProviderDefinitions(JsonObject target, boolean exist for (String knownId : known) { if (!current.contains(knownId) && targetSection.has(knownId) - && targetSection.get(knownId).isJsonObject()) { + && !("ores".equals(sectionName) && LegacyOreReplacements.retains(manifest, knownId, current)) + && targetSection.get(knownId).isJsonObject() + && !bool(targetSection.getAsJsonObject(knownId), "orphaned_provider", false)) { targetSection.getAsJsonObject(knownId).addProperty("orphaned_provider", true); changed = true; } @@ -520,9 +528,21 @@ private static void rebuildActiveProviders() { } } + private static JsonObject nativeOreDefaults(String providerId, JsonObject activeOres) { + if (!FILE_PROVIDERS.containsKey(providerId)) return activeOres; + // A converted file can hide the native catalogue. Use the shipped rule to + // recognise an old duplicate; never treat an authored override as stock. + JsonObject shipped = installedProviderRoot(providerId); + return shipped == null ? new JsonObject() : optionalObject(shipped, "ores"); + } + private static JsonObject selectedProviderRoot(String providerId) { JsonObject root = FILE_PROVIDERS.get(providerId); - if (root == null) root = RESOURCE_PROVIDERS.get(providerId); + return root == null ? installedProviderRoot(providerId) : root; + } + + private static JsonObject installedProviderRoot(String providerId) { + JsonObject root = RESOURCE_PROVIDERS.get(providerId); if (root == null) { WorldgenProvider apiProvider = API_PROVIDERS.get(providerId); root = apiProvider == null ? null : apiProvider.toJson(); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java index a906dc4f..1bb7f283 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java @@ -99,21 +99,31 @@ private GeomeConfig() { } public static synchronized BakedGeomeConfig bake() { + return bakeGlobal(true); + } + + public static synchronized BakedGeomeConfig bakeBeforeLoadComplete() { + // Features need their settings during startup, but other mods can still + // register ore providers. Wait for loadComplete before choosing defaults. + return bakeGlobal(false); + } + + private static BakedGeomeConfig bakeGlobal(boolean chooseOreSourceDefaults) { JsonObject root = loadConfig(); boolean changed = OreSpawnOreIntegration.mergeProviderOres(root); changed |= FluidDepositMigration.normalize(root); - changed |= OreSourcePolicies.initialize(root, false); + if (chooseOreSourceDefaults) changed |= OreSourcePolicies.initialize(root, false); if (changed) { writeProviderMerge(root); } globalBaseConfigRoot = JsonCopies.copy(root); globalBaseProfile = WorldGeologyProfile.fromGlobalConfig(root, - OreSpawnConfig.geologyMode(), OreSpawnConfig.placeCrudeOil()); - root = applyFreshWorldTemplate(root); - globalConfigRoot = JsonCopies.copy(root); - globalProfile = WorldGeologyProfile.fromGlobalConfig(root, - OreSpawnConfig.geologyMode(), OreSpawnConfig.placeCrudeOil()); - bakeDimensions(root); + OreSpawnConfig.geologyMode(), OreSpawnConfig.placeCrudeOil(), false); + JsonObject effective = applyFreshWorldTemplate(root); + globalConfigRoot = JsonCopies.copy(effective); + globalProfile = effective == root ? globalBaseProfile : WorldGeologyProfile.fromGlobalConfig(effective, + OreSpawnConfig.geologyMode(), OreSpawnConfig.placeCrudeOil(), chooseOreSourceDefaults); + bakeDimensions(effective); return bakedConfig; } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index b94ae147..4befc702 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -125,7 +125,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configDirectory, } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.1.5.110021 Upgrade Report"); + lines.add("OreSpawn 4.1.6.110021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + mineralogyVersion diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java index 232c72b0..985b3550 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java @@ -65,7 +65,7 @@ private static void writeInitialUpgradeReport(Path configDirectory, boolean forceRetrogen, boolean flatBedrock, boolean retrogenBedrock, int bedrockLayers) throws IOException { String newline = System.lineSeparator(); - String text = "OreSpawn 4.1.5.110021 Upgrade Report" + newline + String text = "OreSpawn 4.1.6.110021 Upgrade Report" + newline + "================================" + newline + newline + "RESULT: Legacy OreSpawn settings were imported into the OS4 profile." + newline + "- Manage vanilla ores: " + manageVanilla + newline 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 68ba9040..eae9f8ea 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java @@ -487,9 +487,9 @@ private static Candidate preferred(Group group, ResourceLocation channel) { private static Map> priorities() { Map> result = new HashMap<>(); result.put("sulfur", Collections.unmodifiableList(Arrays.asList( - "mineralogy", "baseminerals", "electricadvantage"))); + "mineralogy", "baseminerals", "baseelectric", "electricadvantage"))); result.put("lithium", Collections.unmodifiableList(Arrays.asList( - "baseminerals", "electricadvantage"))); + "baseminerals", "baseelectric", "electricadvantage"))); return Collections.unmodifiableMap(result); } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfile.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfile.java index c07a7f2e..50ed7954 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfile.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfile.java @@ -71,8 +71,13 @@ public static WorldGeologyProfile recommended(boolean placeFluidDeposits) { public static WorldGeologyProfile fromGlobalConfig(JsonObject globalRoot, GeologyMode geologyMode, boolean placeFluidDeposits) { + return fromGlobalConfig(globalRoot, geologyMode, placeFluidDeposits, true); + } + + static WorldGeologyProfile fromGlobalConfig(JsonObject globalRoot, + GeologyMode geologyMode, boolean placeFluidDeposits, boolean chooseOreSourceDefaults) { JsonObject root = JsonCopies.copy(globalRoot); - OreSourcePolicies.initialize(root, false); + if (chooseOreSourceDefaults) OreSourcePolicies.initialize(root, false); if (!root.has("geology_mode")) { root.addProperty("geology_mode", geologyMode.name().toLowerCase(Locale.ROOT)); } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java index 94cd5b18..bec6f1e0 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java @@ -106,7 +106,7 @@ public static synchronized boolean reloadActiveProfile() { JsonObject merged = profile.rootCopy(); boolean changed = WorldgenIntegrationManager.mergeProviderDefinitionsIntoExistingWorld(merged); changed |= OreSourcePolicies.initialize(merged, true); - if (changed) { + if (changed && preserveBeforeLegacyOreRepair(profilePath, profile, merged)) { profile = profile.withRoot(merged); writeProfile(profilePath, profile); } @@ -130,7 +130,7 @@ public static void onServerAboutToStart(FMLServerAboutToStartEvent event) { String beforeMerge = merged.toString(); WorldgenIntegrationManager.mergeProviderDefinitionsIntoExistingWorld(merged); OreSourcePolicies.initialize(merged, true); - if (!beforeMerge.equals(merged.toString())) { + if (!beforeMerge.equals(merged.toString()) && preserveBeforeLegacyOreRepair(profilePath, profile, merged)) { profile = profile.withRoot(merged); writeProfile(profilePath, profile); LOGGER.info("Merged new OreSpawn worldgen-provider definitions into '{}'", profilePath); @@ -310,6 +310,24 @@ static WorldGeologyProfile readProfile(Path path, WorldGeologyProfile fallback) } } + private static boolean preserveBeforeLegacyOreRepair(Path path, WorldGeologyProfile before, JsonObject merged) { + JsonObject beforeRoot = before.rootCopy(); + if (!beforeRoot.has("ores") || !beforeRoot.get("ores").isJsonObject()) return true; + JsonObject newOres = merged.getAsJsonObject("ores"); + for (java.util.Map.Entry rule : beforeRoot.getAsJsonObject("ores").entrySet()) { + if (newOres != null && newOres.has(rule.getKey())) continue; + Path backup = path.resolveSibling("orespawn-worldgen.pre-legacy-ore-repair.bak"); + try { + if (!Files.exists(backup)) Files.copy(path, backup); + return true; + } catch (IOException e) { + LOGGER.warn("Could not back up '{}'; legacy ore repair was not applied", path, e); + return false; + } + } + return true; + } + private static boolean preserveBeforeOreDefaultsRefresh(Path path) { Path backup = path.resolveSibling("orespawn-worldgen.pre-ore-revision-" + GeomeConfig.oreDefaultsRevision() + ".bak"); diff --git a/src/test/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacementsTest.java b/src/test/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacementsTest.java new file mode 100644 index 00000000..a11c331a --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacementsTest.java @@ -0,0 +1,155 @@ +package zone.moddev.mc.orespawn.integration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Test; +import zone.moddev.mc.orespawn.util.JsonCopies; + +class LegacyOreReplacementsTest { + private static final String PROVIDER = "baseminerals"; + private static final String OLD = "baseminerals:legacy/baseminerals_lithium_ore_0"; + private static final String NATIVE = "baseminerals:legacy/lithium_ore"; + private static final String FALLBACK_POLICY = "orespawn:lithium|orespawn:all_except_nether_end"; + + private final JsonObject profile = parse("{\"ores\":{},\"ore_source_policies\":{}}"); + private final JsonObject ores = profile.getAsJsonObject("ores"); + private final JsonObject policies = profile.getAsJsonObject("ore_source_policies"); + private final JsonObject defaults = new JsonObject(); + private final JsonObject manifest = new JsonObject(); + private final Set known = new LinkedHashSet<>(Arrays.asList(OLD)); + + LegacyOreReplacementsTest() { + ores.add(OLD, parse("{\"block\":\"baseminerals:lithium_ore\",\"enabled\":false," + + "\"source_mod\":\"baseminerals\",\"source_provider\":\"baseminerals\"," + + "\"native_generation\":false,\"outputs\":[{\"block\":\"baseminerals:lithium_ore\"}]," + + "\"dimensions\":{\"minecraft:overworld\":{\"frequency\":1.5}}}")); + defaults.add(NATIVE, parse("{\"block\":\"baseminerals:lithium_ore\",\"enabled\":true," + + "\"dimensions\":{\"minecraft:overworld\":{\"frequency\":4.0}}," + + "\"dimension_selectors\":{\"orespawn:all_except_nether_end\":{\"frequency\":4.0}}}")); + } + + @Test + void remembersImportAndPreservesItsScopeAndChoicesOnEveryReload() { + policies.add("existing", parse("{\"mode\":\"consolidated\",\"output_mode\":\"single\",\"outputs\":{\"" + OLD + "\":1}}")); + JsonObject saved = JsonCopies.copy(profile); + assertTrue(merge().suppressed.contains(NATIVE)); + assertEquals(OLD, manifest.getAsJsonObject("legacy_ore_replacements").get(NATIVE).getAsString()); + assertEquals(saved, profile); + assertTrue(LegacyOreReplacements.retains(manifest, OLD, JsonCopies.keys(defaults))); + assertFalse(merge().changed); + // Removing a previously tracked import is not permission to add the native default. + ores.remove(OLD); + assertTrue(merge().suppressed.contains(NATIVE)); + } + + @Test + void dropsOnlyTheUntouchedAddedDefaultAndItsGeneratedFallback() { + addNative(); + policies.add(FALLBACK_POLICY, parse("{\"material\":\"orespawn:lithium\",\"domain\":\"orespawn:all_except_nether_end\"," + + "\"mode\":\"keep_separate\",\"output_mode\":\"custom\",\"status\":\"separate\"," + + "\"review_required\":false,\"outputs\":{\"" + NATIVE + "\":1}," + + "\"placement_sources\":{\"orespawn:standard\":\"" + NATIVE + "\"}}")); + JsonObject original = JsonCopies.copy(ores.getAsJsonObject(OLD)); + assertTrue(merge().changed); + assertFalse(ores.has(NATIVE)); + assertFalse(policies.has(FALLBACK_POLICY)); + assertEquals(original, ores.get(OLD)); + assertFalse(merge().changed); + } + + @Test + void retainsAnEditedNativeRuleAndAsksForReview() { + addNative(); + ores.getAsJsonObject(NATIVE).addProperty("enabled", false); + JsonObject before = JsonCopies.copy(profile); + assertFalse(merge().suppressed.contains(NATIVE)); + assertEquals(before, profile); + assertTrue(manifest.getAsJsonObject("legacy_ore_review").has(NATIVE)); + assertFalse(merge().changed); + } + + @Test + void retainsAnUntouchedNativeRuleWhenPlayerPolicyUsesIt() { + addNative(); + policies.add("existing", parse("{\"mode\":\"consolidated\",\"output_mode\":\"single\",\"outputs\":{\"" + NATIVE + "\":1}}")); + JsonObject before = JsonCopies.copy(profile); + assertFalse(merge().suppressed.contains(NATIVE)); + assertEquals(before, profile); + assertTrue(manifest.getAsJsonObject("legacy_ore_review").has(NATIVE)); + } + + @Test + void doesNotGuessForMultipleImportsOrMultipleNativeRules() { + String second = OLD.substring(0, OLD.length() - 1) + "1"; + known.add(second); + ores.add(second, JsonCopies.copy(ores.get(OLD))); + addNative(); + assertTrue(merge().suppressed.contains(NATIVE)); + assertTrue(ores.has(NATIVE)); + assertFalse(manifest.has("legacy_ore_replacements")); + assertTrue(manifest.getAsJsonObject("legacy_ore_review").has(NATIVE)); + + ores.remove(second); + defaults.add("baseminerals:other_lithium", JsonCopies.copy(defaults.get(NATIVE))); + assertTrue(merge().suppressed.contains(NATIVE)); + assertTrue(ores.has(NATIVE)); + assertFalse(manifest.has("legacy_ore_replacements")); + } + + @Test + void doesNotMatchAnUntrackedRuleOrAnotherOutputVariant() { + known.clear(); + assertFalse(merge().suppressed.contains(NATIVE)); + known.add(OLD); + defaults.getAsJsonObject(NATIVE).addProperty("metadata", 1); + assertFalse(merge().suppressed.contains(NATIVE)); + ores.getAsJsonObject(OLD).addProperty("source_provider", "anothermod"); + defaults.getAsJsonObject(NATIVE).remove("metadata"); + assertFalse(merge().suppressed.contains(NATIVE)); + assertFalse(manifest.has("legacy_ore_replacements")); + } + + @Test + void recognizesStockDuplicateEvenWhenTheOldConvertedFileIsActive() { + addNative(); + JsonObject active = new JsonObject(); + active.add(OLD, JsonCopies.copy(ores.get(OLD))); + assertTrue(LegacyOreReplacements.reconcile(profile, PROVIDER, ores, defaults, active, manifest, known) + .suppressed.contains(NATIVE)); + assertFalse(ores.has(NATIVE)); + } + + @Test + void retainsADefaultChangedByAProviderOverride() { + addNative(); + JsonObject active = JsonCopies.copy(defaults); + active.getAsJsonObject(NATIVE).addProperty("enabled", false); + assertFalse(LegacyOreReplacements.reconcile(profile, PROVIDER, ores, defaults, active, manifest, known) + .suppressed.contains(NATIVE)); + assertTrue(ores.has(NATIVE)); + assertTrue(manifest.getAsJsonObject("legacy_ore_review").has(NATIVE)); + } + + private void addNative() { + JsonObject nativeRule = JsonCopies.copy(defaults.getAsJsonObject(NATIVE)); + nativeRule.addProperty("source_provider", PROVIDER); + ores.add(NATIVE, nativeRule); + known.add(NATIVE); + } + + private LegacyOreReplacements.Result merge() { + return LegacyOreReplacements.reconcile(profile, PROVIDER, ores, defaults, defaults, manifest, known); + } + + private static JsonObject parse(String source) { + return new JsonParser().parse(source).getAsJsonObject(); + } +} diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSourcePoliciesTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSourcePoliciesTest.java index 4240a42a..65a0e96e 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSourcePoliciesTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSourcePoliciesTest.java @@ -373,6 +373,26 @@ void separateLegacyPolicyForOneLoadedOutputIsHiddenWithoutBeingDeleted() { "All Groups must retain harmless single-output material groups"); } + @Test + void baseElectricIsARecognizedOutputWithoutAddingAPlacementBudget() { + JsonObject root = rootWithSulfurConflict(); + addOre(root, "baseelectric:sulfur", "baseelectric", "minecraft:coal_ore", + "orespawn:sulfur", "minecraft:overworld", null); + root.getAsJsonObject("ores").getAsJsonObject("baseelectric:sulfur") + .addProperty("enabled", false); + + OreSourcePolicies.initialize(root, false); + + JsonObject policy = policy(root); + assertEquals("consolidated", policy.get("mode").getAsString()); + assertEquals("balanced", policy.get("output_mode").getAsString()); + assertEquals(3, policy.getAsJsonObject("outputs").entrySet().size()); + assertTrue(policy.getAsJsonObject("outputs").has("baseelectric:sulfur")); + JsonObject sources = policy.getAsJsonObject("placement_sources"); + assertEquals(1, sources.entrySet().size()); + assertEquals("mineralogy:sulfur", sources.get("orespawn:standard").getAsString()); + } + @Test void inactiveManagedProviderRemainsAnOutputWithoutOwningPlacement() { JsonObject root = root();