diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 155a6af6..1f6674f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,9 +132,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.0.10.119041.jar - build/libs/OreSpawn-4.0.10.119041-sources.jar - build/libs/OreSpawn-4.0.10.119041-javadoc.jar + build/libs/OreSpawn-4.0.11.119041.jar + build/libs/OreSpawn-4.0.11.119041-sources.jar + build/libs/OreSpawn-4.0.11.119041-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index c3067502..3c13841f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Version 4.0.11.119041 + +* Preserve biome-dictionary geome weights when a data-driven biome is reached + through its stable registry key rather than the object baked at startup. +* Apply ore biome include and exclude filters by stable registry key so + dynamic-registry biome instances with the same ID are treated consistently. +* Existing chunks and profile formats are unchanged; the corrections apply to + generation in affected provider biomes. + Version 4.0.10.119041 * Evaluate Stable Layers rock min_y and max_y bounds against actual world Y diff --git a/README.md b/README.md index 99400779..0808d2f7 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.119041`: the OreSpawn 4.0.10 +This branch builds target-qualified version `4.0.11.119041`: the OreSpawn 4.0.11 feature set for Minecraft 1.19.4 and Forge. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/build.gradle b/build.gradle index 8b16bfab..2b6722e1 100644 --- a/build.gradle +++ b/build.gradle @@ -845,7 +845,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.0.10.119041' + if (project.mod_version != '4.0.11.119041' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.19.4' || project.forge_version != '45.4.0' @@ -859,9 +859,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.0.10.119041.jar', - 'OreSpawn-4.0.10.119041-sources.jar', - 'OreSpawn-4.0.10.119041-javadoc.jar' + 'OreSpawn-4.0.11.119041.jar', + 'OreSpawn-4.0.11.119041-sources.jar', + 'OreSpawn-4.0.11.119041-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -878,7 +878,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.119041')) { + if (!file(path).getText('UTF-8').contains('4.0.11.119041')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 2f1a298a..614157cc 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -57,7 +57,7 @@ Examples: | 1.16.5 | Forge | `116051` | `4.0.9.116051` | | 1.17.1 | Forge | `117011` | `4.0.9.117011` | | 1.18.2 | Forge | `118021` | `4.0.10.118021` | -| 1.19.4 | Forge | `119041` | `4.0.10.119041` | +| 1.19.4 | Forge | `119041` | `4.0.11.119041` | | 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` | @@ -155,6 +155,10 @@ Forge 1.18.2 and 1.19.4 then advanced to their target-qualified 4.0.10 releases for the distinct Stable Layers actual-height eligibility repair. A branch may therefore legitimately skip functional version numbers. +Forge 1.19.4 then advanced to `4.0.11.119041` to retain biome-dictionary +weights and ore biome filters when a data-driven biome is represented by a +different runtime object with the same stable registry key. + This provides three useful guarantees: 1. A functional version is not used to describe two unrelated change sets. diff --git a/gradle.properties b/gradle.properties index a4721e6d..45c230d3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20230314.122934 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.0.10.119041 +mod_version=4.0.11.119041 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 ae36963c..7dc4f62b 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -77,6 +77,8 @@ public final class SurfaceProbeTestMod { private static final ResourceLocation BIOME_A = new ResourceLocation(MODID + ":surface_a"); private static final ResourceLocation BIOME_B = new ResourceLocation(MODID + ":surface_b"); private static final ResourceLocation PROBE_GEOME = new ResourceLocation(MODID + ":dynamic_biome_geome"); + private static final ResourceLocation PROBE_GEOME_ALTERNATIVE = + new ResourceLocation(MODID + ":dynamic_biome_geome_alternative"); private static final ResourceLocation DYNAMIC_FLUID = new ResourceLocation(MODID + ":fluid/dynamic_water"); private static final Block[] NATURAL_SOURCES = { Blocks.DIRT, Blocks.GRASS_BLOCK, Blocks.COARSE_DIRT, Blocks.PODZOL, @@ -144,19 +146,34 @@ private static void addDynamicBiomeGeology(WorldgenProvider.Builder provider) { provider.geome(PROBE_GEOME, geome -> geome .baseWeight(0.0D) .familyWeight(GeologyFamily.SEDIMENTARY, 1.0D)); + provider.geome(PROBE_GEOME_ALTERNATIVE, geome -> geome + .baseWeight(0.0D) + .familyWeight(GeologyFamily.SEDIMENTARY, 1.0D)); provider.rock(new ResourceLocation(MODID + ":rock/dynamic_biome"), blockId(Blocks.CALCITE), GeologyFamily.SEDIMENTARY, rock -> { rock.dimensions(java.util.Collections.singleton(OPEN_ID)); rock.geomeWeight(PROBE_GEOME, 1.0D); + rock.geomeWeight(PROBE_GEOME_ALTERNATIVE, 0.0D); + for (ResourceLocation geome : BUILT_IN_GEOMES) rock.geomeWeight(geome, 0.0D); + }); + provider.rock(new ResourceLocation(MODID + ":rock/dynamic_biome_alternative"), blockId(Blocks.BASALT), + GeologyFamily.SEDIMENTARY, rock -> { + rock.dimensions(java.util.Collections.singleton(OPEN_ID)); + rock.geomeWeight(PROBE_GEOME, 0.0D); + rock.geomeWeight(PROBE_GEOME_ALTERNATIVE, 1.0D); for (ResourceLocation geome : BUILT_IN_GEOMES) rock.geomeWeight(geome, 0.0D); }); - provider.rock(new ResourceLocation(MODID + ":rock/fallback"), blockId(Blocks.BASALT), + provider.rock(new ResourceLocation(MODID + ":rock/fallback"), blockId(Blocks.DEEPSLATE), GeologyFamily.SEDIMENTARY, rock -> { rock.dimensions(java.util.Collections.singleton(OPEN_ID)); rock.geomeWeight(PROBE_GEOME, 0.0D); + rock.geomeWeight(PROBE_GEOME_ALTERNATIVE, 0.0D); for (ResourceLocation geome : BUILT_IN_GEOMES) rock.geomeWeight(geome, 1.0D); }); - provider.biome(BIOME_A, java.util.Collections.singletonMap(PROBE_GEOME, 100.0D)); + Map biomeAWeights = new LinkedHashMap<>(); + biomeAWeights.put(PROBE_GEOME, 6.0D); + biomeAWeights.put(PROBE_GEOME_ALTERNATIVE, 14.0D); + provider.biome(BIOME_A, biomeAWeights); provider.biome(BIOME_B, java.util.Collections.singletonMap(PROBE_GEOME, 100.0D)); } @@ -171,6 +188,19 @@ private void enableGeologyProbe(ServerAboutToStartEvent event) { } try { root.addProperty("place_fluid_deposits", true); + root.addProperty("place_ores", true); + JsonObject dictionary = root.getAsJsonObject("biome_dictionary"); + if (dictionary == null) { + dictionary = new JsonObject(); + root.add("biome_dictionary", dictionary); + } + JsonObject cold = dictionary.getAsJsonObject("COLD"); + if (cold == null) { + cold = new JsonObject(); + dictionary.add("COLD", cold); + } + cold.addProperty(PROBE_GEOME.toString(), 8.0D); + addDynamicBiomeOre(root); JsonObject terrain = root.getAsJsonObject("terrain_dimensions"); if (terrain == null) { terrain = new JsonObject(); @@ -200,6 +230,49 @@ private void enableGeologyProbe(ServerAboutToStartEvent event) { } } + private static void addDynamicBiomeOre(JsonObject root) { + JsonObject ores = root.getAsJsonObject("ores"); + if (ores == null) { + ores = new JsonObject(); + root.add("ores", ores); + } + JsonObject ore = new JsonObject(); + ore.addProperty("block", blockId(Blocks.DIAMOND_BLOCK).toString()); + ore.addProperty("enabled", true); + ore.addProperty("native_generation", false); + ore.addProperty("suppress_vanilla", false); + ore.addProperty("retrogen", false); + JsonObject dimensions = new JsonObject(); + JsonObject end = new JsonObject(); + end.addProperty("enabled", true); + end.addProperty("min_y", 16); + end.addProperty("max_y", 48); + end.addProperty("frequency", 16.0D); + end.addProperty("quantity", 8); + end.addProperty("pattern", "cluster"); + end.addProperty("height_distribution", "uniform"); + end.addProperty("discard_chance_on_air_exposure", 0.0D); + end.addProperty("spread", 4); + end.addProperty("vertical_spread", 3); + end.addProperty("node_size", 3); + end.add("host_families", new JsonArray()); + JsonArray hosts = new JsonArray(); + hosts.add(blockId(Blocks.CALCITE).toString()); + hosts.add(blockId(Blocks.BASALT).toString()); + end.add("host_blocks", hosts); + end.add("host_tags", new JsonArray()); + end.add("geomes", new JsonObject()); + JsonArray biomes = new JsonArray(); + biomes.add(BIOME_A.toString()); + end.add("biome_ids", biomes); + end.add("excluded_biome_ids", new JsonArray()); + end.add("biome_dictionary", new JsonArray()); + end.add("excluded_biome_dictionary", new JsonArray()); + dimensions.add(OPEN_ID.toString(), end); + ore.add("dimensions", dimensions); + ores.add(MODID + ":ore/dynamic_biome_filter", ore); + } + private static void addPalette(WorldgenProvider.Builder provider, String name, ResourceLocation dimension, boolean ceiling) { BiomeSurfaceDefinition surfaceA = surface(DyeColor.PINK, DyeColor.WHITE, @@ -308,6 +381,8 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { long underwaterPockets = 0L; long rawBedrock = 0L; long rawBlockEntities = 0L; + long dictionaryPrimary = 0L; + long dictionaryAlternative = 0L; BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) { @@ -317,6 +392,10 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { LevelChunk chunk = level.getChunk(chunkX, chunkZ); int chunkMinX = chunkX << 4; int chunkMinZ = chunkZ << 4; + int centerGroundY = findMarkedGround(chunk, pos, chunkMinX + 8, chunkMinZ + 8, + level.getMinBuildHeight(), level.getMaxBuildHeight()); + ResourceLocation generationBiomeId = biomeId(level.getBiome( + pos.set(chunkMinX + 8, centerGroundY, chunkMinZ + 8))); for (int localZ = 0; localZ < 16; localZ++) { for (int localX = 0; localX < 16; localX++) { int x = chunkMinX + localX; @@ -346,8 +425,15 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { } if (!roofed) { for (int depth = 6; depth <= 8; depth++) { - assertBlock(chunk, pos, x, groundY - depth, z, - Blocks.CALCITE.defaultBlockState(), "dynamic-biome geome rock"); + BlockState geologyState = chunk.getBlockState(pos.set(x, groundY - depth, z)); + if (geologyState.is(Blocks.BASALT)) { + dictionaryAlternative++; + } else if (geologyState.is(Blocks.CALCITE)) { + dictionaryPrimary++; + } else { + throw new IllegalStateException("Unexpected dynamic-biome geome rock at " + + pos + " in " + biomeId + ": " + geologyState); + } geology++; } } @@ -364,11 +450,8 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { } } } - ResourceLocation centerBiome = biomeId(level.getBiome(pos.set(chunkMinX + 8, - findMarkedGround(chunk, pos, chunkMinX + 8, chunkMinZ + 8, - level.getMinBuildHeight(), level.getMaxBuildHeight()), chunkMinZ + 8))); - if (previousChunkBiome != null && !previousChunkBiome.equals(centerBiome)) edgeChanges++; - previousChunkBiome = centerBiome; + if (previousChunkBiome != null && !previousChunkBiome.equals(generationBiomeId)) edgeChanges++; + previousChunkBiome = generationBiomeId; sentinels += auditSentinels(level, chunk, pos, chunkMinX, chunkMinZ); if (!roofed) { NaturalSourceAudit natural = auditNaturalSources(level, chunk, pos, @@ -384,6 +467,7 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { } } + long dynamicBiomeOre = roofed ? 0L : auditDynamicBiomeOre(level); if (top != EXPECTED_COLUMNS - 9 || underwater != 9 || filler != EXPECTED_FILLER || biomeA == 0 || biomeB == 0 || edgeChanges == 0 || sentinels != 9 * 4 || geology != (roofed ? 0 : EXPECTED_FILLER) @@ -392,7 +476,9 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { || structureNaturalSources != EXPECTED_NATURAL_SOURCES || vegetationNaturalSources != EXPECTED_NATURAL_SOURCES || cavePockets != 54 || underwaterPockets != 63 - || rawBedrock != 9 || rawBlockEntities != 9))) { + || rawBedrock != 9 || rawBlockEntities != 9 + || dictionaryPrimary != EXPECTED_FILLER || dictionaryAlternative != 0 + || dynamicBiomeOre == 0))) { throw new IllegalStateException("Incomplete surface audit for " + level.dimension().location() + ": top=" + top + ", underwater=" + underwater + ", filler=" + filler + ", biomeA=" + biomeA + ", biomeB=" + biomeB + ", edges=" + edgeChanges @@ -404,13 +490,38 @@ private static AuditResult auditDimension(ServerLevel level, boolean roofed) { + ", cavePockets=" + cavePockets + ", underwaterPockets=" + underwaterPockets + ", rawBedrock=" + rawBedrock - + ", rawBlockEntities=" + rawBlockEntities); + + ", rawBlockEntities=" + rawBlockEntities + + ", dictionaryPrimary=" + dictionaryPrimary + + ", dictionaryAlternative=" + dictionaryAlternative + + ", dynamicBiomeOre=" + dynamicBiomeOre); } 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, + dictionaryPrimary, dictionaryAlternative, dynamicBiomeOre); + } + + private static long auditDynamicBiomeOre(ServerLevel level) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + long count = 0L; + for (int chunkZ = MINIMUM_CHUNK; chunkZ <= MAXIMUM_CHUNK; chunkZ++) { + for (int chunkX = MINIMUM_CHUNK; chunkX <= MAXIMUM_CHUNK; chunkX++) { + LevelChunk chunk = level.getChunk(chunkX, chunkZ); + for (int x = chunk.getPos().getMinBlockX(); x <= chunk.getPos().getMaxBlockX(); x++) { + for (int z = chunk.getPos().getMinBlockZ(); z <= chunk.getPos().getMaxBlockZ(); z++) { + for (int y = 16; y <= 48; y++) { + if (chunk.getBlockState(pos.set(x, y, z)).is(Blocks.DIAMOND_BLOCK)) count++; + } + } + } + } + } + if (count == 0L) { + throw new IllegalStateException("Dynamic-registry biome filter produced no managed ore"); + } + return count; } private static NaturalSourceAudit auditNaturalSources(ServerLevel level, LevelChunk chunk, @@ -427,7 +538,8 @@ private static NaturalSourceAudit auditNaturalSources(ServerLevel level, LevelCh int z = naturalZ(minZ, index); int groundY = findMarkedGround(chunk, pos, x, z, level.getMinBuildHeight(), level.getMaxBuildHeight()); - if (chunk.getBlockState(pos.set(x, groundY - 12, z)).is(Blocks.CALCITE)) rawConverted++; + BlockState converted = chunk.getBlockState(pos.set(x, groundY - 12, z)); + if (converted.is(Blocks.CALCITE) || converted.is(Blocks.BASALT)) rawConverted++; Block pocket = chunk.getBlockState(pos.set(x, groundY - 11, z)).getBlock(); if (index < NATURAL_SOURCES.length / 2) { if (pocket == Blocks.AIR) cavePreserved++; @@ -601,6 +713,9 @@ 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 + "dictionary_primary", Long.toString(result.dictionaryPrimary())); + values.setProperty(prefix + "dictionary_alternative", Long.toString(result.dictionaryAlternative())); + values.setProperty(prefix + "dynamic_biome_ore", Long.toString(result.dynamicBiomeOre())); } return values; } @@ -805,5 +920,6 @@ private record AuditResult(long top, long underwater, long filler, long geology, 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 dictionaryPrimary, long dictionaryAlternative, long dynamicBiomeOre) { } } diff --git a/src/biomeIntegrationTest/resources/data/forge/tags/worldgen/biome/is_cold.json b/src/biomeIntegrationTest/resources/data/forge/tags/worldgen/biome/is_cold.json new file mode 100644 index 00000000..0c73c984 --- /dev/null +++ b/src/biomeIntegrationTest/resources/data/forge/tags/worldgen/biome/is_cold.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "surfaceprobe:surface_a" + ] +} diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeTypeCompatibility.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeTypeCompatibility.java index d8a7b806..4e3abb83 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeTypeCompatibility.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/BiomeTypeCompatibility.java @@ -11,6 +11,7 @@ import java.util.Set; import net.minecraft.core.Holder; +import net.minecraft.core.Registry; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; @@ -26,10 +27,19 @@ */ final class BiomeTypeCompatibility { private static final Map>> TYPES = types(); + private static volatile Registry activeRegistry; private BiomeTypeCompatibility() { } + static void useRegistry(Registry registry) { + activeRegistry = registry; + } + + static void clearRegistry() { + activeRegistry = null; + } + static Set types(Biome biome) { Holder holder = ForgeRegistries.BIOMES.getHolder(biome).orElse(null); if (holder == null) return Collections.emptySet(); @@ -50,6 +60,16 @@ static Set biomes(String type) { static Set> biomeKeys(String type) { Set> result = new LinkedHashSet<>(); + Registry registry = activeRegistry; + if (registry != null) { + for (Map.Entry, Biome> entry : registry.entrySet()) { + if (registry.getHolder(entry.getKey()) + .map(holder -> matches(holder, tags(type))).orElse(false)) { + result.add(entry.getKey()); + } + } + return result; + } for (Biome biome : biomes(type)) { ResourceLocation id = ForgeRegistries.BIOMES.getKey(biome); if (id != null) result.add(ResourceKey.create(Registries.BIOME, id)); @@ -58,10 +78,21 @@ static Set> biomeKeys(String type) { } static boolean hasType(ResourceKey key, String type) { + Registry registry = activeRegistry; + if (registry != null) { + return registry.getHolder(key) + .map(holder -> matches(holder, tags(type))).orElse(false); + } return ForgeRegistries.BIOMES.getHolder(key) .map(holder -> matches(holder, tags(type))).orElse(false); } + static Biome biome(ResourceKey key) { + Registry registry = activeRegistry; + return registry == null ? ForgeRegistries.BIOMES.getValue(key.location()) + : registry.get(key.location()); + } + static boolean hasType(Biome biome, String type) { return ForgeRegistries.BIOMES.getHolder(biome) .map(holder -> matches(holder, tags(type))).orElse(false); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java index 84958c77..5b054461 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java @@ -288,7 +288,8 @@ private static BakedGeomeConfig bake(JsonObject root, ResourceLocation dimension return null; } Map biomeWeights = bakeBiomeWeights(geomeIndexes, biomeRules, dictionaryRules); - Map biomeWeightsById = bakeBiomeIdentifierWeights(geomeIndexes, biomeRules); + Map biomeWeightsById = bakeBiomeIdentifierWeights( + geomeIndexes, biomeRules, dictionaryRules); LOGGER.info("Baked OreSpawn geome config for '{}' with {} geomes, {} rock entries, " + "{} resolved biome profiles, {} identifier profiles, and {} formations", @@ -1040,22 +1041,53 @@ private static Map bakeBiomeWeights(Map geomeI static Map bakeBiomeIdentifierWeights(Map geomeIndexes, Map biomeRules) { + return bakeBiomeIdentifierWeights(geomeIndexes, biomeRules, Collections.emptyMap()); + } + + static Map bakeBiomeIdentifierWeights(Map geomeIndexes, + Map biomeRules, Map dictionaryRules) { + return bakeBiomeIdentifierWeights(geomeIndexes, biomeRules, dictionaryRules, + BiomeTypeCompatibility::biomeKeys); + } + + static Map bakeBiomeIdentifierWeights(Map geomeIndexes, + Map biomeRules, Map dictionaryRules, + java.util.function.Function>> dictionaryResolver) { Map result = new LinkedHashMap<>(); for (Entry entry : biomeRules.entrySet()) { try { ResourceLocation biomeId = new ResourceLocation(entry.getKey()); - double[] weights = new double[geomeIndexes.size()]; - Arrays.fill(weights, 1.0D); - merge(weights, entry.getValue()); - applyBiomeHeuristic(weights, geomeIndexes, biomeId, Float.NaN, Float.NaN); - result.put(biomeId, weights); + merge(identifierWeights(result, biomeId, geomeIndexes.size()), entry.getValue()); } catch (RuntimeException e) { LOGGER.warn("Ignoring invalid OreSpawn biome rule ID '{}'", entry.getKey()); } } + for (Entry entry : dictionaryRules.entrySet()) { + for (ResourceKey biomeKey : dictionaryResolver.apply(entry.getKey())) { + merge(identifierWeights(result, biomeKey.location(), geomeIndexes.size()), entry.getValue()); + } + } + for (Entry entry : result.entrySet()) { + Biome biome = BiomeTypeCompatibility.biome(ResourceKey.create( + Registries.BIOME, entry.getKey())); + if (biome == null) { + applyBiomeHeuristic(entry.getValue(), geomeIndexes, entry.getKey(), Float.NaN, Float.NaN); + } else { + applyBiomeHeuristic(entry.getValue(), geomeIndexes, entry.getKey(), biome); + } + } return result; } + private static double[] identifierWeights(Map result, + ResourceLocation biomeId, int geomeCount) { + return result.computeIfAbsent(biomeId, ignored -> { + double[] weights = new double[geomeCount]; + Arrays.fill(weights, 1.0D); + return weights; + }); + } + private static void applyBiomeHeuristic(double[] weights, Map geomeIndexes, ResourceLocation biomeId, Biome biome) { applyBiomeHeuristic(weights, geomeIndexes, biomeId, 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 01565bf7..6e87f991 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -358,7 +358,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.119041 Upgrade Report"); + lines.add("OreSpawn 4.0.11.119041 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 99fa5153..5c9a3b51 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -204,7 +204,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.119041 Upgrade Report"); + lines.add("OreSpawn 4.0.11.119041 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/OreSpawnOreGeneration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java index a7bdfb44..a54c7171 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java @@ -138,9 +138,10 @@ private static boolean generateChunk(WorldGenLevel world, ChunkAccess chunk, Hol ChunkPos chunkPos = chunk.getPos(); int centerX = chunkPos.getMinBlockX() + 8; int centerZ = chunkPos.getMinBlockZ() + 8; + ResourceKey biomeKey = biome.unwrapKey().orElse(null); int geome = -1; if (Level.OVERWORLD.equals(dimension)) { - ResourceLocation biomeId = biome.unwrapKey().map(ResourceKey::location).orElse(null); + ResourceLocation biomeId = biomeKey == null ? null : biomeKey.location(); geome = classifier(worldSeed).classifyColumn(biome.value(), biomeId, centerX, centerZ, scratch.geomeValues(geomeConfig.geomeCount())); } @@ -148,7 +149,7 @@ private static boolean generateChunk(WorldGenLevel world, ChunkAccess chunk, Hol boolean changed = false; for (BakedOre ore : ores) { if (retrogenOnly && !ore.retrogen) continue; - if (!ore.acceptsBiome(biome.value())) { + if (!ore.acceptsBiome(biomeKey)) { continue; } double frequency = ore.frequency; @@ -420,8 +421,8 @@ private static BakedOre bakeOre(BlockState output, BlockState deepOutput, int de } } } - Set includedBiomes = resolveBiomes(json, "biome_ids", "biome_dictionary"); - Set excludedBiomes = resolveBiomes(json, "excluded_biome_ids", "excluded_biome_dictionary"); + Set> includedBiomes = resolveBiomes(json, "biome_ids", "biome_dictionary"); + Set> excludedBiomes = resolveBiomes(json, "excluded_biome_ids", "excluded_biome_dictionary"); return new BakedOre(output, deepOutput, deepOutputMaxY, outputs, minY, maxY, Math.min(64.0D, frequency), minQuantity, maxQuantity, pattern, heightDistribution, discardChanceOnAirExposure, @@ -485,19 +486,23 @@ private static void addTags(Map target, JsonElement element, } } - private static Set resolveBiomes(JsonObject rule, String idsKey, String dictionaryKey) { - Set result = Collections.newSetFromMap(new IdentityHashMap()); + static Set> resolveBiomes(JsonObject rule, String idsKey, String dictionaryKey) { + return resolveBiomes(rule, idsKey, dictionaryKey, BiomeTypeCompatibility::biomeKeys); + } + + static Set> resolveBiomes(JsonObject rule, String idsKey, String dictionaryKey, + java.util.function.Function>> dictionaryResolver) { + Set> result = new HashSet<>(); if (rule.has(idsKey) && rule.get(idsKey).isJsonArray()) { for (JsonElement element : rule.getAsJsonArray(idsKey)) { ResourceLocation id = resource(element.getAsString()); - Biome biome = id == null ? null : ForgeRegistries.BIOMES.getValue(id); - if (biome != null) result.add(biome); + if (id != null) result.add(ResourceKey.create(Registries.BIOME, id)); } } if (rule.has(dictionaryKey) && rule.get(dictionaryKey).isJsonArray()) { for (JsonElement element : rule.getAsJsonArray(dictionaryKey)) { try { - result.addAll(BiomeTypeCompatibility.biomes(element.getAsString())); + result.addAll(dictionaryResolver.apply(element.getAsString())); } catch (RuntimeException ignored) { } } @@ -505,6 +510,13 @@ private static Set resolveBiomes(JsonObject rule, String idsKey, String d return result; } + static boolean acceptsBiome(Set> includedBiomes, + Set> excludedBiomes, ResourceKey biome) { + if (biome == null) return includedBiomes.isEmpty() && excludedBiomes.isEmpty(); + return !excludedBiomes.contains(biome) + && (includedBiomes.isEmpty() || includedBiomes.contains(biome)); + } + private static Set resolveTag(TagKey tag) { Set result = Collections.newSetFromMap(new IdentityHashMap()); for (Block block : ForgeRegistries.BLOCKS.getValues()) { @@ -609,8 +621,8 @@ private static final class BakedOre { final Map hostBlocks; final int familyMask; final double[] geomeWeights; - final Set includedBiomes; - final Set excludedBiomes; + final Set> includedBiomes; + final Set> excludedBiomes; final boolean retrogen; BakedOre(BlockState output, BlockState deepOutput, int deepOutputMaxY, BakedOutput[] outputs, @@ -619,7 +631,8 @@ private static final class BakedOre { double discardChanceOnAirExposure, int spread, int verticalSpread, int nodeSize, Map hostBlocks, int familyMask, double[] geomeWeights, - Set includedBiomes, Set excludedBiomes, boolean retrogen) { + Set> includedBiomes, Set> excludedBiomes, + boolean retrogen) { this.output = output; this.deepOutput = deepOutput; this.deepOutputMaxY = deepOutputMaxY; @@ -667,9 +680,8 @@ boolean accepts(BlockState state, Random random, BakedGeomeConfig config) { && (familyMask & (1 << family.ordinal())) != 0; } - boolean acceptsBiome(Biome biome) { - return !excludedBiomes.contains(biome) - && (includedBiomes.isEmpty() || includedBiomes.contains(biome)); + boolean acceptsBiome(ResourceKey biome) { + return OreSpawnOreGeneration.acceptsBiome(includedBiomes, excludedBiomes, biome); } } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java index 8e849305..ac362da8 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java @@ -22,6 +22,7 @@ import zone.moddev.mc.orespawn.integration.WorldgenIntegrationManager; import net.minecraft.server.MinecraftServer; +import net.minecraft.core.registries.Registries; import net.minecraft.world.level.Level; import net.minecraft.world.level.storage.LevelResource; import net.minecraftforge.event.server.ServerAboutToStartEvent; @@ -118,6 +119,8 @@ public static synchronized boolean reloadActiveProfile() { public static void onServerAboutToStart(ServerAboutToStartEvent event) { activeServer = event.getServer(); + BiomeTypeCompatibility.useRegistry(event.getServer().registryAccess() + .registryOrThrow(Registries.BIOME)); Path worldRoot = event.getServer().getWorldPath(LevelResource.ROOT).normalize(); Path profilePath = worldRoot.resolve("serverconfig").resolve(PROFILE_FILE_NAME); WorldGeologyProfile fallback = globalProfile(); @@ -174,6 +177,7 @@ public static void onServerStopped(ServerStoppedEvent event) { VanillaSpringCompatibility.clear(event.getServer().registryAccess()); activeServer = null; activeProfile = null; + BiomeTypeCompatibility.clearRegistry(); GeomeConfig.applyWorldProfile(globalProfile()); BiomeWorldgenManager.clear(); StoneReplacer.refreshWorldConfig(); diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java index 72fe1c23..95aafc3b 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/GeomeTransitionTest.java @@ -6,10 +6,14 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Test; import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.Blocks; import zone.moddev.mc.orespawn.worldgen.BakedGeomeConfig.GeomeDefinition; @@ -30,6 +34,21 @@ void configuredBiomeWeightsWorkWithoutAForgeBiomeRegistryEntry() { assertEquals(1, config.pickGeome(null, WINDSWEPT_HILLS, new double[2], 0.0D)); } + @Test + void identifierFallbackRetainsDictionaryWeightContributions() { + Map indexes = new LinkedHashMap<>(); + indexes.put("cakeworld:peppermint_fold", 0); + indexes.put("cakeworld:rock_candy_uplift", 1); + ResourceLocation marshmallowPeaks = new ResourceLocation("cakeworld", "marshmallow_peaks"); + Map weights = GeomeConfig.bakeBiomeIdentifierWeights(indexes, + Map.of(marshmallowPeaks.toString(), new double[] { 6.0D, 14.0D }), + Map.of("COLD", new double[] { 8.0D, 0.0D }), + type -> Set.of(ResourceKey.create(Registries.BIOME, marshmallowPeaks))); + + assertEquals(15.0D, weights.get(marshmallowPeaks)[0]); + assertEquals(15.0D, weights.get(marshmallowPeaks)[1]); + } + @Test void savedWorldBoundaryUsesItsConfiguredBiomeInsteadOfEqualFallbackWeights() { BakedGeomeConfig config = observedWorldConfig(); diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGenerationTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGenerationTest.java index 920b6155..c6b00bad 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGenerationTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGenerationTest.java @@ -10,6 +10,9 @@ import java.util.Map; import java.util.Set; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + import org.junit.jupiter.api.Test; import net.minecraft.resources.ResourceKey; @@ -18,6 +21,42 @@ import net.minecraft.world.level.Level; class OreSpawnOreGenerationTest { + @Test + void biomeFiltersRetainUnknownDynamicRegistryKeys() { + ResourceKey sodaOcean = ResourceKey.create(Registries.BIOME, + new ResourceLocation("cakeworld", "soda_ocean")); + JsonObject rule = new JsonObject(); + JsonArray ids = new JsonArray(); + ids.add("cakeworld:soda_ocean"); + rule.add("biome_ids", ids); + + Set resolved = OreSpawnOreGeneration.resolveBiomes( + rule, "biome_ids", "biome_dictionary"); + + assertEquals(Set.of(sodaOcean), resolved); + } + + @Test + void biomeFiltersMergeDictionaryKeys() { + ResourceKey sodaOcean = ResourceKey.create( + Registries.BIOME, new ResourceLocation("cakeworld", "soda_ocean")); + JsonObject rule = new JsonObject(); + JsonArray dictionary = new JsonArray(); + dictionary.add("OCEAN"); + rule.add("biome_dictionary", dictionary); + + Set> resolved = + OreSpawnOreGeneration.resolveBiomes(rule, "biome_ids", "biome_dictionary", + type -> Set.of(sodaOcean)); + + assertEquals(Set.of(sodaOcean), resolved); + assertTrue(OreSpawnOreGeneration.acceptsBiome(resolved, Set.of(), sodaOcean)); + assertFalse(OreSpawnOreGeneration.acceptsBiome(resolved, Set.of(), ResourceKey.create( + Registries.BIOME, new ResourceLocation("cakeworld", "candy_plains")))); + assertFalse(OreSpawnOreGeneration.acceptsBiome(Set.of(), resolved, sodaOcean)); + assertTrue(OreSpawnOreGeneration.acceptsBiome(Set.of(), resolved, ResourceKey.create( + Registries.BIOME, new ResourceLocation("cakeworld", "candy_plains")))); + } @Test void fixedQuantityDoesNotConsumeRandomState() { CountingRandom random = new CountingRandom(0);