diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4eed68e5..c89a18ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,9 +126,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.1.1.110021.jar - build/libs/OreSpawn-4.1.1.110021-sources.jar - build/libs/OreSpawn-4.1.1.110021-javadoc.jar + build/libs/OreSpawn-4.1.2.110021.jar + build/libs/OreSpawn-4.1.2.110021-sources.jar + build/libs/OreSpawn-4.1.2.110021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index cd32a97f..0ef74c03 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +Version 4.1.2.110021 + +* Respect Manage Vanilla Ores for imported OS1/OS3 rules. With it off, leave + vanilla ore generation alone and keep those rules saved but inactive. +* With vanilla management on, use the imported placement settings without + adding a second Minecraft or OreSpawn default pass. Keep saved settings, + dimension limits and existing chunks unchanged. + Version 4.1.1.110021 * Keep existing provider settings when importing older OS1 or OS3 files for diff --git a/README.md b/README.md index 4b35e455..e78cb932 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It gives mods and modpacks one place to configure ores, deposit shapes, optional rock strata and geomes, provider-owned underground fluid deposits, biome palettes and world materials, flat bedrock, and bounded ore retrogen. -This branch builds target-qualified version `4.1.1.110021`: the OreSpawn 4.1.1 +This branch builds target-qualified version `4.1.2.110021`: the OreSpawn 4.1.2 feature set for Minecraft 1.10.2 and Forge. It adds stable generation identity, non-loading geology sampling, per-resource background control, existing-world provider merge policy, an Ore Sources arbiter for equivalent provider ores and diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 5cc591bc..81ab0f81 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -123,6 +123,28 @@ replacement, retrogen, forced retrogen, flat bedrock, and bedrock thickness. Unknown numeric dimensions and obsolete block states are reported instead of guessed. Review `config/orespawn-migration/migration-report.txt` after import. +**Manage Vanilla Ores** controls imported vanilla-ore rules as well as the +modern defaults. With it off, imported vanilla rules remain saved but inactive: +they neither place ore nor suppress Minecraft's normal ore pass. Importing a +rule does not turn the switch on. Migration copies the old **Replace Vanilla +Oregen** setting when present; an existing saved world keeps its own setting. + +With management on, an enabled imported vanilla-ore rule replaces the matching +Minecraft ore pass in the dimensions it covers. Its custom frequency, height, +vein size, hosts, biome limits and retrogen setting are kept. If OreSpawn's newer +vanilla default is also enabled, the imported rule takes precedence rather +than adding a second pass. Saved Ore Sources selections are applied before +removing duplicate defaults. Disabled rules and dimensions do not claim that ore. + +This also applies to previously imported `orespawn:legacy/...` rules and the +bridge's provider-owned `:legacy/...` rules in saved world profiles. +Rules mapped to a modern provider ID keep the importer's `source_mod: legacy` +marker and follow the same switch. +The saved switch, placement settings and retrogen options are kept, +and ore already placed in existing chunks is left alone. Newly generated +chunks follow the saved management switch; existing excess ore is not removed. +Turning management off and back on does not discard the imported settings. + Global schemas 1-7 and world schemas 1-6 are upgraded in memory and persisted where safe. A schema-1 world held only mode/oil/formation choices and is overlaid on the effective installed-pack profile. A schema-2 world is already diff --git a/docs/PLAYER_GUIDE.md b/docs/PLAYER_GUIDE.md index bcc6829f..a065b905 100644 --- a/docs/PLAYER_GUIDE.md +++ b/docs/PLAYER_GUIDE.md @@ -46,6 +46,8 @@ dormant instead of breaking world creation. region. - **Manage Vanilla Ores** lets OreSpawn replace vanilla ore features with the configured OreSpawn rules. Leave it off to keep normal Minecraft placement. + Imported vanilla-ore rules stay saved but inactive while it is off. Turning + it on uses those settings without adding a second vanilla placement pass. - **Ore Sources** opens the material-group editor beside **Add Block**. Compact scrolling lists keep relevant groups on the left and the deduplicated union of installed ore blocks represented by their aliases on the right. Ordinary diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 6cb33c22..6684c80e 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -51,7 +51,7 @@ Examples: | Minecraft | Loader | Target | Example full version | | --- | --- | ---: | --- | -| 1.10.2 | Forge | `110021` | `4.1.1.110021` | +| 1.10.2 | Forge | `110021` | `4.1.2.110021` | | 1.12.2 | Forge | `112021` | `4.0.6.112021` | | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | diff --git a/gradle.properties b/gradle.properties index df156223..9e05572c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -30,7 +30,7 @@ mod_vendor=Minecraft Mod Development mod_license=LGPL-2.1 mod_license_name=GNU Lesser General Public License, Version 2.1 mod_license_url=https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -mod_version=4.1.1.110021 +mod_version=4.1.2.110021 mod_group=zone.moddev.mc.orespawn mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/gradle/release/artifacts.gradle b/gradle/release/artifacts.gradle index 11f7c36b..f2580a87 100644 --- a/gradle/release/artifacts.gradle +++ b/gradle/release/artifacts.gradle @@ -77,7 +77,7 @@ tasks.register('verifyReleaseConfiguration') { description = 'Validates the target-qualified release, API, schemas, reports, and publishing identity.' doLast { - if (project.mod_version != '4.1.1.110021' + if (project.mod_version != '4.1.2.110021' || project.mod_group != expectedMavenGroup) { throw new GradleException("Unexpected OreSpawn release version: ${project.mod_version}") } @@ -106,8 +106,8 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.1.1.110021')) { - throw new GradleException("Authoritative release location does not contain 4.1.1.110021: ${path}") + if (!file(path).getText('UTF-8').contains('4.1.2.110021')) { + throw new GradleException("Authoritative release location does not contain 4.1.2.110021: ${path}") } } if (!file('docs/API.md').getText('UTF-8').contains('orespawn@[4.0.6,5.0.0)')) { 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 394c1986..0a2bcaef 100644 --- a/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java +++ b/src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java @@ -1071,12 +1071,12 @@ private static void placeRawNaturalSources(World world, Chunk chunk, int minX, i BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); for (int index = 0; index < NATURAL_SOURCES.length; index++) { int x = naturalX(minX, index), z = naturalZ(minZ, index); - chunk.setBlockState(pos.setPos(x, GROUND_Y - 12, z), NATURAL_SOURCES[index]); - chunk.setBlockState(pos.setPos(x, GROUND_Y - 11, z), + chunk.setBlockState(pos.setPos(x, GROUND_Y - 12, z).toImmutable(), NATURAL_SOURCES[index]); + chunk.setBlockState(pos.setPos(x, GROUND_Y - 11, z).toImmutable(), index < NATURAL_SOURCES.length / 2 ? Blocks.AIR.getDefaultState() : Blocks.WATER.getDefaultState()); } - chunk.setBlockState(pos.setPos(minX + 11, GROUND_Y - 24, minZ + 12), + chunk.setBlockState(pos.setPos(minX + 11, GROUND_Y - 24, minZ + 12).toImmutable(), Blocks.BEDROCK.getDefaultState()); BlockPos chestPos = new BlockPos(minX + 12, GROUND_Y - 24, minZ + 12); world.setBlockState(chestPos, Blocks.CHEST.getDefaultState(), 2); diff --git a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java index 24b503d1..654f7b3a 100644 --- a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java +++ b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java @@ -1027,7 +1027,7 @@ private static void writeHumanUpgradeReport(Path destination) throws IOException } } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.1.1.110021 Upgrade Report"); + lines.add("OreSpawn 4.1.2.110021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn configuration was consumed and translated for OS4."); diff --git a/src/main/java/cyano/orespawn/OreSpawn.java b/src/main/java/cyano/orespawn/OreSpawn.java index 54304afe..95cdc0a4 100644 --- a/src/main/java/cyano/orespawn/OreSpawn.java +++ b/src/main/java/cyano/orespawn/OreSpawn.java @@ -19,7 +19,7 @@ public class OreSpawn { public static OreSpawn INSTANCE; public static final String MODID = "orespawn"; public static final String NAME = "OreSpawn"; - public static final String VERSION = "4.1.1.110021"; + public static final String VERSION = "4.1.2.110021"; public static final List oreSpawnConfigFiles = new ArrayList<>(); public static final List additionalStoneBlocks = new ArrayList<>(); public static boolean disableVanillaOreGen; diff --git a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java index 470a7567..33524512 100644 --- a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java +++ b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java @@ -52,7 +52,7 @@ public class OreSpawn { public static final String MODID = "orespawn"; public static final String NAME = "OreSpawn"; - public static final String VERSION = "4.1.1.110021"; + public static final String VERSION = "4.1.2.110021"; private static final Logger LOGGER = LogManager.getLogger(); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index ed77fb71..ac40f223 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -125,7 +125,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configDirectory, } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.1.1.110021 Upgrade Report"); + lines.add("OreSpawn 4.1.2.110021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + mineralogyVersion diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java index e7e3feb2..d29b0d5f 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java @@ -65,7 +65,7 @@ private static void writeInitialUpgradeReport(Path configDirectory, boolean forceRetrogen, boolean flatBedrock, boolean retrogenBedrock, int bedrockLayers) throws IOException { String newline = System.lineSeparator(); - String text = "OreSpawn 4.1.1.110021 Upgrade Report" + newline + String text = "OreSpawn 4.1.2.110021 Upgrade Report" + newline + "================================" + newline + newline + "RESULT: Legacy OreSpawn settings were imported into the OS4 profile." + newline + "- Manage vanilla ores: " + manageVanilla + newline diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java index 08932753..440ddc9c 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java @@ -201,11 +201,12 @@ private static Map discover(JsonObject root) { for (Entry oreEntry : ores.entrySet()) { if (!oreEntry.getValue().isJsonObject()) continue; JsonObject ore = oreEntry.getValue().getAsJsonObject(); - boolean oreActive = bool(ore, "enabled", true) - && (!bool(ore, "native_generation", false) || manageVanillaOres); ResourceLocation blockId = resource(string(ore, "block", oreEntry.getKey())); Block block = blockId == null ? null : ForgeRegistries.BLOCKS.getValue(blockId); if (block == null || block == Blocks.AIR) continue; + boolean vanillaRule = bool(ore, "native_generation", false) + || OreSpawnOreGeneration.isImportedVanillaOre(resource(oreEntry.getKey()), block, ore); + boolean oreActive = bool(ore, "enabled", true) && (!vanillaRule || manageVanillaOres); int metadata = integer(ore, "metadata", 0); ItemStack stack = new ItemStack(block, 1, metadata); Inference inferred = inferMaterial(root, oreNames(stack)); 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 fe67fa47..1259e511 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java @@ -345,12 +345,14 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) { if (ruleId == null) ruleId = oreId; outputBundles.put(ruleId.toString(), new BakedOutputBundle( outputState, deepOutput, deepOutputMaxY, outputs)); - if (!oreEnabled || (nativeGeneration && !manageVanillaOres)) continue; + boolean importedVanilla = !nativeGeneration && isImportedVanillaOre(ruleId, output, oreJson); + // Keep imported settings, but leave their vanilla ores alone while management is off. + if (!oreEnabled || (!manageVanillaOres && (nativeGeneration || importedVanilla))) continue; ResourceLocation material = OreSourcePolicies.material(profile, oreJson, new ItemStack(output, 1, integer(oreJson, "metadata", 0))); boolean materialDeclared = resource(string(oreJson, "material", "")) != null; BakedOreRule rule = new BakedOreRule(ruleId, output, material, materialDeclared, - manageVanillaOres && suppressVanilla); + manageVanillaOres && (importedVanilla || suppressVanilla), nativeGeneration, importedVanilla); for (Entry dimensionEntry : dimensions.entrySet()) { ResourceLocation dimensionId = resource(dimensionEntry.getKey()); @@ -423,6 +425,7 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) { } } combined = arbitrate(profile, combined, outputBundles); + combined = preferImportedVanilla(rules, combined); result.put(dimension, combined.toArray(new BakedOre[combined.size()])); vanillaOutputs.put(dimension, Collections.unmodifiableSet(suppressed)); generationScales.put(dimension, Collections.unmodifiableMap(scales)); @@ -444,6 +447,7 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) { } } selectorList = arbitrate(profile, selectorList, outputBundles); + selectorList = preferImportedVanilla(rules, selectorList); BakedOre[] selectorResult = selectorList.toArray(new BakedOre[selectorList.size()]); LOGGER.info("Baked {} OreSpawn-managed ore definitions across {} dimensions", rules.size(), result.size()); @@ -460,6 +464,36 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) { Collections.unmodifiableMap(selectorMaterialScales)); } + static boolean isImportedVanillaOre(ResourceLocation ruleId, Block output, JsonObject ore) { + // A provider-aware import may keep the provider's modern ID instead of a legacy path. + if (ruleId == null || (!ruleId.getResourcePath().startsWith("legacy/") + && !"legacy".equals(string(ore, "source_mod", "")))) return false; + + return output == Blocks.COAL_ORE || output == Blocks.IRON_ORE || output == Blocks.GOLD_ORE + || output == Blocks.REDSTONE_ORE || output == Blocks.LAPIS_ORE + || output == Blocks.DIAMOND_ORE || output == Blocks.EMERALD_ORE || output == Blocks.QUARTZ_ORE; + } + + private static List preferImportedVanilla(List rules, List ores) { + Map definitions = new HashMap<>(); + for (BakedOreRule rule : rules) definitions.put(rule.ruleId, rule); + + Set outputs = Collections.newSetFromMap(new IdentityHashMap()); + for (BakedOre ore : ores) { + BakedOreRule rule = definitions.get(ore.ruleId); + if (rule != null && rule.importedVanilla) outputs.add(rule.output); + } + if (outputs.isEmpty()) return ores; + + // Apply saved Ore Sources choices first, then remove any duplicate modern defaults. + List result = new ArrayList<>(); + for (BakedOre ore : ores) { + BakedOreRule rule = definitions.get(ore.ruleId); + if (rule == null || !rule.nativeGeneration || !outputs.contains(rule.output)) result.add(ore); + } + return result; + } + private static List arbitrate(JsonObject profile, List original, Map outputBundles) { if (!profile.has(OreSourcePolicies.SECTION) @@ -1051,18 +1085,22 @@ private static final class BakedOreRule { final ResourceLocation material; final boolean materialDeclared; final boolean suppressVanilla; + final boolean nativeGeneration; + final boolean importedVanilla; final Map explicit = new HashMap<>(); final Set explicitDimensions = new HashSet<>(); BakedOre selector; BakedOreRule(ResourceLocation ruleId, Block output, ResourceLocation material, boolean materialDeclared, - boolean suppressVanilla) { + boolean suppressVanilla, boolean nativeGeneration, boolean importedVanilla) { this.ruleId = ruleId; this.output = output; this.material = material; this.materialDeclared = materialDeclared; this.suppressVanilla = suppressVanilla; + this.nativeGeneration = nativeGeneration; + this.importedVanilla = importedVanilla; } } diff --git a/src/test/java/zone/moddev/mc/orespawn/client/OreSpawnModDirectoryModelTest.java b/src/test/java/zone/moddev/mc/orespawn/client/OreSpawnModDirectoryModelTest.java index f466c0c9..06391d78 100644 --- a/src/test/java/zone/moddev/mc/orespawn/client/OreSpawnModDirectoryModelTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/client/OreSpawnModDirectoryModelTest.java @@ -30,7 +30,7 @@ void discoversLoadedNativeLegacyUiOnlyInactiveAndRejectedIntegrations() { "directory_ui_only", parent -> new GuiScreen() { }); List loaded = Arrays.asList( - new LoadedMod("orespawn", "MMD OreSpawn", "4.1.1.110021"), + new LoadedMod("orespawn", "MMD OreSpawn", "4.1.2.110021"), new LoadedMod("directory_realistic", "Realistic Deposits", "0.1.0.110021"), new LoadedMod("directory_base", "Base Metals", "2.5.0-beta5"), new LoadedMod("directory_mineralogy", "Mineralogy", "6.0.2.110021"), diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyVanillaOreTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyVanillaOreTest.java new file mode 100644 index 00000000..fe0ba515 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyVanillaOreTest.java @@ -0,0 +1,433 @@ +package zone.moddev.mc.orespawn.worldgen; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Set; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.block.Block; +import net.minecraft.init.Blocks; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.event.RegistryEvent; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import zone.moddev.mc.orespawn.init.OreSpawnPatterns; +import zone.moddev.mc.orespawn.api.OreSpawnPatternRegistry; +import zone.moddev.mc.orespawn.test.Forge12TestBootstrap; + +class LegacyVanillaOreTest { + + @BeforeAll + static void bootstrap() { + Forge12TestBootstrap.registerVanilla(); + if (OreSpawnPatterns.registry().getValue(new ResourceLocation("orespawn:default")) == null) { + OreSpawnPatterns.registerTypes(new RegistryEvent.Register<>( + OreSpawnPatternRegistry.REGISTRY_NAME, OreSpawnPatterns.registry())); + } + } + + @Test + void importedDiamondStaysInactiveWhileVanillaManagementIsOff() throws Exception { + JsonObject profile = profile(false); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", rule(false, true)); + String original = profile.toString(); + + Object baked = bake(profile); + + assertEquals(0, ores(baked, WorldIds.OVERWORLD).length); + assertFalse(takeover(baked, WorldIds.OVERWORLD, Blocks.DIAMOND_ORE)); + assertFalse(takeover(baked, WorldIds.OVERWORLD, Blocks.IRON_ORE)); + assertFalse(takeover(baked, WorldIds.NETHER, Blocks.DIAMOND_ORE)); + assertEquals(original, profile.toString(), "baking must not rewrite a saved profile"); + } + + @Test + void explicitImportedRulesAlsoRespectTheSwitchInEveryDimension() throws Exception { + JsonObject profile = profile(false); + JsonObject imported = rule(false, true); + JsonObject placement = imported.getAsJsonObject("dimension_selectors") + .getAsJsonObject("orespawn:all_except_nether_end"); + imported.remove("dimension_selectors"); + JsonObject dimensions = new JsonObject(); + for (ResourceLocation dimension : new ResourceLocation[] { + WorldIds.OVERWORLD, WorldIds.NETHER, WorldIds.END }) { + dimensions.add(dimension.toString(), new com.google.gson.JsonParser().parse(placement.toString())); + } + imported.add("dimensions", dimensions); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", imported); + String original = profile.toString(); + + Object baked = bake(profile); + + for (ResourceLocation dimension : new ResourceLocation[] { + WorldIds.OVERWORLD, WorldIds.NETHER, WorldIds.END }) { + assertEquals(0, ores(baked, dimension).length); + assertFalse(takeover(baked, dimension, Blocks.DIAMOND_ORE)); + } + assertEquals(original, profile.toString()); + } + + @Test + void switchingOffAndBackOnPreservesTheImportedSettings() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", rule(false, true)); + String original = profile.toString(); + + assertEquals(1, ores(bake(profile), WorldIds.OVERWORLD).length); + profile.addProperty("manage_vanilla_ores", false); + Object disabled = bake(profile); + assertEquals(0, ores(disabled, WorldIds.OVERWORLD).length); + assertFalse(takeover(disabled, WorldIds.OVERWORLD, Blocks.DIAMOND_ORE)); + profile.addProperty("manage_vanilla_ores", true); + Object[] restored = ores(bake(profile), WorldIds.OVERWORLD); + assertEquals(1, restored.length); + assertEquals(new ResourceLocation("orespawn:legacy/diamond_ore"), field(restored[0], "ruleId")); + assertEquals(0.375D, field(restored[0], "frequency")); + assertEquals(7, field(restored[0], "minY")); + assertEquals(11, field(restored[0], "maxY")); + assertEquals(false, field(restored[0], "retrogen")); + assertEquals(original, profile.toString()); + } + + @Test + void aMissingManagementSwitchDefaultsToVanillaGeneration() throws Exception { + JsonObject profile = profile(false); + profile.remove("manage_vanilla_ores"); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", rule(false, true)); + + Object baked = bake(profile); + + assertEquals(0, ores(baked, WorldIds.OVERWORLD).length); + assertFalse(takeover(baked, WorldIds.OVERWORLD, Blocks.DIAMOND_ORE)); + assertFalse(profile.has("manage_vanilla_ores")); + } + + @Test + void importedSettingsWinOverModernDefaultsWhenBothAreEnabled() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", rule(false, true)); + + Object[] ores = ores(bake(profile), WorldIds.OVERWORLD); + + assertEquals(1, ores.length, "the native default must not add a second placement pass"); + assertEquals(new ResourceLocation("orespawn:legacy/diamond_ore"), field(ores[0], "ruleId")); + assertEquals(0.375D, field(ores[0], "frequency")); + assertEquals(7, field(ores[0], "minY")); + assertEquals(11, field(ores[0], "maxY")); + assertEquals(4, field(ores[0], "minQuantity")); + assertEquals(4, field(ores[0], "maxQuantity")); + assertEquals(false, field(ores[0], "retrogen")); + } + + @Test + void disabledImportedRuleDoesNotSuppressAnOtherwiseEnabledVanillaOre() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", rule(false, false)); + + Object[] ores = ores(bake(profile), WorldIds.OVERWORLD); + + assertEquals(1, ores.length); + assertEquals(new ResourceLocation("minecraft:diamond_ore"), field(ores[0], "ruleId")); + profile.addProperty("manage_vanilla_ores", false); + assertFalse(takeover(bake(profile), WorldIds.OVERWORLD, Blocks.DIAMOND_ORE)); + } + + @Test + void anExplicitDimensionLimitDoesNotRemoveTheOverworldDefault() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + JsonObject imported = rule(false, true); + JsonObject placement = imported.getAsJsonObject("dimension_selectors") + .getAsJsonObject("orespawn:all_except_nether_end"); + imported.remove("dimension_selectors"); + JsonObject dimensions = new JsonObject(); + dimensions.add("examplemod:moon", placement); + imported.add("dimensions", dimensions); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", imported); + + Object baked = bake(profile); + + assertEquals(new ResourceLocation("minecraft:diamond_ore"), + field(ores(baked, WorldIds.OVERWORLD)[0], "ruleId")); + assertTrue(takeover(baked, new ResourceLocation("examplemod:moon"), Blocks.DIAMOND_ORE)); + } + + @Test + void aDisabledDimensionOverridesAnImportedSelectorWithoutClaimingVanilla() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + JsonObject imported = rule(false, true); + JsonObject disabled = new JsonObject(); + disabled.addProperty("enabled", false); + JsonObject dimensions = new JsonObject(); + dimensions.add("minecraft:overworld", disabled); + imported.add("dimensions", dimensions); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", imported); + + Object baked = bake(profile); + + assertEquals(1, ores(baked, WorldIds.OVERWORLD).length); + assertEquals(new ResourceLocation("minecraft:diamond_ore"), + field(ores(baked, WorldIds.OVERWORLD)[0], "ruleId")); + assertTrue(takeover(baked, new ResourceLocation("examplemod:moon"), Blocks.DIAMOND_ORE)); + } + + @Test + void anExplicitOreSourceChoiceStillControlsPlacement() throws Exception { + for (String selected : new String[] { "minecraft:diamond_ore", "orespawn:legacy/diamond_ore" }) { + JsonObject profile = profile(true); + JsonObject nativeRule = rule(true, true); + nativeRule.addProperty("material", "minecraft:diamond"); + JsonObject imported = rule(false, true); + imported.addProperty("material", "minecraft:diamond"); + JsonObject dimensions = new JsonObject(); + dimensions.add("minecraft:overworld", imported.getAsJsonObject("dimension_selectors") + .getAsJsonObject("orespawn:all_except_nether_end")); + imported.remove("dimension_selectors"); + imported.add("dimensions", dimensions); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", nativeRule); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", imported); + + JsonObject policy = new JsonObject(); + policy.addProperty("mode", "consolidated"); + JsonObject outputs = new JsonObject(); + outputs.addProperty("minecraft:diamond_ore", 1.0D); + policy.add("outputs", outputs); + JsonObject placements = new JsonObject(); + placements.addProperty("orespawn:default", selected); + policy.add("placement_sources", placements); + JsonObject policies = new JsonObject(); + policies.add(OreSourcePolicies.key(new ResourceLocation("minecraft:diamond"), + WorldIds.OVERWORLD), policy); + profile.add(OreSourcePolicies.SECTION, policies); + + Object[] ores = ores(bake(profile), WorldIds.OVERWORLD); + + assertEquals(1, ores.length, selected); + assertEquals(new ResourceLocation(selected), field(ores[0], "ruleId")); + profile.addProperty("manage_vanilla_ores", false); + String saved = profile.toString(); + Object disabled = bake(profile); + assertEquals(0, ores(disabled, WorldIds.OVERWORLD).length, selected); + assertFalse(takeover(disabled, WorldIds.OVERWORLD, Blocks.DIAMOND_ORE)); + assertEquals(saved, profile.toString()); + } + } + + @Test + void ordinaryAddOnRulesKeepTheirExistingBehaviour() throws Exception { + JsonObject profile = profile(false); + profile.getAsJsonObject("ores").add("examplemod:diamond_ore", rule(false, true)); + + Object baked = bake(profile); + + assertEquals(1, ores(baked, WorldIds.OVERWORLD).length); + assertFalse(takeover(baked, WorldIds.OVERWORLD, Blocks.DIAMOND_ORE)); + } + + @Test + void providerOwnedImportsRespectTheSameManagementSwitch() throws Exception { + JsonObject profile = profile(false); + JsonObject imported = rule(false, true); + imported.addProperty("source_mod", "basemetals"); + profile.getAsJsonObject("ores").add("basemetals:legacy/diamond_ore", imported); + Object disabled = bake(profile); + assertEquals(0, ores(disabled, WorldIds.OVERWORLD).length); + assertFalse(takeover(disabled, WorldIds.OVERWORLD, Blocks.DIAMOND_ORE)); + profile.addProperty("manage_vanilla_ores", true); + Object enabled = bake(profile); + assertEquals(1, ores(enabled, WorldIds.OVERWORLD).length); + assertTrue(takeover(enabled, WorldIds.OVERWORLD, Blocks.DIAMOND_ORE)); + } + + @Test + void importedNonVanillaBlocksDoNotRequireVanillaManagement() throws Exception { + JsonObject profile = profile(false); + JsonObject imported = rule(false, true); + imported.addProperty("block", "minecraft:gold_block"); + profile.getAsJsonObject("ores").add("orespawn:legacy/gold_block", imported); + Object baked = bake(profile); + assertEquals(1, ores(baked, WorldIds.OVERWORLD).length); + assertFalse(takeover(baked, WorldIds.OVERWORLD, Blocks.GOLD_BLOCK)); + } + + @Test + void anImportMappedToAModernProviderIdStillRespectsTheSwitch() throws Exception { + JsonObject profile = profile(false); + JsonObject imported = rule(false, true); + imported.addProperty("source_mod", "legacy"); + profile.getAsJsonObject("ores").add("basemetals:custom_diamond", imported); + Object disabled = bake(profile); + assertEquals(0, ores(disabled, WorldIds.OVERWORLD).length); + assertFalse(takeover(disabled, WorldIds.OVERWORLD, Blocks.DIAMOND_ORE)); + profile.addProperty("manage_vanilla_ores", true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + Object enabled = bake(profile); + Object[] ores = ores(enabled, WorldIds.OVERWORLD); + assertEquals(1, ores.length); + assertEquals(new ResourceLocation("basemetals:custom_diamond"), field(ores[0], "ruleId")); + assertTrue(takeover(enabled, WorldIds.OVERWORLD, Blocks.DIAMOND_ORE)); + } + + @Test + void oreSourcesShowsImportedVanillaPlacementsAsInactiveWhileOff() throws Exception { + for (String id : new String[] { "orespawn:legacy/diamond_ore", "basemetals:legacy/diamond_ore" }) { + JsonObject profile = profile(false); + JsonObject imported = rule(false, true); + imported.addProperty("source_mod", "basemetals"); + imported.addProperty("material", "minecraft:diamond"); + profile.getAsJsonObject("ores").add(id, imported); + OreSourcePolicies.initialize(profile, true); + OreSourcePolicies.Candidate candidate = OreSourcePolicies.snapshot(profile).groups().get(0).candidates.get(0); + assertEquals(id, candidate.sourceId); + assertTrue(candidate.loaded); + assertFalse(candidate.active); + profile.addProperty("manage_vanilla_ores", true); + OreSourcePolicies.initialize(profile, true); + assertTrue(OreSourcePolicies.snapshot(profile).groups().get(0).candidates.get(0).active); + } + } + + @Test + void allEightImportedVanillaOresRespectOffAndOn() throws Exception { + Block[] blocks = { Blocks.COAL_ORE, Blocks.IRON_ORE, Blocks.GOLD_ORE, Blocks.REDSTONE_ORE, + Blocks.LAPIS_ORE, Blocks.DIAMOND_ORE, Blocks.EMERALD_ORE, Blocks.QUARTZ_ORE }; + for (Block block : blocks) { + JsonObject profile = profile(false); + JsonObject imported = rule(false, true); + imported.addProperty("block", block.getRegistryName().toString()); + profile.getAsJsonObject("ores").add("orespawn:legacy/" + block.getRegistryName().getResourcePath(), imported); + Object disabled = bake(profile); + assertEquals(0, ores(disabled, WorldIds.OVERWORLD).length, block.getRegistryName().toString()); + assertFalse(takeover(disabled, WorldIds.OVERWORLD, block), block.getRegistryName().toString()); + profile.addProperty("manage_vanilla_ores", true); + Object enabled = bake(profile); + assertEquals(1, ores(enabled, WorldIds.OVERWORLD).length, block.getRegistryName().toString()); + assertTrue(takeover(enabled, WorldIds.OVERWORLD, block), block.getRegistryName().toString()); + } + } + + @Test + void invalidImportedRulesDoNotSuppressVanillaWhenManagementIsOn() throws Exception { + for (boolean disabledSelector : new boolean[] { false, true }) { + JsonObject profile = profile(true); + JsonObject imported = rule(false, true); + JsonObject placement = imported.getAsJsonObject("dimension_selectors") + .getAsJsonObject("orespawn:all_except_nether_end"); + if (disabledSelector) placement.addProperty("enabled", false); + else placement.addProperty("frequency", 0.0D); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", imported); + Object baked = bake(profile); + assertEquals(0, ores(baked, WorldIds.OVERWORLD).length); + assertFalse(takeover(baked, WorldIds.OVERWORLD, Blocks.DIAMOND_ORE)); + } + } + + @Test + void importedNetherQuartzDoesNotTakeOverOtherDimensions() throws Exception { + JsonObject profile = profile(true); + JsonObject imported = rule(false, true); + JsonObject placement = imported.getAsJsonObject("dimension_selectors") + .getAsJsonObject("orespawn:all_except_nether_end"); + imported.remove("dimension_selectors"); + imported.addProperty("block", "minecraft:quartz_ore"); + JsonObject dimensions = new JsonObject(); + dimensions.add(WorldIds.NETHER.toString(), placement); + imported.add("dimensions", dimensions); + profile.getAsJsonObject("ores").add("orespawn:legacy/quartz_ore", imported); + Object baked = bake(profile); + assertTrue(takeover(baked, WorldIds.NETHER, Blocks.QUARTZ_ORE)); + assertFalse(takeover(baked, WorldIds.OVERWORLD, Blocks.QUARTZ_ORE)); + assertFalse(takeover(baked, WorldIds.END, Blocks.QUARTZ_ORE)); + } + + @Test + void importedDiamondDoesNotRemoveTheIndependentIronDefault() throws Exception { + JsonObject profile = profile(true); + JsonObject iron = rule(true, true); + iron.addProperty("block", "minecraft:iron_ore"); + profile.getAsJsonObject("ores").add("minecraft:iron_ore", iron); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", rule(false, true)); + Object[] ores = ores(bake(profile), WorldIds.OVERWORLD); + assertEquals(2, ores.length); + for (Object ore : ores) assertNotEquals(new ResourceLocation("minecraft:diamond_ore"), field(ore, "ruleId")); + } + + @Test + void intentionalLegacyDeclarationsAreNotCollapsedIntoOneRule() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("minecraft:diamond_ore", rule(true, true)); + profile.getAsJsonObject("ores").add("orespawn:legacy/a/diamond_ore", rule(false, true)); + profile.getAsJsonObject("ores").add("orespawn:legacy/b/diamond_ore", rule(false, true)); + Object baked = bake(profile); + Object[] ores = ores(baked, WorldIds.OVERWORLD); + assertEquals(2, ores.length); + for (Object ore : ores) assertNotEquals(new ResourceLocation("minecraft:diamond_ore"), field(ore, "ruleId")); + assertTrue(takeover(baked, WorldIds.OVERWORLD, Blocks.DIAMOND_ORE)); + } + + private static JsonObject profile(boolean manageVanilla) { + JsonObject profile = new JsonObject(); + profile.addProperty("manage_vanilla_ores", manageVanilla); + profile.add("ores", new JsonObject()); + return profile; + } + + private static JsonObject rule(boolean nativeGeneration, boolean enabled) { + JsonObject ore = new JsonObject(); + ore.addProperty("block", "minecraft:diamond_ore"); + ore.addProperty("native_generation", nativeGeneration); + ore.addProperty("enabled", enabled); + ore.addProperty("retrogen", false); + JsonObject placement = new JsonObject(); + placement.addProperty("frequency", nativeGeneration ? 1.0D : 0.375D); + placement.addProperty("min_y", nativeGeneration ? 0 : 7); + placement.addProperty("max_y", nativeGeneration ? 15 : 11); + placement.addProperty("quantity", nativeGeneration ? 8 : 4); + placement.addProperty("pattern", "orespawn:default"); + JsonArray hosts = new JsonArray(); + hosts.add(new JsonPrimitive("minecraft:stone")); + placement.add("host_blocks", hosts); + JsonObject dimensions = new JsonObject(); + dimensions.add(nativeGeneration ? "minecraft:overworld" : "orespawn:all_except_nether_end", placement); + ore.add(nativeGeneration ? "dimensions" : "dimension_selectors", dimensions); + return ore; + } + + private static Object bake(JsonObject profile) throws Exception { + Method geology = GeomeConfig.class.getDeclaredMethod("bake", JsonObject.class, ResourceLocation.class); + geology.setAccessible(true); + Object config = geology.invoke(null, new JsonObject(), WorldIds.OVERWORLD); + Method bake = OreSpawnOreGeneration.class.getDeclaredMethod("bakeOres", JsonObject.class, BakedGeomeConfig.class); + bake.setAccessible(true); + return bake.invoke(null, profile, config); + } + + @SuppressWarnings("unchecked") + private static Object[] ores(Object baked, ResourceLocation dimension) throws Exception { + Object[] explicit = ((Map) field(baked, "byDimension")).get(dimension); + return explicit != null ? explicit : (Object[]) field(baked, "selectorOres"); + } + + @SuppressWarnings("unchecked") + private static boolean takeover(Object baked, ResourceLocation dimension, Block block) throws Exception { + Set explicit = ((Map>) field(baked, "vanillaOutputs")).get(dimension); + return explicit != null ? explicit.contains(block) : OreSpawnOreGeneration.selectorAllows(dimension) + && ((Set) field(baked, "selectorVanillaOutputs")).contains(block); + } + + private static Object field(Object owner, String name) throws Exception { + Field field = owner.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(owner); + } +} diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/SurfaceFixturePositionTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/SurfaceFixturePositionTest.java new file mode 100644 index 00000000..9e9e7696 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/SurfaceFixturePositionTest.java @@ -0,0 +1,27 @@ +package zone.moddev.mc.orespawn.worldgen; + +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.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; + +class SurfaceFixturePositionTest { + @Test + void rawFixtureWritesUseImmutablePositions() throws Exception { + String source = new String(Files.readAllBytes(Paths.get( + "src/biomeIntegrationTest/java/zone/moddev/mc/orespawn/testmod/SurfaceProbeTestMod.java")), + StandardCharsets.UTF_8); + int start = source.indexOf("private static void placeRawNaturalSources("); + assertTrue(start >= 0); + int end = source.indexOf("\n\tprivate static ", start + 1); + String setup = source.substring(start, end); + assertFalse(Pattern.compile("chunk\\.setBlockState\\(pos\\.setPos\\([^)]*\\),").matcher(setup).find(), + "Falling blocks may schedule a tick as soon as the fixture places them"); + assertEquals(3, setup.split("\\.toImmutable\\(\\)", -1).length - 1); + } +}