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"); } }