From 90e74475474b679a88f3d4da0e42ede1d597e80f Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 22:40:13 +0100 Subject: [PATCH 1/6] Fix provider-declared natural terrain replacement on Forge 1.13.2 --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 9 + README.md | 2 +- build.gradle | 10 +- docs/BIOMES.md | 7 + docs/VERSIONS.md | 5 +- gradle.properties | 2 +- .../orespawn/testmod/SurfaceProbeTestMod.java | 200 +++++++++++++++++- .../worldgen/BakedTerrainDimension.java | 5 + .../worldgen/BiomeFeatureInstaller.java | 31 ++- .../moddev/mc/orespawn/worldgen/Geology.java | 6 +- .../mc/orespawn/worldgen/GeomeGeology.java | 7 +- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../mc/orespawn/api/WorldgenProviderTest.java | 26 +++ .../BiomeSurfaceFeatureOrderTest.java | 9 +- .../orespawn/worldgen/StoneReplacerTest.java | 22 ++ 17 files changed, 321 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8b2034e..e92b0854 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,9 +56,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.8.113021.jar - build/libs/OreSpawn-4.0.8.113021-sources.jar - build/libs/OreSpawn-4.0.8.113021-javadoc.jar + build/libs/OreSpawn-4.0.9.113021.jar + build/libs/OreSpawn-4.0.9.113021-sources.jar + build/libs/OreSpawn-4.0.9.113021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8db0c4be..54596bf8 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Version 4.0.9.113021 + +* Replace provider-declared natural terrain hosts during the existing geology + scan before structure and vegetation features can author matching blocks. +* Keep air, fluids, bedrock, and block entities protected even when their block + IDs are mistakenly declared as terrain hosts. +* Apply the correction only while generating new chunks; existing chunks and + saved profiles remain unchanged. + Version 4.0.8.113021 * Preserve long host, tag, and biome-list values when OreSpawn editors load diff --git a/README.md b/README.md index e8785a02..b65054ee 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ End" policy used by mods such as Base Metals. This is not the unrelated mod that adds mobs and dimensions under the same name. -This branch builds target-qualified version `4.0.8.113021`: the OreSpawn 4.0.8 +This branch builds target-qualified version `4.0.9.113021`: the OreSpawn 4.0.9 feature set for Minecraft 1.13.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 453268a0..a05802d7 100644 --- a/build.gradle +++ b/build.gradle @@ -727,7 +727,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.8.113021' + if (project.mod_version != '4.0.9.113021' || project.minecraft_version != '1.13.2' || project.forge_version != '25.0.223' || project.mapping_channel != 'snapshot' @@ -740,9 +740,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.8.113021.jar', - 'OreSpawn-4.0.8.113021-sources.jar', - 'OreSpawn-4.0.8.113021-javadoc.jar' + 'OreSpawn-4.0.9.113021.jar', + 'OreSpawn-4.0.9.113021-sources.jar', + 'OreSpawn-4.0.9.113021-javadoc.jar' ] if (base.archivesName.get() != 'OreSpawn' || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -759,7 +759,7 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.0.8.113021')) { + if (!file(path).getText('UTF-8').contains('4.0.9.113021')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/BIOMES.md b/docs/BIOMES.md index 51388daa..3a922201 100644 --- a/docs/BIOMES.md +++ b/docs/BIOMES.md @@ -133,6 +133,13 @@ lets OreSpawn replace the actual exposed ground while preserving later trees, plants, authored structures, and block entities. In ceiling dimensions, `ceiling_block` applies to the roof underside and does not replace the roof top. +Provider-declared `terrain_dimensions.host_blocks` are resolved by one terrain +scan at the start of `LOCAL_MODIFICATIONS`, immediately before provider +surfaces. Matching natural blocks already present in base terrain are eligible +for geology; matching blocks authored by later structure or vegetation stages +are not. Air, fluids, bedrock, and block-entity states remain protected even if +a provider mistakenly lists their block IDs as terrain hosts. + Surface correction is generation-only. Installing or updating OreSpawn does not rewrite already generated chunks; travel into new terrain to see a changed provider surface definition. diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index f76d3d21..619fee31 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -51,7 +51,7 @@ Examples: | Minecraft | Loader | Target | Example full OreSpawn version | | --- | --- | ---: | --- | -| 1.13.2 | Forge | `113021` | `4.0.8.113021` | +| 1.13.2 | Forge | `113021` | `4.0.9.113021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -143,7 +143,8 @@ unaffected branches remained on their target-qualified 4.0.6 versions. If a different branch later receives a shared fix, it uses the next unused Bug number, such as Forge 1.13.2's `4.0.8.113021`, even though the 4.0.7 repair -was not applicable to it. +was not applicable to it. Forge 1.13.2 then advanced to `4.0.9.113021` for a +target-qualified terrain-host ordering repair. A branch may therefore legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 09f8c850..9407d73b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20190213.203750 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.8.113021 +mod_version=4.0.9.113021 mod_group=zone.moddev.mc mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java index 8e73c743..b88e478a 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -111,6 +111,15 @@ public final class SurfaceProbeTestMod { private static final ResourceLocation SPRING_ROCK = new ResourceLocation(MODID + ":rock/spring_host"); private static final ResourceLocation DYNAMIC_FLUID = new ResourceLocation(MODID + ":fluid/dynamic_water"); private static final BlockPos SPRING_POS = new BlockPos(1128, 32, 1128); + private static final Block[] NATURAL_SOURCES = { + Blocks.DIRT, Blocks.GRASS_BLOCK, Blocks.COARSE_DIRT, Blocks.PODZOL, + Blocks.GRAVEL, Blocks.SAND, Blocks.RED_SAND, Blocks.CLAY, + Blocks.TERRACOTTA, Blocks.WHITE_TERRACOTTA, + Blocks.ORANGE_TERRACOTTA, Blocks.RED_TERRACOTTA + }; + private static final Block[] INVALID_TERRAIN_HOSTS = { + Blocks.AIR, Blocks.WATER, Blocks.BEDROCK, Blocks.CHEST + }; private static final ResourceLocation[] BUILT_IN_GEOMES = { new ResourceLocation("orespawn:stable_craton"), new ResourceLocation("orespawn:mountain_belt"), new ResourceLocation("orespawn:volcanic_arc"), new ResourceLocation("orespawn:sedimentary_basin"), @@ -131,9 +140,11 @@ public final class SurfaceProbeTestMod { private static final int FLUID_PROBE_MAX_CHUNK_X = 62; private static final int EXPECTED_COLUMNS = 9 * 16 * 16; private static final int EXPECTED_FILLER = EXPECTED_COLUMNS * 3; + private static final int EXPECTED_NATURAL_SOURCES = 9 * NATURAL_SOURCES.length; private static final String PHASE_PROPERTY = "surfaceprobe.integrationPhase"; private static final String MARKER_NAME = "surfaceprobe-integration.properties"; private static final String CHEST_ITEM_NAME = "surfaceprobe sentinel"; + private static final String RAW_CHEST_ITEM_NAME = "surfaceprobe raw block entity sentinel"; public SurfaceProbeTestMod() { FMLJavaModLoadingContext context = FMLJavaModLoadingContext.get(); @@ -269,6 +280,8 @@ private void enableGeologyProbe(FMLServerAboutToStartEvent event) { end.add("biome_namespaces", namespaces); JsonArray hosts = new JsonArray(); hosts.add(blockId(Blocks.END_STONE).toString()); + for (Block source : NATURAL_SOURCES) hosts.add(blockId(source).toString()); + for (Block source : INVALID_TERRAIN_HOSTS) hosts.add(blockId(source).toString()); end.add("host_blocks", hosts); end.add("host_tags", new JsonArray()); terrain.add(OPEN_ID.toString(), end); @@ -440,6 +453,13 @@ private static AuditResult auditDimension(WorldServer level, boolean roofed) { int biomeB = 0; int edgeChanges = 0; int sentinels = 0; + long rawNaturalSources = 0L; + long structureNaturalSources = 0L; + long vegetationNaturalSources = 0L; + long cavePockets = 0L; + long underwaterPockets = 0L; + long rawBedrock = 0L; + long rawBlockEntities = 0L; BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) { @@ -508,22 +528,92 @@ private static AuditResult auditDimension(WorldServer level, boolean roofed) { } } sentinels += auditSentinels(level, chunk, pos, chunkMinX, chunkMinZ); + if (!roofed) { + NaturalSourceAudit natural = auditNaturalSources(level, chunk, pos, chunkMinX, chunkMinZ); + rawNaturalSources += natural.rawConverted; + structureNaturalSources += natural.structurePreserved; + vegetationNaturalSources += natural.vegetationPreserved; + cavePockets += natural.cavePreserved; + underwaterPockets += natural.underwaterPreserved; + rawBedrock += natural.bedrockPreserved; + rawBlockEntities += natural.blockEntityPreserved; + } } } if (top != EXPECTED_COLUMNS - 9 || underwater != 9 || filler != EXPECTED_FILLER || biomeA == 0 || biomeB == 0 || edgeChanges == 0 || sentinels != 9 * 4 || geology != (roofed ? 0 : EXPECTED_FILLER) - || (roofed && (ceiling != EXPECTED_COLUMNS || roofTop != EXPECTED_COLUMNS))) { + || (roofed && (ceiling != EXPECTED_COLUMNS || roofTop != EXPECTED_COLUMNS)) + || (!roofed && (rawNaturalSources != EXPECTED_NATURAL_SOURCES + || structureNaturalSources != EXPECTED_NATURAL_SOURCES + || vegetationNaturalSources != EXPECTED_NATURAL_SOURCES + || cavePockets != EXPECTED_NATURAL_SOURCES / 2 + || underwaterPockets != EXPECTED_NATURAL_SOURCES / 2 + || rawBedrock != 9 || rawBlockEntities != 9))) { throw new IllegalStateException("Incomplete surface audit for " + DimensionType.func_212678_a(level.dimension.getType()) + ": top=" + top + ", underwater=" + underwater + ", filler=" + filler + ", biomeA=" + biomeA + ", biomeB=" + biomeB + ", edges=" + edgeChanges + ", sentinels=" + sentinels + ", geology=" + geology - + ", ceiling=" + ceiling + ", roofTop=" + roofTop); + + ", ceiling=" + ceiling + ", roofTop=" + roofTop + + ", rawNatural=" + rawNaturalSources + + ", structureNatural=" + structureNaturalSources + + ", vegetationNatural=" + vegetationNaturalSources + + ", cavePockets=" + cavePockets + + ", underwaterPockets=" + underwaterPockets + + ", rawBedrock=" + rawBedrock + + ", rawBlockEntities=" + rawBlockEntities); } long aquiferFluid = roofed ? 0L : auditDynamicFluid(level); return new AuditResult(top, underwater, filler, geology, ceiling, roofTop, - biomeA, biomeB, edgeChanges, sentinels, aquiferFluid); + biomeA, biomeB, edgeChanges, sentinels, aquiferFluid, + rawNaturalSources, structureNaturalSources, vegetationNaturalSources, + cavePockets, underwaterPockets, rawBedrock, rawBlockEntities); + } + + private static NaturalSourceAudit auditNaturalSources(WorldServer level, IChunk chunk, + BlockPos.MutableBlockPos pos, int minX, int minZ) { + long rawConverted = 0L; + long structurePreserved = 0L; + long vegetationPreserved = 0L; + long cavePreserved = 0L; + long underwaterPreserved = 0L; + long bedrockPreserved = 0L; + long blockEntityPreserved = 0L; + for (int index = 0; index < NATURAL_SOURCES.length; index++) { + int x = naturalX(minX, index); + int z = naturalZ(minZ, index); + int groundY = findMarkedGround(chunk, pos, x, z, 0, 256); + if (chunk.getBlockState(pos.setPos(x, groundY - 12, z)).getBlock() == Blocks.DIORITE) { + rawConverted++; + } + Block pocket = chunk.getBlockState(pos.setPos(x, groundY - 11, z)).getBlock(); + if (index < NATURAL_SOURCES.length / 2) { + if (pocket == Blocks.AIR) cavePreserved++; + } else if (pocket == Blocks.WATER) { + underwaterPreserved++; + } + if (chunk.getBlockState(pos.setPos(x, groundY - 16, z)).getBlock() + == NATURAL_SOURCES[index]) structurePreserved++; + if (chunk.getBlockState(pos.setPos(x, groundY - 20, z)).getBlock() + == NATURAL_SOURCES[index]) vegetationPreserved++; + } + int bedrockGroundY = findMarkedGround(chunk, pos, minX + 11, minZ + 12, 0, 256); + if (chunk.getBlockState(pos.setPos(minX + 11, bedrockGroundY - 24, minZ + 12)).getBlock() + == Blocks.BEDROCK) bedrockPreserved++; + int chestGroundY = findMarkedGround(chunk, pos, minX + 12, minZ + 12, 0, 256); + pos.setPos(minX + 12, chestGroundY - 24, minZ + 12); + if (chunk.getBlockState(pos).getBlock() == Blocks.CHEST + && level.getTileEntity(pos) instanceof TileEntityChest) { + TileEntityChest chest = (TileEntityChest) level.getTileEntity(pos); + if (chest.getStackInSlot(0).getItem() == Items.EMERALD + && RAW_CHEST_ITEM_NAME.equals(chest.getStackInSlot(0).getDisplayName().getString())) { + blockEntityPreserved++; + } + } + return new NaturalSourceAudit(rawConverted, structurePreserved, + vegetationPreserved, cavePreserved, underwaterPreserved, + bedrockPreserved, blockEntityPreserved); } private static long auditDynamicFluid(WorldServer level) { @@ -667,6 +757,13 @@ private static Properties properties(long seed, Map results values.setProperty(prefix + "edge_changes", Integer.toString(result.edgeChanges())); values.setProperty(prefix + "sentinels", Integer.toString(result.sentinels())); values.setProperty(prefix + "aquifer_fluid", Long.toString(result.aquiferFluid())); + values.setProperty(prefix + "raw_natural_sources", Long.toString(result.rawNaturalSources())); + values.setProperty(prefix + "structure_natural_sources", Long.toString(result.structureNaturalSources())); + values.setProperty(prefix + "vegetation_natural_sources", Long.toString(result.vegetationNaturalSources())); + values.setProperty(prefix + "cave_pockets", Long.toString(result.cavePockets())); + values.setProperty(prefix + "underwater_pockets", Long.toString(result.underwaterPockets())); + values.setProperty(prefix + "raw_bedrock", Long.toString(result.rawBedrock())); + values.setProperty(prefix + "raw_block_entities", Long.toString(result.rawBlockEntities())); } return values; } @@ -766,9 +863,37 @@ private static boolean prepareTerrain(IWorld world, IChunk chunk) { // Decoration writes do not update Forge 25's frozen *_WG maps. Rebuild // the controlled fixture map so the production pass sees the exposed Y. SurfaceProbeSpringBridge.rebuildWorldSurfaceHeight(chunk); + if (!roofed) placeRawNaturalSources(world, chunk, pos, minX, minZ); return true; } + private static void placeRawNaturalSources(IWorld world, IChunk chunk, + BlockPos.MutableBlockPos pos, int minX, int minZ) { + for (int index = 0; index < NATURAL_SOURCES.length; index++) { + int x = naturalX(minX, index); + int z = naturalZ(minZ, index); + int groundY = findMarkedGround(chunk, pos, x, z, 0, 256); + chunk.setBlockState(pos.setPos(x, groundY - 12, z), + NATURAL_SOURCES[index].getDefaultState(), false); + chunk.setBlockState(pos.setPos(x, groundY - 11, z), + (index < NATURAL_SOURCES.length / 2 ? Blocks.AIR : Blocks.WATER) + .getDefaultState(), false); + } + int bedrockGroundY = findMarkedGround(chunk, pos, minX + 11, minZ + 12, 0, 256); + chunk.setBlockState(pos.setPos(minX + 11, bedrockGroundY - 24, minZ + 12), + Blocks.BEDROCK.getDefaultState(), false); + int chestGroundY = findMarkedGround(chunk, pos, minX + 12, minZ + 12, 0, 256); + world.setBlockState(pos.setPos(minX + 12, chestGroundY - 24, minZ + 12), + Blocks.CHEST.getDefaultState(), 2); + if (world.getTileEntity(pos) instanceof TileEntityChest) { + TileEntityChest chest = (TileEntityChest) world.getTileEntity(pos); + ItemStack sentinel = new ItemStack(Items.EMERALD); + sentinel.setDisplayName(new TextComponentString(RAW_CHEST_ITEM_NAME)); + chest.setInventorySlotContents(0, sentinel); + chest.markDirty(); + } + } + private static boolean solid(IBlockState state) { return !state.isAir() && state.getFluidState().isEmpty(); } @@ -790,6 +915,7 @@ private static boolean placeStructureSentinels(IWorld world, IChunk chunk) { chest.setInventorySlotContents(0, sentinel); chest.markDirty(); } + placeAuthoredNaturalSources(world, chunk, pos, minX, minZ, 16); return true; } @@ -806,9 +932,30 @@ private static boolean placeVegetationSentinels(IWorld world, IChunk chunk) { int vegetationY = markedGround(chunk, pos, minX + 6, minZ + 6, world); world.setBlockState(pos.setPos(minX + 6, vegetationY + 1, minZ + 6), Blocks.DIRT.getDefaultState(), 2); world.setBlockState(pos.setPos(minX + 6, vegetationY + 2, minZ + 6), Blocks.OAK_SAPLING.getDefaultState(), 2); + placeAuthoredNaturalSources(world, chunk, pos, minX, minZ, 20); return true; } + private static void placeAuthoredNaturalSources(IWorld world, IChunk chunk, + BlockPos.MutableBlockPos pos, int minX, int minZ, int depth) { + if (!OPEN_ID.equals(DimensionType.func_212678_a(world.getWorld().dimension.getType()))) return; + for (int index = 0; index < NATURAL_SOURCES.length; index++) { + int x = naturalX(minX, index); + int z = naturalZ(minZ, index); + int groundY = findMarkedGround(chunk, pos, x, z, 0, 256); + world.setBlockState(pos.setPos(x, groundY - depth, z), + NATURAL_SOURCES[index].getDefaultState(), 2); + } + } + + private static int naturalX(int minX, int index) { + return minX + 12 + index % 4; + } + + private static int naturalZ(int minZ, int index) { + return minZ + 1 + index / 4; + } + private static int markedGround(IChunk chunk, BlockPos.MutableBlockPos pos, int x, int z, IWorld world) { return findMarkedGround(chunk, pos, x, z, 0, 256); @@ -851,6 +998,28 @@ private static final class SpringResult { ResourceLocation block() { return block; } } + private static final class NaturalSourceAudit { + final long rawConverted; + final long structurePreserved; + final long vegetationPreserved; + final long cavePreserved; + final long underwaterPreserved; + final long bedrockPreserved; + final long blockEntityPreserved; + + NaturalSourceAudit(long rawConverted, long structurePreserved, + long vegetationPreserved, long cavePreserved, long underwaterPreserved, + long bedrockPreserved, long blockEntityPreserved) { + this.rawConverted = rawConverted; + this.structurePreserved = structurePreserved; + this.vegetationPreserved = vegetationPreserved; + this.cavePreserved = cavePreserved; + this.underwaterPreserved = underwaterPreserved; + this.bedrockPreserved = bedrockPreserved; + this.blockEntityPreserved = blockEntityPreserved; + } + } + private static final class AuditResult { private final long top; private final long underwater; @@ -863,10 +1032,19 @@ private static final class AuditResult { private final int edgeChanges; private final int sentinels; private final long aquiferFluid; + private final long rawNaturalSources; + private final long structureNaturalSources; + private final long vegetationNaturalSources; + private final long cavePockets; + private final long underwaterPockets; + private final long rawBedrock; + private final long rawBlockEntities; AuditResult(long top, long underwater, long filler, long geology, long ceiling, long roofTop, int biomeA, int biomeB, int edgeChanges, int sentinels, - long aquiferFluid) { + long aquiferFluid, long rawNaturalSources, long structureNaturalSources, + long vegetationNaturalSources, long cavePockets, long underwaterPockets, + long rawBedrock, long rawBlockEntities) { this.top = top; this.underwater = underwater; this.filler = filler; @@ -878,6 +1056,13 @@ private static final class AuditResult { this.edgeChanges = edgeChanges; this.sentinels = sentinels; this.aquiferFluid = aquiferFluid; + this.rawNaturalSources = rawNaturalSources; + this.structureNaturalSources = structureNaturalSources; + this.vegetationNaturalSources = vegetationNaturalSources; + this.cavePockets = cavePockets; + this.underwaterPockets = underwaterPockets; + this.rawBedrock = rawBedrock; + this.rawBlockEntities = rawBlockEntities; } long top() { return top; } @@ -891,5 +1076,12 @@ private static final class AuditResult { int edgeChanges() { return edgeChanges; } int sentinels() { return sentinels; } long aquiferFluid() { return aquiferFluid; } + long rawNaturalSources() { return rawNaturalSources; } + long structureNaturalSources() { return structureNaturalSources; } + long vegetationNaturalSources() { return vegetationNaturalSources; } + long cavePockets() { return cavePockets; } + long underwaterPockets() { return underwaterPockets; } + long rawBedrock() { return rawBedrock; } + long rawBlockEntities() { return rawBlockEntities; } } } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedTerrainDimension.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedTerrainDimension.java index c23f3f0e..908f9d4b 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedTerrainDimension.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedTerrainDimension.java @@ -7,6 +7,7 @@ import net.minecraft.util.ResourceLocation; import net.minecraft.block.Block; import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; /** Immutable setup-time resolution of one terrain replacement dimension. */ final class BakedTerrainDimension { @@ -36,6 +37,10 @@ boolean hasBiomeFilter() { } boolean isReplaceable(IBlockState state) { + if (state.isAir() || !state.getFluidState().isEmpty() || state.hasTileEntity() + || state.getBlock() == Blocks.BEDROCK) { + return false; + } if (smallHostSet != null) { Block block = state.getBlock(); for (Block host : smallHostSet) { diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeFeatureInstaller.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeFeatureInstaller.java index 75522973..c3e153d2 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeFeatureInstaller.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeFeatureInstaller.java @@ -67,7 +67,6 @@ static void installFeatures(Biome biome, boolean terrain, if (vanillaOreGate) VanillaOreFeatureGate.wrapFeatureList(underground); if (terrain) { StoneReplacer.removeVanillaMatchingStoneFeatures(underground); - addUnique(underground, StoneReplacer.configuredFeature()); } if (managedOres) addUnique(underground, OreSpawnOreGeneration.configuredFeature()); if (fluidDeposits) addUnique(underground, FluidDepositFeature.configuredFeature()); @@ -76,13 +75,22 @@ static void installFeatures(Biome biome, boolean terrain, VanillaOreFeatureGate.wrapFeatureList( biome.getFeatures(GenerationStage.Decoration.UNDERGROUND_DECORATION)); } - installSurfaceStages(biome, surfaces, flatBedrock); + installSurfaceStages(biome, terrain, surfaces, flatBedrock); } - static boolean installSurfaceStages(Biome biome, boolean surfaces, boolean flatBedrock) { - boolean changed = surfaces && addUnique(biome.getFeatures( - GenerationStage.Decoration.LOCAL_MODIFICATIONS), - BiomeSurfaceFeature.configuredFeature()); + static boolean installSurfaceStages(Biome biome, boolean terrain, + boolean surfaces, boolean flatBedrock) { + List> local = + biome.getFeatures(GenerationStage.Decoration.LOCAL_MODIFICATIONS); + boolean changed = false; + if (terrain) { + changed |= placeUniqueAt(local, StoneReplacer.configuredFeature(), 0); + if (surfaces) { + changed |= placeUniqueAt(local, BiomeSurfaceFeature.configuredFeature(), 1); + } + } else if (surfaces) { + changed |= addUnique(local, BiomeSurfaceFeature.configuredFeature()); + } changed |= flatBedrock && addUnique(biome.getFeatures( GenerationStage.Decoration.TOP_LAYER_MODIFICATION), FlatBedrockFeature.configuredFeature()); @@ -103,4 +111,15 @@ private static boolean addUnique(List> features, features.add(feature); return true; } + + private static boolean placeUniqueAt(List> features, + CompositeFeature feature, int index) { + if (feature == null) return false; + int current = features.indexOf(feature); + int target = Math.min(index, features.size() - (current >= 0 ? 1 : 0)); + if (current == target) return false; + if (current >= 0) features.remove(current); + features.add(target, feature); + return true; + } } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/Geology.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/Geology.java index 50a429ee..55e863b7 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/Geology.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/Geology.java @@ -111,8 +111,10 @@ public void replaceStoneInChunk(IWorld world, IChunk chunk, BakedTerrainDimensio for (; y >= 0; y--) { cursor.setPos(x, y, z); IBlockState current = chunk.getBlockState(cursor); - if (terrain.isReplaceable(current) - || (realisticCoalLayers && current.getBlock() == Blocks.COAL_ORE)) { + if (!current.hasTileEntity() + && (terrain.isReplaceable(current) + || (realisticCoalLayers && current.getBlock() == Blocks.COAL_ORE)) + && chunk.getTileEntity(cursor) == null) { IBlockState replacement = pickReplacement(baseRockVal, geomeBase, y); if (!GeomeGeology.changes(current, replacement)) continue; chunk.setBlockState(cursor, replacement, false); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java index 68edcb8a..99dfde9c 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java @@ -115,7 +115,8 @@ public void replaceStoneInChunk(IWorld world, IChunk chunk, BakedTerrainDimensio for (int y = surfaceY; y >= 0; y--) { cursor.setPos(x, y, z); IBlockState current = chunk.getBlockState(cursor); - if (terrain.isReplaceable(current)) { + if (terrain.isReplaceable(current) && !current.hasTileEntity() + && chunk.getTileEntity(cursor) == null) { IBlockState replacement = pickReplacement( geomeIndex, baseRockValue, formationRegion, x, y, z); if (!changes(current, replacement)) continue; @@ -157,7 +158,9 @@ private boolean replaceStableColumn(IChunk chunk, BlockPos.MutableBlockPos curso } cursor.setY(y); IBlockState current = chunk.getBlockState(cursor); - if (terrain.isReplaceable(current) && changes(current, replacement)) { + if (terrain.isReplaceable(current) && !current.hasTileEntity() + && chunk.getTileEntity(cursor) == null + && changes(current, replacement)) { chunk.setBlockState(cursor, replacement, false); changed = true; } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index e2cb66e2..9eb9f3f0 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -359,7 +359,7 @@ private static void writeReport(Path config, List lines) { private static void writeUpgradeReport(Path config, int imported, List detail) { List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.8.113021 Upgrade Report"); + lines.add("OreSpawn 4.0.9.113021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn settings were imported into the OS4 profile."); 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 9199114a..9e211a5b 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -199,7 +199,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.8.113021 Upgrade Report"); + lines.add("OreSpawn 4.0.9.113021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); diff --git a/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java b/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java index 41fb3180..d32b0ca3 100644 --- a/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java @@ -13,6 +13,32 @@ import net.minecraft.util.ResourceLocation; class WorldgenProviderTest { + @Test + void terrainHostContractRetainsNaturalSourceOrder() { + ResourceLocation dimension = id("surfaceprobe:the_end"); + WorldgenProvider provider = WorldgenProvider.builder("surfaceprobe", 1) + .terrainDimension(dimension, terrain -> terrain + .hostBlock(id("minecraft:dirt")) + .hostBlock(id("minecraft:grass_block")) + .hostBlock(id("minecraft:coarse_dirt")) + .hostBlock(id("minecraft:podzol")) + .hostBlock(id("minecraft:gravel")) + .hostBlock(id("minecraft:sand")) + .hostBlock(id("minecraft:red_sand")) + .hostBlock(id("minecraft:clay")) + .hostBlock(id("minecraft:terracotta"))) + .build(); + + assertEquals("[\"minecraft:dirt\",\"minecraft:grass_block\"," + + "\"minecraft:coarse_dirt\",\"minecraft:podzol\"," + + "\"minecraft:gravel\",\"minecraft:sand\"," + + "\"minecraft:red_sand\",\"minecraft:clay\"," + + "\"minecraft:terracotta\"]", + provider.toJson().getAsJsonObject("terrain_dimensions") + .getAsJsonObject(dimension.toString()) + .getAsJsonArray("host_blocks").toString()); + } + @Test void serializesTypedSchemaFourProvider() { ResourceLocation overworld = id("minecraft:overworld"); 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 d326f9bf..b869f7eb 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeatureOrderTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/BiomeSurfaceFeatureOrderTest.java @@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.List; import zone.moddev.mc.orespawn.test.Forge25TestBootstrap; import net.minecraft.world.biome.Biome; @@ -39,7 +40,7 @@ void staticInstallerKeepsSurfacesEarlyAndBedrockLast() throws Exception { Biome.GRASS_DIRT_GRAVEL_SURFACE))); BiomeFeatureInstaller.installFeatures(biome, true, true, true, true, true, true); - assertTrue(biome.getFeatures(GenerationStage.Decoration.UNDERGROUND_ORES) + assertFalse(biome.getFeatures(GenerationStage.Decoration.UNDERGROUND_ORES) .contains(StoneReplacer.configuredFeature())); assertTrue(biome.getFeatures(GenerationStage.Decoration.UNDERGROUND_ORES) .contains(OreSpawnOreGeneration.configuredFeature())); @@ -47,7 +48,11 @@ void staticInstallerKeepsSurfacesEarlyAndBedrockLast() throws Exception { .contains(FluidDepositFeature.configuredFeature())); CompositeFeature surfaces = BiomeSurfaceFeature.configuredFeature(); CompositeFeature bedrock = FlatBedrockFeature.configuredFeature(); - assertTrue(biome.getFeatures(GenerationStage.Decoration.LOCAL_MODIFICATIONS).contains(surfaces)); + List> local = + biome.getFeatures(GenerationStage.Decoration.LOCAL_MODIFICATIONS); + assertTrue(local.size() >= 2); + assertTrue(local.get(0) == StoneReplacer.configuredFeature()); + assertTrue(local.get(1) == surfaces); assertFalse(biome.getFeatures(GenerationStage.Decoration.TOP_LAYER_MODIFICATION).contains(surfaces)); assertTrue(biome.getFeatures(GenerationStage.Decoration.TOP_LAYER_MODIFICATION).contains(bedrock)); assertFalse(biome.getFeatures(GenerationStage.Decoration.LOCAL_MODIFICATIONS).contains(bedrock)); diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/StoneReplacerTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/StoneReplacerTest.java index b3dc8403..b2927814 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/StoneReplacerTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/StoneReplacerTest.java @@ -3,6 +3,9 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Collections; +import java.util.LinkedHashSet; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -39,4 +42,23 @@ void nonOverworldBiomesAreNeverChanged() { assertFalse(TerrainFeaturePolicy.shouldRemoveVanillaMatchingStoneFeatures( Category.THEEND, true, true)); } + + @Test + void invalidTerrainHostsRemainUnsafeEvenWhenDeclared() { + LinkedHashSet hosts = new LinkedHashSet<>(); + hosts.add(Blocks.AIR); + hosts.add(Blocks.WATER); + hosts.add(Blocks.BEDROCK); + hosts.add(Blocks.CHEST); + hosts.add(Blocks.DIRT); + BakedTerrainDimension terrain = new BakedTerrainDimension( + new net.minecraft.util.ResourceLocation("surfaceprobe:the_end"), + Collections.emptySet(), Collections.emptySet(), hosts); + + assertFalse(terrain.isReplaceable(Blocks.AIR.getDefaultState())); + assertFalse(terrain.isReplaceable(Blocks.WATER.getDefaultState())); + assertFalse(terrain.isReplaceable(Blocks.BEDROCK.getDefaultState())); + assertFalse(terrain.isReplaceable(Blocks.CHEST.getDefaultState())); + assertTrue(terrain.isReplaceable(Blocks.DIRT.getDefaultState())); + } } From e0d5ac51cd730822f849f4a512da83ad9e1f5010 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 22:46:22 +0100 Subject: [PATCH 2/6] Fix Stable Layers rock height eligibility --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 9 ++ README.md | 2 +- build.gradle | 10 +- docs/VERSIONS.md | 7 +- gradle.properties | 2 +- .../orespawn/worldgen/BakedGeomeConfig.java | 101 +++++++++++++++--- .../mc/orespawn/worldgen/GeomeGeology.java | 12 +-- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../StableLayerHeightEligibilityTest.java | 55 ++++++++++ 11 files changed, 173 insertions(+), 35 deletions(-) create mode 100644 src/test/java/zone/moddev/mc/orespawn/worldgen/StableLayerHeightEligibilityTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e92b0854..0dd24ed8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,9 +56,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.9.113021.jar - build/libs/OreSpawn-4.0.9.113021-sources.jar - build/libs/OreSpawn-4.0.9.113021-javadoc.jar + build/libs/OreSpawn-4.0.10.113021.jar + build/libs/OreSpawn-4.0.10.113021-sources.jar + build/libs/OreSpawn-4.0.10.113021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 54596bf8..d1f71004 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Version 4.0.10.113021 + +* Evaluate Stable Layers rock min_y and max_y bounds against actual world Y + instead of the vertically shifted formation coordinate. +* Preserve the shifted formation identity for layer, family, and rock choice + while preventing vanilla Stone fallback near dimension floors and ceilings. +* Apply the correction only while generating new chunks; existing chunks and + saved profiles remain unchanged. + Version 4.0.9.113021 * Replace provider-declared natural terrain hosts during the existing geology diff --git a/README.md b/README.md index b65054ee..17bd2128 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ End" policy used by mods such as Base Metals. This is not the unrelated mod that adds mobs and dimensions under the same name. -This branch builds target-qualified version `4.0.9.113021`: the OreSpawn 4.0.9 +This branch builds target-qualified version `4.0.10.113021`: the OreSpawn 4.0.10 feature set for Minecraft 1.13.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index a05802d7..3837661c 100644 --- a/build.gradle +++ b/build.gradle @@ -727,7 +727,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.9.113021' + if (project.mod_version != '4.0.10.113021' || project.minecraft_version != '1.13.2' || project.forge_version != '25.0.223' || project.mapping_channel != 'snapshot' @@ -740,9 +740,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.9.113021.jar', - 'OreSpawn-4.0.9.113021-sources.jar', - 'OreSpawn-4.0.9.113021-javadoc.jar' + 'OreSpawn-4.0.10.113021.jar', + 'OreSpawn-4.0.10.113021-sources.jar', + 'OreSpawn-4.0.10.113021-javadoc.jar' ] if (base.archivesName.get() != 'OreSpawn' || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -759,7 +759,7 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.0.9.113021')) { + if (!file(path).getText('UTF-8').contains('4.0.10.113021')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 619fee31..bf241b1b 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -51,7 +51,7 @@ Examples: | Minecraft | Loader | Target | Example full OreSpawn version | | --- | --- | ---: | --- | -| 1.13.2 | Forge | `113021` | `4.0.9.113021` | +| 1.13.2 | Forge | `113021` | `4.0.10.113021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -143,8 +143,9 @@ unaffected branches remained on their target-qualified 4.0.6 versions. If a different branch later receives a shared fix, it uses the next unused Bug number, such as Forge 1.13.2's `4.0.8.113021`, even though the 4.0.7 repair -was not applicable to it. Forge 1.13.2 then advanced to `4.0.9.113021` for a -target-qualified terrain-host ordering repair. +was not applicable there. Forge 1.13.2 then advanced to its target-qualified +4.0.9 release for the provider terrain-host ordering repair and to 4.0.10 for +the distinct Stable Layers actual-height eligibility repair. A branch may therefore legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 9407d73b..bc429856 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20190213.203750 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.9.113021 +mod_version=4.0.10.113021 mod_group=zone.moddev.mc mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedGeomeConfig.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedGeomeConfig.java index 87e4212a..5e03dc9d 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedGeomeConfig.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/BakedGeomeConfig.java @@ -35,6 +35,7 @@ public final class BakedGeomeConfig { private final Map biomeWeights; private final Map biomeWeightsById; private final double[] fallbackWeights; + private final RockEntry[] rocks; private final IBlockState[] rockStates; private final Set sedimentaryBlocks; private final Set oreReplaceableBlocks; @@ -45,6 +46,9 @@ public final class BakedGeomeConfig { private WeightedBlockPicker[][][] legacyRockPickers; private byte[] stableFamilyChoices; private int[] stableRockChoices; + private int[][] familyRockIndexes; + private double[][][] stableRockLogWeights; + private double[][][] stableRockPriorities; BakedGeomeConfig(GeomeDefinition[] geomes, double geomeScale, double biomeInfluence, double regionalNoiseInfluence, double boundaryNoiseInfluence, Map biomeWeights, @@ -72,6 +76,7 @@ public final class BakedGeomeConfig { noiseOffsetZ[i] = -((i + 1) * 6151); } + this.rocks = rocks.clone(); rockStates = new IBlockState[rocks.length]; for (int i = 0; i < rocks.length; i++) { rockStates[i] = rocks[i].state; @@ -154,13 +159,59 @@ RockFamily pickFamily(int geomeIndex, int y, int formationValue, int diversitySl public IBlockState pickRock(int geomeIndex, RockFamily family, int y, int formationValue) { if (formations.usesStableLayers()) { - int index = stableRockIndex(geomeIndex, family.ordinal(), clampStableY(y), formationValue & 0xFF); - int rockIndex = stableRockChoices[index]; - return rockIndex < 0 ? FALLBACK : rockStates[rockIndex]; + return pickStableRockAtWorldY(geomeIndex, family, y, y, formationValue); } return legacyRockPickers[geomeIndex][family.ordinal()][clampLegacyY(y)].pick(formationValue); } + RockFamily pickStableFamilyAtWorldY(int geomeIndex, int worldY, int formationY, + int formationValue, int diversitySlot) { + RockFamily preferred = pickFamily(geomeIndex, formationY, formationValue, diversitySlot); + if (hasEligibleStableRock(geomeIndex, preferred, worldY, formationY)) return preferred; + + int bucket = formationValue & 0xFF; + int boundedFormationY = clampStableValue(formationY); + double bestScore = Double.NEGATIVE_INFINITY; + RockFamily bestFamily = preferred; + for (RockFamily family : RockFamily.values()) { + if (!hasEligibleStableRock(geomeIndex, family, worldY, formationY)) continue; + double weight = Math.pow(geomes[geomeIndex].familyWeights[family.ordinal()], 2.5D) + * familyDepthWeight(family, boundedFormationY); + if (weight <= 0.0D) continue; + double score = Math.log(weight) + gumbelPriority(bucket, geomeIndex, family.ordinal(), + isStableBucket(bucket, -1), 0x6A09E667F3BCC909L); + if (score > bestScore) { + bestScore = score; + bestFamily = family; + } + } + return bestFamily; + } + + IBlockState pickStableRockAtWorldY(int geomeIndex, RockFamily family, int worldY, + int formationY, int formationValue) { + int yIndex = clampStableY(formationY); + int bucket = formationValue & 0xFF; + int choiceIndex = stableRockIndex(geomeIndex, family.ordinal(), yIndex, bucket); + int selectedRock = stableRockChoices[choiceIndex]; + if (isEligibleStableRock(geomeIndex, selectedRock, worldY, yIndex)) { + return rockStates[selectedRock]; + } + + double bestScore = Double.NEGATIVE_INFINITY; + int bestRock = -1; + for (int rockIndex : familyRockIndexes[family.ordinal()]) { + if (!isEligibleStableRock(geomeIndex, rockIndex, worldY, yIndex)) continue; + double score = stableRockLogWeights[geomeIndex][rockIndex][yIndex] + + stableRockPriorities[geomeIndex][rockIndex][bucket]; + if (score > bestScore) { + bestScore = score; + bestRock = rockIndex; + } + } + return bestRock < 0 ? FALLBACK : rockStates[bestRock]; + } + public String geomeName(int geomeIndex) { return geomes[geomeIndex].name; } @@ -283,9 +334,9 @@ private void buildStablePickers(RockEntry[] rocks) { stableRockChoices = new int[geomes.length * RockFamily.values().length * HEIGHT * FORMATION_BUCKETS]; Arrays.fill(stableRockChoices, -1); int familyCount = RockFamily.values().length; - int[][] familyRockIndexes = groupRockIndexes(rocks); - double[][][] rockLogWeights = new double[geomes.length][rocks.length][HEIGHT]; - double[][][] rockPriorities = new double[geomes.length][rocks.length][FORMATION_BUCKETS]; + familyRockIndexes = groupRockIndexes(rocks); + stableRockLogWeights = new double[geomes.length][rocks.length][HEIGHT]; + stableRockPriorities = new double[geomes.length][rocks.length][FORMATION_BUCKETS]; double[][][] familyLogWeights = new double[geomes.length][familyCount][HEIGHT]; double[][][] familyWeights = new double[geomes.length][familyCount][HEIGHT]; double[][][] familyPriorities = new double[geomes.length][familyCount][FORMATION_BUCKETS]; @@ -294,14 +345,13 @@ private void buildStablePickers(RockEntry[] rocks) { for (int rockIndex = 0; rockIndex < rocks.length; rockIndex++) { RockEntry rock = rocks[rockIndex]; for (int y = MIN_Y; y <= MAX_Y; y++) { - double rawWeight = y < rock.minY || y > rock.maxY ? 0.0D - : rock.weight * rock.geomeWeights[geome] - * depthWeight(y, rock.depthPeak, rock.depthSpread); - rockLogWeights[geome][rockIndex][y - MIN_Y] = rawWeight > 0.0D + double rawWeight = rock.weight * rock.geomeWeights[geome] + * depthWeight(y, rock.depthPeak, rock.depthSpread); + stableRockLogWeights[geome][rockIndex][y - MIN_Y] = rawWeight > 0.0D ? Math.log(rawWeight) : Double.NEGATIVE_INFINITY; } for (int bucket = 0; bucket < FORMATION_BUCKETS; bucket++) { - rockPriorities[geome][rockIndex][bucket] = gumbelPriority(bucket, geome, rockIndex, + stableRockPriorities[geome][rockIndex][bucket] = gumbelPriority(bucket, geome, rockIndex, isStableBucket(bucket, rock.family.ordinal()), 0xBB67AE8584CAA73BL ^ ((long) rock.family.ordinal() << 32)); } @@ -313,7 +363,9 @@ private void buildStablePickers(RockEntry[] rocks) { int yIndex = y - MIN_Y; boolean available = false; for (int rockIndex : familyRockIndexes[familyIndex]) { - if (rockLogWeights[geome][rockIndex][yIndex] != Double.NEGATIVE_INFINITY) { + RockEntry rock = rocks[rockIndex]; + if (y >= rock.minY && y <= rock.maxY + && stableRockLogWeights[geome][rockIndex][yIndex] != Double.NEGATIVE_INFINITY) { available = true; break; } @@ -363,8 +415,10 @@ private void buildStablePickers(RockEntry[] rocks) { double bestRockScore = Double.NEGATIVE_INFINITY; int bestRock = -1; for (int rockIndex : familyRockIndexes[familyIndex]) { - double rockScore = rockLogWeights[geome][rockIndex][yIndex] - + rockPriorities[geome][rockIndex][bucket]; + RockEntry rock = rocks[rockIndex]; + if (y < rock.minY || y > rock.maxY) continue; + double rockScore = stableRockLogWeights[geome][rockIndex][yIndex] + + stableRockPriorities[geome][rockIndex][bucket]; if (rockScore > bestRockScore) { bestRockScore = rockScore; bestRock = rockIndex; @@ -377,6 +431,21 @@ private void buildStablePickers(RockEntry[] rocks) { } } + private boolean hasEligibleStableRock(int geomeIndex, RockFamily family, int worldY, int formationY) { + int yIndex = clampStableY(formationY); + for (int rockIndex : familyRockIndexes[family.ordinal()]) { + if (isEligibleStableRock(geomeIndex, rockIndex, worldY, yIndex)) return true; + } + return false; + } + + private boolean isEligibleStableRock(int geomeIndex, int rockIndex, int worldY, int formationYIndex) { + if (rockIndex < 0) return false; + RockEntry rock = rocks[rockIndex]; + return worldY >= rock.minY && worldY <= rock.maxY + && stableRockLogWeights[geomeIndex][rockIndex][formationYIndex] != Double.NEGATIVE_INFINITY; + } + private void fillBalancedFamilyCycle(int geome, int yIndex, int bucket, double[][][] familyWeights, double[][][] familyPriorities, int[] quotas, int[] remaining, double[] remainders, boolean[] bonusAwarded) { @@ -631,6 +700,10 @@ private static int clampStableY(int y) { return Math.max(MIN_Y, Math.min(MAX_Y, y)) - MIN_Y; } + private static int clampStableValue(int y) { + return Math.max(MIN_Y, Math.min(MAX_Y, y)); + } + private static int clampLegacyY(int y) { return Math.max(0, Math.min(LEGACY_MAX_Y, y)); } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java index 99dfde9c..68a94fe6 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeGeology.java @@ -143,7 +143,6 @@ private boolean replaceStableColumn(IChunk chunk, BlockPos.MutableBlockPos curso int layerStart = layerIndex * layerThickness; int layerGeome = pickStableLayerGeome(geomeScores, geomeIndex, secondGeome, layerIndex, geomeTransitionPhase); - IBlockState replacement = pickStableReplacement(layerGeome, formationRegion, layerIndex); boolean changed = false; cursor.setPos(x, surfaceY, z); @@ -154,10 +153,10 @@ private boolean replaceStableColumn(IChunk chunk, BlockPos.MutableBlockPos curso layerStart -= layerThickness; layerGeome = pickStableLayerGeome(geomeScores, geomeIndex, secondGeome, layerIndex, geomeTransitionPhase); - replacement = pickStableReplacement(layerGeome, formationRegion, layerIndex); } cursor.setY(y); IBlockState current = chunk.getBlockState(cursor); + IBlockState replacement = pickStableReplacement(layerGeome, formationRegion, layerIndex, y); if (terrain.isReplaceable(current) && !current.hasTileEntity() && chunk.getTileEntity(cursor) == null && changes(current, replacement)) { @@ -258,7 +257,7 @@ private net.minecraft.block.state.IBlockState pickReplacement(int geomeIndex, in int stratum = baseRockValue + y; int layerIndex = Math.floorDiv(stratum, layerThickness); if (stableLayers) { - return pickStableReplacement(geomeIndex, formationRegion, layerIndex); + return pickStableReplacement(geomeIndex, formationRegion, layerIndex, y); } int layerY = y + (layerThickness / 2) - Math.floorMod(stratum, layerThickness); @@ -268,7 +267,7 @@ private net.minecraft.block.state.IBlockState pickReplacement(int geomeIndex, in return config.pickRock(geomeIndex, family, layerY, rockHash); } - private IBlockState pickStableReplacement(int geomeIndex, long formationRegion, int layerIndex) { + private IBlockState pickStableReplacement(int geomeIndex, long formationRegion, int layerIndex, int worldY) { // A dipping or uplifted layer keeps the depth identity it had in stratum space. int formationY = (layerIndex * layerThickness) + (layerThickness / 2); int layerBucket = layerIndex & 0xFF; @@ -294,8 +293,9 @@ private IBlockState pickStableReplacement(int geomeIndex, long formationRegion, // from collapsing onto one exact rock. rockBucket ^= LITHOLOGY_ROCK_SALTS[familySlot]; } - RockFamily family = config.pickFamily(geomeIndex, formationY, familyBucket, familySlot); - return config.pickRock(geomeIndex, family, formationY, rockBucket); + RockFamily family = config.pickStableFamilyAtWorldY(geomeIndex, worldY, formationY, + familyBucket, familySlot); + return config.pickStableRockAtWorldY(geomeIndex, family, worldY, formationY, rockBucket); } int stratumOffsetAt(int x, int z) { diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 9eb9f3f0..7a261306 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -359,7 +359,7 @@ private static void writeReport(Path config, List lines) { private static void writeUpgradeReport(Path config, int imported, List detail) { List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.9.113021 Upgrade Report"); + lines.add("OreSpawn 4.0.10.113021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn settings were imported into the OS4 profile."); 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 9e211a5b..8a4331c5 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -199,7 +199,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.9.113021 Upgrade Report"); + lines.add("OreSpawn 4.0.10.113021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/StableLayerHeightEligibilityTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/StableLayerHeightEligibilityTest.java new file mode 100644 index 00000000..5ae746ef --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/StableLayerHeightEligibilityTest.java @@ -0,0 +1,55 @@ +package zone.moddev.mc.orespawn.worldgen; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import net.minecraft.init.Blocks; + +import zone.moddev.mc.orespawn.test.Forge25TestBootstrap; +import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.GeomeDefinition; +import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.RockEntry; + +class StableLayerHeightEligibilityTest { + static { + Forge25TestBootstrap.registerVanilla(); + } + + @Test + void legalWorldYRemainsEligibleWhenFormationFallsBelowMinimum() { + BakedGeomeConfig config = boundedConfig(); + GeomeGeology geology = new GeomeGeology(0L, config); + double[] geomeScores = { 1.0D }; + + assertEquals(Blocks.DIORITE, + geology.getStoneAt(0, geomeScores, -64, 0L, 0, 20, 0), + "a legal Y must not fall back to Stone when waviness shifts its formation below min_y"); + } + + @Test + void illegalWorldYRemainsIneligibleWhenFormationFallsInsideRange() { + BakedGeomeConfig config = boundedConfig(); + GeomeGeology geology = new GeomeGeology(0L, config); + double[] geomeScores = { 1.0D }; + + assertEquals(Blocks.STONE, + geology.getStoneAt(0, geomeScores, -184, 0L, 0, 200, 0), + "a shifted formation inside the range must not make an illegal actual Y eligible"); + } + + private static BakedGeomeConfig boundedConfig() { + GeomeDefinition[] geomes = { + new GeomeDefinition("test:floor", 1.0D, new double[] { 0.0D, 0.0D, 0.0D, 1.0D }) + }; + RockEntry[] rocks = { + new RockEntry(Blocks.DIORITE.getDefaultState(), RockFamily.IGNEOUS_VOLCANIC, + 24, 68, 16, 127, 1.0D, true, new double[] { 1.0D }) + }; + FormationSettings formations = new FormationSettings(FormationSettings.Algorithm.STABLE_LAYERS, + 32.0D, 8192.0D, 8, 512.0D, 96.0D, 24.0D, 3, 0.85D); + return new BakedGeomeConfig(geomes, 384.0D, 1.15D, 0.9D, 0.45D, + Collections.emptyMap(), Collections.emptyMap(), rocks, formations); + } +} From a7ddc15c2697a9bf2b20c1a742510b8cb08f5213 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 22:49:50 +0100 Subject: [PATCH 3/6] Qualify cold Forge bootstrap --- .github/workflows/ci.yml | 78 +++++++++++++++- .github/workflows/codeql-analysis.yml | 23 ++++- README.md | 8 +- build.gradle | 37 +++++++- gradle.properties | 2 +- .../orespawn/ReleaseWorkflowContractTest.java | 91 +++++++++++++++++++ 6 files changed, 222 insertions(+), 17 deletions(-) create mode 100644 src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dd24ed8..bbb360a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,67 @@ concurrency: cancel-in-progress: true jobs: + cold-forge-bootstrap: + name: Cold Forge bootstrap + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + + - name: Install pinned Java 8 compilation toolchain + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '8.0.502+7' + + - name: Install pinned Java 17 Gradle runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17.0.1+12' + + - name: Bootstrap Forge from an empty cache + shell: bash + env: + GRADLE_USER_HOME: ${{ runner.temp }}/orespawn-cold-gradle + run: | + set -euo pipefail + test ! -e .gradle + test ! -e "$GRADLE_USER_HOME" + mkdir -p "$GRADLE_USER_HOME" + chmod +x ./gradlew + gradle_args=( + classes verifyLegacyFixtures + --no-daemon --no-build-cache --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + + - name: Verify same-cache bootstrap offline + shell: bash + env: + GRADLE_USER_HOME: ${{ runner.temp }}/orespawn-cold-gradle + run: | + set -euo pipefail + gradle_args=( + classes verifyLegacyFixtures + --rerun-tasks --offline --no-daemon --no-build-cache --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" + build: name: Build, test, and audit runs-on: ubuntu-latest @@ -26,17 +87,23 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Install Java 8 toolchain + - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + + - name: Install pinned Java 8 compilation toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '8.0.502+7' - - name: Install Java 17 for Gradle + - name: Install pinned Java 17 Gradle runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: - distribution: microsoft - java-version: '17' + distribution: temurin + java-version: '17.0.1+12' - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 @@ -48,6 +115,9 @@ jobs: run: >- ./gradlew clean check build javadoc verifyReleaseArtifacts writeReleaseChecksums verifyEclipseProductionClasspath --no-daemon --stacktrace + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false - name: Upload audited release candidate uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5312991f..e55aff14 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,17 +26,23 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - name: Install Java 8 toolchain + - name: Install pinned Java 25 ForgeGradle Mavenizer runtime + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '25.0.3+9.0.LTS' + + - name: Install pinned Java 8 compilation toolchain uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: distribution: temurin java-version: '8.0.502+7' - - name: Install Java 17 for Gradle + - name: Install pinned Java 17 Gradle runtime uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: - distribution: microsoft - java-version: '17' + distribution: temurin + java-version: '17.0.1+12' - name: Set up Gradle uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6 @@ -49,7 +55,14 @@ jobs: - name: Compile production code run: | chmod +x ./gradlew - ./gradlew clean classes --no-daemon --stacktrace + gradle_args=( + clean classes + --no-daemon --stacktrace --max-workers=2 + -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" + -Dorg.gradle.java.installations.auto-detect=false + -Dorg.gradle.java.installations.auto-download=false + ) + ./gradlew "${gradle_args[@]}" - name: Analyze uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 diff --git a/README.md b/README.md index 17bd2128..07c870ab 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,11 @@ exported to `config/orespawn-guide/` without overwriting existing files. ## Building -Run Gradle with Java 17 from the repository root. Install the exact Temurin -`8.0.502+7` toolchain used to compile production code and test fixtures for -Minecraft 1.13.2; the build rejects a different Java 8 toolchain: +Run Gradle with Java 17 from the repository root. Install exact Temurin +`25.0.3+9` for ForgeGradle's Mavenizer and exact Temurin `8.0.502+7` for the +Minecraft 1.13.2 compilation and fixture toolchain. Java 17 remains the Gradle +runtime, while production bytecode remains Java 8; the build rejects a +different Java 8 toolchain: ```powershell .\gradlew.bat clean check build javadoc verifyReleaseArtifacts writeReleaseChecksums --no-daemon diff --git a/build.gradle b/build.gradle index 3837661c..425d2927 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ import groovy.json.JsonSlurper +import groovy.xml.XmlSlurper import java.nio.charset.StandardCharsets import java.security.MessageDigest import java.util.jar.Manifest @@ -35,6 +36,9 @@ if (versionParts[3] != expectedTargetVersion) { ext.functional_version = versionParts[0..2].join('.') ext.display_version = project.mod_version ext.release_tag = project.mod_version +def expectedMavenGroup = 'zone.moddev.mc.orespawn' +def expectedMavenArtifact = 'OreSpawn' +def expectedMavenCoordinate = "${expectedMavenGroup}:${expectedMavenArtifact}:${project.version}" java { toolchain { @@ -671,7 +675,7 @@ tasks.named('jar', Jar) { 'Implementation-Vendor' : 'SkyBlade1978', 'OreSpawn-API-Version' : '1', 'FMLAT' : 'accesstransformer.cfg', - 'Maven-Artifact' : "${project.group}:${base.archivesName.get()}:${project.version}", + 'Maven-Artifact' : expectedMavenCoordinate, 'Built-On-Java' : '8', 'Built-On' : "${project.minecraft_version}-${project.forge_version}" ]) @@ -728,6 +732,7 @@ tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { if (project.mod_version != '4.0.10.113021' + || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.13.2' || project.forge_version != '25.0.223' || project.mapping_channel != 'snapshot' @@ -744,7 +749,7 @@ tasks.register('verifyReleaseConfiguration') { 'OreSpawn-4.0.10.113021-sources.jar', 'OreSpawn-4.0.10.113021-javadoc.jar' ] - if (base.archivesName.get() != 'OreSpawn' + if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { throw new GradleException('Public artifacts must use the version-only OreSpawn filename contract') } @@ -871,6 +876,7 @@ tasks.register('verifyReleaseArtifacts') { || manifest.getValue('Implementation-Version') != project.mod_version || manifest.getValue('OreSpawn-API-Version') != '1' || manifest.getValue('FMLAT') != 'accesstransformer.cfg' + || manifest.getValue('Maven-Artifact') != expectedMavenCoordinate || manifest.getValue('Implementation-Timestamp') != null) { throw new GradleException('Release manifest is incorrect or volatile') } @@ -969,8 +975,8 @@ def mavenUploadPassword = providers.environmentVariable('MAVEN_UPLOAD_PASSWORD') publishing { publications { mavenJava(MavenPublication) { - groupId = project.group.toString() - artifactId = base.archivesName.get() + groupId = expectedMavenGroup + artifactId = expectedMavenArtifact version = project.version.toString() if (preparedReleaseDir.isPresent()) { File prepared = file(preparedReleaseDir.get()) @@ -1006,6 +1012,26 @@ publishing { } } } +tasks.register('verifyMavenCoordinates') { + group = 'verification' + description = 'Verifies the generated POM uses OreSpawn\'s mod-specific Maven namespace.' + dependsOn tasks.named('generatePomFileForMavenJavaPublication') + doLast { + File pomFile = layout.buildDirectory.file( + 'publications/mavenJava/pom-default.xml').get().asFile + if (!pomFile.isFile()) { + throw new GradleException("Generated Maven POM does not exist: ${pomFile}") + } + def pom = new XmlSlurper(false, false).parse(pomFile) + def actual = [pom.groupId.text(), pom.artifactId.text(), pom.version.text()] + def expected = [expectedMavenGroup, expectedMavenArtifact, + project.version.toString()] + if (project.group.toString() != expectedMavenGroup || actual != expected) { + throw new GradleException("Expected Maven coordinate ${expected.join(':')}, " + + "found ${actual.join(':')}") + } + } +} tasks.register('validateMavenReleaseCredentials') { group = 'publishing' doLast { @@ -1021,11 +1047,14 @@ tasks.register('validateMavenReleaseCredentials') { } tasks.withType(PublishToMavenRepository).configureEach { dependsOn tasks.named('validateMavenReleaseCredentials') + dependsOn tasks.named('verifyMavenCoordinates') dependsOn preparedReleaseDir.isPresent() ? tasks.named('verifyPreparedReleaseArtifacts') : tasks.named('verifyReleaseArtifacts') } +tasks.named('check') { dependsOn tasks.named('verifyMavenCoordinates') } + eclipse { classpath { downloadSources = true diff --git a/gradle.properties b/gradle.properties index bc429856..9f74e3e6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,7 +20,7 @@ mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 mod_version=4.0.10.113021 -mod_group=zone.moddev.mc +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/src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java b/src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java new file mode 100644 index 00000000..995c4de5 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/ReleaseWorkflowContractTest.java @@ -0,0 +1,91 @@ +package zone.moddev.mc.orespawn; + +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.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +class ReleaseWorkflowContractTest { + @Test + void releaseAndHostedWorkflowContractsRemainTargetNative() throws Exception { + Properties properties = new Properties(); + try (InputStream input = Files.newInputStream(Paths.get("gradle.properties"))) { + properties.load(input); + } + assertEquals("zone.moddev.mc.orespawn", properties.getProperty("mod_group")); + + String build = new String(Files.readAllBytes(Paths.get("build.gradle")), StandardCharsets.UTF_8); + assertTrue(build.contains("tasks.register('verifyMavenCoordinates')")); + assertTrue(build.contains("generatePomFileForMavenJavaPublication")); + assertTrue(build.contains("dependsOn tasks.named('verifyMavenCoordinates')")); + assertTrue(build.contains("expectedMavenCoordinate")); + + String ci = readWorkflow("ci.yml"); + String codeql = readWorkflow("codeql-analysis.yml"); + + for (String workflow : new String[] { ci, codeql }) { + assertFalse(workflow.contains("distribution: microsoft")); + assertTrue(workflow.contains("java-version: '25.0.3+9.0.LTS'")); + assertTrue(workflow.contains("java-version: '8.0.502+7'")); + assertTrue(workflow.contains("java-version: '17.0.1+12'")); + assertTrue(workflow.contains("-Dorg.gradle.java.installations.auto-detect=false")); + assertTrue(workflow.contains("-Dorg.gradle.java.installations.auto-download=false")); + } + assertPinnedToolchains(ci, 2, 3); + assertPinnedToolchains(codeql, 1, 1); + + assertTrue(ci.contains("name: Cold Forge bootstrap")); + assertTrue(ci.contains("GRADLE_USER_HOME: ${{ runner.temp }}/orespawn-cold-gradle")); + assertTrue(ci.contains("test ! -e .gradle")); + assertTrue(ci.contains("test ! -e \"$GRADLE_USER_HOME\"")); + assertTrue(ci.contains("classes verifyLegacyFixtures")); + assertTrue(ci.contains("--rerun-tasks --offline --no-daemon --no-build-cache")); + assertFalse(ci.contains("Mavenizer compatibility")); + } + + private static String readWorkflow(String name) throws Exception { + return new String(Files.readAllBytes(Paths.get(".github", "workflows", name)), + StandardCharsets.UTF_8); + } + + private static void assertPinnedToolchains(String workflow, int expectedJobs, int expectedPathUses) { + String java25 = "java-version: '25.0.3+9.0.LTS'"; + String java8 = "java-version: '8.0.502+7'"; + String java17 = "java-version: '17.0.1+12'"; + String paths = "$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64"; + + assertEquals(expectedJobs, occurrences(workflow, java25)); + assertEquals(expectedJobs, occurrences(workflow, java8)); + assertEquals(expectedJobs, occurrences(workflow, java17)); + assertEquals(expectedPathUses, occurrences(workflow, paths)); + + int cursor = 0; + for (int job = 0; job < expectedJobs; job++) { + int java25Index = workflow.indexOf(java25, cursor); + int java8Index = workflow.indexOf(java8, java25Index + java25.length()); + int java17Index = workflow.indexOf(java17, java8Index + java8.length()); + assertTrue(java25Index >= cursor); + assertTrue(java8Index > java25Index); + assertTrue(java17Index > java8Index); + cursor = java17Index + java17.length(); + } + } + + private static int occurrences(String value, String needle) { + int count = 0; + int offset = 0; + while ((offset = value.indexOf(needle, offset)) >= 0) { + count++; + offset += needle.length(); + } + return count; + } +} From 09c976aa2cc0146dc990b8c40ae99bb8963aff4a Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 22:56:09 +0100 Subject: [PATCH 4/6] Fix provider biome filters and namespaced geomes --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 9 +++ README.md | 2 +- build.gradle | 10 ++-- docs/API.md | 7 +++ docs/CONFIGURATION.md | 4 +- docs/DEVELOPER_GUIDE.md | 6 ++ docs/VERSIONS.md | 7 ++- gradle.properties | 2 +- .../mc/orespawn/api/WorldgenProvider.java | 27 +++++++++ .../orespawn/client/GeologyEditorSession.java | 13 +++- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../mc/orespawn/api/WorldgenProviderTest.java | 59 +++++++++++++++++++ .../client/GeologyEditorSessionTest.java | 33 +++++++++++ 15 files changed, 171 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbb360a1..be671bcd 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.0.10.113021.jar - build/libs/OreSpawn-4.0.10.113021-sources.jar - build/libs/OreSpawn-4.0.10.113021-javadoc.jar + build/libs/OreSpawn-4.0.13.113021.jar + build/libs/OreSpawn-4.0.13.113021-sources.jar + build/libs/OreSpawn-4.0.13.113021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d1f71004..5f9ef75b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Version 4.0.13.113021 + +* Give the public ore-dimension builder the exact biome include/exclude and + biome-dictionary filter support already available in provider JSON. +* Accept valid namespaced geome IDs in both creation-editor validation paths + while preserving legacy unnamespaced geome keys. +* API major 1, schemas, existing profiles, generated chunks, and worldgen + behaviour are unchanged. + Version 4.0.10.113021 * Evaluate Stable Layers rock min_y and max_y bounds against actual world Y diff --git a/README.md b/README.md index 07c870ab..ae9fd134 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ End" policy used by mods such as Base Metals. This is not the unrelated mod that adds mobs and dimensions under the same name. -This branch builds target-qualified version `4.0.10.113021`: the OreSpawn 4.0.10 +This branch builds target-qualified version `4.0.13.113021`: the OreSpawn 4.0.13 feature set for Minecraft 1.13.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 425d2927..cb269279 100644 --- a/build.gradle +++ b/build.gradle @@ -731,7 +731,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.10.113021' + if (project.mod_version != '4.0.13.113021' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.13.2' || project.forge_version != '25.0.223' @@ -745,9 +745,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.10.113021.jar', - 'OreSpawn-4.0.10.113021-sources.jar', - 'OreSpawn-4.0.10.113021-javadoc.jar' + 'OreSpawn-4.0.13.113021.jar', + 'OreSpawn-4.0.13.113021-sources.jar', + 'OreSpawn-4.0.13.113021-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -764,7 +764,7 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.0.10.113021')) { + if (!file(path).getText('UTF-8').contains('4.0.13.113021')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/API.md b/docs/API.md index 6436bd28..de09df69 100644 --- a/docs/API.md +++ b/docs/API.md @@ -83,6 +83,13 @@ WorldgenProvider provider = WorldgenProvider.builder("examplemod", 1) `OilDefinition` and template `.oil(...)` remain deprecated migration adapters for one legacy oil rule. New integrations should use `FluidDepositDefinition`. +Ore dimension builders expose the same biome filters as provider JSON and +fluid-deposit builders. Use `.biome(...)` and `.biomeDictionary(...)` for +inclusions, with `.excludeBiome(...)` and `.excludeBiomeDictionary(...)` for +exclusions. These methods work on both explicit `.dimension(...)` rules and +`.dimensionSelector(...)` fallbacks; built definitions and their returned +filter sets are immutable. + Create one `BiomeRegistrar` during normal mod construction. It attaches to the calling mod's event bus and defers biome factories until Forge's biome registry event. `OreSpawnBiomes.copyAndRegister` clones a known biome without adding a diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 8e774a11..026f66ec 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -117,7 +117,9 @@ is omitted. `dimensions` limits membership, and `geomes` multiplies selection weight by province. A weight of zero prevents selection in that context. Geomes contain a non-negative `base` weight and non-negative weights for each -rock family. Biome and biome-dictionary maps multiply those geome weights. +rock family. Keys may retain the legacy unnamespaced form or use a provider +resource ID such as `examplemod:crystal_basin`; the creation editor preserves +both forms. Biome and biome-dictionary maps multiply those geome weights. Missing optional-mod biome IDs are ignored during baking. Terrain dimensions require `enabled`, `host_blocks`, and `host_tags`. diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 36695682..8c754315 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -100,6 +100,10 @@ private void enqueueWorldgen(InterModEnqueueEvent event) { .quantityRange(4, 11) .pattern(OrePattern.VEIN) .heightDistribution(OreHeightDistribution.TRIANGLE) + .biome(new ResourceLocation("minecraft", "plains")) + .biomeDictionary("FOREST") + .excludeBiome(new ResourceLocation("minecraft", "dark_forest")) + .excludeBiomeDictionary("SPOOKY") .hostTag(new ResourceLocation("forge", "stone")))) .build(); @@ -114,6 +118,8 @@ Use `.quantity(8)` when every attempt should have a fixed budget. The selector above preserves old OS3 behavior in every ordinary dimension except Nether and End. Add an explicit `.dimension(overworld, ...)` as well when the Overworld needs different settings; the explicit rule overrides the selector there. +Ore dimension builders support the same exact-ID and biome-dictionary include +and exclude filters as provider JSON and fluid-deposit builders. ## Pack Override Quick Start diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index bf241b1b..b63acdfd 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -51,7 +51,7 @@ Examples: | Minecraft | Loader | Target | Example full OreSpawn version | | --- | --- | ---: | --- | -| 1.13.2 | Forge | `113021` | `4.0.10.113021` | +| 1.13.2 | Forge | `113021` | `4.0.13.113021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -145,7 +145,10 @@ If a different branch later receives a shared fix, it uses the next unused Bug number, such as Forge 1.13.2's `4.0.8.113021`, even though the 4.0.7 repair was not applicable there. Forge 1.13.2 then advanced to its target-qualified 4.0.9 release for the provider terrain-host ordering repair and to 4.0.10 for -the distinct Stable Layers actual-height eligibility repair. +the distinct Stable Layers actual-height eligibility repair. Forge 1.13 uses a +static, two-dimensional biome provider, so the dynamic-registry and vertical +sampler repairs released as 4.0.11 and 4.0.12 are not applicable; it advances +directly to 4.0.13 for provider biome-filter parity and namespaced geome support. A branch may therefore legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 9f74e3e6..60c036b0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20190213.203750 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.10.113021 +mod_version=4.0.13.113021 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/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java b/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java index bf8d421e..366ca40e 100644 --- a/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java +++ b/src/main/java/zone/moddev/mc/orespawn/api/WorldgenProvider.java @@ -528,6 +528,10 @@ public static final class OreDimensionDefinition implements JsonDefinition { private final Map geomes; private final Set hostBlocks; private final Set hostTags; + private final Set biomeIds; + private final Set excludedBiomeIds; + private final Set biomeDictionary; + private final Set excludedBiomeDictionary; private final Map hostBlockWeights; private final Map hostTagWeights; @@ -551,6 +555,11 @@ private OreDimensionDefinition(Builder builder) { geomes = immutableMap(builder.geomes); hostBlocks = immutableSet(builder.hostBlocks); hostTags = immutableSet(builder.hostTags); + biomeIds = immutableSet(builder.biomeIds); + excludedBiomeIds = immutableSet(builder.excludedBiomeIds); + biomeDictionary = Collections.unmodifiableSet(new LinkedHashSet<>(builder.biomeDictionary)); + excludedBiomeDictionary = Collections.unmodifiableSet( + new LinkedHashSet<>(builder.excludedBiomeDictionary)); hostBlockWeights = immutableMap(builder.hostBlockWeights); hostTagWeights = immutableMap(builder.hostTagWeights); } @@ -577,6 +586,10 @@ private OreDimensionDefinition(Builder builder) { public Map geomes() { return geomes; } public Set hostBlocks() { return hostBlocks; } public Set hostTags() { return hostTags; } + public Set biomeIds() { return biomeIds; } + public Set excludedBiomeIds() { return excludedBiomeIds; } + public Set biomeDictionary() { return biomeDictionary; } + public Set excludedBiomeDictionary() { return excludedBiomeDictionary; } public Map hostBlockWeights() { return hostBlockWeights; } public Map hostTagWeights() { return hostTagWeights; } @@ -612,6 +625,10 @@ public JsonObject toJson() { json.add("geomes", weights(geomes)); json.add("host_blocks", weightedIds(hostBlocks, hostBlockWeights, "block")); json.add("host_tags", weightedIds(hostTags, hostTagWeights, "tag")); + json.add("biome_ids", ids(biomeIds)); + json.add("excluded_biome_ids", ids(excludedBiomeIds)); + json.add("biome_dictionary", strings(biomeDictionary)); + json.add("excluded_biome_dictionary", strings(excludedBiomeDictionary)); return json; } @@ -635,6 +652,10 @@ public static final class Builder { private final Map geomes = new LinkedHashMap<>(); private final Set hostBlocks = new LinkedHashSet<>(); private final Set hostTags = new LinkedHashSet<>(); + private final Set biomeIds = new LinkedHashSet<>(); + private final Set excludedBiomeIds = new LinkedHashSet<>(); + private final Set biomeDictionary = new LinkedHashSet<>(); + private final Set excludedBiomeDictionary = new LinkedHashSet<>(); private final Map hostBlockWeights = new LinkedHashMap<>(); private final Map hostTagWeights = new LinkedHashMap<>(); @@ -665,6 +686,12 @@ public Builder pattern(ResourceLocation type, JsonObject settings) { public Builder geomeWeight(ResourceLocation geome, double value) { geomes.put(geome, value); return this; } public Builder hostBlock(ResourceLocation value) { hostBlocks.add(value); return this; } public Builder hostTag(ResourceLocation value) { hostTags.add(value); return this; } + public Builder biome(ResourceLocation value) { biomeIds.add(value); return this; } + public Builder excludeBiome(ResourceLocation value) { excludedBiomeIds.add(value); return this; } + public Builder biomeDictionary(String value) { biomeDictionary.add(nonBlank(value)); return this; } + public Builder excludeBiomeDictionary(String value) { + excludedBiomeDictionary.add(nonBlank(value)); return this; + } public Builder hostBlock(ResourceLocation value, double weight) { hostBlocks.add(value); hostBlockWeights.put(value, replacementWeight(weight)); diff --git a/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java b/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java index 20bce863..0ad223ba 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java @@ -623,8 +623,8 @@ JsonObject weightMap(String section, String id) { } void addGeome(String id) { - String normalized = id.trim().toLowerCase(Locale.ROOT); - if (!normalized.matches("[a-z0-9_.-]+") || section("geomes").has(normalized)) { + String normalized = id.trim(); + if (!validGeomeId(normalized) || section("geomes").has(normalized)) { return; } JsonObject geome = new JsonObject(); @@ -684,7 +684,7 @@ List validate() { } for (Entry entry : terrainActive ? geomes.entrySet() : Collections.>emptySet()) { - if (!entry.getKey().matches("[a-z0-9_.-]+") || !entry.getValue().isJsonObject()) { + if (!validGeomeId(entry.getKey()) || !entry.getValue().isJsonObject()) { errors.add("Invalid geome: " + entry.getKey()); continue; } @@ -1168,6 +1168,13 @@ private static boolean validResource(String id) { } } + private static boolean validGeomeId(String id) { + if (id == null || id.isEmpty()) return false; + if (id.indexOf(':') < 0) return id.matches("[a-z0-9_.-]+"); + if (!validResource(id)) return false; + return id.equals(new ResourceLocation(id).toString()); + } + static String string(JsonObject json, String key, String fallback) { try { return json.has(key) ? json.get(key).getAsString() : fallback; } catch (RuntimeException e) { return fallback; } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 7a261306..81951d3a 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -359,7 +359,7 @@ private static void writeReport(Path config, List lines) { private static void writeUpgradeReport(Path config, int imported, List detail) { List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.10.113021 Upgrade Report"); + lines.add("OreSpawn 4.0.13.113021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn settings were imported into the OS4 profile."); 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 8a4331c5..20b3ad19 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -199,7 +199,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.10.113021 Upgrade Report"); + lines.add("OreSpawn 4.0.13.113021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); diff --git a/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java b/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java index d32b0ca3..d2c31437 100644 --- a/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/api/WorldgenProviderTest.java @@ -268,6 +268,65 @@ void serializesRangedQuantityAndBroadDimensionSelector() { assertFalse(rule.has("quantity")); } + @Test + void oreBiomeFiltersMatchFluidBuilderForDimensionsAndSelectors() { + ResourceLocation overworld = id("minecraft:overworld"); + ResourceLocation plains = id("minecraft:plains"); + ResourceLocation darkForest = id("minecraft:dark_forest"); + WorldgenProvider.OreDimensionDefinition explicit = WorldgenProvider.OreDimensionDefinition + .builder(overworld) + .enabled(false) + .hostTag(id("minecraft:stone_ore_replaceables")) + .biome(plains) + .biomeDictionary("FOREST") + .excludeBiome(darkForest) + .excludeBiomeDictionary("SPOOKY") + .build(); + WorldgenProvider.OreDimensionDefinition selector = WorldgenProvider.OreDimensionDefinition + .builder(OreDimensionSelector.ALL_EXCEPT_NETHER_AND_END.id()) + .hostTag(id("minecraft:stone_ore_replaceables")) + .biome(plains) + .biomeDictionary("FOREST") + .excludeBiome(darkForest) + .excludeBiomeDictionary("SPOOKY") + .build(); + + assertEquals(Collections.singleton(plains), explicit.biomeIds()); + assertEquals(Collections.singleton(darkForest), explicit.excludedBiomeIds()); + assertEquals(Collections.singleton("FOREST"), explicit.biomeDictionary()); + assertEquals(Collections.singleton("SPOOKY"), explicit.excludedBiomeDictionary()); + assertThrows(UnsupportedOperationException.class, + () -> explicit.biomeIds().add(id("minecraft:forest"))); + + WorldgenProvider provider = WorldgenProvider.builder("examplemod", 1) + .ore(id("examplemod:filtered_ore"), ore -> ore + .dimension(explicit) + .dimensionSelector(OreDimensionSelector.ALL_EXCEPT_NETHER_AND_END, selector)) + .build(); + JsonObject ore = provider.toJson().getAsJsonObject("ores") + .getAsJsonObject("examplemod:ore/examplemod/filtered_ore"); + assertFalse(ore.getAsJsonObject("dimensions").getAsJsonObject(overworld.toString()) + .get("enabled").getAsBoolean()); + assertTrue(ore.getAsJsonObject("dimension_selectors").getAsJsonObject( + OreDimensionSelector.ALL_EXCEPT_NETHER_AND_END.id().toString()) + .get("enabled").getAsBoolean()); + for (JsonObject rule : new JsonObject[] { + ore.getAsJsonObject("dimensions").getAsJsonObject(overworld.toString()), + ore.getAsJsonObject("dimension_selectors").getAsJsonObject( + OreDimensionSelector.ALL_EXCEPT_NETHER_AND_END.id().toString()) }) { + assertEquals("[\"minecraft:plains\"]", rule.getAsJsonArray("biome_ids").toString()); + assertEquals("[\"minecraft:dark_forest\"]", rule.getAsJsonArray("excluded_biome_ids").toString()); + assertEquals("[\"FOREST\"]", rule.getAsJsonArray("biome_dictionary").toString()); + assertEquals("[\"SPOOKY\"]", rule.getAsJsonArray("excluded_biome_dictionary").toString()); + } + ore.getAsJsonObject("dimensions").getAsJsonObject(overworld.toString()) + .getAsJsonArray("biome_ids").add("minecraft:forest"); + assertEquals("[\"minecraft:plains\"]", provider.toJson().getAsJsonObject("ores") + .getAsJsonObject("examplemod:ore/examplemod/filtered_ore") + .getAsJsonObject("dimensions").getAsJsonObject(overworld.toString()) + .getAsJsonArray("biome_ids").toString()); + } + @Test void rejectsInvalidQuantityRangesEarly() { assertThrows(IllegalStateException.class, () -> WorldgenProvider.OreDimensionDefinition diff --git a/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java b/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java index 5b39a506..6b7d9deb 100644 --- a/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/client/GeologyEditorSessionTest.java @@ -144,4 +144,37 @@ void standaloneFluidPickerCreatesAUsableCoveredOverworldRule() { java.util.List errors = session.validate(); assertTrue(errors.isEmpty(), errors.toString()); } + + @Test + void namespacedGeomesCanBeAddedValidatedAndRoundTripped() { + String geomeId = "cakeworld:cocoa_basin"; + GeologyEditorSession session = new GeologyEditorSession(WorldGeologyProfile.recommended(false)); + session.configureDefaultVanillaStrata(); + session.addGeome(geomeId); + + assertTrue(session.section("geomes").has(geomeId)); + session.weightMap("biomes", "minecraft:plains").addProperty(geomeId, 2.0D); + session.rock("minecraft:stone").getAsJsonObject("geomes").addProperty(geomeId, 3.0D); + java.util.List errors = session.validate(); + assertTrue(errors.isEmpty(), errors.toString()); + + WorldGeologyProfile saved = session.profile(); + GeologyEditorSession reopened = new GeologyEditorSession(saved); + assertEquals(saved.rootCopy(), reopened.profile().rootCopy()); + assertTrue(reopened.validate().isEmpty(), reopened.validate().toString()); + assertEquals(2.0D, reopened.weightMap("biomes", "minecraft:plains") + .get(geomeId).getAsDouble()); + assertEquals(3.0D, reopened.rock("minecraft:stone").getAsJsonObject("geomes") + .get(geomeId).getAsDouble()); + + GeologyEditorSession invalidSession = new GeologyEditorSession(WorldGeologyProfile.recommended(false)); + invalidSession.configureDefaultVanillaStrata(); + invalidSession.addGeome(""); + invalidSession.addGeome("BAD:UPPER"); + invalidSession.addGeome(geomeId); + invalidSession.addGeome(geomeId); + assertFalse(invalidSession.section("geomes").has("")); + assertFalse(invalidSession.section("geomes").has("bad:upper")); + assertTrue(invalidSession.section("geomes").has(geomeId)); + } } From c5c24f84e7e92cc5498e1212327161ef2e2392d4 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 23:21:08 +0100 Subject: [PATCH 5/6] Fix exposed snow material conversion --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 8 + README.md | 2 +- build.gradle | 10 +- docs/VERSIONS.md | 8 +- gradle.properties | 2 +- .../orespawn/testmod/SurfaceProbeTestMod.java | 197 ++++++++++++++++-- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../worldgen/WorldMaterialWeather.java | 8 + 10 files changed, 207 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be671bcd..4fdab4e9 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.0.13.113021.jar - build/libs/OreSpawn-4.0.13.113021-sources.jar - build/libs/OreSpawn-4.0.13.113021-javadoc.jar + build/libs/OreSpawn-4.0.14.113021.jar + build/libs/OreSpawn-4.0.14.113021-sources.jar + build/libs/OreSpawn-4.0.14.113021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5f9ef75b..cb08233f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +Version 4.0.14.113021 + +* Convert exposed one-layer vanilla Snow from the first free cell immediately + above Minecraft's motion-blocking surface when a dimension supplies a custom + snow material. +* Retain the existing surface Ice conversion and leave buried or authored Snow + and Ice unchanged. + Version 4.0.13.113021 * Give the public ore-dimension builder the exact biome include/exclude and diff --git a/README.md b/README.md index ae9fd134..99de4378 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ End" policy used by mods such as Base Metals. This is not the unrelated mod that adds mobs and dimensions under the same name. -This branch builds target-qualified version `4.0.13.113021`: the OreSpawn 4.0.13 +This branch builds target-qualified version `4.0.14.113021`: the OreSpawn 4.0.14 feature set for Minecraft 1.13.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index cb269279..76b0503b 100644 --- a/build.gradle +++ b/build.gradle @@ -731,7 +731,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.13.113021' + if (project.mod_version != '4.0.14.113021' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.13.2' || project.forge_version != '25.0.223' @@ -745,9 +745,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.13.113021.jar', - 'OreSpawn-4.0.13.113021-sources.jar', - 'OreSpawn-4.0.13.113021-javadoc.jar' + 'OreSpawn-4.0.14.113021.jar', + 'OreSpawn-4.0.14.113021-sources.jar', + 'OreSpawn-4.0.14.113021-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -764,7 +764,7 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.0.13.113021')) { + if (!file(path).getText('UTF-8').contains('4.0.14.113021')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index b63acdfd..7abaf9c9 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -51,7 +51,7 @@ Examples: | Minecraft | Loader | Target | Example full OreSpawn version | | --- | --- | ---: | --- | -| 1.13.2 | Forge | `113021` | `4.0.13.113021` | +| 1.13.2 | Forge | `113021` | `4.0.14.113021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -148,8 +148,10 @@ was not applicable there. Forge 1.13.2 then advanced to its target-qualified the distinct Stable Layers actual-height eligibility repair. Forge 1.13 uses a static, two-dimensional biome provider, so the dynamic-registry and vertical sampler repairs released as 4.0.11 and 4.0.12 are not applicable; it advances -directly to 4.0.13 for provider biome-filter parity and namespaced geome support. -A branch may therefore legitimately skip functional version numbers. +directly to 4.0.13 for provider biome-filter parity and namespaced geome support, +then to 4.0.14 so one-layer Snow above the motion-blocking surface is included +in configured weather-material conversion. A branch may therefore legitimately +skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 60c036b0..29286d05 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20190213.203750 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.13.113021 +mod_version=4.0.14.113021 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/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java index b88e478a..38f2e7ab 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -144,7 +144,8 @@ public final class SurfaceProbeTestMod { private static final String PHASE_PROPERTY = "surfaceprobe.integrationPhase"; private static final String MARKER_NAME = "surfaceprobe-integration.properties"; private static final String CHEST_ITEM_NAME = "surfaceprobe sentinel"; - private static final String RAW_CHEST_ITEM_NAME = "surfaceprobe raw block entity sentinel"; + private static final IBlockState WEATHER_SNOW_REPLACEMENT = Blocks.WHITE_WOOL.getDefaultState(); + private static final IBlockState WEATHER_ICE_REPLACEMENT = Blocks.BLUE_ICE.getDefaultState(); public SurfaceProbeTestMod() { FMLJavaModLoadingContext context = FMLJavaModLoadingContext.get(); @@ -223,6 +224,9 @@ private void enqueueProvider(InterModEnqueueEvent event) { .hostBlock(blockId(Blocks.DIORITE)))); addPalette(provider, "open_palette_0", OPEN_ID, false); addPalette(provider, "roofed_palette_0", ROOFED_ID, true); + provider.dimensionMaterials(new ResourceLocation(MODID + ":materials/end"), OPEN_ID, + materials -> materials.snowBlock(blockId(Blocks.WHITE_WOOL)) + .iceBlock(blockId(Blocks.BLUE_ICE))); provider.dimensionMaterials(new ResourceLocation(MODID + ":materials/nether"), ROOFED_ID, materials -> materials.defaultFluid(blockId(Blocks.WATER))); if (!OreSpawnApi.enqueue(provider.build())) { @@ -460,12 +464,22 @@ private static AuditResult auditDimension(WorldServer level, boolean roofed) { long underwaterPockets = 0L; long rawBedrock = 0L; long rawBlockEntities = 0L; + long exposedSnowConverted = 0L; + long surfaceIceConverted = 0L; + long buriedSnowPreserved = 0L; + long buriedIcePreserved = 0L; + long unconfiguredSnowPreserved = 0L; + long unconfiguredIcePreserved = 0L; BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) { for (int chunkX = MINIMUM_CHUNK; chunkX <= MAXIMUM_CHUNK; chunkX++) { level.getChunk(chunkX, chunkZ); Chunk chunk = level.getChunk(chunkX, chunkZ); + // Exercise the production conversion seam after later weather/authored + // sentinels have been placed, as a real chunk reload would. + zone.moddev.mc.orespawn.worldgen.WorldMaterialWeather.onChunkLoad( + new net.minecraftforge.event.world.ChunkEvent.Load(chunk)); int chunkMinX = chunkX << 4; int chunkMinZ = chunkZ << 4; for (int localZ = 0; localZ < 16; localZ++) { @@ -538,19 +552,33 @@ private static AuditResult auditDimension(WorldServer level, boolean roofed) { rawBedrock += natural.bedrockPreserved; rawBlockEntities += natural.blockEntityPreserved; } + WeatherMaterialAudit weather = auditWeatherMaterials(chunk, pos, + chunkMinX, chunkMinZ, 0, 256, roofed); + exposedSnowConverted += weather.exposedSnowConverted(); + surfaceIceConverted += weather.surfaceIceConverted(); + buriedSnowPreserved += weather.buriedSnowPreserved(); + buriedIcePreserved += weather.buriedIcePreserved(); + unconfiguredSnowPreserved += weather.unconfiguredSnowPreserved(); + unconfiguredIcePreserved += weather.unconfiguredIcePreserved(); } } if (top != EXPECTED_COLUMNS - 9 || underwater != 9 || filler != EXPECTED_FILLER || biomeA == 0 || biomeB == 0 || edgeChanges == 0 || sentinels != 9 * 4 || geology != (roofed ? 0 : EXPECTED_FILLER) - || (roofed && (ceiling != EXPECTED_COLUMNS || roofTop != EXPECTED_COLUMNS)) + || (roofed && (ceiling != EXPECTED_COLUMNS || roofTop != EXPECTED_COLUMNS + || unconfiguredSnowPreserved != 9 || unconfiguredIcePreserved != 9 + || exposedSnowConverted != 0 || surfaceIceConverted != 0 + || buriedSnowPreserved != 0 || buriedIcePreserved != 0)) || (!roofed && (rawNaturalSources != EXPECTED_NATURAL_SOURCES || structureNaturalSources != EXPECTED_NATURAL_SOURCES || vegetationNaturalSources != EXPECTED_NATURAL_SOURCES || cavePockets != EXPECTED_NATURAL_SOURCES / 2 || underwaterPockets != EXPECTED_NATURAL_SOURCES / 2 - || rawBedrock != 9 || rawBlockEntities != 9))) { + || rawBedrock != 9 || rawBlockEntities != 9 + || exposedSnowConverted != 9 || surfaceIceConverted != 9 + || buriedSnowPreserved != 9 || buriedIcePreserved != 9 + || unconfiguredSnowPreserved != 0 || unconfiguredIcePreserved != 0))) { throw new IllegalStateException("Incomplete surface audit for " + DimensionType.func_212678_a(level.dimension.getType()) + ": top=" + top + ", underwater=" + underwater + ", filler=" + filler + ", biomeA=" + biomeA + ", biomeB=" + biomeB + ", edges=" + edgeChanges @@ -562,13 +590,58 @@ private static AuditResult auditDimension(WorldServer level, boolean roofed) { + ", cavePockets=" + cavePockets + ", underwaterPockets=" + underwaterPockets + ", rawBedrock=" + rawBedrock - + ", rawBlockEntities=" + rawBlockEntities); + + ", rawBlockEntities=" + rawBlockEntities + + ", exposedSnowConverted=" + exposedSnowConverted + + ", surfaceIceConverted=" + surfaceIceConverted + + ", buriedSnowPreserved=" + buriedSnowPreserved + + ", buriedIcePreserved=" + buriedIcePreserved + + ", unconfiguredSnowPreserved=" + unconfiguredSnowPreserved + + ", unconfiguredIcePreserved=" + unconfiguredIcePreserved); } long aquiferFluid = roofed ? 0L : auditDynamicFluid(level); return new AuditResult(top, underwater, filler, geology, ceiling, roofTop, biomeA, biomeB, edgeChanges, sentinels, aquiferFluid, rawNaturalSources, structureNaturalSources, vegetationNaturalSources, - cavePockets, underwaterPockets, rawBedrock, rawBlockEntities); + cavePockets, underwaterPockets, rawBedrock, rawBlockEntities, + exposedSnowConverted, surfaceIceConverted, + buriedSnowPreserved, buriedIcePreserved, + unconfiguredSnowPreserved, unconfiguredIcePreserved); + } + + private static WeatherMaterialAudit auditWeatherMaterials(IChunk chunk, + BlockPos.MutableBlockPos pos, int minX, int minZ, int minY, int maxY, + boolean roofed) { + int snowGroundY = findMarkedGround(chunk, pos, minX + 2, minZ + 2, minY, maxY); + int iceGroundY = findMarkedGround(chunk, pos, minX + 3, minZ + 2, minY, maxY); + if (roofed) { + return new WeatherMaterialAudit(0L, 0L, 0L, 0L, + assertState(chunk, pos.setPos(minX + 2, snowGroundY + 11, minZ + 2), + Blocks.SNOW.getDefaultState(), "unconfigured exposed Snow preservation"), + assertState(chunk, pos.setPos(minX + 3, iceGroundY + 11, minZ + 2), + Blocks.ICE.getDefaultState(), "unconfigured surface Ice preservation")); + } + int buriedSnowGroundY = findMarkedGround(chunk, pos, minX + 2, minZ + 3, minY, maxY); + int buriedIceGroundY = findMarkedGround(chunk, pos, minX + 3, minZ + 3, minY, maxY); + return new WeatherMaterialAudit( + assertState(chunk, pos.setPos(minX + 2, snowGroundY + 1, minZ + 2), + WEATHER_SNOW_REPLACEMENT, "exposed Snow weather replacement"), + assertState(chunk, pos.setPos(minX + 3, iceGroundY + 1, minZ + 2), + WEATHER_ICE_REPLACEMENT, "surface Ice weather replacement"), + assertState(chunk, pos.setPos(minX + 2, buriedSnowGroundY - 24, minZ + 3), + Blocks.SNOW.getDefaultState(), "buried authored Snow preservation"), + assertState(chunk, pos.setPos(minX + 3, buriedIceGroundY - 24, minZ + 3), + Blocks.ICE.getDefaultState(), "buried authored Ice preservation"), + 0L, 0L); + } + + private static long assertState(IChunk chunk, BlockPos pos, + IBlockState expected, String label) { + IBlockState actual = chunk.getBlockState(pos); + if (!actual.equals(expected)) { + throw new IllegalStateException(label + " changed at " + pos + + ": expected " + expected + " but found " + actual); + } + return 1L; } private static NaturalSourceAudit auditNaturalSources(WorldServer level, IChunk chunk, @@ -601,13 +674,13 @@ private static NaturalSourceAudit auditNaturalSources(WorldServer level, IChunk int bedrockGroundY = findMarkedGround(chunk, pos, minX + 11, minZ + 12, 0, 256); if (chunk.getBlockState(pos.setPos(minX + 11, bedrockGroundY - 24, minZ + 12)).getBlock() == Blocks.BEDROCK) bedrockPreserved++; - int chestGroundY = findMarkedGround(chunk, pos, minX + 12, minZ + 12, 0, 256); - pos.setPos(minX + 12, chestGroundY - 24, minZ + 12); + int chestGroundY = findMarkedGround(chunk, pos, minX + 10, minZ + 10, 0, 256); + pos.setPos(minX + 10, chestGroundY + 1, minZ + 10); if (chunk.getBlockState(pos).getBlock() == Blocks.CHEST && level.getTileEntity(pos) instanceof TileEntityChest) { TileEntityChest chest = (TileEntityChest) level.getTileEntity(pos); - if (chest.getStackInSlot(0).getItem() == Items.EMERALD - && RAW_CHEST_ITEM_NAME.equals(chest.getStackInSlot(0).getDisplayName().getString())) { + if (chest.getStackInSlot(0).getItem() == Items.DIAMOND + && CHEST_ITEM_NAME.equals(chest.getStackInSlot(0).getDisplayName().getString())) { blockEntityPreserved++; } } @@ -764,6 +837,12 @@ private static Properties properties(long seed, Map results values.setProperty(prefix + "underwater_pockets", Long.toString(result.underwaterPockets())); values.setProperty(prefix + "raw_bedrock", Long.toString(result.rawBedrock())); values.setProperty(prefix + "raw_block_entities", Long.toString(result.rawBlockEntities())); + values.setProperty(prefix + "exposed_snow_converted", Long.toString(result.exposedSnowConverted())); + values.setProperty(prefix + "surface_ice_converted", Long.toString(result.surfaceIceConverted())); + values.setProperty(prefix + "buried_snow_preserved", Long.toString(result.buriedSnowPreserved())); + values.setProperty(prefix + "buried_ice_preserved", Long.toString(result.buriedIcePreserved())); + values.setProperty(prefix + "unconfigured_snow_preserved", Long.toString(result.unconfiguredSnowPreserved())); + values.setProperty(prefix + "unconfigured_ice_preserved", Long.toString(result.unconfiguredIcePreserved())); } return values; } @@ -805,6 +884,8 @@ public boolean func_212245_a(IWorld world, BlockPos origin, NoFeatureConfig config) { IChunk chunk = world.getChunk((origin.getX() >> 4) + 1, (origin.getZ() >> 4) + 1); + if (chunk.getPos().x < MINIMUM_CHUNK || chunk.getPos().x > MAXIMUM_CHUNK + || chunk.getPos().z < MINIMUM_CHUNK || chunk.getPos().z > MAXIMUM_CHUNK) return false; if (stage == ProbeStage.TERRAIN) return prepareTerrain(world, chunk); if (stage == ProbeStage.STRUCTURE) return placeStructureSentinels(world, chunk); return placeVegetationSentinels(world, chunk); @@ -869,6 +950,11 @@ private static boolean prepareTerrain(IWorld world, IChunk chunk) { private static void placeRawNaturalSources(IWorld world, IChunk chunk, BlockPos.MutableBlockPos pos, int minX, int minZ) { + // Forge 25 decorates through a wider ProtoChunk neighbourhood than the + // three-by-three audit. Do not leave block-entity sentinels in incomplete + // boundary chunks that the server cannot serialize safely. + if (chunk.getPos().x < MINIMUM_CHUNK || chunk.getPos().x > MAXIMUM_CHUNK + || chunk.getPos().z < MINIMUM_CHUNK || chunk.getPos().z > MAXIMUM_CHUNK) return; for (int index = 0; index < NATURAL_SOURCES.length; index++) { int x = naturalX(minX, index); int z = naturalZ(minZ, index); @@ -882,16 +968,6 @@ private static void placeRawNaturalSources(IWorld world, IChunk chunk, int bedrockGroundY = findMarkedGround(chunk, pos, minX + 11, minZ + 12, 0, 256); chunk.setBlockState(pos.setPos(minX + 11, bedrockGroundY - 24, minZ + 12), Blocks.BEDROCK.getDefaultState(), false); - int chestGroundY = findMarkedGround(chunk, pos, minX + 12, minZ + 12, 0, 256); - world.setBlockState(pos.setPos(minX + 12, chestGroundY - 24, minZ + 12), - Blocks.CHEST.getDefaultState(), 2); - if (world.getTileEntity(pos) instanceof TileEntityChest) { - TileEntityChest chest = (TileEntityChest) world.getTileEntity(pos); - ItemStack sentinel = new ItemStack(Items.EMERALD); - sentinel.setDisplayName(new TextComponentString(RAW_CHEST_ITEM_NAME)); - chest.setInventorySlotContents(0, sentinel); - chest.markDirty(); - } } private static boolean solid(IBlockState state) { @@ -907,9 +983,10 @@ private static boolean placeStructureSentinels(IWorld world, IChunk chunk) { world.setBlockState(pos.setPos(minX + 8, structureY + 1, minZ + 8), Blocks.GOLD_BLOCK.getDefaultState(), 2); int chestY = markedGround(chunk, pos, minX + 10, minZ + 10, world); - world.setBlockState(pos.setPos(minX + 10, chestY + 1, minZ + 10), Blocks.CHEST.getDefaultState(), 2); - if (world.getTileEntity(pos) instanceof TileEntityChest) { - TileEntityChest chest = (TileEntityChest) world.getTileEntity(pos); + BlockPos chestPos = new BlockPos(minX + 10, chestY + 1, minZ + 10); + world.setBlockState(chestPos, Blocks.CHEST.getDefaultState(), 2); + if (world.getTileEntity(chestPos) instanceof TileEntityChest) { + TileEntityChest chest = (TileEntityChest) world.getTileEntity(chestPos); ItemStack sentinel = new ItemStack(Items.DIAMOND); sentinel.setDisplayName(new TextComponentString(CHEST_ITEM_NAME)); chest.setInventorySlotContents(0, sentinel); @@ -933,9 +1010,35 @@ private static boolean placeVegetationSentinels(IWorld world, IChunk chunk) { world.setBlockState(pos.setPos(minX + 6, vegetationY + 1, minZ + 6), Blocks.DIRT.getDefaultState(), 2); world.setBlockState(pos.setPos(minX + 6, vegetationY + 2, minZ + 6), Blocks.OAK_SAPLING.getDefaultState(), 2); placeAuthoredNaturalSources(world, chunk, pos, minX, minZ, 20); + placeWeatherMaterialSentinels(world, chunk, pos, minX, minZ); return true; } + private static void placeWeatherMaterialSentinels(IWorld world, IChunk chunk, + BlockPos.MutableBlockPos pos, int minX, int minZ) { + int snowGroundY = markedGround(chunk, pos, minX + 2, minZ + 2, world); + int iceGroundY = markedGround(chunk, pos, minX + 3, minZ + 2, world); + ResourceLocation dimension = DimensionType.func_212678_a(world.getWorld().dimension.getType()); + if (ROOFED_ID.equals(dimension)) { + world.setBlockState(pos.setPos(minX + 2, snowGroundY + 11, minZ + 2), + Blocks.SNOW.getDefaultState(), 2); + world.setBlockState(pos.setPos(minX + 3, iceGroundY + 11, minZ + 2), + Blocks.ICE.getDefaultState(), 2); + return; + } + if (!OPEN_ID.equals(dimension)) return; + world.setBlockState(pos.setPos(minX + 2, snowGroundY + 1, minZ + 2), + Blocks.SNOW.getDefaultState(), 2); + world.setBlockState(pos.setPos(minX + 3, iceGroundY + 1, minZ + 2), + Blocks.ICE.getDefaultState(), 2); + int buriedSnowGroundY = markedGround(chunk, pos, minX + 2, minZ + 3, world); + int buriedIceGroundY = markedGround(chunk, pos, minX + 3, minZ + 3, world); + world.setBlockState(pos.setPos(minX + 2, buriedSnowGroundY - 24, minZ + 3), + Blocks.SNOW.getDefaultState(), 2); + world.setBlockState(pos.setPos(minX + 3, buriedIceGroundY - 24, minZ + 3), + Blocks.ICE.getDefaultState(), 2); + } + private static void placeAuthoredNaturalSources(IWorld world, IChunk chunk, BlockPos.MutableBlockPos pos, int minX, int minZ, int depth) { if (!OPEN_ID.equals(DimensionType.func_212678_a(world.getWorld().dimension.getType()))) return; @@ -1020,6 +1123,33 @@ private static final class NaturalSourceAudit { } } + private static final class WeatherMaterialAudit { + private final long exposedSnowConverted; + private final long surfaceIceConverted; + private final long buriedSnowPreserved; + private final long buriedIcePreserved; + private final long unconfiguredSnowPreserved; + private final long unconfiguredIcePreserved; + + WeatherMaterialAudit(long exposedSnowConverted, long surfaceIceConverted, + long buriedSnowPreserved, long buriedIcePreserved, + long unconfiguredSnowPreserved, long unconfiguredIcePreserved) { + this.exposedSnowConverted = exposedSnowConverted; + this.surfaceIceConverted = surfaceIceConverted; + this.buriedSnowPreserved = buriedSnowPreserved; + this.buriedIcePreserved = buriedIcePreserved; + this.unconfiguredSnowPreserved = unconfiguredSnowPreserved; + this.unconfiguredIcePreserved = unconfiguredIcePreserved; + } + + long exposedSnowConverted() { return exposedSnowConverted; } + long surfaceIceConverted() { return surfaceIceConverted; } + long buriedSnowPreserved() { return buriedSnowPreserved; } + long buriedIcePreserved() { return buriedIcePreserved; } + long unconfiguredSnowPreserved() { return unconfiguredSnowPreserved; } + long unconfiguredIcePreserved() { return unconfiguredIcePreserved; } + } + private static final class AuditResult { private final long top; private final long underwater; @@ -1039,12 +1169,21 @@ private static final class AuditResult { private final long underwaterPockets; private final long rawBedrock; private final long rawBlockEntities; + private final long exposedSnowConverted; + private final long surfaceIceConverted; + private final long buriedSnowPreserved; + private final long buriedIcePreserved; + private final long unconfiguredSnowPreserved; + private final long unconfiguredIcePreserved; AuditResult(long top, long underwater, long filler, long geology, long ceiling, long roofTop, int biomeA, int biomeB, int edgeChanges, int sentinels, long aquiferFluid, long rawNaturalSources, long structureNaturalSources, long vegetationNaturalSources, long cavePockets, long underwaterPockets, - long rawBedrock, long rawBlockEntities) { + long rawBedrock, long rawBlockEntities, + long exposedSnowConverted, long surfaceIceConverted, + long buriedSnowPreserved, long buriedIcePreserved, + long unconfiguredSnowPreserved, long unconfiguredIcePreserved) { this.top = top; this.underwater = underwater; this.filler = filler; @@ -1063,6 +1202,12 @@ private static final class AuditResult { this.underwaterPockets = underwaterPockets; this.rawBedrock = rawBedrock; this.rawBlockEntities = rawBlockEntities; + this.exposedSnowConverted = exposedSnowConverted; + this.surfaceIceConverted = surfaceIceConverted; + this.buriedSnowPreserved = buriedSnowPreserved; + this.buriedIcePreserved = buriedIcePreserved; + this.unconfiguredSnowPreserved = unconfiguredSnowPreserved; + this.unconfiguredIcePreserved = unconfiguredIcePreserved; } long top() { return top; } @@ -1083,5 +1228,11 @@ private static final class AuditResult { long underwaterPockets() { return underwaterPockets; } long rawBedrock() { return rawBedrock; } long rawBlockEntities() { return rawBlockEntities; } + long exposedSnowConverted() { return exposedSnowConverted; } + long surfaceIceConverted() { return surfaceIceConverted; } + long buriedSnowPreserved() { return buriedSnowPreserved; } + long buriedIcePreserved() { return buriedIcePreserved; } + long unconfiguredSnowPreserved() { return unconfiguredSnowPreserved; } + long unconfiguredIcePreserved() { return unconfiguredIcePreserved; } } } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 81951d3a..9685f8a1 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -359,7 +359,7 @@ private static void writeReport(Path config, List lines) { private static void writeUpgradeReport(Path config, int imported, List detail) { List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.13.113021 Upgrade Report"); + lines.add("OreSpawn 4.0.14.113021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn settings were imported into the OS4 profile."); 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 20b3ad19..e5e34b43 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -199,7 +199,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.13.113021 Upgrade Report"); + lines.add("OreSpawn 4.0.14.113021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldMaterialWeather.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldMaterialWeather.java index 72bae63a..50787d94 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldMaterialWeather.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldMaterialWeather.java @@ -54,6 +54,14 @@ private static void convertChunk(IChunk chunk, DimensionMaterials materials) { for (int localX = 0; localX < 16; localX++) { for (int localZ = 0; localZ < 16; localZ++) { int top = chunk.getTopBlockY(Heightmap.Type.MOTION_BLOCKING, localX, localZ); + // One-layer Snow is non-motion-blocking and occupies the first free + // cell immediately above this heightmap's highest occupied surface. + if (materials.snow != null && top + 1 < 256) { + cursor.setPos(minX + localX, top + 1, minZ + localZ); + if (chunk.getBlockState(cursor).getBlock() == Blocks.SNOW) { + chunk.setBlockState(cursor, materials.snow, false); + } + } for (int offset = 0; offset <= 2; offset++) { cursor.setPos(minX + localX, top - offset, minZ + localZ); IBlockState state = chunk.getBlockState(cursor); From eab2cd06b996e3e69da0342c348f948a0389b4ae Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 7 Sep 2026 23:23:21 +0100 Subject: [PATCH 6/6] Record shared 4.0.16 release identity --- .github/workflows/ci.yml | 6 +++--- CHANGELOG.txt | 9 +++++++++ README.md | 2 +- build.gradle | 12 +++++++----- docs/VERSIONS.md | 9 ++++++--- gradle.properties | 2 +- .../mc/orespawn/worldgen/LegacyConfigMigrator.java | 2 +- .../worldgen/LegacyMineralogyProfileMigration.java | 2 +- 8 files changed, 29 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fdab4e9..8535b6a9 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.0.14.113021.jar - build/libs/OreSpawn-4.0.14.113021-sources.jar - build/libs/OreSpawn-4.0.14.113021-javadoc.jar + build/libs/OreSpawn-4.0.16.113021.jar + build/libs/OreSpawn-4.0.16.113021-sources.jar + build/libs/OreSpawn-4.0.16.113021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index cb08233f..ceccc816 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Version 4.0.16.113021 + +* Adopt the shared 4.0.16 release identity. Forge 1.13 has neither + three-dimensional biome-cell attribution nor a server-side GameTest harness, + so the 4.0.15 attribution and 4.0.16 GameTest lifecycle repairs are not + applicable on this target. +* Ordinary dedicated benchmark servers continue to stop automatically when + requested. + Version 4.0.14.113021 * Convert exposed one-layer vanilla Snow from the first free cell immediately diff --git a/README.md b/README.md index 99de4378..6603a033 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ End" policy used by mods such as Base Metals. This is not the unrelated mod that adds mobs and dimensions under the same name. -This branch builds target-qualified version `4.0.14.113021`: the OreSpawn 4.0.14 +This branch builds target-qualified version `4.0.16.113021`: the OreSpawn 4.0.16 feature set for Minecraft 1.13.2 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 76b0503b..e29ea486 100644 --- a/build.gradle +++ b/build.gradle @@ -466,6 +466,7 @@ level-name=surface-integration-world level-seed=zsjpxah level-type=default online-mode=false +server-port=0 allow-nether=true generate-structures=false spawn-protection=0 @@ -731,7 +732,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.14.113021' + if (project.mod_version != '4.0.16.113021' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.13.2' || project.forge_version != '25.0.223' @@ -745,9 +746,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.14.113021.jar', - 'OreSpawn-4.0.14.113021-sources.jar', - 'OreSpawn-4.0.14.113021-javadoc.jar' + 'OreSpawn-4.0.16.113021.jar', + 'OreSpawn-4.0.16.113021-sources.jar', + 'OreSpawn-4.0.16.113021-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -764,7 +765,7 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.0.14.113021')) { + if (!file(path).getText('UTF-8').contains('4.0.16.113021')) { throw new GradleException("Release identity missing from ${path}") } } @@ -1213,6 +1214,7 @@ level-name=surface-integration-world level-seed=zsjpxah level-type=default online-mode=false +server-port=0 allow-nether=true generate-structures=false spawn-protection=0 diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 7abaf9c9..eb27966a 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -51,7 +51,7 @@ Examples: | Minecraft | Loader | Target | Example full OreSpawn version | | --- | --- | ---: | --- | -| 1.13.2 | Forge | `113021` | `4.0.14.113021` | +| 1.13.2 | Forge | `113021` | `4.0.16.113021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -150,8 +150,11 @@ static, two-dimensional biome provider, so the dynamic-registry and vertical sampler repairs released as 4.0.11 and 4.0.12 are not applicable; it advances directly to 4.0.13 for provider biome-filter parity and namespaced geome support, then to 4.0.14 so one-layer Snow above the motion-blocking surface is included -in configured weather-material conversion. A branch may therefore legitimately -skip functional version numbers. +in configured weather-material conversion. Forge 1.13 has no Y-sensitive biome +cell attribution or server-side GameTest harness, so 4.0.15 and the GameTest +lifecycle portion of 4.0.16 are not applicable; it adopts the shared 4.0.16 +identity while retaining ordinary benchmark auto-stop. A branch may therefore +legitimately skip functional version numbers. This provides three useful guarantees: diff --git a/gradle.properties b/gradle.properties index 29286d05..7e8dce16 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20190213.203750 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.14.113021 +mod_version=4.0.16.113021 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/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 9685f8a1..cd228a09 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -359,7 +359,7 @@ private static void writeReport(Path config, List lines) { private static void writeUpgradeReport(Path config, int imported, List detail) { List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.14.113021 Upgrade Report"); + lines.add("OreSpawn 4.0.16.113021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn settings were imported into the OS4 profile."); 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 e5e34b43..74646eba 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -199,7 +199,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.0.14.113021 Upgrade Report"); + lines.add("OreSpawn 4.0.16.113021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected.");