diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08243bbe..39b1a869 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.0.113021.jar - build/libs/OreSpawn-4.1.0.113021-sources.jar - build/libs/OreSpawn-4.1.0.113021-javadoc.jar + build/libs/OreSpawn-4.1.6.113021.jar + build/libs/OreSpawn-4.1.6.113021-sources.jar + build/libs/OreSpawn-4.1.6.113021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d39eb57c..a8585068 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,17 @@ +Version 4.1.6.113021 + +* Leave vanilla ores to Minecraft when Manage Vanilla Ores is off. When + it is on, an imported rule takes over without an extra copy of the ore. +* Keep saved Ore Sources choices for imported patterns. Built-in patterns + share the standard channel for new choices; custom channels stay separate. +* Show vanilla ores in Ore Sources before creating a world, even when Manage + Vanilla Ores is off. +* Keep imported provider rules when opening a saved world. Back up the + profile before removing an untouched duplicate; keep edited rules for review. +* Choose sulfur and lithium defaults after mods have loaded, including Base + Electric. Existing worlds keep their choices. +* Fix the Austrian German language ID and add Pirate Speak menu and guide text. + Version 4.1.0.113021 * Fix managed Nether quartz hosts on Forge 1.13.2, including saved profiles diff --git a/README.md b/README.md index 49e436ae..a3bfd44b 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.1.0.113021` for Minecraft 1.13.2 +This branch builds target-qualified version `4.1.6.113021` for Minecraft 1.13.2 and Forge. It adds material-group ore arbitration, exact biome replacements, and a directory for installed provider mods to the OreSpawn 4.0.16 base. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 9b72e899..475ca950 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -185,6 +185,9 @@ Each enabled ore dimension uses: | `placement_channel` | registry ID | Independent placement budget shared by equivalent ores in consolidated modes | At least one of `host_families`, `host_blocks`, or `host_tags` must be present. +On 1.13.2, plain stone, granite, diorite, and andesite are separate block +IDs. Use `host_blocks: ["minecraft:stone"]` for plain stone only; no +pre-flattening metadata is needed. On Forge 1.13.2, the default Nether quartz rule uses `host_blocks: ["minecraft:netherrack"]`, because Forge does not ship a diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 04b6b4e2..5f2c7344 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -11,6 +11,18 @@ and are called out in `orespawn-migration/migration-report.txt` for manual review. Files owned by mods without an active provider retain the original legacy-ID behaviour. +Importing a rule does not turn on **Manage Vanilla Ores**. With that +setting off, Minecraft still generates its own ores and the imported vanilla +rules remain available in the profile for later use. Turning it on lets an +active imported rule replace the modern default for that ore and dimension. + +If a saved world has a proven legacy rule and an unchanged duplicate from the +same provider, OreSpawn keeps the imported rule and backs up the profile as +`serverconfig/orespawn-worldgen.pre-legacy-ore-repair.bak` before removing the +duplicate. Edited or ambiguous rules are kept and noted in the log. Old +metadata is not used to guess a new block ID, and existing chunks are not +regenerated by the repair. + Migration is non-destructive. OreSpawn writes `config/orespawn-worldgen.json` only when that target does not already exist and retains every source file. When legacy OreSpawn rules are translated, a concise player-facing summary is diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 46f75b22..8fbb2a6c 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -124,3 +124,8 @@ referenced biome; ordinary compatibility hints belong in `similar_biomes`. See `examples/examplemod-orespawn.json` for rocks, weighted ore output, a fluid deposit, a custom dimension, biome palette, world materials, and a selectable template. + +Ore Sources chooses defaults once mods finish loading. Both `baseelectric` +and `electricadvantage` are recognised for sulfur and lithium, but +automatic balancing still requires loaded providers with an exact shared +ore tag. Existing worlds keep their saved choices. diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index e3ad1184..d2f90f4b 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.1.0.113021` | +| 1.13.2 | Forge | `113021` | `4.1.6.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` | diff --git a/gradle.properties b/gradle.properties index ce4e04c2..2753eb54 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.1.0.113021 +mod_version=4.1.6.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/gradle/release/artifacts.gradle b/gradle/release/artifacts.gradle index 75daaa86..98d5ca2e 100644 --- a/gradle/release/artifacts.gradle +++ b/gradle/release/artifacts.gradle @@ -81,7 +81,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.1.0.113021' + if (project.mod_version != '4.1.6.113021' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.13.2' || project.forge_version != '25.0.223' @@ -95,9 +95,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.1.0.113021.jar', - 'OreSpawn-4.1.0.113021-sources.jar', - 'OreSpawn-4.1.0.113021-javadoc.jar' + 'OreSpawn-4.1.6.113021.jar', + 'OreSpawn-4.1.6.113021-sources.jar', + 'OreSpawn-4.1.6.113021-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -114,7 +114,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.1.0.113021')) { + if (!file(path).getText('UTF-8').contains('4.1.6.113021')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/src/clientIntegrationTest/java/zone/moddev/mc/orespawn/client/ClientProbeTestMod.java b/src/clientIntegrationTest/java/zone/moddev/mc/orespawn/client/ClientProbeTestMod.java index 324b9d7f..81f2b429 100644 --- a/src/clientIntegrationTest/java/zone/moddev/mc/orespawn/client/ClientProbeTestMod.java +++ b/src/clientIntegrationTest/java/zone/moddev/mc/orespawn/client/ClientProbeTestMod.java @@ -17,6 +17,7 @@ import net.minecraft.client.gui.GuiCreateWorld; import net.minecraft.client.gui.GuiMainMenu; import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; import net.minecraft.util.text.TextFormatting; import net.minecraft.world.GameType; import net.minecraft.world.WorldSettings; @@ -48,6 +49,7 @@ public final class ClientProbeTestMod { private boolean worldSettingsOpened; private boolean longEditorRoundTrip; private boolean oreSourceFixturesVerified; + private boolean vanillaOreSourcesVerified; private List worldCreationButtons; public ClientProbeTestMod() { @@ -117,6 +119,21 @@ private void handleClientTick() { worldSettingsOpened = true; validateCaptions((OreSpawnWorldSettingsScreen) minecraft.currentScreen); validateLongEditorRoundTrip(minecraft, minecraft.currentScreen); + String localizedCaption = System.getProperty("clientprobe.recommendedCaption", ""); + if (!localizedCaption.isEmpty()) { + String actual = I18n.format("button.orespawn.recommended"); + if (!localizedCaption.equals(actual)) { + throw new IllegalStateException("Unexpected Recommended Defaults caption: " + actual); + } + Properties values = new Properties(); + values.setProperty("recommended", actual); + try (FileOutputStream output = new FileOutputStream("client-locale-pass.properties")) { + values.store(output, "OreSpawn localized editor check"); + } + minecraft.shutdown(); + nextState(10); + break; + } if (Boolean.getBoolean("clientprobe.oreSourcesFixtures")) { validateOreSourceFixtures(minecraft, minecraft.currentScreen); oreSourceFixturesVerified = true; @@ -202,8 +219,8 @@ private Button nextNavigationButton(OreSpawnWorldSettingsScreen root) { Button button = (Button) widget; String caption = TextFormatting.getTextWithoutFormattingCodes(button.getMessage()); if (!attemptedButtons.add(caption)) continue; - String lower = caption.toLowerCase(java.util.Locale.ROOT); - if (lower.equals("done") || lower.equals("cancel") || lower.contains("recommended")) continue; + if (I18n.format("gui.done").equals(caption) || I18n.format("gui.cancel").equals(caption) + || I18n.format("button.orespawn.recommended").equals(caption)) continue; return button; } return null; @@ -224,7 +241,7 @@ private static void validateCaptions(OreSpawnScreen screen) { } } - private static void validateOreSourcesRoute(Minecraft minecraft, OreSpawnScreen materials) { + private void validateOreSourcesRoute(Minecraft minecraft, OreSpawnScreen materials) { Button ores = buttonWithCaption(materials, net.minecraft.client.resources.I18n.format("tab.orespawn.ores")); if (ores == null) throw new IllegalStateException("Missing ORES tab"); @@ -239,6 +256,10 @@ private static void validateOreSourcesRoute(Minecraft minecraft, OreSpawnScreen throw new IllegalStateException("Ore Sources route did not open"); } OreSourceListScreen opened = (OreSourceListScreen) minecraft.currentScreen; + if (!vanillaOreSourcesVerified && Boolean.getBoolean("clientprobe.vanillaOreSources")) { + validateVanillaOreSources(opened); + vanillaOreSourcesVerified = true; + } validateEmptyOreSources(minecraft, materials); Button addGroup = buttonWithCaption(opened, "+"); if (addGroup == null) throw new IllegalStateException("Missing Add Group control"); @@ -254,6 +275,55 @@ private static void validateOreSourcesRoute(Minecraft minecraft, OreSpawnScreen } } + private static void validateVanillaOreSources(OreSourceListScreen screen) { + WorldGeologyProfile pending = WorldGeologyProfileManager.pendingNewWorldProfile(); + if (pending.manageVanillaOres()) { + throw new IllegalStateException("The standalone Show All check must run with vanilla management off"); + } + validateVanillaOreRows(screen); + GeologyEditorSession session = new GeologyEditorSession(pending); + JsonObject before = session.profile().rootCopy(); + for (GeologyEditorSession.OreSourceGroup group : session.oreSourceGroups()) { + if (group.hasManagedPlacementSource() || !"keep_separate".equals(group.mode)) { + throw new IllegalStateException("Show All should not put OreSpawn in charge of vanilla generation"); + } + } + if (!before.equals(session.profile().rootCopy())) { + throw new IllegalStateException("Listing vanilla ores must not change the profile"); + } + System.out.println("VANILLA_ORE_SOURCES_PASS management=off rows=8 profile=unchanged"); + } + + private static void validateVanillaOreRows(OreSourceListScreen screen) { + if (firstList(screen).size() != 0) { + throw new IllegalStateException("Routine vanilla ores should start hidden"); + } + Button showAll = buttonWithCaption(screen, I18n.format("button.orespawn.show_all")); + if (showAll == null) throw new IllegalStateException("Missing Show All control"); + ((GuiButton) showAll).onClick(0, 0); + CompactScrollList groups = firstList(screen); + String[] expected = { "Coal", "Diamond", "Emerald", "Gold", "Iron", "Lapis", "Quartz", "Redstone" }; + if (groups.size() != expected.length) { + throw new IllegalStateException("Show All should list eight vanilla ores, found " + groups.size()); + } + for (int row = 0; row < expected.length; row++) { + if (!groups.rowText(row).startsWith(expected[row] + " - ")) { + throw new IllegalStateException("Unexpected vanilla ore row: " + groups.rowText(row)); + } + } + Button hideSingles = buttonWithCaption(screen, I18n.format("button.orespawn.ore_source.hide_single")); + if (hideSingles == null) throw new IllegalStateException("Missing Hide Singles control"); + ((GuiButton) hideSingles).onClick(0, 0); + if (firstList(screen).size() != 0) throw new IllegalStateException("Hide Singles should hide vanilla ores"); + } + + private static CompactScrollList firstList(OreSpawnScreen screen) { + for (GuiButton widget : screen.qualificationButtons()) { + if (widget instanceof CompactScrollList) return (CompactScrollList) widget; + } + throw new IllegalStateException("Missing Groups list"); + } + private static void validateEmptyOreSources(Minecraft minecraft, GuiScreen parent) { for (int[] size : new int[][] { { 426, 265 }, { 320, 240 } }) { OreSourceListScreen empty = new OreSourceListScreen(parent, @@ -440,7 +510,7 @@ private static void pressDone(OreSpawnScreen screen) { for (GuiButton widget : screen.qualificationButtons()) { if (!(widget instanceof Button)) continue; String caption = TextFormatting.getTextWithoutFormattingCodes(((Button) widget).getMessage()); - if ("done".equalsIgnoreCase(caption)) { + if (I18n.format("gui.done").equals(caption)) { ((GuiButton) widget).onClick(0, 0); return; } @@ -462,6 +532,7 @@ private void writeMarker() throws IOException { values.setProperty("world_settings_opened", Boolean.toString(worldSettingsOpened)); values.setProperty("long_editor_roundtrip", Boolean.toString(longEditorRoundTrip)); values.setProperty("ore_source_fixtures_verified", Boolean.toString(oreSourceFixturesVerified)); + values.setProperty("vanilla_ore_sources_verified", Boolean.toString(vanillaOreSourcesVerified)); values.setProperty("editor_routes", Integer.toString(editorRoutes.size())); values.setProperty("editor_classes", editorRoutes.toString()); values.setProperty("first_world_rendered", Boolean.toString(firstWorldFrames >= 8)); diff --git a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java index beaaa78d..56d58776 100644 --- a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java +++ b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java @@ -87,7 +87,7 @@ private void processInterMod(final InterModProcessEvent event) { // Files are authoritative, so scan them before accepting API submissions. WorldgenIntegrationManager.initialize(); WorldgenIntegrationManager.processImcMessages(); - GeomeConfig.bake(); + GeomeConfig.bakeBeforeLoadComplete(); OreSpawnOreGeneration.refreshWorldConfig(); FlatBedrockFeature.refreshWorldConfig(); OreRetrogenManager.refreshWorldConfig(); @@ -96,7 +96,7 @@ private void processInterMod(final InterModProcessEvent event) { private void setup(final FMLCommonSetupEvent event) { OreSpawnConfig.bake(); WorldgenIntegrationManager.initialize(); - GeomeConfig.bake(); + GeomeConfig.bakeBeforeLoadComplete(); logGeomeSampler(); BiomeWorldgenBootstrap.registerCodecs(); DocumentationExporter.exportBundledGuide(); 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 a871f7d4..37fe4af9 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java @@ -404,7 +404,7 @@ List oreSourceGroups() { List result = new ArrayList<>(); for (Entry entry : section("ore_source_policies").entrySet()) { if (!entry.getValue().isJsonObject()) continue; - JsonObject policy = entry.getValue().getAsJsonObject(); + JsonObject policy = OreSourceEditorView.policy(root, entry.getValue().getAsJsonObject()); String material = string(policy, "material", ""); String domain = string(policy, "domain", ""); if (!validResource(material) || !validResource(domain)) continue; @@ -454,6 +454,7 @@ List oreSourceGroups() { candidates, decimalMap(policy, "outputs"), stringMap(policy, "placement_sources")); if (!bool(policy, "dormant", false)) result.add(group); } + OreSourceEditorView.addVanillaPreviews(root, result); Collections.sort(result, (left, right) -> { int name = left.displayName.compareToIgnoreCase(right.displayName); if (name != 0) return name; @@ -976,8 +977,8 @@ private static boolean hasManagedPlacementSource(JsonObject policy) { } private static void refreshOreSourceStatus(JsonObject policy) { - Set missing = new HashSet<>(); - Set inactivePlacements = new HashSet<>(); + Set loaded = new HashSet<>(); + Set loadedPlacements = new HashSet<>(); boolean external = false; boolean review = bool(policy, "review_required", false); if (policy.has("candidates") && policy.get("candidates").isJsonArray()) { @@ -985,8 +986,10 @@ private static void refreshOreSourceStatus(JsonObject policy) { if (!element.isJsonObject()) continue; JsonObject candidate = element.getAsJsonObject(); String sourceId = string(candidate, "source_id", ""); - if (!bool(candidate, "loaded", false)) missing.add(sourceId); - if (!bool(candidate, "placement_active", false)) inactivePlacements.add(sourceId); + if (bool(candidate, "loaded", false)) { + loaded.add(sourceId); + loadedPlacements.add(sourceId + "|" + string(candidate, "placement_channel", "orespawn:standard")); + } external |= bool(candidate, "external", false) && bool(candidate, "loaded", false); } } @@ -998,10 +1001,10 @@ private static void refreshOreSourceStatus(JsonObject policy) { return; } for (String id : outputs.keySet()) { - if (missing.contains(id)) { policy.addProperty("status", "missing_source"); return; } + if (!loaded.contains(id)) { policy.addProperty("status", "missing_source"); return; } } - for (String id : placements.values()) { - if (missing.contains(id) || inactivePlacements.contains(id)) { + for (Entry placement : placements.entrySet()) { + if (!loadedPlacements.contains(placement.getValue() + "|" + placement.getKey())) { policy.addProperty("status", "missing_source"); return; } } @@ -1045,11 +1048,20 @@ static final class OreSourceGroup { final List candidates; final Map outputs; final Map placements; + final boolean preview; OreSourceGroup(String key, String material, String domain, String displayName, List blockTagEntries, boolean curated, String mode, String outputMode, String status, List candidates, Map outputs, Map placements) { + this(key, material, domain, displayName, blockTagEntries, curated, mode, outputMode, status, + candidates, outputs, placements, false); + } + + OreSourceGroup(String key, String material, String domain, String displayName, + List blockTagEntries, boolean curated, String mode, String outputMode, String status, + List candidates, Map outputs, + Map placements, boolean preview) { this.key = key; this.material = material; this.domain = domain; @@ -1062,6 +1074,7 @@ static final class OreSourceGroup { this.candidates = Collections.unmodifiableList(new ArrayList<>(candidates)); this.outputs = outputs; this.placements = placements; + this.preview = preview; } List channels() { diff --git a/src/main/java/zone/moddev/mc/orespawn/client/OreSourceEditorView.java b/src/main/java/zone/moddev/mc/orespawn/client/OreSourceEditorView.java new file mode 100644 index 00000000..cd38d6ef --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/OreSourceEditorView.java @@ -0,0 +1,169 @@ +package zone.moddev.mc.orespawn.client; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.minecraft.tags.BlockTags; +import net.minecraft.util.ResourceLocation; +import zone.moddev.mc.orespawn.client.GeologyEditorSession.OreSourceCandidate; +import zone.moddev.mc.orespawn.client.GeologyEditorSession.OreSourceGroup; +import zone.moddev.mc.orespawn.util.JsonCopies; + +/** Builds the editor's view without rewriting the world's saved choices. */ +final class OreSourceEditorView { + private static final String STANDARD = "orespawn:standard"; + private static final Set OLD_CHANNELS = new HashSet<>(Arrays.asList( + "orespawn:default", "orespawn:vein", "orespawn:normal_cloud", + "orespawn:precision", "orespawn:clusters", "orespawn:underfluids")); + + private OreSourceEditorView() { } + + static void addVanillaPreviews(JsonObject root, List groups) { + // Forge ships these tags, but Create New World can open before server data loads. + // Keep these rows out of the profile so they never participate in world generation. + addVanillaPreview(root, groups, "coal", "Coal", "coal_ore", "minecraft:overworld"); + addVanillaPreview(root, groups, "diamond", "Diamond", "diamond_ore", "minecraft:overworld"); + addVanillaPreview(root, groups, "emerald", "Emerald", "emerald_ore", "minecraft:overworld"); + addVanillaPreview(root, groups, "gold", "Gold", "gold_ore", "minecraft:overworld"); + addVanillaPreview(root, groups, "iron", "Iron", "iron_ore", "minecraft:overworld"); + addVanillaPreview(root, groups, "lapis", "Lapis", "lapis_ore", "minecraft:overworld"); + addVanillaPreview(root, groups, "quartz", "Quartz", "nether_quartz_ore", "minecraft:the_nether"); + addVanillaPreview(root, groups, "redstone", "Redstone", "redstone_ore", "minecraft:overworld"); + } + + private static void addVanillaPreview(JsonObject root, List groups, + String name, String label, String block, String domain) { + String blockId = "minecraft:" + block; + String material = "orespawn:" + name; + String key = material + '|' + domain; + String tag = "forge:ores/" + name; + JsonObject ore = object(object(root, "ores"), blockId); + String output = text(ore, "block"); + if (!flag(ore, "native_generation", false) || !object(ore, "dimensions").has(domain) + || ore.has("material") || (!output.isEmpty() && !blockId.equals(output)) + || BlockTags.getCollection().get(new ResourceLocation(tag)) != null) return; + for (OreSourceGroup group : groups) { + if (key.equals(group.key)) return; + if (domain.equals(group.domain) && group.candidates.stream() + .anyMatch(candidate -> blockId.equals(candidate.registryId))) return; + } + + List tags = Collections.singletonList(tag); + boolean managed = flag(root, "manage_vanilla_ores", false) && flag(ore, "enabled", true) + && flag(object(object(ore, "dimensions"), domain), "enabled", true); + OreSourceCandidate candidate = new OreSourceCandidate(blockId, "minecraft", "Minecraft", "1.13.2", + blockId, 0, STANDARD, tags, true, managed, false, false, false); + groups.add(new OreSourceGroup(key, material, domain, label, tags, false, "keep_separate", "custom", + "separate", Collections.singletonList(candidate), Collections.singletonMap(blockId, 1.0D), + Collections.emptyMap(), true)); + } + + private static boolean flag(JsonObject object, String key, boolean fallback) { + return object.has(key) && object.get(key).isJsonPrimitive() ? object.get(key).getAsBoolean() : fallback; + } + + static JsonObject policy(JsonObject root, JsonObject saved) { + JsonObject view = JsonCopies.copy(saved); + JsonObject placements = object(view, "placement_sources"); + JsonArray candidates = view.has("candidates") && view.get("candidates").isJsonArray() + ? view.getAsJsonArray("candidates") : new JsonArray(); + String domain = text(view, "domain"); + List aliases = new ArrayList<>(); + + for (Map.Entry entry : placements.entrySet()) { + String channel = entry.getKey(); + if (!OLD_CHANNELS.contains(channel) || !entry.getValue().isJsonPrimitive()) continue; + + String source = entry.getValue().getAsString(); + if (inferredAlias(root, source, domain, channel) + && loadedCandidate(candidates, source, STANDARD) != null) { + aliases.add(channel); + } + } + + // More than one old pattern may have been selected deliberately. Keep + // those rows separate rather than choosing a winner on the player's behalf. + boolean combine = placements.has(STANDARD) || aliases.size() == 1; + if (combine) { + for (String alias : aliases) { + if (!placements.has(STANDARD)) placements.add(STANDARD, placements.get(alias)); + placements.remove(alias); + } + } + + JsonArray visible = new JsonArray(); + for (JsonElement element : candidates) { + if (!element.isJsonObject()) continue; + JsonObject candidate = element.getAsJsonObject(); + String source = text(candidate, "source_id"); + String channel = text(candidate, "placement_channel"); + JsonObject current = loadedCandidate(candidates, source, STANDARD); + + if (!combine && !aliases.isEmpty() && STANDARD.equals(channel) && current != null) { + boolean projected = false; + for (String alias : aliases) { + if (!inferredAlias(root, source, domain, alias)) continue; + + JsonObject retained = JsonCopies.copy(current); + retained.addProperty("placement_channel", alias); + visible.add(retained); + projected = true; + } + if (projected) continue; + } + + if (current != null && OLD_CHANNELS.contains(channel) + && inferredAlias(root, source, domain, channel)) { + continue; + } + visible.add(candidate); + } + + view.add("candidates", visible); + view.add("placement_sources", placements); + return view; + } + + private static boolean inferredAlias(JsonObject root, String source, String domain, String channel) { + JsonObject ore = object(object(root, "ores"), source); + JsonObject dimensions = object(ore, "dimensions"); + JsonObject rule = dimensions.has(domain) ? object(dimensions, domain) + : object(object(ore, "dimension_selectors"), domain); + if (rule.has("placement_channel")) return false; + + JsonElement pattern = rule.get("pattern"); + if (pattern == null || (!pattern.isJsonObject() && !pattern.isJsonPrimitive())) return false; + return channel.equals(pattern.isJsonObject() + ? text(pattern.getAsJsonObject(), "type") : pattern.getAsString()); + } + + private static JsonObject loadedCandidate(JsonArray candidates, String source, String channel) { + for (JsonElement element : candidates) { + if (!element.isJsonObject()) continue; + JsonObject candidate = element.getAsJsonObject(); + if (source.equals(text(candidate, "source_id")) + && channel.equals(text(candidate, "placement_channel")) + && candidate.has("loaded") && candidate.get("loaded").getAsBoolean()) { + return candidate; + } + } + return null; + } + + private static JsonObject object(JsonObject parent, String key) { + return parent.has(key) && parent.get(key).isJsonObject() + ? parent.getAsJsonObject(key) : new JsonObject(); + } + + private static String text(JsonObject parent, String key) { + return parent.has(key) && parent.get(key).isJsonPrimitive() ? parent.get(key).getAsString() : ""; + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/client/OreSourceGroupSettingsScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/OreSourceGroupSettingsScreen.java index c3cd7408..d631fd35 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/OreSourceGroupSettingsScreen.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/OreSourceGroupSettingsScreen.java @@ -150,11 +150,10 @@ protected void init() { @Override protected String rowText(int index) { String channel = channels.get(index); List choices = OreSourceListScreen.placementCandidates(group, channel); - String selected = group.placements.get(channel); - int choice = OreSourceListScreen.indexOf(choices, selected); - String source = choice < 0 + OreSourceCandidate selected = selectedPlacementCandidate(group, channel); + String source = selected == null ? I18n.format("label.orespawn.ore_source.missing") - : OreSourceListScreen.candidateLabel(choices.get(choice)); + : OreSourceListScreen.candidateLabel(selected); return channelLabel(channel) + (placementSelectable(group, channel) ? " > " : " = ") + placementSourceLabel(source, choices.size()); } @@ -187,6 +186,9 @@ protected void init() { lines.add(I18n.format("tooltip.orespawn.ore_source.placement_rules")); if ("keep_separate".equals(group.mode)) { lines.add(I18n.format("tooltip.orespawn.ore_source.placement_requires_consolidated")); + } else if (selectedPlacementCandidate(group, channel) != null + && !selectedPlacementCandidate(group, channel).active) { + lines.add(I18n.format("tooltip.orespawn.ore_source.output_only")); } else if (choices.isEmpty()) { lines.add(I18n.format("tooltip.orespawn.ore_source.placement_missing")); } else if (choices.size() == 1) { @@ -323,6 +325,16 @@ static boolean placementSelectable(OreSourceGroup group, String channel) { && OreSourceListScreen.placementCandidates(group, channel).size() > 1; } + static OreSourceCandidate selectedPlacementCandidate(OreSourceGroup group, String channel) { + String selected = group.placements.get(channel); + for (OreSourceCandidate candidate : group.candidates) { + if (candidate.loaded && candidate.sourceId.equals(selected) && candidate.channel.equals(channel)) { + return candidate; + } + } + return null; + } + static String placementStatus(OreSourceGroup group, List channels) { if (channels.isEmpty()) return I18n.format("label.orespawn.ore_source.no_placement_rules"); if ("keep_separate".equals(group.mode)) { @@ -331,7 +343,11 @@ static String placementStatus(OreSourceGroup group, List channels) { boolean selectable = false; for (String channel : channels) { int choices = OreSourceListScreen.placementCandidates(group, channel).size(); - if (choices == 0) return I18n.format("tooltip.orespawn.ore_source.placement_missing"); + if (choices == 0) { + return I18n.format(selectedPlacementCandidate(group, channel) == null + ? "tooltip.orespawn.ore_source.placement_missing" + : "tooltip.orespawn.ore_source.output_only"); + } selectable |= choices > 1; } return I18n.format(selectable ? "tooltip.orespawn.ore_source.placement_select" diff --git a/src/main/java/zone/moddev/mc/orespawn/client/OreSourceListScreen.java b/src/main/java/zone/moddev/mc/orespawn/client/OreSourceListScreen.java index 8810692d..f45e3333 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/OreSourceListScreen.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/OreSourceListScreen.java @@ -152,7 +152,7 @@ private void initGroups(final List groups) { return groupTooltip(groups.get(index)); } - @Override protected int rowActionWidth(int index) { return 16; } + @Override protected int rowActionWidth(int index) { return groups.get(index).preview ? 0 : 16; } @Override protected void drawRowAction(Minecraft minecraft, int index, int x, int y, int actionWidth, int rowHeight, boolean hovered) { @@ -198,6 +198,7 @@ private void initOutputs(final OreSourceGroup group) { outputScroll = 0; rebuild(); })); + modeButton.enabled = !group.preview; OreSpawnScreenLayout.explain(this, modeButton, "tooltip.orespawn.ore_source.output_mode"); if (acceptWidth > 0) { Button accept = addButton(OreSpawnScreenLayout.button(this, font, diff --git a/src/main/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacements.java b/src/main/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacements.java new file mode 100644 index 00000000..ac8af8ff --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacements.java @@ -0,0 +1,228 @@ +package zone.moddev.mc.orespawn.integration; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import zone.moddev.mc.orespawn.util.JsonCopies; + +/** Keeps a saved imported rule in charge when its provider starts supplying a native default. */ +final class LegacyOreReplacements { + private static final Logger LOGGER = LogManager.getLogger(); + private static final String REPLACEMENTS = "legacy_ore_replacements"; + private static final String REVIEW = "legacy_ore_review"; + + static final class Result { + final Set suppressed = new HashSet<>(); + boolean changed; + } + + private LegacyOreReplacements() {} + + static Result reconcile(JsonObject profile, String provider, JsonObject ores, JsonObject defaults, + JsonObject active, JsonObject manifest, Set known, boolean allowRemoval) { + Result result = new Result(); + JsonObject previous = object(manifest, REPLACEMENTS); + JsonObject replacements = JsonCopies.copy(previous); + JsonObject review = new JsonObject(); + Map> imports = new HashMap<>(); + for (Map.Entry entry : ores.entrySet()) { + String id = entry.getKey(); + JsonObject ore = object(ores, id); + String output = output(ore); + if (output != null && isLegacyImport(provider, id, ore, known.contains(id))) { + imports.computeIfAbsent(output, ignored -> new ArrayList<>()).add(id); + } + } + if (imports.isEmpty() && previous.entrySet().isEmpty() && !manifest.has(REVIEW)) return result; + + Map countByOutput = new HashMap<>(); + for (Map.Entry entry : defaults.entrySet()) { + JsonObject ore = object(defaults, entry.getKey()); + if (!isLegacyImport(provider, entry.getKey(), ore, true)) { + String output = output(ore); + if (output != null) countByOutput.merge(output, 1, Integer::sum); + } + } + for (Map.Entry entry : defaults.entrySet()) { + String id = entry.getKey(); + JsonObject nativeOre = object(defaults, id); + if (isLegacyImport(provider, id, nativeOre, true)) continue; + String remembered = text(previous, id); + String output = output(nativeOre); + List matches = imports.getOrDefault(output, Collections.emptyList()); + if (remembered.isEmpty() && matches.isEmpty()) continue; + + if (remembered.isEmpty() && (matches.size() != 1 || countByOutput.getOrDefault(output, 0) != 1)) { + flag(review, manifest, provider, id, "ambiguous_import"); + result.suppressed.add(id); + continue; + } + String legacyId = remembered.isEmpty() ? matches.get(0) : remembered; + if (id.equals(legacyId)) continue; + boolean removedImport = !remembered.isEmpty() && known.contains(legacyId) && !ores.has(legacyId); + if (!removedImport && !isLegacyImport(provider, legacyId, object(ores, legacyId), + known.contains(legacyId))) continue; + if (active.has(id) && !entry.getValue().equals(active.get(id))) { + flag(review, manifest, provider, id, "provider_override_differs"); + continue; + } + if (ores.has(id)) { + if (!allowRemoval) continue; + // Never undo an edited rule or a selection that depends on it. + JsonObject expected = JsonCopies.copy(nativeOre); + expected.addProperty("source_provider", provider); + JsonObject saved = JsonCopies.copy(object(ores, id)); + saved.remove("orphaned_provider"); + // Ore Sources can add empty scope objects while reading a rule. + removeEmptyScopes(expected); + removeEmptyScopes(saved); + List disposablePolicies = stockPolicyReferences(profile, id, legacyId); + if (!known.contains(id) || !ores.has(legacyId) || !ores.get(id).isJsonObject() + || !expected.equals(saved) || disposablePolicies == null) { + flag(review, manifest, provider, id, "saved_rule_or_policy_differs"); + continue; + } + ores.remove(id); + JsonObject policies = object(profile, "ore_source_policies"); + for (String key : disposablePolicies) policies.remove(key); + result.changed = true; + LOGGER.info("Kept imported ore rule {} and removed untouched duplicate {}", legacyId, id); + } + replacements.addProperty(id, legacyId); + result.suppressed.add(id); + result.changed |= known.add(legacyId); + // The provider still supplies a counterpart for the imported rule. + JsonObject imported = object(ores, legacyId); + if (imported.has("orphaned_provider")) { + imported.remove("orphaned_provider"); + result.changed = true; + } + } + result.changed |= update(manifest, REPLACEMENTS, replacements); + result.changed |= update(manifest, REVIEW, review); + return result; + } + + private static void removeEmptyScopes(JsonObject rule) { + for (String key : new String[] { "dimensions", "dimension_selectors" }) { + JsonElement scope = rule.get(key); + if (scope != null && scope.isJsonObject() && scope.getAsJsonObject().entrySet().isEmpty()) { + rule.remove(key); + } + } + } + + static boolean retains(JsonObject manifest, String id, Set currentDefaults) { + JsonObject replacements = object(manifest, REPLACEMENTS); + for (Map.Entry entry : replacements.entrySet()) { + if (currentDefaults.contains(entry.getKey()) && id.equals(text(replacements, entry.getKey()))) { + return true; + } + } + return false; + } + + private static boolean isLegacyImport(String provider, String id, JsonObject ore, boolean known) { + String block = text(ore, "block"); + if (!block.startsWith(provider + ":") || !"false".equals(text(ore, "native_generation"))) return false; + + // The Forge 25 importer puts the source file's owner in the fallback ID. + if (id.startsWith("orespawn:legacy/" + provider + "/") + && "legacy".equals(text(ore, "source_mod")) + && "true".equals(text(ore, "suppress_vanilla"))) return true; + + // A converted OS1 profile can have come from an older Minecraft version. + if (!known || !provider.equals(text(ore, "source_mod"))) return false; + if ("os1".equals(text(ore, "legacy_import"))) return true; + String prefix = provider + ":legacy/" + block.replace(':', '_'); + return provider.equals(text(ore, "source_provider")) && id.startsWith(prefix) + && id.substring(prefix.length()).matches("_[0-9]+"); + } + + private static String output(JsonObject ore) { + String block = text(ore, "block"); + String variant = metadata(ore); + if (block.isEmpty() || variant == null) return null; + if (ore.has("deep_output") && !block.equals(text(ore, "deep_output"))) return null; + if (ore.has("outputs")) { + JsonElement value = ore.get("outputs"); + if (!value.isJsonArray()) return null; + JsonArray outputs = value.getAsJsonArray(); + if (outputs.size() != 1 || !outputs.get(0).isJsonObject()) return null; + JsonObject only = outputs.get(0).getAsJsonObject(); + if (!block.equals(text(only, "block")) || !variant.equals(metadata(only))) return null; + } + return block + "#" + variant; + } + + private static String metadata(JsonObject ore) { + if (!ore.has("metadata")) return "0"; + String meta = text(ore, "metadata"); + // A nonzero pre-flattening metadata value is not proof of a matching 1.13 block. + return "0".equals(meta) ? meta : null; + } + + /** Only remove the fallback policy generated for a lone native rule. */ + private static List stockPolicyReferences(JsonObject profile, String nativeId, String legacyId) { + List disposable = new ArrayList<>(); + JsonObject imported = object(object(profile, "ores"), legacyId); + for (Map.Entry entry : object(profile, "ore_source_policies").entrySet()) { + if (!entry.getValue().isJsonObject()) continue; + JsonObject policy = entry.getValue().getAsJsonObject(); + JsonObject outputs = object(policy, "outputs"); + JsonObject placements = object(policy, "placement_sources"); + if (!outputs.has(nativeId) && !placements.entrySet().stream() + .anyMatch(place -> nativeId.equals(text(placements, place.getKey())))) continue; + String domain = text(policy, "domain"); + if (!entry.getKey().equals(text(policy, "material") + "|" + domain) || policy.has("dormant") + || object(imported, "dimensions").has(domain) || object(imported, "dimension_selectors").has(domain) + || !"keep_separate".equals(text(policy, "mode")) || !"custom".equals(text(policy, "output_mode")) + || !"separate".equals(text(policy, "status")) || !"false".equals(text(policy, "review_required")) + || outputs.entrySet().size() != 1 || !unitWeight(outputs.get(nativeId)) + || placements.entrySet().size() != 1 + || !nativeId.equals(text(placements, "orespawn:standard"))) return null; + disposable.add(entry.getKey()); + } + return disposable; + } + + private static boolean unitWeight(JsonElement value) { + return value != null && value.isJsonPrimitive() && value.getAsJsonPrimitive().isNumber() + && value.getAsDouble() == 1.0D; + } + + private static void flag(JsonObject review, JsonObject manifest, String provider, String id, String reason) { + review.addProperty(id, reason); + if (!reason.equals(text(object(manifest, REVIEW), id))) { + LOGGER.warn("Review legacy ore overlap for provider {} and rule {} ({}); saved settings were kept", + provider, id, reason); + } + } + + private static boolean update(JsonObject manifest, String key, JsonObject value) { + if (value.equals(object(manifest, key))) return false; + if (value.entrySet().isEmpty()) manifest.remove(key); + else manifest.add(key, value); + return true; + } + + private static JsonObject object(JsonObject root, String key) { + JsonElement value = root.get(key); + return value != null && value.isJsonObject() ? value.getAsJsonObject() : new JsonObject(); + } + + private static String text(JsonObject root, String key) { + JsonElement value = root.get(key); + return value != null && value.isJsonPrimitive() ? value.getAsString() : ""; + } +} diff --git a/src/main/java/zone/moddev/mc/orespawn/integration/WorldgenIntegrationManager.java b/src/main/java/zone/moddev/mc/orespawn/integration/WorldgenIntegrationManager.java index 261c3b0f..1ca4f4cf 100644 --- a/src/main/java/zone/moddev/mc/orespawn/integration/WorldgenIntegrationManager.java +++ b/src/main/java/zone/moddev/mc/orespawn/integration/WorldgenIntegrationManager.java @@ -222,6 +222,15 @@ private static void copyOwnedDefaults(ProviderDefinition provider, String sectio /** Merge new provider-owned defaults without overwriting pack or world values. */ public static synchronized boolean mergeProviderDefinitions(JsonObject target) { + return mergeProviderDefinitions(target, false); + } + + /** Only saved-world callers may remove a duplicate, after arranging a profile backup. */ + public static synchronized boolean mergeSavedProviderDefinitions(JsonObject target) { + return mergeProviderDefinitions(target, true); + } + + private static boolean mergeProviderDefinitions(JsonObject target, boolean savedWorld) { JsonObject manifests = object(target, "providers"); JsonObject legacyOreManifests = object(target, "ore_providers"); boolean changed = false; @@ -245,9 +254,15 @@ public static synchronized boolean mergeProviderDefinitions(JsonObject target) { String knownKey = "known_" + sectionName; Set known = stringSet(manifest.get(knownKey)); Set current = JsonCopies.keys(providerSection); + LegacyOreReplacements.Result replacements = "ores".equals(sectionName) + ? LegacyOreReplacements.reconcile(target, provider.modId, targetSection, + nativeOreDefaults(provider.modId, providerSection), providerSection, manifest, known, savedWorld) + : new LegacyOreReplacements.Result(); + changed |= replacements.changed; for (Entry entry : providerSection.entrySet()) { String id = entry.getKey(); + if (replacements.suppressed.contains(id)) continue; if (!targetSection.has(id) && !known.contains(id)) { JsonObject value = JsonCopies.copy(entry.getValue().getAsJsonObject()); if (!"biome_rules".equals(sectionName)) { @@ -264,7 +279,9 @@ public static synchronized boolean mergeProviderDefinitions(JsonObject target) { for (String knownId : known) { if (!current.contains(knownId) && targetSection.has(knownId) - && targetSection.get(knownId).isJsonObject()) { + && !("ores".equals(sectionName) && LegacyOreReplacements.retains(manifest, knownId, current)) + && targetSection.get(knownId).isJsonObject() + && !bool(targetSection.getAsJsonObject(knownId), "orphaned_provider", false)) { targetSection.getAsJsonObject(knownId).addProperty("orphaned_provider", true); changed = true; } @@ -307,6 +324,19 @@ public static synchronized boolean mergeProviderDefinitions(JsonObject target) { return changed; } + private static JsonObject nativeOreDefaults(String providerId, JsonObject activeOres) { + if (!FILE_PROVIDERS.containsKey(providerId)) return activeOres; + JsonObject shipped = installedProviderRoot(providerId); + JsonElement ores = shipped == null ? null : shipped.get("ores"); + return ores != null && ores.isJsonObject() ? ores.getAsJsonObject() : new JsonObject(); + } + + private static JsonObject installedProviderRoot(String providerId) { + JsonObject root = RESOURCE_PROVIDERS.get(providerId); + WorldgenProvider apiProvider = API_PROVIDERS.get(providerId); + return root == null && apiProvider != null ? apiProvider.toJson() : root; + } + public static synchronized List templates() { return Collections.unmodifiableList(new ArrayList<>(TEMPLATES.values())); } @@ -469,13 +499,7 @@ private static void rebuildActiveProviders() { continue; } JsonObject root = FILE_PROVIDERS.get(providerId); - if (root == null) { - root = RESOURCE_PROVIDERS.get(providerId); - } - if (root == null) { - WorldgenProvider apiProvider = API_PROVIDERS.get(providerId); - root = apiProvider == null ? null : apiProvider.toJson(); - } + if (root == null) root = installedProviderRoot(providerId); if (root == null || !ModList.get().isLoaded(providerId)) { continue; } 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 18a526d7..1e946ee5 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java @@ -97,21 +97,30 @@ private GeomeConfig() { } public static synchronized BakedGeomeConfig bake() { + return bakeGlobal(true); + } + + public static synchronized BakedGeomeConfig bakeBeforeLoadComplete() { + // Features need provisional settings before every mod has submitted its ores. + return bakeGlobal(false); + } + + private static BakedGeomeConfig bakeGlobal(boolean chooseOreSourceDefaults) { JsonObject root = loadConfig(); boolean changed = OreSpawnOreIntegration.mergeProviderOres(root); changed |= FluidDepositMigration.normalize(root); - changed |= OreSourcePolicies.initialize(root, false); + if (chooseOreSourceDefaults) changed |= OreSourcePolicies.initialize(root, false); if (changed) { writeProviderMerge(root); } globalBaseConfigRoot = JsonCopies.copy(root); globalBaseProfile = WorldGeologyProfile.fromGlobalConfig(root, - OreSpawnConfig.geologyMode(), OreSpawnConfig.placeCrudeOil()); - root = applyFreshWorldTemplate(root); - globalConfigRoot = JsonCopies.copy(root); - globalProfile = WorldGeologyProfile.fromGlobalConfig(root, - OreSpawnConfig.geologyMode(), OreSpawnConfig.placeCrudeOil()); - bakeDimensions(root); + OreSpawnConfig.geologyMode(), OreSpawnConfig.placeCrudeOil(), false); + JsonObject effective = applyFreshWorldTemplate(root); + globalConfigRoot = JsonCopies.copy(effective); + globalProfile = effective == root ? globalBaseProfile : WorldGeologyProfile.fromGlobalConfig(effective, + OreSpawnConfig.geologyMode(), OreSpawnConfig.placeCrudeOil(), chooseOreSourceDefaults); + bakeDimensions(effective); return bakedConfig; } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 4423dfa4..fcb9e277 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.1.0.113021 Upgrade Report"); + lines.add("OreSpawn 4.1.6.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 87032a7a..4bdba6db 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -197,7 +197,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.1.0.113021 Upgrade Report"); + lines.add("OreSpawn 4.1.6.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/MmdOreConflictCatalog.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/MmdOreConflictCatalog.java index 38b282dd..6cf901c0 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/MmdOreConflictCatalog.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/MmdOreConflictCatalog.java @@ -14,7 +14,7 @@ final class MmdOreConflictCatalog { private static final Set ORDINARY = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList( "basemetals", "modernmetals", "basegems", "baseminerals", "fantasymetals", - "electricadvantage", "steamadvantage", "poweradvantage", "mineralogy"))); + "baseelectric", "electricadvantage", "steamadvantage", "poweradvantage", "mineralogy"))); private static final Map> PRIORITIES = priorities(); private MmdOreConflictCatalog() { } @@ -56,9 +56,9 @@ static boolean automaticallyBalance(Map> tagsByOwn private static Map> priorities() { Map> result = new LinkedHashMap<>(); result.put("sulfur", Collections.unmodifiableList(Arrays.asList( - "mineralogy", "baseminerals", "electricadvantage"))); + "mineralogy", "baseminerals", "baseelectric", "electricadvantage"))); result.put("lithium", Collections.unmodifiableList(Arrays.asList( - "baseminerals", "electricadvantage"))); + "baseminerals", "baseelectric", "electricadvantage"))); return Collections.unmodifiableMap(result); } } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java index ac74c41f..b8a62723 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java @@ -166,11 +166,48 @@ static ResourceLocation placementChannel(JsonObject rule) { ? rule.getAsJsonObject("pattern") : null; if (pattern != null) { ResourceLocation type = resource(string(pattern, "type", "")); - if (type != null) return type; + if (type != null) return patternChannel(type); } String legacy = string(rule, "pattern", ""); ResourceLocation type = legacy.indexOf(':') >= 0 ? resource(legacy) : null; - return type != null ? type : STANDARD; + return type != null ? patternChannel(type) : STANDARD; + } + + static ResourceLocation patternChannel(ResourceLocation pattern) { + if ("orespawn".equals(pattern.getNamespace())) { + switch (pattern.getPath()) { + case "default": + case "vein": + case "normal_cloud": + case "precision": + case "clusters": + case "underfluids": + return STANDARD; + default: + break; + } + } + return pattern; + } + + static boolean legacyChannelSelection(JsonObject profile, ResourceLocation source, + ResourceLocation domain, ResourceLocation savedChannel, ResourceLocation currentChannel) { + if (!STANDARD.equals(currentChannel) || STANDARD.equals(savedChannel) + || !STANDARD.equals(patternChannel(savedChannel))) return false; + JsonObject ore = readObject(readObject(profile, "ores"), source.toString()); + JsonObject dimensions = readObject(ore, "dimensions"); + JsonObject rule = dimensions.has(domain.toString()) ? readObject(dimensions, domain.toString()) + : readObject(readObject(ore, "dimension_selectors"), domain.toString()); + // A saved inferred name may still select its original rule. An explicit channel wins. + if (rule.has("placement_channel")) return false; + JsonElement pattern = rule.get("pattern"); + String oldChannel = pattern != null && pattern.isJsonObject() + ? string(pattern.getAsJsonObject(), "type", "") : string(rule, "pattern", ""); + return savedChannel.toString().equals(oldChannel); + } + + private static JsonObject readObject(JsonObject parent, String key) { + return parent.has(key) && parent.get(key).isJsonObject() ? parent.getAsJsonObject(key) : new JsonObject(); } static Inference inferMaterial(Iterable blockTags) { @@ -195,11 +232,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 nativeOrImportedVanilla = bool(ore, "native_generation", false) + || OreSpawnOreGeneration.isImportedVanillaOre(resource(oreEntry.getKey()), block, ore); + boolean oreActive = bool(ore, "enabled", true) && (!nativeOrImportedVanilla || manageVanillaOres); int metadata = integer(ore, "metadata", 0); Inference inferred = inferMaterial(root, blockTags(block)); ResourceLocation material = resource(string(ore, "material", "")); 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 6b6cc50b..6b0e17b3 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java @@ -283,9 +283,11 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) { if (ruleId == null) ruleId = oreId; outputBundles.put(ruleId.toString(), new BakedOutputBundle( output.getDefaultState(), deepOutput, deepOutputMaxY, outputs)); - if (!oreEnabled || (nativeGeneration && !manageVanillaOres)) continue; + boolean importedVanilla = !nativeGeneration && isImportedVanillaOre(ruleId, output, oreJson); + if (!oreEnabled || (!manageVanillaOres && (nativeGeneration || importedVanilla))) continue; ResourceLocation material = OreSourcePolicies.material(profile, oreJson, output); - BakedOreRule rule = new BakedOreRule(output, manageVanillaOres && suppressVanilla); + BakedOreRule rule = new BakedOreRule(ruleId, output, manageVanillaOres && suppressVanilla, + nativeGeneration, importedVanilla); for (Entry dimensionEntry : dimensions.entrySet()) { ResourceLocation dimensionId = resource(dimensionEntry.getKey()); @@ -337,6 +339,8 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) { Map result = new HashMap<>(); Map> vanillaOutputs = new HashMap<>(); + Map definitions = new HashMap<>(); + for (BakedOreRule rule : rules) definitions.put(rule.ruleId, rule); for (ResourceLocation dimension : explicitDimensions) { List combined = new ArrayList<>(); Set suppressed = Collections.newSetFromMap(new IdentityHashMap()); @@ -348,7 +352,7 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) { if (rule.suppressVanilla) suppressed.add(rule.output); } } - combined = arbitrate(profile, combined, outputBundles); + combined = preferImportedVanilla(definitions, arbitrate(profile, combined, outputBundles), suppressed); result.put(dimension, combined.toArray(new BakedOre[combined.size()])); vanillaOutputs.put(dimension, Collections.unmodifiableSet(suppressed)); } @@ -359,7 +363,7 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) { selectorList.add(rule.selector); if (rule.suppressVanilla) selectorOutputs.add(rule.output); } - selectorList = arbitrate(profile, selectorList, outputBundles); + selectorList = preferImportedVanilla(definitions, arbitrate(profile, selectorList, outputBundles), selectorOutputs); BakedOre[] selectorResult = selectorList.toArray(new BakedOre[selectorList.size()]); LOGGER.info("Baked {} OreSpawn-managed ore definitions across {} dimensions", rules.size(), result.size()); @@ -372,6 +376,33 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) { Collections.unmodifiableSet(selectorOutputs)); } + static boolean isImportedVanillaOre(ResourceLocation ruleId, Block output, JsonObject ore) { + // Provider-aware imports can keep a modern ID, but still carry their legacy source. + if (ruleId == null || (!ruleId.getPath().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.NETHER_QUARTZ_ORE; + } + + private static List preferImportedVanilla(Map definitions, + List ores, Set suppressed) { + Set importedOutputs = Collections.newSetFromMap(new IdentityHashMap()); + for (BakedOre ore : ores) { + BakedOreRule rule = definitions.get(ore.ruleId); + if (rule != null && rule.importedVanilla) importedOutputs.add(rule.output); + } + if (importedOutputs.isEmpty()) return ores; + suppressed.addAll(importedOutputs); + // A selected imported rule replaces the modern default, not another authored rule. + List result = new ArrayList<>(); + for (BakedOre ore : ores) { + BakedOreRule rule = definitions.get(ore.ruleId); + if (rule == null || !rule.nativeGeneration || !importedOutputs.contains(rule.output)) result.add(ore); + } + return result; + } + private static List arbitrate(JsonObject profile, List original, Map outputBundles) { if (!profile.has(OreSourcePolicies.SECTION) @@ -397,13 +428,17 @@ private static List arbitrate(JsonObject profile, List origi Map available = new HashMap<>(); for (BakedOre ore : entry.getValue()) available.put(ore.ruleId.toString(), ore); List selected = new ArrayList<>(); - for (Entry placement : objectOrEmpty(policy, "placement_sources").entrySet()) { + JsonObject placements = objectOrEmpty(policy, "placement_sources"); + for (Entry placement : placements.entrySet()) { ResourceLocation channel = resource(placement.getKey()); String sourceId; try { sourceId = placement.getValue().getAsString(); } catch (RuntimeException invalid) { continue; } BakedOre ore = available.get(sourceId); - if (ore != null && channel != null && channel.equals(ore.channel) && !sources.isEmpty()) { + if (ore == null || channel == null || sources.isEmpty()) continue; + boolean oldAlias = !placements.has(OreSourcePolicies.STANDARD.toString()) + && OreSourcePolicies.legacyChannelSelection(profile, ore.ruleId, ore.policyDomain, channel, ore.channel); + if (channel.equals(ore.channel) || oldAlias) { selected.add(ore.withSources(sources)); } } @@ -863,15 +898,22 @@ boolean acceptsBiome(Biome biome, ResourceLocation biomeId) { } private static final class BakedOreRule { + final ResourceLocation ruleId; final Block output; final boolean suppressVanilla; + final boolean nativeGeneration; + final boolean importedVanilla; final Map explicit = new HashMap<>(); final Set explicitDimensions = new HashSet<>(); BakedOre selector; - BakedOreRule(Block output, boolean suppressVanilla) { + BakedOreRule(ResourceLocation ruleId, Block output, boolean suppressVanilla, + boolean nativeGeneration, boolean importedVanilla) { + this.ruleId = ruleId; this.output = output; this.suppressVanilla = suppressVanilla; + this.nativeGeneration = nativeGeneration; + this.importedVanilla = importedVanilla; } } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfile.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfile.java index 370c3252..93379940 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfile.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfile.java @@ -71,8 +71,13 @@ public static WorldGeologyProfile recommended(boolean placeFluidDeposits) { public static WorldGeologyProfile fromGlobalConfig(JsonObject globalRoot, GeologyMode geologyMode, boolean placeFluidDeposits) { + return fromGlobalConfig(globalRoot, geologyMode, placeFluidDeposits, true); + } + + static WorldGeologyProfile fromGlobalConfig(JsonObject globalRoot, + GeologyMode geologyMode, boolean placeFluidDeposits, boolean chooseOreSourceDefaults) { JsonObject root = JsonCopies.copy(globalRoot); - OreSourcePolicies.initialize(root, false); + if (chooseOreSourceDefaults) OreSourcePolicies.initialize(root, false); if (!root.has("geology_mode")) { root.addProperty("geology_mode", geologyMode.name().toLowerCase(Locale.ROOT)); } diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java index 7eec662e..3e9b46d6 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java @@ -20,7 +20,6 @@ import com.google.gson.JsonSyntaxException; import zone.moddev.mc.orespawn.OreSpawnConfig; import zone.moddev.mc.orespawn.OreSpawnConfig.GeologyMode; -import zone.moddev.mc.orespawn.api.OreSpawnOreIntegration; import zone.moddev.mc.orespawn.integration.WorldgenIntegrationManager; import net.minecraft.server.MinecraftServer; @@ -110,9 +109,9 @@ public static synchronized boolean reloadActiveProfile() { .resolve("serverconfig").resolve(PROFILE_FILE_NAME); WorldGeologyProfile profile = readProfile(profilePath, globalProfile()); JsonObject merged = profile.rootCopy(); - boolean changed = OreSpawnOreIntegration.mergeProviderOres(merged); + boolean changed = WorldgenIntegrationManager.mergeSavedProviderDefinitions(merged); changed |= OreSourcePolicies.initialize(merged, true); - if (changed) { + if (changed && preserveBeforeLegacyOreRepair(profilePath, profile, merged)) { profile = profile.withRoot(merged); writeProfile(profilePath, profile); } @@ -136,9 +135,9 @@ public static void onServerAboutToStart(FMLServerAboutToStartEvent event) { profile = readProfile(profilePath, fallback); JsonObject merged = profile.rootCopy(); String beforeMerge = merged.toString(); - OreSpawnOreIntegration.mergeProviderOres(merged); + WorldgenIntegrationManager.mergeSavedProviderDefinitions(merged); OreSourcePolicies.initialize(merged, true); - if (!beforeMerge.equals(merged.toString())) { + if (!beforeMerge.equals(merged.toString()) && preserveBeforeLegacyOreRepair(profilePath, profile, merged)) { profile = profile.withRoot(merged); writeProfile(profilePath, profile); LOGGER.info("Merged new OreSpawn worldgen-provider definitions into '{}'", profilePath); @@ -387,6 +386,25 @@ static WorldGeologyProfile readProfile(Path path, WorldGeologyProfile fallback) } } + private static boolean preserveBeforeLegacyOreRepair(Path path, WorldGeologyProfile before, JsonObject merged) { + JsonElement priorOres = before.rootCopy().get("ores"); + if (priorOres == null || !priorOres.isJsonObject() || !Files.isRegularFile(path)) return true; + JsonObject original = priorOres.getAsJsonObject(); + JsonObject current = merged.getAsJsonObject("ores"); + for (java.util.Map.Entry rule : original.entrySet()) { + if (current != null && current.has(rule.getKey())) continue; + Path backup = path.resolveSibling("orespawn-worldgen.pre-legacy-ore-repair.bak"); + try { + if (!Files.exists(backup)) Files.copy(path, backup); + return true; + } catch (IOException e) { + LOGGER.warn("Could not back up '{}'; legacy ore repair was not applied", path, e); + return false; + } + } + return true; + } + private static boolean preserveBeforeOreDefaultsRefresh(Path path) { Path backup = path.resolveSibling("orespawn-worldgen.pre-ore-revision-" + GeomeConfig.oreDefaultsRevision() + ".bak"); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/math/PerlinNoise2D.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/math/PerlinNoise2D.java index 0a5fdcb0..29b0437c 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/math/PerlinNoise2D.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/math/PerlinNoise2D.java @@ -15,8 +15,8 @@ public PerlinNoise2D(long seed, float initialRange, float initialSize, int numOv for (int i = 0; i < layers.length; i++) { seed = scramble(seed); layers[i] = new NoiseLayer2D(seed, initialSize, initialRange); - initialSize *= 0.5; - initialRange *= 0.5; + initialSize *= 0.5F; + initialRange *= 0.5F; } } diff --git a/src/main/resources/assets/orespawn/lang/de_au.json b/src/main/resources/assets/orespawn/lang/de_at.json similarity index 100% rename from src/main/resources/assets/orespawn/lang/de_au.json rename to src/main/resources/assets/orespawn/lang/de_at.json diff --git a/src/main/resources/assets/orespawn/lang/en_en.json b/src/main/resources/assets/orespawn/lang/en_en.json deleted file mode 100644 index 02c75c0c..00000000 --- a/src/main/resources/assets/orespawn/lang/en_en.json +++ /dev/null @@ -1,512 +0,0 @@ -{ - "tooltip.orespawn.fluid.add_deposit": "Choose an installed fluid block and create a new underground fluid-deposit rule for it.", - "tooltip.orespawn.assignment.ore": "Assign this installed block as an ore, then edit its dimensions, deposit shape, hosts, and geome rules.", - "tooltip.orespawn.assignment.rock_family": "Assign this installed block as a rock in the selected family, then edit its depth and geome rules.", - "tooltip.orespawn.picker.mod_filter": "Limit the installed block list to one mod namespace, or choose All Mods.", - "tooltip.orespawn.material.add_block": "Choose an installed, unassigned block and create a rock or ore rule for the current tab.", - "tooltip.orespawn.material.safe_only": "Hide blocks with block entities or unusual collision and show only ordinary full solid blocks.", - "tooltip.orespawn.material.show_all": "Include blocks with block entities or unusual collision that are normally hidden because terrain replacement may be unsafe.", - "tooltip.orespawn.material.tab.unassigned": "Show installed blocks that are not yet assigned as an OreSpawn rock, ore, or fluid.", - "tooltip.orespawn.material.tab.ores": "Show configured ore entries and open their dimension, shape, host, and geome rules.", - "tooltip.orespawn.material.tab.igneous": "Show intrusive and volcanic igneous rocks and open their generation rules.", - "tooltip.orespawn.material.tab.metamorphic": "Show rocks classified as metamorphic and open their generation rules.", - "tooltip.orespawn.material.tab.sedimentary": "Show rocks classified as sedimentary and open their generation rules.", - "tooltip.orespawn.geome.new_id.dictionary": "Enter a Forge biome type name used by the installed biome dictionary.", - "tooltip.orespawn.geome.new_id.biomes": "Enter an installed biome registry ID, for example minecraft:plains.", - "tooltip.orespawn.geome.new_id.geomes": "Enter a new geome name. OreSpawn stores it in lowercase.", - "tooltip.orespawn.geome.tab.dictionary": "Map Forge biome type names to the geomes they should favour.", - "tooltip.orespawn.geome.tab.biomes": "Map exact biome registry IDs to the geomes they should favour.", - "tooltip.orespawn.geome.tab.geomes": "Edit named geology regions and their base and rock-family weights.", - "tooltip.orespawn.geome.biome_weight": "Influence this biome or biome type adds to the named geome. Valid range: 0 to 1000; 0 adds no influence.", - "tooltip.orespawn.geome.entry_weight": "Relative chance for this rock, ore, or fluid deposit inside the named geome. Valid range: 0 to 1000; 0 excludes it.", - "tooltip.orespawn.geome.family_weight": "Relative preference for this rock family inside the geome. Valid range: 0 to 1000; 0 excludes the family.", - "tooltip.orespawn.geome.base_weight": "Base chance for this geome before biome influences are added. Valid range: 0 to 1000; 0 leaves only biome influence.", - "tooltip.orespawn.numeric.rock_layer_thickness": "Base thickness of legacy Cyano rock layers. Whole numbers from 1 to 255 are accepted.", - "tooltip.orespawn.numeric.rock_layer_noise": "Amount of vertical variation in legacy Cyano rock layers. Valid range: 1 to 32767.", - "tooltip.orespawn.numeric.geome_size": "Horizontal size of legacy Cyano geome regions. Whole numbers from 4 to 32767 are accepted.", - "tooltip.orespawn.numeric.continuity": "Chance that a formation keeps its identity across a boundary. Valid range: 0 to 1.", - "tooltip.orespawn.numeric.edge_octaves": "Number of detail-noise layers combined at formation edges. Whole numbers from 1 to 8 are accepted.", - "tooltip.orespawn.numeric.edge_amplitude": "Maximum vertical displacement caused by boundary detail. Valid range: 0 to 256.", - "tooltip.orespawn.numeric.edge_wavelength": "Horizontal wavelength of small-scale boundary detail. Valid range: 8 to 512.", - "tooltip.orespawn.numeric.waviness_amplitude": "Maximum vertical displacement caused by broad layer waviness. Valid range: 0 to 512.", - "tooltip.orespawn.numeric.waviness_wavelength": "Horizontal wavelength of broad vertical layer bends. Valid range: 32 to 2048.", - "tooltip.orespawn.numeric.vertical_thickness": "Typical vertical thickness of a Sky stratum. Whole numbers from 1 to 192 are accepted.", - "tooltip.orespawn.numeric.family_region_wavelength": "Horizontal wavelength of rock-family regions. Larger values make broader regions. Valid range: 16 to 8192.", - "tooltip.orespawn.numeric.stratum_wavelength": "Horizontal wavelength of Sky strata. The editor accepts 16 to 8192; Stable Layers effectively uses at least 32.", - "tooltip.orespawn.advanced.fluid_deposits": "Open the configured underground fluid pockets and their dimension-specific placement rules.", - "tooltip.orespawn.advanced.cyano": "Edit the legacy Cyano engine's region size, layer variation, and layer thickness.", - "tooltip.orespawn.advanced.formations": "Edit the exact Sky formation values used when a formation control is set to Custom.", - "tooltip.orespawn.main.fluid_editor": "Open each configured fluid deposit to edit dimensions, rarity, size, hosts, biome filters, and geome weights.", - "tooltip.orespawn.main.advanced": "Open exact numeric controls for custom Sky formations, legacy Cyano layers, and configured fluid deposits.", - "tooltip.orespawn.main.biomes_materials": "Configure optional biome placement plus dimension-wide aquifer, snow, ice, and surface-material overrides.", - "tooltip.orespawn.main.configure_strata": "Create editable rock rules for Minecraft's standard stone, granite, diorite, and andesite strata.", - "tooltip.orespawn.main.materials": "Open the current rock and ore rules to edit families, depth ranges, hosts, deposit shapes, and per-geome weights.", - "tooltip.orespawn.main.recommended": "Set the geology engine and formation controls to the recommended Sky and Average choices. Detailed rock, ore, biome, and fluid rules are left unchanged.", - "tooltip.orespawn.main.template": "Select a complete geology setup supplied by an installed mod or mod pack. Pack Defaults keeps the pack's normal selection.", - "tooltip.orespawn.enabled": "Enable or disable this entry without deleting its saved settings.", - "tooltip.orespawn.weight": "Relative chance compared with other eligible entries. Valid range: 0 to 1000; 0 prevents selection and larger values make this entry more likely.", - "tooltip.orespawn.geome_weights": "Set this entry's relative chance in each Overworld geome. A weight of 0 prevents it there.", - "tooltip.orespawn.host_family": "Allow generation in blocks assigned to this rock family. An enabled rule needs at least one family, block, or tag host.", - "tooltip.orespawn.host_blocks": "Comma-separated block registry IDs that may be replaced, for example minecraft:stone.", - "tooltip.orespawn.host_tags": "Comma-separated block-tag registry IDs whose blocks may be replaced, for example forge:stone.", - "tooltip.orespawn.fluid.dimension_settings": "Open the first configured dimension. Use the dimension list below to open a specific dimension.", - "tooltip.orespawn.fluid.available_dimension": "Choose an installed dimension to add, then edit its placement, host, and biome rules.", - "tooltip.orespawn.fluid.min_y": "Lowest Y allowed for the deposit center. The editor accepts 0 to 255, but the value must also be inside the target dimension's build height and must not exceed Maximum Y.", - "tooltip.orespawn.fluid.max_y": "Highest Y allowed for the deposit center. The editor accepts 0 to 255, but the value must also be inside the target dimension's build height and must not be below Minimum Y.", - "tooltip.orespawn.fluid.frequency": "Average deposit attempts per chunk. 0 disables attempts; decimals are allowed up to 64.", - "tooltip.orespawn.fluid.min_radius": "Smallest horizontal radius selected for a deposit lobe. Valid range: 1 to 64.", - "tooltip.orespawn.fluid.max_radius": "Largest horizontal radius selected for a deposit lobe. It must be at least Minimum Radius and no more than 64.", - "tooltip.orespawn.fluid.min_vertical_radius": "Smallest vertical radius selected for a deposit lobe. Valid range: 1 to 64.", - "tooltip.orespawn.fluid.max_vertical_radius": "Largest vertical radius selected for a deposit lobe. It must be at least Minimum Vertical Radius and no more than 64.", - "tooltip.orespawn.fluid.max_lobes": "Maximum rounded lobes joined into one deposit. 1 creates a single pocket; valid range: 1 to 16.", - "tooltip.orespawn.fluid.min_solid_cover": "Minimum solid blocks required above a deposit. 0 disables extra roof protection; valid range: 0 to 64.", - "tooltip.orespawn.fluid.min_solid_shell": "Minimum solid blocks required around the sides and floor. 0 disables extra shell protection; valid range: 0 to 64.", - "tooltip.orespawn.fluid.biome_ids": "If set, deposits may generate only in these comma-separated biome registry IDs. Leave blank for no exact-biome restriction.", - "tooltip.orespawn.fluid.excluded_biome_ids": "Deposits never generate in these comma-separated biome registry IDs. Exclusions override inclusions.", - "tooltip.orespawn.fluid.biome_dictionary": "Include biomes matching these comma-separated Forge biome type names, for example OCEAN. Leave blank for no type restriction.", - "tooltip.orespawn.fluid.excluded_biome_dictionary": "Exclude biomes matching these comma-separated Forge biome type names. Exclusions override inclusions.", - "tooltip.orespawn.ore.min_y": "Lowest Y at which an ore placement attempt may start. The editor accepts 0 to 255, but the value must also be inside the target dimension's build height and must not exceed Maximum Y.", - "tooltip.orespawn.ore.max_y": "Highest Y at which an ore placement attempt may start. The editor accepts 0 to 255, but the value must also be inside the target dimension's build height and must not be below Minimum Y.", - "tooltip.orespawn.ore.frequency": "Average ore placement attempts per chunk. 0 disables attempts; decimals are allowed up to 64.", - "tooltip.orespawn.ore.min_quantity": "Smallest block budget assigned to one deposit attempt. Valid range: 1 to 64.", - "tooltip.orespawn.ore.max_quantity": "Largest block budget assigned to one deposit attempt. It must be at least Minimum Block Budget and no more than 64.", - "tooltip.orespawn.ore.discard_air_exposure": "Chance to reject ore that would touch air. 0 keeps exposed ore; 1 rejects every exposed placement.", - "tooltip.orespawn.ore.pattern": "Choose the deposit shape. Pattern-specific controls below are enabled only when the selected pattern uses them.", - "tooltip.orespawn.ore.height_distribution": "Choose how placement attempts are distributed between Minimum Y and Maximum Y.", - "tooltip.orespawn.ore.spread": "Horizontal range used by cluster and cloud patterns. Valid range: 0 to 64.", - "tooltip.orespawn.ore.vertical_spread": "Vertical range used by cluster and cloud patterns. Valid range: 0 to 64.", - "tooltip.orespawn.ore.node_size": "Block budget for each node in the Clusters pattern. Valid range: 1 to 32.", - "tooltip.orespawn.rock.family": "Classify this rock as sedimentary, metamorphic, intrusive igneous, or volcanic igneous for geome and depth preferences.", - "tooltip.orespawn.rock.depth_peak": "Y level where this rock receives its strongest depth preference. Valid range: 0 to 255.", - "tooltip.orespawn.rock.depth_spread": "How gradually the rock's depth preference falls away from Depth Peak. Larger values cover a broader vertical range; valid range: 1 to 512.", - "tooltip.orespawn.rock.min_y": "Lowest Y where this rock may replace terrain. Valid range: 0 to 255; it must not exceed Maximum Y.", - "tooltip.orespawn.rock.max_y": "Highest Y where this rock may replace terrain. Valid range: 0 to 255; it must not be below Minimum Y.", - "tooltip.orespawn.rock.ore_replaceable": "Allow OreSpawn-managed ores to replace this rock when it is selected as a host family.", - "tooltip.orespawn.biome.dimension": "Select the dimension whose biome-placement and world-material settings are shown.", - "tooltip.orespawn.biome.palette_enabled": "Enable provider-supplied biome placement in this dimension. Turning it off preserves the saved biome entries.", - "tooltip.orespawn.biome.mode": "Augment mixes configured biomes with the original biome. Replace chooses only from eligible configured biomes.", - "tooltip.orespawn.biome.scope": "Choose which existing biome namespaces may be replaced: all biomes, Minecraft only, or selected mod namespaces.", - "tooltip.orespawn.biome.region_size": "Controls the horizontal size of biome-placement regions. Larger values create broader, less frequent boundaries.", - "tooltip.orespawn.biome.entries": "Open this dimension's biome entries to configure weights, climate limits, similarity rules, and surface materials.", - "tooltip.orespawn.biome.dimension_materials": "Configure dimension-wide aquifer fluids plus snow and ice replacements.", - "tooltip.orespawn.biome.geome_influences": "Map installed biomes to relative geome weights used by Sky geology.", - "tooltip.orespawn.biome.similar_biomes": "Allow this output only when the original biome matches one of these IDs. An empty list allows any biome within the climate limits.", - "tooltip.orespawn.biome.required_similar_biomes": "Like Similar Biomes, but this output is disabled if any listed biome is not installed.", - "tooltip.orespawn.biome.min_temperature": "Lowest original-biome temperature eligible for this output. Valid range: -2 to 2.", - "tooltip.orespawn.biome.max_temperature": "Highest original-biome temperature eligible for this output. Valid range: -2 to 2.", - "tooltip.orespawn.biome.min_downfall": "Lowest original-biome downfall eligible for this output. Valid range: 0 to 1.", - "tooltip.orespawn.biome.max_downfall": "Highest original-biome downfall eligible for this output. Valid range: 0 to 1.", - "tooltip.orespawn.biome.top_block": "Replace this biome's top surface block. Not set keeps the generated biome's normal top block.", - "tooltip.orespawn.biome.filler_block": "Replace the blocks immediately below the top surface. Filler Depth controls how many layers are changed.", - "tooltip.orespawn.biome.underwater_block": "Replace the biome's exposed underwater surface block. Not set keeps the normal block.", - "tooltip.orespawn.biome.ceiling_block": "Replace the biome's ceiling surface block in dimensions that generate ceilings. Not set keeps the normal block.", - "tooltip.orespawn.biome.filler_depth": "Number of layers below the top block that use Filler Block. Valid range: 0 to 16.", - "tooltip.orespawn.material.default_fluid": "Replace the normal aquifer fluid below sea level across every biome in this dimension. Not set keeps the generator's original fluid. Existing chunks are unchanged; fluids with unusual lighting may generate more slowly.", - "tooltip.orespawn.material.deep_aquifer_fluid": "Unavailable in Minecraft 1.13.2; the stored value is retained for later versions.", - "tooltip.orespawn.material.deep_aquifer_y": "Unavailable in Minecraft 1.13.2; the stored value is retained for later versions.", - "tooltip.orespawn.material.snow_block": "Replace vanilla snow placed near the surface in this dimension. Not set keeps normal snow.", - "tooltip.orespawn.material.ice_block": "Replace ordinary vanilla ice placed near the surface in this dimension. Not set keeps normal ice.", - "option.orespawn.min_quantity": "Minimum Block Budget", - "option.orespawn.max_quantity": "Maximum Block Budget", - "value.orespawn.dimension.all_except_nether_end": "All except Nether and End", - "option.orespawn.host_blocks_short": "Host Blocks", - "option.orespawn.host_tags_short": "Host Block Tags", - "option.orespawn.biome_ids_short": "Included Biomes", - "option.orespawn.excluded_biome_ids_short": "Excluded Biomes", - "option.orespawn.biome_dictionary_short": "Included Biome Types", - "option.orespawn.excluded_biome_dictionary_short": "Excluded Biome Types", - "screen.orespawn.world_settings": "OreSpawn Geology", - "button.orespawn.world_settings": "OreSpawn...", - "button.orespawn.configure_strata": "Configure Rock Strata...", - "button.orespawn.fluid_deposit_details": "Fluid Deposits...", - "button.orespawn.fluid_deposits_count": "Fluid Deposits: %s / %s", - "screen.orespawn.fluid_deposits": "Fluid Deposits", - "screen.orespawn.fluid_deposit": "Fluid Deposit", - "screen.orespawn.fluid_deposit_dimension": "Fluid Deposit Dimension", - "option.orespawn.fluid_deposits": "Fluid Deposits", - "option.orespawn.output_fluid": "Output Fluid Block", - "option.orespawn.biome_ids": "Biome IDs (comma separated)", - "option.orespawn.excluded_biome_ids": "Excluded Biome IDs (comma separated)", - "option.orespawn.biome_dictionary": "Biome Types (comma separated)", - "option.orespawn.excluded_biome_dictionary": "Excluded Biome Types (comma separated)", - "tooltip.orespawn.fluid_deposits": "ON generates configured covered underground fluid pockets. OFF keeps their settings but does not place them.", - "error.orespawn.host_required": "Choose at least one host family, block, or tag.", - "error.orespawn.invalid_values": "Check the values and registry IDs.", - "button.orespawn.recommended": "Recommended Defaults", - "button.orespawn.advanced": "Advanced Settings...", - "button.orespawn.help": "Help & Guide", - "button.orespawn.previous": "Previous", - "button.orespawn.next": "Next", - "button.orespawn.materials": "Rocks & Ores...", - "button.orespawn.geomes": "Geomes & Biomes...", - "button.orespawn.formation_details": "Custom Shapes...", - "button.orespawn.cyano_details": "Cyano Settings...", - "button.orespawn.geome_weights": "Geome Weights...", - "button.orespawn.search": "Search", - "button.orespawn.show_all": "Show All", - "button.orespawn.safe_only": "Safe Only", - "button.orespawn.add": "Add", - "button.orespawn.add_block": "Add Block...", - "button.orespawn.reset": "Reset", - "button.orespawn.unassign": "Unassign", - "button.orespawn.remove": "Remove", - "button.orespawn.remove_dimension": "Remove Dimension", - "screen.orespawn.numeric_settings": "Detailed Geology Settings", - "screen.orespawn.materials": "Worldgen Materials", - "screen.orespawn.assign_block": "Assign Block", - "screen.orespawn.choose_block": "Choose Installed Block", - "screen.orespawn.rock_entry": "Rock Settings", - "screen.orespawn.geome_weights": "Geome Weights", - "screen.orespawn.ore_entry": "Ore Settings", - "screen.orespawn.ore_dimension": "Ore Dimension Settings", - "screen.orespawn.geomes": "Geomes & Biome Influences", - "screen.orespawn.geome_entry": "Geome Settings", - "screen.orespawn.advanced": "Advanced Geology Settings", - "screen.orespawn.guide": "OreSpawn Player Guide", - "guide.orespawn.welcome.title": "Welcome to OreSpawn", - "guide.orespawn.welcome.1": "OreSpawn is a world-building engine used by other mods. Installed alone, it leaves terrain and vanilla ores unchanged.", - "guide.orespawn.welcome.2": "Provider mods can supply rocks, ores, fluid deposits, and ready-made settings. Ore-only mods do not have to add rock layers.", - "guide.orespawn.welcome.3": "Recommended Defaults restores the choices supplied by your installed mods and mod pack. It is the safest place to start.", - "guide.orespawn.world.title": "Choose Your Geology", - "guide.orespawn.world.1": "Your choices are copied into this world when it is created. A template loads a complete setup supplied by a mod or mod pack.", - "guide.orespawn.world.2": "Rock layers are optional. Sky makes biome-influenced geological regions called geomes. Cyano (Legacy) uses the classic older layer engine.", - "guide.orespawn.world.3": "Formation Reach controls width. Layer Thickness controls depth. Waviness bends layers, Edge Detail roughens their borders, and Continuity keeps formations together.", - "guide.orespawn.rocks.title": "Rocks and Geomes", - "guide.orespawn.rocks.1": "Sedimentary, metamorphic, and igneous rocks form in different places and at different depths. Geomes decide which families suit each region.", - "guide.orespawn.rocks.2": "Open Rocks & Ores to edit a block. Removing a rock only stops it appearing in new terrain; the block and its recipes stay available for old worlds.", - "guide.orespawn.rocks.3": "Configure Rock Strata starts with stone, granite, diorite, and andesite. Add Block can deliberately include mod rocks or special vanilla blocks.", - "guide.orespawn.ores.title": "Ores", - "guide.orespawn.ores.1": "Manage Vanilla Ores lets OreSpawn replace Minecraft's normal ore features with the rules shown here. Leave it off to keep vanilla placement.", - "guide.orespawn.ores.2": "Attempts per Chunk controls how often an ore tries to generate. Minimum and Maximum Block Budget control the possible size of each deposit.", - "guide.orespawn.ores.3": "Minimum and Maximum Y set depth. Hosts choose which rock families, blocks, or block tags may be replaced; geome weights favour particular regions.", - "guide.orespawn.mods.title": "Using Other Mods", - "guide.orespawn.mods.1": "Blocks and biomes from installed mods appear in the pickers. OreSpawn does not need a hard dependency just to use their registry names.", - "guide.orespawn.mods.2": "Provider mods and mod packs can supply ready-made rocks, ores, biome influences, dimensions, and templates. OreSpawn validates them before generation.", - "guide.orespawn.mods.3": "Full registry IDs, such as minecraft:granite, identify blocks safely. Missing blocks or invalid settings are rejected before a world is generated.", - "guide.orespawn.server.title": "Worlds and Servers", - "guide.orespawn.server.1": "Your choices are saved in serverconfig/orespawn-worldgen.json inside the world. Keep that file when moving the world to a dedicated server.", - "guide.orespawn.server.2": "Changes affect newly generated chunks only. Land you have already explored is never rewritten.", - "guide.orespawn.server.3": "A server needs the same mods used by the profile. For a new server world, a pack author may instead prepare config/orespawn-worldgen.json.", - "tab.orespawn.sedimentary": "Sedimentary", - "tab.orespawn.metamorphic": "Metamorphic", - "tab.orespawn.igneous": "Igneous", - "tab.orespawn.ores": "Ores", - "tab.orespawn.unassigned": "Unassigned", - "tab.orespawn.geomes": "Geomes", - "tab.orespawn.biomes": "Biomes", - "tab.orespawn.dictionary": "Biome Types", - "tab.orespawn.placement": "Placement", - "tab.orespawn.pattern": "Pattern", - "tab.orespawn.hosts": "Hosts", - "option.orespawn.geology_mode": "Geology", - "option.orespawn.template": "Template", - "option.orespawn.manage_vanilla_ores": "Manage Vanilla Ores", - "option.orespawn.search": "Search", - "option.orespawn.block_id": "Block registry ID", - "option.orespawn.registry_id": "Registry ID", - "option.orespawn.dimension": "Dimension ID", - "option.orespawn.available_dimension": "Available Dimension", - "option.orespawn.mod_filter": "Mod", - "option.orespawn.enabled": "Enabled", - "option.orespawn.family": "Rock Family", - "option.orespawn.weight": "Selection Weight", - "option.orespawn.depth_peak": "Preferred Y", - "option.orespawn.depth_spread": "Depth Spread", - "option.orespawn.min_y": "Minimum Y", - "option.orespawn.max_y": "Maximum Y", - "option.orespawn.frequency": "Attempts / Chunk", - "option.orespawn.ore_richness": "Ore Richness", - "option.orespawn.quantity": "Block Budget", - "option.orespawn.pattern": "Deposit Pattern", - "option.orespawn.height_distribution": "Height Distribution", - "option.orespawn.discard_air_exposure": "Air-Exposure Discard", - "option.orespawn.spread": "Horizontal Spread", - "option.orespawn.vertical_spread": "Vertical Spread", - "option.orespawn.node_size": "Cluster Node Size", - "option.orespawn.ore_replaceable": "Accepts Ores", - "option.orespawn.host_blocks": "Additional Host Blocks (comma separated)", - "option.orespawn.host_tags": "Host Block Tags (comma separated)", - "option.orespawn.base_weight": "Regional Base Weight", - "option.orespawn.stratum_wavelength": "Stratum Wavelength", - "option.orespawn.family_region_wavelength": "Family Region Wavelength", - "option.orespawn.waviness_wavelength": "Waviness Wavelength", - "option.orespawn.waviness_amplitude": "Waviness Amplitude", - "option.orespawn.edge_wavelength": "Edge Wavelength", - "option.orespawn.edge_amplitude": "Edge Amplitude", - "option.orespawn.edge_octaves": "Edge Octaves", - "option.orespawn.continuity": "Continuity", - "option.orespawn.geome_size": "Cyano Region Size", - "option.orespawn.rock_layer_noise": "Cyano Layer Reach", - "option.orespawn.rock_layer_thickness": "Cyano Layer Thickness", - "option.orespawn.min_radius": "Minimum Radius", - "option.orespawn.max_radius": "Maximum Radius", - "option.orespawn.min_vertical_radius": "Minimum Vertical Radius", - "option.orespawn.max_vertical_radius": "Maximum Vertical Radius", - "option.orespawn.max_lobes": "Maximum Lobes", - "option.orespawn.min_solid_cover": "Minimum Solid Cover", - "option.orespawn.min_solid_shell": "Minimum Solid Shell", - "option.orespawn.horizontal_size": "Formation Reach", - "option.orespawn.vertical_thickness": "Layer Thickness", - "option.orespawn.waviness": "Waviness", - "option.orespawn.edge_irregularity": "Edge Detail", - "option.orespawn.formation_continuity": "Continuity", - "value.orespawn.geology_mode.geome": "Sky", - "value.orespawn.geology_mode.legacy": "Cyano (Legacy)", - "value.orespawn.template.none": "Pack Defaults", - "value.orespawn.intrusive": "Igneous (Intrusive)", - "value.orespawn.volcanic": "Igneous (Volcanic)", - "value.orespawn.all_mods": "All Mods", - "value.orespawn.dimension.overworld": "Overworld", - "value.orespawn.dimension.the_nether": "The Nether", - "value.orespawn.dimension.the_end": "The End", - "label.orespawn.adding_to": "Add to %s", - "value.orespawn.family.sedimentary": "Sedimentary", - "value.orespawn.family.metamorphic": "Metamorphic", - "value.orespawn.family.igneous_intrusive": "Igneous (Intrusive)", - "value.orespawn.family.igneous_volcanic": "Igneous (Volcanic)", - "value.orespawn.ore_pattern.vein": "Vein", - "value.orespawn.ore_pattern.cluster": "Clusters", - "value.orespawn.ore_pattern.cloud": "Cloud", - "value.orespawn.height_distribution.uniform": "Uniform", - "value.orespawn.height_distribution.triangle": "Triangular", - "value.orespawn.height_distribution.bottom_triangle": "Bottom-Biased Triangle", - "value.orespawn.height_distribution.uniform_bottom_triangle": "Uniform + Bottom Triangle", - "value.orespawn.ore_richness.ultra_poor": "Ultra Poor", - "value.orespawn.ore_richness.poor": "Poor", - "value.orespawn.ore_richness.average": "Average", - "value.orespawn.ore_richness.rich": "Rich", - "value.orespawn.ore_richness.ultra_rich": "Ultra Rich", - "value.orespawn.preset.tiny": "Tiny", - "value.orespawn.preset.small": "Small", - "value.orespawn.preset.average": "Average", - "value.orespawn.preset.large": "Large", - "value.orespawn.preset.huge": "Huge", - "value.orespawn.preset.custom": "Custom", - "tooltip.orespawn.geology_mode": "Sky uses biome-influenced geomes. Cyano (Legacy) uses the original Cyano rock-layer engine.", - "tooltip.orespawn.manage_vanilla_ores": "ON disables Minecraft's normal ore features and generates those ores with OreSpawn's configured rules. OFF keeps vanilla ore placement.", - "tooltip.orespawn.ore_richness": "Scales attempts per chunk from this ore's installed-pack default. Each step halves or doubles abundance, up to the 64-attempt safety limit; depth and deposit shape stay unchanged.", - "tooltip.orespawn.available_dimension": "Lists dimensions from the current world settings and installed mod data. The registry ID below remains editable for server-only dimensions.", - "tooltip.orespawn.horizontal_size": "Controls how far individual rock formations persist horizontally.", - "tooltip.orespawn.vertical_thickness": "Controls the typical vertical thickness of rock layers.", - "tooltip.orespawn.waviness": "Controls the broad vertical slope and curvature of rock layers.", - "tooltip.orespawn.edge_irregularity": "Adds independent small-scale detail along layer boundaries.", - "tooltip.orespawn.formation_continuity": "Controls how often formations retain their identity across regional and geome boundaries.", - "value.orespawn.ore_pattern.default": "Compact", - "value.orespawn.ore_pattern.normal_cloud": "Cloud", - "value.orespawn.ore_pattern.precision": "Precision", - "value.orespawn.ore_pattern.clusters": "Clusters", - "value.orespawn.ore_pattern.underfluids": "Under Fluids", - "message.orespawn.external_pattern_read_only": "Settings for this registered pattern are read-only here.", - "screen.orespawn.biomes_world_materials": "Biomes & World Materials", - "screen.orespawn.biome_palette": "Biome Palette", - "screen.orespawn.choose_biome": "Choose Installed Biome", - "screen.orespawn.biome_placement": "Biome Settings", - "screen.orespawn.biome_references": "Similar Biomes", - "screen.orespawn.dimension_materials": "%s Materials", - "button.orespawn.biomes_world_materials": "Biomes & World Materials...", - "button.orespawn.biome_palette_count": "Biomes: %s", - "button.orespawn.dimension_materials": "World Materials...", - "button.orespawn.geome_influences": "Geome Influences...", - "button.orespawn.add_biome": "Add Biome...", - "button.orespawn.similar_biomes": "Similar: %s", - "button.orespawn.required_biomes": "Required: %s", - "button.orespawn.clear": "Clear", - "button.orespawn.biome_selected": "%s (selected)", - "button.orespawn.biome_available": "%s", - "option.orespawn.biome_palette": "Biome Palette", - "option.orespawn.biome_mode": "Placement Mode", - "option.orespawn.biome_scope": "Replacement Scope", - "option.orespawn.biome_region_size": "Region Size", - "option.orespawn.min_temperature": "Minimum Temperature", - "option.orespawn.max_temperature": "Maximum Temperature", - "option.orespawn.min_downfall": "Minimum Downfall", - "option.orespawn.max_downfall": "Maximum Downfall", - "option.orespawn.filler_depth": "Filler Depth", - "option.orespawn.top_block": "Top: %s", - "option.orespawn.filler_block": "Filler: %s", - "option.orespawn.underwater_block": "Underwater: %s", - "option.orespawn.ceiling_block": "Ceiling: %s", - "option.orespawn.default_fluid": "Aquifer Fluid: %s", - "option.orespawn.deep_aquifer_fluid": "Deep Aquifer: %s", - "option.orespawn.deep_aquifer_y": "Deep Aquifer Below Y", - "option.orespawn.snow_block": "Snow: %s", - "option.orespawn.ice_block": "Ice: %s", - "value.orespawn.not_set": "Not set", - "value.orespawn.biome_mode.augment": "Augment", - "value.orespawn.biome_mode.replace": "Replace", - "value.orespawn.biome_scope.all": "All Biomes", - "value.orespawn.biome_scope.minecraft_only": "Minecraft Only", - "value.orespawn.biome_scope.selected_namespaces": "Selected Mods", - "tab.orespawn.biome_placement": "Placement", - "tab.orespawn.biome_surface": "Surface", - "tab.orespawn.biome_climate": "Climate", - "guide.orespawn.biomes.title": "Biomes", - "guide.orespawn.biomes.1": "Provider mods can offer new biomes. Augment mixes them with the existing world, while Replace uses only the configured choices where the rule applies.", - "guide.orespawn.biomes.2": "Scope decides which existing biome namespaces can be replaced. Minecraft Only protects every modded biome; Selected Mods changes only namespaces you choose.", - "guide.orespawn.biomes.3": "Region Size controls broad patches. Each biome can use weight, climate limits, similar-biome hints, and optional top, filler, underwater, and ceiling blocks.", - "guide.orespawn.patterns.title": "Ore Shapes and Visibility", - "guide.orespawn.patterns.1": "Compact makes familiar blobs. Veins stretch through rock, Clusters form groups, Clouds spread broadly, and Under Fluids favours blocks beneath liquids.", - "guide.orespawn.patterns.2": "Height Distribution controls where attempts land between Minimum and Maximum Y. Uniform spreads them evenly; triangle choices favour particular depths.", - "guide.orespawn.patterns.3": "Air-Exposure Discard hides some ore touching caves. 0 keeps every exposed attempt and 1 rejects all exposed attempts; values between are chances.", - "guide.orespawn.fluids.title": "Fluid Deposits", - "guide.orespawn.fluids.1": "Fluid Deposits create covered underground pockets of water, lava, oil, or another installed fluid. They are separate from oceans and World Materials.", - "guide.orespawn.fluids.2": "Attempts per Chunk controls rarity. Horizontal and Vertical Radius set deposit size, while Lobes join several rounded pockets into a less regular shape.", - "guide.orespawn.fluids.3": "Solid Cover protects the roof and Solid Shell protects the sides and floor from caves. Hosts and biome rules decide where a deposit is allowed.", - "guide.orespawn.materials.title": "World Materials", - "guide.orespawn.materials.1": "World Materials changes background materials across a whole dimension. Use Fluid Deposits instead when you only want occasional underground pockets.", - "guide.orespawn.materials.2": "Aquifer Fluid changes the normal fluid used below sea level. Deep Aquifer uses a second fluid below the chosen Y level.", - "guide.orespawn.materials.3": "Snow and Ice replace vanilla snow and ordinary ice near the surface. Not set leaves Minecraft's normal material unchanged.", - "button.orespawn.dimension_materials_named": "%s Materials...", - "label.orespawn.dimension_materials.scope": "Applies to every biome in newly generated %s terrain.", - "button.orespawn.mods": "Mods", - "button.orespawn.mod.configure": "Configure", - "tooltip.orespawn.main.mods": "View loaded mods integrated with OreSpawn.", - "screen.orespawn.mods": "OreSpawn Mods", - "tooltip.orespawn.mod.configure": "Open %s configuration.", - "tooltip.orespawn.mod.unavailable": "%s does not provide an OreSpawn-linked configuration screen.", - "label.orespawn.mod.id_version": "%s - Mod version %s", - "label.orespawn.mod.additional_lineage": "Also detected through OreSpawn %s compatibility.", - "label.orespawn.mod.status": "Status: %s", - "label.orespawn.mod.os4_version": "Native OS4 - schema %s, provider revision %s", - "label.orespawn.mod.os4": "Native OS4 provider", - "label.orespawn.mod.legacy_version": "OreSpawn %s compatibility -> OS4 schema %s, provider revision %s", - "label.orespawn.mod.legacy": "OreSpawn %s compatibility -> OS4", - "label.orespawn.mod.config_only": "Client configuration integration", - "label.orespawn.mod.none": "No loaded OreSpawn integrations were found.", - "label.orespawn.mod.integration_status": "%s - %s", - "status.orespawn.mod.active": "Active", - "status.orespawn.mod.pending": "Pending", - "status.orespawn.mod.inactive": "Inactive", - "status.orespawn.mod.rejected": "Rejected", - "status.orespawn.mod.config_only": "Configuration screen only", - "error.orespawn.mod.screen_missing": "%s did not create a configuration screen.", - "error.orespawn.mod.screen_failed": "Could not open %s configuration (%s).", - "button.orespawn.ore_sources": "Ore Sources...", - "tooltip.orespawn.ore_sources": "Review equivalent ore outputs without multiplying placement frequency.", - "screen.orespawn.ore_sources": "Ore Sources", - "screen.orespawn.ore_source_detail": "Ore Source Policy", - "label.orespawn.ore_source.none": "No equivalent managed ore sources were found.", - "label.orespawn.ore_source.row": "%s - %s - %s candidates - %s", - "label.orespawn.ore_source.intro": "Choose how equivalent ores generate.", - "label.orespawn.ore_source.summary": "%s candidates - %s", - "button.orespawn.ore_source.review": "Review...", - "button.orespawn.ore_source.configure": "Configure...", - "label.orespawn.ore_source.material": "Material: %s", - "label.orespawn.ore_source.domain": "Dimension/domain: %s", - "label.orespawn.ore_source.channels": "Placement channels: %s", - "label.orespawn.ore_source.missing": "Missing placement source", - "label.orespawn.ore_source.owner": "Owner: %s, version %s", - "label.orespawn.ore_source.registry": "Registry output: %s, metadata %s", - "label.orespawn.ore_source.dictionary": "Block tags: %s", - "option.orespawn.ore_source.mode": "Mode: %s", - "option.orespawn.ore_source.channel": "Channel: %s", - "option.orespawn.ore_source.placement": "Placement source: %s", - "option.orespawn.ore_source.weight": "Weight", - "mode.orespawn.ore_source.consolidated": "Consolidated", - "mode.orespawn.ore_source.keep_separate": "Keep Separate", - "status.orespawn.ore_source.consolidated": "Consolidated", - "status.orespawn.ore_source.separate": "Separate", - "status.orespawn.ore_source.review_required": "Review required", - "status.orespawn.ore_source.missing_source": "Missing source", - "status.orespawn.ore_source.external_generation": "External generation", - "tooltip.orespawn.ore_source.open": "Open this material policy.", - "tooltip.orespawn.ore_source.mode": "Consolidated uses one placement budget per channel. Keep Separate preserves every configured rule.", - "tooltip.orespawn.ore_source.channel": "Placement engines are arbitrated independently.", - "tooltip.orespawn.ore_source.placement": "Choose the rule that owns frequency, shape, hosts, filters and height for this channel.", - "tooltip.orespawn.ore_source.weight": "Positive relative chance of selecting this output for a whole vein or identified deposit body.", - "tooltip.orespawn.ore_source.external": "This mod registers generation outside OreSpawn; OreSpawn reports it but cannot suppress it.", - "tooltip.orespawn.ore_source.controlled": "OreSpawn controls this configured placement and output source.", - "tooltip.orespawn.ore_source.enrichment": "This entry is enrichment, not an interchangeable ore source.", - "tooltip.orespawn.ore_source.missing": "This source is unavailable. Its saved selection will be restored if the owning mod returns.", - "error.orespawn.ore_source.weight": "Output weights must be positive numbers.", - "button.orespawn.back": "Back", - "button.orespawn.ore_source.all_groups": "All Groups", - "button.orespawn.ore_source.add_group": "Add Group", - "button.orespawn.ore_source.accept": "Accept", - "tooltip.orespawn.ore_source.accept": "Keep the current Keep Original policy unchanged and mark this group as reviewed. It is saved only when the main editor's Done is used.", - "button.orespawn.ore_source.delete_group": "Delete", - "button.orespawn.ore_source.dissolve_group": "Dissolve Group", - "button.orespawn.ore_source.confirm_dissolve": "Confirm Dissolve", - "button.orespawn.ore_source.move": "Move", - "button.orespawn.ore_source.advanced": "Advanced...", - "button.orespawn.ore_source.hide_advanced": "Hide Advanced", - "option.orespawn.ore_source.group_name": "Group name", - "option.orespawn.ore_source.alias": "Block tag", - "option.orespawn.ore_source.output_mode": "Output: %s", - "mode.orespawn.ore_source.keep_original": "Keep Original", - "mode.orespawn.ore_source.balanced": "Balanced", - "mode.orespawn.ore_source.single": "Single", - "mode.orespawn.ore_source.custom": "Custom", - "label.orespawn.ore_source.no_aliases": "No block tags", - "status.orespawn.ore_source.external_short": "external", - "status.orespawn.ore_source.missing_short": "missing", - "status.orespawn.ore_source.output_only": "output only", - "tooltip.orespawn.ore_source.output_only": "This provider output is available, but its placement rule is inactive.", - "tooltip.orespawn.ore_source.output_mode": "Keep Original preserves every rule. Balanced shares equally, Single chooses one block, and Custom enables weighted choices.", - "tooltip.orespawn.ore_source.alias": "Add an exact block tag ID, such as forge:ores/sulfur. Press Move again to confirm reassignment from another group.", - "tooltip.orespawn.ore_source.reset_group": "Restore this curated group name and block tag entries.", - "tooltip.orespawn.ore_source.delete_group": "Delete this empty custom group without rewriting generated chunks.", - "tooltip.orespawn.ore_source.dissolve_group": "Return every entry to its inferred or curated group. Press twice to confirm.", - "error.orespawn.ore_source.confirm_dissolve": "Press Confirm Dissolve to return every block tag entry to its original group.", - "error.orespawn.ore_source.alias": "Use an exact block tag ID such as forge:ores/sulfur.", - "error.orespawn.ore_source.alias_owned": "%s already owns this entry. Press Move to reassign it.", - "error.orespawn.ore_source.manage_vanilla_first": "Enable Manage Vanilla Ores before moving a native vanilla ore into another group.", - "error.orespawn.ore_source.defaults_write": "Could not save material-group defaults.", - "screen.orespawn.ore_source.group_settings": "Ore Source Group Settings", - "label.orespawn.ore_source.groups": "Groups", - "label.orespawn.ore_source.aliases": "Block tags", - "label.orespawn.ore_source.placement_rules": "Placement Rules", - "label.orespawn.ore_source.placement_explanation": "Outputs choose the block; placement rules choose frequency, shape, depth and hosts.", - "label.orespawn.ore_source.standard_veins": "Standard veins", - "label.orespawn.ore_source.placement_choice_count": "%s (%s choices)", - "label.orespawn.ore_source.no_placement_rules": "No managed placement rules", - "tooltip.orespawn.ore_source.group_settings": "Edit this group's name, aliases and placement rules.", - "tooltip.orespawn.ore_source.remove_alias": "Remove %s from this group.", - "tooltip.orespawn.ore_source.placement_rules": "Outputs select the ore block; placement rules select frequency, shape, depth and host restrictions.", - "tooltip.orespawn.ore_source.placement_requires_consolidated": "Placement ownership applies only to consolidated Balanced, Single or Custom modes.", - "tooltip.orespawn.ore_source.placement_missing": "The saved placement source is missing; this channel remains disabled.", - "tooltip.orespawn.ore_source.placement_readonly": "Only one active managed rule is available, so this source is read-only.", - "tooltip.orespawn.ore_source.placement_select": "Click to choose between active managed placement sources.", - "tooltip.orespawn.ore_source.external_excluded": "Independent external generators are reported but cannot be selected as placement rules.", - "button.orespawn.ore_source.hide_single": "Hide Singles", - "tooltip.orespawn.ore_source.group_colours": "Green entries have one alias and one output and need no action. Yellow groups have a saved rule. Red groups need attention. Show All includes green entries.", - "button.orespawn.ore_source.reset_all": "Reset All", - "button.orespawn.ore_source.confirm_reset_all": "Confirm Reset", - "tooltip.orespawn.ore_source.reset_all": "Restore built-in material groups and recreate source policies from the currently loaded ores. The change remains pending until the main editor's Done button is used.", - "screen.orespawn.biomes": "Biomes", - "screen.orespawn.biome_palettes": "Biome Palettes", - "screen.orespawn.biome_palette_settings": "Biome Palette Settings", - "button.orespawn.biome.hide_routine": "Hide Routine", - "button.orespawn.biome.replace_new_terrain": "Replace in new terrain with...", - "button.orespawn.biome.replace_with": "Replace in new terrain with %s...", - "button.orespawn.biome.leave_original": "Leave original behaviour", - "button.orespawn.biome.palettes": "Palettes: %s", - "button.orespawn.biome.reset_selected": "Reset Biome", - "button.orespawn.biome.confirm_reset": "Confirm Reset", - "button.orespawn.biome.reset_palette": "Reset Palette", - "button.orespawn.biome.confirm_reset_palette": "Confirm Palette Reset", - "button.orespawn.biome.reset_dimension": "Reset Dimension", - "button.orespawn.biome.confirm_reset_dimension": "Confirm Dimension Reset", - "button.orespawn.biome.reset_all": "Reset All Biomes", - "button.orespawn.biome.confirm_reset_all": "Confirm All-Biome Reset", - "value.orespawn.biome.status.user_replaced": "Replaced in new terrain", - "value.orespawn.biome.status.layered": "Several placement rules", - "value.orespawn.biome.status.disabled": "Placement disabled", - "value.orespawn.biome.status.missing": "Missing biome or provider", - "value.orespawn.biome.status.provider_managed": "Provider-managed", - "value.orespawn.biome.status.unmanaged": "Registered, unmanaged", - "label.orespawn.biome.rule_count": "Placement rules: %s", - "label.orespawn.biome.replaced_by": "New-terrain replacement: %s", - "label.orespawn.biome.palette_owner_order": "Owner: %s; effective order: %s", - "label.orespawn.biome.surface_effective": "This rule supplies the effective surface.", - "label.orespawn.biome.surface_overridden": "A later enabled rule overrides this surface.", - "label.orespawn.biome.placement_rules": "Placement rules", - "label.orespawn.biome.palette_entries": "Placement entries: %s", - "tooltip.orespawn.biome.no_retrogen": "Biome replacements apply only while new terrain is generated. Existing chunks are never rewritten.", - "tooltip.orespawn.biome.override_palette": "Reserved final replacement layer. Edit it through each biome's replacement control.", - "tooltip.orespawn.biome.palette_entries_read_only": "Palette membership is provider/profile-defined. Select a biome in the directory to edit an existing placement rule.", - "option.orespawn.biome.coverage": "Coverage", - "option.orespawn.biome.fallback_weight": "Original-biome weight", - "option.orespawn.biome.include_namespaces": "Included namespaces (comma-separated)", - "option.orespawn.biome.exclude_namespaces": "Excluded namespaces (comma-separated)" -} diff --git a/src/main/resources/assets/orespawn/lang/en_pt.json b/src/main/resources/assets/orespawn/lang/en_pt.json index 02c75c0c..ba2ec9fd 100644 --- a/src/main/resources/assets/orespawn/lang/en_pt.json +++ b/src/main/resources/assets/orespawn/lang/en_pt.json @@ -115,15 +115,15 @@ "option.orespawn.excluded_biome_ids_short": "Excluded Biomes", "option.orespawn.biome_dictionary_short": "Included Biome Types", "option.orespawn.excluded_biome_dictionary_short": "Excluded Biome Types", - "screen.orespawn.world_settings": "OreSpawn Geology", - "button.orespawn.world_settings": "OreSpawn...", - "button.orespawn.configure_strata": "Configure Rock Strata...", - "button.orespawn.fluid_deposit_details": "Fluid Deposits...", - "button.orespawn.fluid_deposits_count": "Fluid Deposits: %s / %s", - "screen.orespawn.fluid_deposits": "Fluid Deposits", - "screen.orespawn.fluid_deposit": "Fluid Deposit", - "screen.orespawn.fluid_deposit_dimension": "Fluid Deposit Dimension", - "option.orespawn.fluid_deposits": "Fluid Deposits", + "screen.orespawn.world_settings": "OreSpawn's Chart Room", + "button.orespawn.world_settings": "OreSpawn Ahoy...", + "button.orespawn.configure_strata": "Chart Rock Layers...", + "button.orespawn.fluid_deposit_details": "Liquid Caches...", + "button.orespawn.fluid_deposits_count": "Liquid Caches: %s / %s", + "screen.orespawn.fluid_deposits": "Liquid Caches", + "screen.orespawn.fluid_deposit": "Liquid Cache", + "screen.orespawn.fluid_deposit_dimension": "Liquid Cache Waters", + "option.orespawn.fluid_deposits": "Liquid Caches", "option.orespawn.output_fluid": "Output Fluid Block", "option.orespawn.biome_ids": "Biome IDs (comma separated)", "option.orespawn.excluded_biome_ids": "Excluded Biome IDs (comma separated)", @@ -132,61 +132,61 @@ "tooltip.orespawn.fluid_deposits": "ON generates configured covered underground fluid pockets. OFF keeps their settings but does not place them.", "error.orespawn.host_required": "Choose at least one host family, block, or tag.", "error.orespawn.invalid_values": "Check the values and registry IDs.", - "button.orespawn.recommended": "Recommended Defaults", - "button.orespawn.advanced": "Advanced Settings...", - "button.orespawn.help": "Help & Guide", - "button.orespawn.previous": "Previous", - "button.orespawn.next": "Next", - "button.orespawn.materials": "Rocks & Ores...", - "button.orespawn.geomes": "Geomes & Biomes...", - "button.orespawn.formation_details": "Custom Shapes...", - "button.orespawn.cyano_details": "Cyano Settings...", - "button.orespawn.geome_weights": "Geome Weights...", - "button.orespawn.search": "Search", - "button.orespawn.show_all": "Show All", - "button.orespawn.safe_only": "Safe Only", - "button.orespawn.add": "Add", - "button.orespawn.add_block": "Add Block...", - "button.orespawn.reset": "Reset", - "button.orespawn.unassign": "Unassign", - "button.orespawn.remove": "Remove", - "button.orespawn.remove_dimension": "Remove Dimension", - "screen.orespawn.numeric_settings": "Detailed Geology Settings", - "screen.orespawn.materials": "Worldgen Materials", - "screen.orespawn.assign_block": "Assign Block", - "screen.orespawn.choose_block": "Choose Installed Block", - "screen.orespawn.rock_entry": "Rock Settings", - "screen.orespawn.geome_weights": "Geome Weights", - "screen.orespawn.ore_entry": "Ore Settings", - "screen.orespawn.ore_dimension": "Ore Dimension Settings", - "screen.orespawn.geomes": "Geomes & Biome Influences", - "screen.orespawn.geome_entry": "Geome Settings", - "screen.orespawn.advanced": "Advanced Geology Settings", - "screen.orespawn.guide": "OreSpawn Player Guide", - "guide.orespawn.welcome.title": "Welcome to OreSpawn", - "guide.orespawn.welcome.1": "OreSpawn is a world-building engine used by other mods. Installed alone, it leaves terrain and vanilla ores unchanged.", - "guide.orespawn.welcome.2": "Provider mods can supply rocks, ores, fluid deposits, and ready-made settings. Ore-only mods do not have to add rock layers.", - "guide.orespawn.welcome.3": "Recommended Defaults restores the choices supplied by your installed mods and mod pack. It is the safest place to start.", - "guide.orespawn.world.title": "Choose Your Geology", - "guide.orespawn.world.1": "Your choices are copied into this world when it is created. A template loads a complete setup supplied by a mod or mod pack.", - "guide.orespawn.world.2": "Rock layers are optional. Sky makes biome-influenced geological regions called geomes. Cyano (Legacy) uses the classic older layer engine.", - "guide.orespawn.world.3": "Formation Reach controls width. Layer Thickness controls depth. Waviness bends layers, Edge Detail roughens their borders, and Continuity keeps formations together.", - "guide.orespawn.rocks.title": "Rocks and Geomes", - "guide.orespawn.rocks.1": "Sedimentary, metamorphic, and igneous rocks form in different places and at different depths. Geomes decide which families suit each region.", - "guide.orespawn.rocks.2": "Open Rocks & Ores to edit a block. Removing a rock only stops it appearing in new terrain; the block and its recipes stay available for old worlds.", - "guide.orespawn.rocks.3": "Configure Rock Strata starts with stone, granite, diorite, and andesite. Add Block can deliberately include mod rocks or special vanilla blocks.", - "guide.orespawn.ores.title": "Ores", - "guide.orespawn.ores.1": "Manage Vanilla Ores lets OreSpawn replace Minecraft's normal ore features with the rules shown here. Leave it off to keep vanilla placement.", - "guide.orespawn.ores.2": "Attempts per Chunk controls how often an ore tries to generate. Minimum and Maximum Block Budget control the possible size of each deposit.", - "guide.orespawn.ores.3": "Minimum and Maximum Y set depth. Hosts choose which rock families, blocks, or block tags may be replaced; geome weights favour particular regions.", - "guide.orespawn.mods.title": "Using Other Mods", - "guide.orespawn.mods.1": "Blocks and biomes from installed mods appear in the pickers. OreSpawn does not need a hard dependency just to use their registry names.", - "guide.orespawn.mods.2": "Provider mods and mod packs can supply ready-made rocks, ores, biome influences, dimensions, and templates. OreSpawn validates them before generation.", - "guide.orespawn.mods.3": "Full registry IDs, such as minecraft:granite, identify blocks safely. Missing blocks or invalid settings are rejected before a world is generated.", - "guide.orespawn.server.title": "Worlds and Servers", - "guide.orespawn.server.1": "Your choices are saved in serverconfig/orespawn-worldgen.json inside the world. Keep that file when moving the world to a dedicated server.", - "guide.orespawn.server.2": "Changes affect newly generated chunks only. Land you have already explored is never rewritten.", - "guide.orespawn.server.3": "A server needs the same mods used by the profile. For a new server world, a pack author may instead prepare config/orespawn-worldgen.json.", + "button.orespawn.recommended": "Captain's Defaults", + "button.orespawn.advanced": "Fine-Tune the Charts...", + "button.orespawn.help": "Captain's Handbook", + "button.orespawn.previous": "Astern", + "button.orespawn.next": "Ahead", + "button.orespawn.materials": "Rocks & Buried Ores...", + "button.orespawn.geomes": "Chart Geomes & Biomes...", + "button.orespawn.formation_details": "Shape the Layers...", + "button.orespawn.cyano_details": "Rig the Cyano Engine...", + "button.orespawn.geome_weights": "Weigh the Geomes...", + "button.orespawn.search": "Spyglass", + "button.orespawn.show_all": "Show the Lot", + "button.orespawn.safe_only": "Safe Cargo Only", + "button.orespawn.add": "Bring Aboard", + "button.orespawn.add_block": "Bring a Block Aboard...", + "button.orespawn.reset": "Restore the Chart", + "button.orespawn.unassign": "Unclaim", + "button.orespawn.remove": "Strike Off", + "button.orespawn.remove_dimension": "Strike Off Dimension", + "screen.orespawn.numeric_settings": "Geology Chart Details", + "screen.orespawn.materials": "World Materials Ledger", + "screen.orespawn.assign_block": "Give the Block a Job", + "screen.orespawn.choose_block": "Pick a Block from the Hold", + "screen.orespawn.rock_entry": "Rock Chart", + "screen.orespawn.geome_weights": "Weigh the Geomes", + "screen.orespawn.ore_entry": "Ore Chart", + "screen.orespawn.ore_dimension": "Ore Chart by Dimension", + "screen.orespawn.geomes": "Geomes & Biome Charts", + "screen.orespawn.geome_entry": "Geome Chart", + "screen.orespawn.advanced": "Fine-Tune the Geology Charts", + "screen.orespawn.guide": "OreSpawn Captain's Handbook", + "guide.orespawn.welcome.title": "Welcome Aboard OreSpawn", + "guide.orespawn.welcome.1": "OreSpawn helps other mods build yer world. With no providers aboard, it leaves the land and vanilla ores as they were.", + "guide.orespawn.welcome.2": "Provider mods can bring rocks, ores, liquid caches and ready-made orders aboard. An ore-only mod needn't lay any rock layers.", + "guide.orespawn.welcome.3": "Captain's Defaults restores the choices from yer mods and mod pack. Start there if ye haven't charted a course.", + "guide.orespawn.world.title": "Chart Yer Geology", + "guide.orespawn.world.1": "Yer choices are copied into the world when ye make it. A template brings aboard a complete setup from a mod or mod pack.", + "guide.orespawn.world.2": "Rock layers are optional. Sky draws regions called geomes with help from the biomes. Cyano (Legacy) follows the old layer charts.", + "guide.orespawn.world.3": "Formation Reach sets width; Layer Thickness sets depth. Waviness bends the layers, Edge Detail roughens the borders, and Continuity holds the formations together.", + "guide.orespawn.rocks.title": "Rocks Beneath the Keel", + "guide.orespawn.rocks.1": "Sedimentary, metamorphic and igneous rocks belong in different places and at different depths. The geome charts say which families suit each region.", + "guide.orespawn.rocks.2": "Open Rocks & Buried Ores to edit a block. Strike a rock off the chart and it stops appearing in new land; its block and recipes stay aboard for old worlds.", + "guide.orespawn.rocks.3": "Chart Rock Layers starts with stone, granite, diorite and andesite. Bring a Block Aboard can add mod rocks or special vanilla blocks.", + "guide.orespawn.ores.title": "Chart the Buried Ores", + "guide.orespawn.ores.1": "Manage Vanilla Loot lets OreSpawn use these rules in place of Minecraft's usual ore features. Leave it off to keep vanilla placement.", + "guide.orespawn.ores.2": "Attempts per Chunk says how often ye try for ore. Minimum and Maximum Block Budget set the possible size of each deposit.", + "guide.orespawn.ores.3": "Minimum and Maximum Y set depth. Hosts tell ye which rock families, blocks or tags can be replaced; geome weights favour some regions over others.", + "guide.orespawn.mods.title": "Bring Other Mods Aboard", + "guide.orespawn.mods.1": "Blocks and biomes from mods aboard appear in the pickers. OreSpawn needn't depend on a mod just to use its registry names.", + "guide.orespawn.mods.2": "Provider mods and packs can bring ready-made rocks, ores, biome influences, dimensions and templates. OreSpawn checks the cargo before generation.", + "guide.orespawn.mods.3": "Use full registry IDs like minecraft:granite to point to the right block. Missing blocks and bad settings are caught before the world is made.", + "guide.orespawn.server.title": "Keep Yer World and Server Charts", + "guide.orespawn.server.1": "Yer world keeps its orders in serverconfig/orespawn-worldgen.json. Take that file aboard when ye move the world to a dedicated server.", + "guide.orespawn.server.2": "These changes only reach newly charted chunks. Land ye already explored won't be rewritten.", + "guide.orespawn.server.3": "A server needs the same mods aboard as the saved profile. For a new server world, a pack author can prepare config/orespawn-worldgen.json instead.", "tab.orespawn.sedimentary": "Sedimentary", "tab.orespawn.metamorphic": "Metamorphic", "tab.orespawn.igneous": "Igneous", @@ -200,14 +200,14 @@ "tab.orespawn.hosts": "Hosts", "option.orespawn.geology_mode": "Geology", "option.orespawn.template": "Template", - "option.orespawn.manage_vanilla_ores": "Manage Vanilla Ores", - "option.orespawn.search": "Search", + "option.orespawn.manage_vanilla_ores": "Manage Vanilla Loot", + "option.orespawn.search": "Spyglass", "option.orespawn.block_id": "Block registry ID", "option.orespawn.registry_id": "Registry ID", "option.orespawn.dimension": "Dimension ID", "option.orespawn.available_dimension": "Available Dimension", "option.orespawn.mod_filter": "Mod", - "option.orespawn.enabled": "Enabled", + "option.orespawn.enabled": "In Service", "option.orespawn.family": "Rock Family", "option.orespawn.weight": "Selection Weight", "option.orespawn.depth_peak": "Preferred Y", @@ -252,10 +252,10 @@ "option.orespawn.formation_continuity": "Continuity", "value.orespawn.geology_mode.geome": "Sky", "value.orespawn.geology_mode.legacy": "Cyano (Legacy)", - "value.orespawn.template.none": "Pack Defaults", + "value.orespawn.template.none": "Pack's Own Orders", "value.orespawn.intrusive": "Igneous (Intrusive)", "value.orespawn.volcanic": "Igneous (Volcanic)", - "value.orespawn.all_mods": "All Mods", + "value.orespawn.all_mods": "All Mods Aboard", "value.orespawn.dimension.overworld": "Overworld", "value.orespawn.dimension.the_nether": "The Nether", "value.orespawn.dimension.the_end": "The End", @@ -297,20 +297,20 @@ "value.orespawn.ore_pattern.clusters": "Clusters", "value.orespawn.ore_pattern.underfluids": "Under Fluids", "message.orespawn.external_pattern_read_only": "Settings for this registered pattern are read-only here.", - "screen.orespawn.biomes_world_materials": "Biomes & World Materials", - "screen.orespawn.biome_palette": "Biome Palette", - "screen.orespawn.choose_biome": "Choose Installed Biome", - "screen.orespawn.biome_placement": "Biome Settings", - "screen.orespawn.biome_references": "Similar Biomes", - "screen.orespawn.dimension_materials": "%s Materials", + "screen.orespawn.biomes_world_materials": "Biomes & the World's Cargo", + "screen.orespawn.biome_palette": "Biome Chart", + "screen.orespawn.choose_biome": "Pick a Biome Aboard", + "screen.orespawn.biome_placement": "Biome Chart Settings", + "screen.orespawn.biome_references": "Lookalike Biomes", + "screen.orespawn.dimension_materials": "%s Cargo", "button.orespawn.biomes_world_materials": "Biomes & World Materials...", - "button.orespawn.biome_palette_count": "Biomes: %s", - "button.orespawn.dimension_materials": "World Materials...", - "button.orespawn.geome_influences": "Geome Influences...", - "button.orespawn.add_biome": "Add Biome...", + "button.orespawn.biome_palette_count": "Biomes Aboard: %s", + "button.orespawn.dimension_materials": "World Cargo...", + "button.orespawn.geome_influences": "Chart Geome Influences...", + "button.orespawn.add_biome": "Bring a Biome Aboard...", "button.orespawn.similar_biomes": "Similar: %s", "button.orespawn.required_biomes": "Required: %s", - "button.orespawn.clear": "Clear", + "button.orespawn.clear": "Clear the Slate", "button.orespawn.biome_selected": "%s (selected)", "button.orespawn.biome_available": "%s", "option.orespawn.biome_palette": "Biome Palette", @@ -340,28 +340,28 @@ "tab.orespawn.biome_placement": "Placement", "tab.orespawn.biome_surface": "Surface", "tab.orespawn.biome_climate": "Climate", - "guide.orespawn.biomes.title": "Biomes", - "guide.orespawn.biomes.1": "Provider mods can offer new biomes. Augment mixes them with the existing world, while Replace uses only the configured choices where the rule applies.", - "guide.orespawn.biomes.2": "Scope decides which existing biome namespaces can be replaced. Minecraft Only protects every modded biome; Selected Mods changes only namespaces you choose.", - "guide.orespawn.biomes.3": "Region Size controls broad patches. Each biome can use weight, climate limits, similar-biome hints, and optional top, filler, underwater, and ceiling blocks.", - "guide.orespawn.patterns.title": "Ore Shapes and Visibility", - "guide.orespawn.patterns.1": "Compact makes familiar blobs. Veins stretch through rock, Clusters form groups, Clouds spread broadly, and Under Fluids favours blocks beneath liquids.", - "guide.orespawn.patterns.2": "Height Distribution controls where attempts land between Minimum and Maximum Y. Uniform spreads them evenly; triangle choices favour particular depths.", - "guide.orespawn.patterns.3": "Air-Exposure Discard hides some ore touching caves. 0 keeps every exposed attempt and 1 rejects all exposed attempts; values between are chances.", - "guide.orespawn.fluids.title": "Fluid Deposits", - "guide.orespawn.fluids.1": "Fluid Deposits create covered underground pockets of water, lava, oil, or another installed fluid. They are separate from oceans and World Materials.", - "guide.orespawn.fluids.2": "Attempts per Chunk controls rarity. Horizontal and Vertical Radius set deposit size, while Lobes join several rounded pockets into a less regular shape.", - "guide.orespawn.fluids.3": "Solid Cover protects the roof and Solid Shell protects the sides and floor from caves. Hosts and biome rules decide where a deposit is allowed.", - "guide.orespawn.materials.title": "World Materials", - "guide.orespawn.materials.1": "World Materials changes background materials across a whole dimension. Use Fluid Deposits instead when you only want occasional underground pockets.", - "guide.orespawn.materials.2": "Aquifer Fluid changes the normal fluid used below sea level. Deep Aquifer uses a second fluid below the chosen Y level.", - "guide.orespawn.materials.3": "Snow and Ice replace vanilla snow and ordinary ice near the surface. Not set leaves Minecraft's normal material unchanged.", - "button.orespawn.dimension_materials_named": "%s Materials...", - "label.orespawn.dimension_materials.scope": "Applies to every biome in newly generated %s terrain.", - "button.orespawn.mods": "Mods", - "button.orespawn.mod.configure": "Configure", + "guide.orespawn.biomes.title": "Chart the Biomes", + "guide.orespawn.biomes.1": "Provider mods can bring new biomes aboard. Augment mixes them into the existing world; Replace uses only the choices on yer chart where the rule applies.", + "guide.orespawn.biomes.2": "Scope sets which biome namespaces can be swapped. Minecraft Only protects modded biomes; Selected Mods changes only the namespaces ye pick.", + "guide.orespawn.biomes.3": "Region Size sets the width of each patch. Ye can set weights, climate limits, similar biomes and optional top, filler, underwater and ceiling blocks.", + "guide.orespawn.patterns.title": "Shape the Buried Ores", + "guide.orespawn.patterns.1": "Compact makes familiar lumps. Veins stretch through rock, Clusters gather in groups, Clouds scatter wider, and Under Fluids favours blocks below liquids.", + "guide.orespawn.patterns.2": "Height Distribution sets where attempts land between Minimum and Maximum Y. Uniform spreads them evenly; triangle choices favour particular depths on yer chart.", + "guide.orespawn.patterns.3": "Air-Exposure Discard can hide ore that touches a cave. Set it to 0 to keep every exposed attempt, or 1 to reject them all. Values in between are chances.", + "guide.orespawn.fluids.title": "Liquid Caches Below Deck", + "guide.orespawn.fluids.1": "Liquid Caches make covered pockets of water, lava, oil or another fluid aboard. They don't change oceans or the materials in the World Cargo chart.", + "guide.orespawn.fluids.2": "Attempts per Chunk controls how rare the caches are. Horizontal and Vertical Radius set size, while Lobes join rounded pockets into a less regular shape.", + "guide.orespawn.fluids.3": "Solid Cover guards the roof and Solid Shell guards the sides and floor against caves. Host and biome rules tell ye where a cache may form.", + "guide.orespawn.materials.title": "Take Stock of World Cargo", + "guide.orespawn.materials.1": "World Cargo swaps background materials across an entire dimension. Use Liquid Caches if ye only want the odd underground pocket.", + "guide.orespawn.materials.2": "Aquifer Fluid sets the usual fluid below sea level. Deep Aquifer uses another fluid below the Y level ye choose.", + "guide.orespawn.materials.3": "Snow and Ice swap the vanilla materials near the surface. Leave a material unset to keep Minecraft's usual block.", + "button.orespawn.dimension_materials_named": "%s Cargo...", + "label.orespawn.dimension_materials.scope": "Every biome in newly charted %s terrain uses this cargo.", + "button.orespawn.mods": "Mods Aboard", + "button.orespawn.mod.configure": "Set Up", "tooltip.orespawn.main.mods": "View loaded mods integrated with OreSpawn.", - "screen.orespawn.mods": "OreSpawn Mods", + "screen.orespawn.mods": "OreSpawn Mods Aboard", "tooltip.orespawn.mod.configure": "Open %s configuration.", "tooltip.orespawn.mod.unavailable": "%s does not provide an OreSpawn-linked configuration screen.", "label.orespawn.mod.id_version": "%s - Mod version %s", @@ -381,16 +381,16 @@ "status.orespawn.mod.config_only": "Configuration screen only", "error.orespawn.mod.screen_missing": "%s did not create a configuration screen.", "error.orespawn.mod.screen_failed": "Could not open %s configuration (%s).", - "button.orespawn.ore_sources": "Ore Sources...", + "button.orespawn.ore_sources": "Ore Supplies...", "tooltip.orespawn.ore_sources": "Review equivalent ore outputs without multiplying placement frequency.", - "screen.orespawn.ore_sources": "Ore Sources", - "screen.orespawn.ore_source_detail": "Ore Source Policy", - "label.orespawn.ore_source.none": "No equivalent managed ore sources were found.", + "screen.orespawn.ore_sources": "Ore Supplies", + "screen.orespawn.ore_source_detail": "Ore Supply Orders", + "label.orespawn.ore_source.none": "No matching managed ore supplies aboard.", "label.orespawn.ore_source.row": "%s - %s - %s candidates - %s", - "label.orespawn.ore_source.intro": "Choose how equivalent ores generate.", + "label.orespawn.ore_source.intro": "Set the orders for matching ores.", "label.orespawn.ore_source.summary": "%s candidates - %s", - "button.orespawn.ore_source.review": "Review...", - "button.orespawn.ore_source.configure": "Configure...", + "button.orespawn.ore_source.review": "Inspect...", + "button.orespawn.ore_source.configure": "Give Orders...", "label.orespawn.ore_source.material": "Material: %s", "label.orespawn.ore_source.domain": "Dimension/domain: %s", "label.orespawn.ore_source.channels": "Placement channels: %s", @@ -419,17 +419,17 @@ "tooltip.orespawn.ore_source.enrichment": "This entry is enrichment, not an interchangeable ore source.", "tooltip.orespawn.ore_source.missing": "This source is unavailable. Its saved selection will be restored if the owning mod returns.", "error.orespawn.ore_source.weight": "Output weights must be positive numbers.", - "button.orespawn.back": "Back", - "button.orespawn.ore_source.all_groups": "All Groups", - "button.orespawn.ore_source.add_group": "Add Group", - "button.orespawn.ore_source.accept": "Accept", + "button.orespawn.back": "Back to Deck", + "button.orespawn.ore_source.all_groups": "Every Crew", + "button.orespawn.ore_source.add_group": "Form a Crew", + "button.orespawn.ore_source.accept": "Aye", "tooltip.orespawn.ore_source.accept": "Keep the current Keep Original policy unchanged and mark this group as reviewed. It is saved only when the main editor's Done is used.", - "button.orespawn.ore_source.delete_group": "Delete", - "button.orespawn.ore_source.dissolve_group": "Dissolve Group", - "button.orespawn.ore_source.confirm_dissolve": "Confirm Dissolve", - "button.orespawn.ore_source.move": "Move", - "button.orespawn.ore_source.advanced": "Advanced...", - "button.orespawn.ore_source.hide_advanced": "Hide Advanced", + "button.orespawn.ore_source.delete_group": "Strike Off", + "button.orespawn.ore_source.dissolve_group": "Disband Crew", + "button.orespawn.ore_source.confirm_dissolve": "Aye, Disband Crew", + "button.orespawn.ore_source.move": "Move to This Crew", + "button.orespawn.ore_source.advanced": "More Orders...", + "button.orespawn.ore_source.hide_advanced": "Stow Extra Orders", "option.orespawn.ore_source.group_name": "Group name", "option.orespawn.ore_source.alias": "Block tag", "option.orespawn.ore_source.output_mode": "Output: %s", @@ -452,8 +452,8 @@ "error.orespawn.ore_source.alias_owned": "%s already owns this entry. Press Move to reassign it.", "error.orespawn.ore_source.manage_vanilla_first": "Enable Manage Vanilla Ores before moving a native vanilla ore into another group.", "error.orespawn.ore_source.defaults_write": "Could not save material-group defaults.", - "screen.orespawn.ore_source.group_settings": "Ore Source Group Settings", - "label.orespawn.ore_source.groups": "Groups", + "screen.orespawn.ore_source.group_settings": "Ore Crew Orders", + "label.orespawn.ore_source.groups": "Crews", "label.orespawn.ore_source.aliases": "Block tags", "label.orespawn.ore_source.placement_rules": "Placement Rules", "label.orespawn.ore_source.placement_explanation": "Outputs choose the block; placement rules choose frequency, shape, depth and hosts.", @@ -468,27 +468,27 @@ "tooltip.orespawn.ore_source.placement_readonly": "Only one active managed rule is available, so this source is read-only.", "tooltip.orespawn.ore_source.placement_select": "Click to choose between active managed placement sources.", "tooltip.orespawn.ore_source.external_excluded": "Independent external generators are reported but cannot be selected as placement rules.", - "button.orespawn.ore_source.hide_single": "Hide Singles", + "button.orespawn.ore_source.hide_single": "Hide Lone Sources", "tooltip.orespawn.ore_source.group_colours": "Green entries have one alias and one output and need no action. Yellow groups have a saved rule. Red groups need attention. Show All includes green entries.", - "button.orespawn.ore_source.reset_all": "Reset All", - "button.orespawn.ore_source.confirm_reset_all": "Confirm Reset", + "button.orespawn.ore_source.reset_all": "Restore All Orders", + "button.orespawn.ore_source.confirm_reset_all": "Aye, Restore", "tooltip.orespawn.ore_source.reset_all": "Restore built-in material groups and recreate source policies from the currently loaded ores. The change remains pending until the main editor's Done button is used.", - "screen.orespawn.biomes": "Biomes", - "screen.orespawn.biome_palettes": "Biome Palettes", - "screen.orespawn.biome_palette_settings": "Biome Palette Settings", - "button.orespawn.biome.hide_routine": "Hide Routine", - "button.orespawn.biome.replace_new_terrain": "Replace in new terrain with...", - "button.orespawn.biome.replace_with": "Replace in new terrain with %s...", - "button.orespawn.biome.leave_original": "Leave original behaviour", - "button.orespawn.biome.palettes": "Palettes: %s", - "button.orespawn.biome.reset_selected": "Reset Biome", - "button.orespawn.biome.confirm_reset": "Confirm Reset", - "button.orespawn.biome.reset_palette": "Reset Palette", - "button.orespawn.biome.confirm_reset_palette": "Confirm Palette Reset", - "button.orespawn.biome.reset_dimension": "Reset Dimension", - "button.orespawn.biome.confirm_reset_dimension": "Confirm Dimension Reset", - "button.orespawn.biome.reset_all": "Reset All Biomes", - "button.orespawn.biome.confirm_reset_all": "Confirm All-Biome Reset", + "screen.orespawn.biomes": "Biome Charts", + "screen.orespawn.biome_palettes": "Biome Chart Sets", + "screen.orespawn.biome_palette_settings": "Biome Chart Orders", + "button.orespawn.biome.hide_routine": "Hide Ordinary Biomes", + "button.orespawn.biome.replace_new_terrain": "Swap in New Land...", + "button.orespawn.biome.replace_with": "Swap New Land for %s...", + "button.orespawn.biome.leave_original": "Keep the Old Course", + "button.orespawn.biome.palettes": "Chart Sets: %s", + "button.orespawn.biome.reset_selected": "Restore Biome", + "button.orespawn.biome.confirm_reset": "Aye, Restore", + "button.orespawn.biome.reset_palette": "Restore Chart Set", + "button.orespawn.biome.confirm_reset_palette": "Aye, Restore Chart Set", + "button.orespawn.biome.reset_dimension": "Restore Dimension", + "button.orespawn.biome.confirm_reset_dimension": "Aye, Restore Dimension", + "button.orespawn.biome.reset_all": "Restore All Biomes", + "button.orespawn.biome.confirm_reset_all": "Aye, Restore All Biomes", "value.orespawn.biome.status.user_replaced": "Replaced in new terrain", "value.orespawn.biome.status.layered": "Several placement rules", "value.orespawn.biome.status.disabled": "Placement disabled", diff --git a/src/nativeOreIntegrationTest/java/zone/moddev/mc/orespawn/testmod/NativeOreProbeTestMod.java b/src/nativeOreIntegrationTest/java/zone/moddev/mc/orespawn/testmod/NativeOreProbeTestMod.java index f3c0e7da..a7320196 100644 --- a/src/nativeOreIntegrationTest/java/zone/moddev/mc/orespawn/testmod/NativeOreProbeTestMod.java +++ b/src/nativeOreIntegrationTest/java/zone/moddev/mc/orespawn/testmod/NativeOreProbeTestMod.java @@ -38,6 +38,7 @@ public final class NativeOreProbeTestMod { private static final String QUARTZ = "minecraft:nether_quartz_ore"; private static final String NETHER = "minecraft:the_nether"; + private static final String IMPORTED_QUARTZ = "orespawn:legacy/minecraft/quartz"; public NativeOreProbeTestMod() { MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGHEST, this::prepare); @@ -47,6 +48,7 @@ public NativeOreProbeTestMod() { private void prepare(FMLServerAboutToStartEvent event) { Path profile = profilePath(event.getServer()); String phase = System.getProperty("nativeoreprobe.phase", "fresh"); + String scenario = System.getProperty("nativeoreprobe.importMode", ""); if ("reload".equals(phase)) return; try { JsonObject root; @@ -83,6 +85,19 @@ private void prepare(FMLServerAboutToStartEvent event) { tags.add("forge:netherrack"); rule.add("host_tags", tags); } + if (scenario.startsWith("import-")) { + JsonObject ores = root.getAsJsonObject("ores"); + JsonObject imported = new JsonParser().parse(ores.get(QUARTZ).toString()).getAsJsonObject(); + imported.addProperty("block", QUARTZ); + imported.addProperty("source_mod", "legacy"); + imported.addProperty("native_generation", false); + ores.add(IMPORTED_QUARTZ, imported); + // If the modern rule still runs, it will place quartz outside the imported band. + JsonObject nativeRule = ores.getAsJsonObject(QUARTZ).getAsJsonObject("dimensions").getAsJsonObject(NETHER); + nativeRule.addProperty("min_y", 85); + nativeRule.addProperty("max_y", 91); + } + if (scenario.endsWith("-off")) root.addProperty("manage_vanilla_ores", false); Files.createDirectories(profile.getParent()); Files.write(profile, new GsonBuilder().setPrettyPrinting().create().toJson(root) .getBytes(StandardCharsets.UTF_8)); @@ -94,8 +109,9 @@ private void prepare(FMLServerAboutToStartEvent event) { private void audit(FMLServerStartedEvent event) { MinecraftServer server = event.getServer(); String phase = System.getProperty("nativeoreprobe.phase", "fresh"); - if (!NativeOreProbeBridge.controlsQuartz()) { - throw new IllegalStateException("Quartz is not controlled by OreSpawn before generation"); + boolean managementOff = System.getProperty("nativeoreprobe.importMode", "").endsWith("-off"); + if (NativeOreProbeBridge.controlsQuartz() == managementOff) { + throw new IllegalStateException("Quartz control does not match Manage Vanilla Ores"); } if (WorldGeologyProfileManager.activeProfile().oreRetrogenEnabled()) { throw new IllegalStateException("The quartz test must not enable retrogen"); @@ -108,7 +124,7 @@ private void audit(FMLServerStartedEvent event) { if (!"fresh".equals(phase)) { try (InputStream input = Files.newInputStream(marker)) { previous.load(input); } } - Properties current = inspect(world, -8, 24, 28); + Properties current = inspect(world, -8, managementOff ? 0 : 24, managementOff ? 255 : 28); current.setProperty("profile_sha256", hex(digest().digest(Files.readAllBytes(profilePath(server))))); if (!"fresh".equals(phase)) { for (String name : current.stringPropertyNames()) { diff --git a/src/test/java/zone/moddev/mc/orespawn/LocalizationParityTest.java b/src/test/java/zone/moddev/mc/orespawn/LocalizationParityTest.java index d74367fa..d4d1f78d 100644 --- a/src/test/java/zone/moddev/mc/orespawn/LocalizationParityTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/LocalizationParityTest.java @@ -26,7 +26,7 @@ class LocalizationParityTest { private static final Path LANG_DIR = Paths.get("src", "main", "resources", "assets", "orespawn", "lang"); private static final Set EXPECTED_LOCALES = ImmutableSet.of( - "de_au.json", "de_de.json", "en_ca.json", "en_en.json", + "de_at.json", "de_de.json", "en_ca.json", "en_gb.json", "en_pt.json", "en_us.json", "es_es.json", "es_mx.json", "fr_ca.json", "fr_fr.json", "ja_jp.json", "ko_kr.json", "pt_br.json", "ru_ru.json", "zh_cn.json"); @@ -37,18 +37,18 @@ class LocalizationParityTest { private static final Map> INTENTIONAL_ENGLISH_VALUES = ImmutableMap.>builder() .put("button.orespawn.world_settings", ImmutableSet.of( - "de_au.json", "de_de.json", "es_es.json", "es_mx.json", "fr_ca.json", + "de_at.json", "de_de.json", "es_es.json", "es_mx.json", "fr_ca.json", "fr_fr.json", "ja_jp.json", "ko_kr.json", "pt_br.json", "ru_ru.json", "zh_cn.json")) .put("button.orespawn.biome_available", ImmutableSet.of( - "de_au.json", "de_de.json", "es_es.json", "es_mx.json", "fr_ca.json", + "de_at.json", "de_de.json", "es_es.json", "es_mx.json", "fr_ca.json", "fr_fr.json", "ja_jp.json", "ko_kr.json", "pt_br.json", "ru_ru.json", "zh_cn.json")) .put("option.orespawn.mod_filter", ImmutableSet.of( - "de_au.json", "de_de.json", "fr_ca.json", "fr_fr.json")) + "de_at.json", "de_de.json", "fr_ca.json", "fr_fr.json")) .put("screen.orespawn.biomes", ImmutableSet.of("fr_ca.json", "fr_fr.json")) .put("tab.orespawn.biomes", ImmutableSet.of("fr_ca.json", "fr_fr.json")) - .put("tab.orespawn.geomes", ImmutableSet.of("de_au.json", "de_de.json")) + .put("tab.orespawn.geomes", ImmutableSet.of("de_at.json", "de_de.json")) .put("tab.orespawn.placement", ImmutableSet.of("fr_ca.json", "fr_fr.json")) .put("tab.orespawn.biome_placement", ImmutableSet.of("fr_ca.json", "fr_fr.json")) .put("tab.orespawn.biome_surface", ImmutableSet.of("fr_ca.json", "fr_fr.json")) @@ -56,19 +56,19 @@ class LocalizationParityTest { .put("value.orespawn.geology_mode.geome", ImmutableSet.of( "es_es.json", "es_mx.json", "fr_ca.json", "fr_fr.json", "ja_jp.json", "ko_kr.json", "pt_br.json", "ru_ru.json", "zh_cn.json")) - .put("value.orespawn.geology_mode.legacy", ImmutableSet.of("de_au.json", "de_de.json")) - .put("value.orespawn.height_distribution.uniform", ImmutableSet.of("de_au.json", "de_de.json")) + .put("value.orespawn.geology_mode.legacy", ImmutableSet.of("de_at.json", "de_de.json")) + .put("value.orespawn.height_distribution.uniform", ImmutableSet.of("de_at.json", "de_de.json")) .put("value.orespawn.height_distribution.triangle", ImmutableSet.of("pt_br.json")) .put("value.orespawn.ore_pattern.default", ImmutableSet.of( - "de_au.json", "de_de.json", "es_es.json", "es_mx.json", "fr_ca.json", + "de_at.json", "de_de.json", "es_es.json", "es_mx.json", "fr_ca.json", "fr_fr.json", "ja_jp.json", "ko_kr.json", "pt_br.json", "ru_ru.json", "zh_cn.json")) .put("value.orespawn.ore_pattern.precision", ImmutableSet.of( - "de_au.json", "de_de.json", "es_es.json", "es_mx.json", "fr_ca.json", + "de_at.json", "de_de.json", "es_es.json", "es_mx.json", "fr_ca.json", "fr_fr.json", "ja_jp.json", "ko_kr.json", "pt_br.json", "ru_ru.json", "zh_cn.json")) .put("value.orespawn.ore_pattern.underfluids", ImmutableSet.of( - "de_au.json", "de_de.json", "es_es.json", "es_mx.json", "fr_ca.json", + "de_at.json", "de_de.json", "es_es.json", "es_mx.json", "fr_ca.json", "fr_fr.json", "ja_jp.json", "ko_kr.json", "pt_br.json", "ru_ru.json", "zh_cn.json")) .build(); @@ -81,6 +81,14 @@ class LocalizationParityTest { "\uad11\ubb3c\ud559", "\u9271\u7269\u5b66", "\u77ff\u7269\u5b66", "\u7926\u7269\u5b78" }; + @Test + void shippedGameLocaleIdsHaveTheirOwnMenuText() throws Exception { + assertEquals("Empfohlene Standardeinstellungen", read(LANG_DIR.resolve("de_at.json")) + .get("button.orespawn.recommended").getAsString()); + assertEquals("Captain's Defaults", read(LANG_DIR.resolve("en_pt.json")) + .get("button.orespawn.recommended").getAsString()); + } + @Test void everyLocaleMatchesEnglishKeysAndFormatting() throws Exception { JsonObject english = read(LANG_DIR.resolve("en_us.json")); diff --git a/src/test/java/zone/moddev/mc/orespawn/client/OreSourceEditorViewTest.java b/src/test/java/zone/moddev/mc/orespawn/client/OreSourceEditorViewTest.java new file mode 100644 index 00000000..3b4d391b --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/client/OreSourceEditorViewTest.java @@ -0,0 +1,32 @@ +package zone.moddev.mc.orespawn.client; + +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 com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Test; + +class OreSourceEditorViewTest { + @Test + void oldInferredChannelShowsOnceWithoutRewritingTheSavedChoice() { + JsonObject profile = parse("{\"ores\":{\"example:copper\":{\"dimensions\":" + + "{\"minecraft:overworld\":{\"pattern\":\"orespawn:precision\"}}}}}"); + JsonObject saved = parse("{\"domain\":\"minecraft:overworld\"," + + "\"placement_sources\":{\"orespawn:precision\":\"example:copper\"}," + + "\"candidates\":[{\"source_id\":\"example:copper\",\"placement_channel\":\"orespawn:standard\",\"loaded\":true}," + + "{\"source_id\":\"example:copper\",\"placement_channel\":\"orespawn:precision\",\"loaded\":false}]}"); + String before = saved.toString(); + + JsonObject view = OreSourceEditorView.policy(profile, saved); + assertEquals(1, view.getAsJsonArray("candidates").size()); + assertTrue(view.getAsJsonObject("placement_sources").has("orespawn:standard")); + assertFalse(view.getAsJsonObject("placement_sources").has("orespawn:precision")); + assertEquals(before, saved.toString()); + } + + private static JsonObject parse(String json) { + return new JsonParser().parse(json).getAsJsonObject(); + } +} diff --git a/src/test/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacementsTest.java b/src/test/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacementsTest.java new file mode 100644 index 00000000..1ec93821 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacementsTest.java @@ -0,0 +1,202 @@ +package zone.moddev.mc.orespawn.integration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Test; +import zone.moddev.mc.orespawn.util.JsonCopies; + +class LegacyOreReplacementsTest { + private static final String PROVIDER = "baseminerals"; + private static final String OLD = "baseminerals:legacy/baseminerals_lithium_ore_0"; + private static final String NATIVE = "baseminerals:legacy/lithium_ore"; + private static final String FALLBACK_POLICY = "orespawn:lithium|orespawn:all_except_nether_end"; + + private final JsonObject profile = parse("{\"ores\":{},\"ore_source_policies\":{}}"); + private final JsonObject ores = profile.getAsJsonObject("ores"); + private final JsonObject policies = profile.getAsJsonObject("ore_source_policies"); + private final JsonObject defaults = new JsonObject(); + private final JsonObject manifest = new JsonObject(); + private final Set known = new LinkedHashSet<>(Arrays.asList(OLD)); + + LegacyOreReplacementsTest() { + ores.add(OLD, parse("{\"block\":\"baseminerals:lithium_ore\",\"enabled\":false," + + "\"source_mod\":\"baseminerals\",\"source_provider\":\"baseminerals\"," + + "\"native_generation\":false,\"outputs\":[{\"block\":\"baseminerals:lithium_ore\"}]," + + "\"dimensions\":{\"minecraft:overworld\":{\"frequency\":1.5}}}")); + defaults.add(NATIVE, parse("{\"block\":\"baseminerals:lithium_ore\",\"enabled\":true," + + "\"dimensions\":{\"minecraft:overworld\":{\"frequency\":4.0}}," + + "\"dimension_selectors\":{\"orespawn:all_except_nether_end\":{\"frequency\":4.0}}}")); + } + + @Test + void forge25ImportCanBeRecognisedBeforeAProviderHasAManifest() { + String imported = "orespawn:legacy/baseminerals/lithium"; + JsonObject converted = ores.remove(OLD).getAsJsonObject(); + converted.addProperty("source_mod", "legacy"); + converted.addProperty("suppress_vanilla", true); + converted.remove("source_provider"); + ores.add(imported, converted); + known.clear(); + + assertTrue(merge().suppressed.contains(NATIVE)); + assertEquals(imported, manifest.getAsJsonObject("legacy_ore_replacements").get(NATIVE).getAsString()); + assertEquals(converted, ores.get(imported)); + } + + @Test + void instanceMergeDoesNotRemoveAnExistingRuleWithoutAWorldBackup() { + addNative(); + LegacyOreReplacements.Result result = LegacyOreReplacements.reconcile(profile, PROVIDER, ores, + defaults, defaults, manifest, known, false); + assertFalse(result.suppressed.contains(NATIVE)); + assertTrue(ores.has(NATIVE)); + } + + @Test + void remembersImportAndPreservesItsScopeAndChoicesOnEveryReload() { + policies.add("existing", parse("{\"mode\":\"consolidated\",\"output_mode\":\"single\",\"outputs\":{\"" + OLD + "\":1}}")); + JsonObject saved = JsonCopies.copy(profile); + assertTrue(merge().suppressed.contains(NATIVE)); + assertEquals(OLD, manifest.getAsJsonObject("legacy_ore_replacements").get(NATIVE).getAsString()); + assertEquals(saved, profile); + assertTrue(LegacyOreReplacements.retains(manifest, OLD, JsonCopies.keys(defaults))); + assertFalse(merge().changed); + // Removing a previously tracked import is not permission to add the native default. + ores.remove(OLD); + assertTrue(merge().suppressed.contains(NATIVE)); + } + + @Test + void dropsOnlyTheUntouchedAddedDefaultAndItsGeneratedFallback() { + addNative(); + policies.add(FALLBACK_POLICY, parse("{\"material\":\"orespawn:lithium\",\"domain\":\"orespawn:all_except_nether_end\"," + + "\"mode\":\"keep_separate\",\"output_mode\":\"custom\",\"status\":\"separate\"," + + "\"review_required\":false,\"outputs\":{\"" + NATIVE + "\":1}," + + "\"placement_sources\":{\"orespawn:standard\":\"" + NATIVE + "\"}}")); + JsonObject original = JsonCopies.copy(ores.getAsJsonObject(OLD)); + assertTrue(merge().changed); + assertFalse(ores.has(NATIVE)); + assertFalse(policies.has(FALLBACK_POLICY)); + assertEquals(original, ores.get(OLD)); + assertFalse(merge().changed); + } + + @Test + void emptyScopeAddedByOreSourceDiscoveryIsNotAUserEdit() { + defaults.getAsJsonObject(NATIVE).remove("dimension_selectors"); + addNative(); + ores.getAsJsonObject(NATIVE).add("dimension_selectors", new JsonObject()); + + assertTrue(merge().suppressed.contains(NATIVE)); + assertFalse(ores.has(NATIVE)); + } + + @Test + void keepsANativeRuleWithAnAddedPlacementScope() { + defaults.getAsJsonObject(NATIVE).remove("dimension_selectors"); + addNative(); + ores.getAsJsonObject(NATIVE).add("dimension_selectors", + parse("{\"orespawn:all_except_nether_end\":{\"frequency\":1}}")); + JsonObject before = JsonCopies.copy(profile); + + assertFalse(merge().suppressed.contains(NATIVE)); + assertEquals(before, profile); + assertTrue(manifest.getAsJsonObject("legacy_ore_review").has(NATIVE)); + } + + @Test + void retainsAnEditedNativeRuleAndAsksForReview() { + addNative(); + ores.getAsJsonObject(NATIVE).addProperty("enabled", false); + JsonObject before = JsonCopies.copy(profile); + assertFalse(merge().suppressed.contains(NATIVE)); + assertEquals(before, profile); + assertTrue(manifest.getAsJsonObject("legacy_ore_review").has(NATIVE)); + assertFalse(merge().changed); + } + + @Test + void retainsAnUntouchedNativeRuleWhenPlayerPolicyUsesIt() { + addNative(); + policies.add("existing", parse("{\"mode\":\"consolidated\",\"output_mode\":\"single\",\"outputs\":{\"" + NATIVE + "\":1}}")); + JsonObject before = JsonCopies.copy(profile); + assertFalse(merge().suppressed.contains(NATIVE)); + assertEquals(before, profile); + assertTrue(manifest.getAsJsonObject("legacy_ore_review").has(NATIVE)); + } + + @Test + void doesNotGuessForMultipleImportsOrMultipleNativeRules() { + String second = OLD.substring(0, OLD.length() - 1) + "1"; + known.add(second); + ores.add(second, JsonCopies.copy(ores.get(OLD))); + addNative(); + assertTrue(merge().suppressed.contains(NATIVE)); + assertTrue(ores.has(NATIVE)); + assertFalse(manifest.has("legacy_ore_replacements")); + assertTrue(manifest.getAsJsonObject("legacy_ore_review").has(NATIVE)); + + ores.remove(second); + defaults.add("baseminerals:other_lithium", JsonCopies.copy(defaults.get(NATIVE))); + assertTrue(merge().suppressed.contains(NATIVE)); + assertTrue(ores.has(NATIVE)); + assertFalse(manifest.has("legacy_ore_replacements")); + } + + @Test + void doesNotMatchAnUntrackedRuleOrAnotherOutputVariant() { + known.clear(); + assertFalse(merge().suppressed.contains(NATIVE)); + known.add(OLD); + defaults.getAsJsonObject(NATIVE).addProperty("metadata", 1); + assertFalse(merge().suppressed.contains(NATIVE)); + ores.getAsJsonObject(OLD).addProperty("source_provider", "anothermod"); + defaults.getAsJsonObject(NATIVE).remove("metadata"); + assertFalse(merge().suppressed.contains(NATIVE)); + assertFalse(manifest.has("legacy_ore_replacements")); + } + + @Test + void recognizesStockDuplicateEvenWhenTheOldConvertedFileIsActive() { + addNative(); + JsonObject active = new JsonObject(); + active.add(OLD, JsonCopies.copy(ores.get(OLD))); + assertTrue(LegacyOreReplacements.reconcile(profile, PROVIDER, ores, defaults, active, manifest, known, true) + .suppressed.contains(NATIVE)); + assertFalse(ores.has(NATIVE)); + } + + @Test + void retainsADefaultChangedByAProviderOverride() { + addNative(); + JsonObject active = JsonCopies.copy(defaults); + active.getAsJsonObject(NATIVE).addProperty("enabled", false); + assertFalse(LegacyOreReplacements.reconcile(profile, PROVIDER, ores, defaults, active, manifest, known, true) + .suppressed.contains(NATIVE)); + assertTrue(ores.has(NATIVE)); + assertTrue(manifest.getAsJsonObject("legacy_ore_review").has(NATIVE)); + } + + private void addNative() { + JsonObject nativeRule = JsonCopies.copy(defaults.getAsJsonObject(NATIVE)); + nativeRule.addProperty("source_provider", PROVIDER); + ores.add(NATIVE, nativeRule); + known.add(NATIVE); + } + + private LegacyOreReplacements.Result merge() { + return LegacyOreReplacements.reconcile(profile, PROVIDER, ores, defaults, defaults, manifest, known, true); + } + + private static JsonObject parse(String source) { + return new JsonParser().parse(source).getAsJsonObject(); + } +} diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigratorTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigratorTest.java index 52eef7b3..76ec7466 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigratorTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigratorTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; @@ -22,6 +23,17 @@ class LegacyConfigMigratorTest { @TempDir Path temporary; + @Test + void leavesAnExistingInstanceProfileAlone() throws IOException { + Path legacy = Files.createDirectories(temporary.resolve("orespawn3")).resolve("basemetals.json"); + write(legacy, baseMetalsFixture().toString()); + Path target = temporary.resolve("orespawn-worldgen.json"); + String original = "{\"ores\":{},\"manage_vanilla_ores\":false}"; + write(target, original); + assertNull(LegacyConfigMigrator.migrateIfNeeded(target, new JsonObject())); + assertEquals(original, read(target)); + } + @Test void migratesBaseMetalsOs3FixtureExactly() throws IOException { Path legacyDirectory = Files.createDirectories(temporary.resolve("orespawn3")); diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/MmdOreConflictCatalogTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/MmdOreConflictCatalogTest.java index 4f9e6db7..68c8820b 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/MmdOreConflictCatalogTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/MmdOreConflictCatalogTest.java @@ -38,9 +38,9 @@ void unrelatedExternalAndEnrichmentProvidersDoNotTriggerBalancing() { @Test void establishedPrioritiesAndDimensionProvidersRemainDistinct() { - assertEquals(Arrays.asList("mineralogy", "baseminerals", "electricadvantage"), + assertEquals(Arrays.asList("mineralogy", "baseminerals", "baseelectric", "electricadvantage"), MmdOreConflictCatalog.priority("sulfur")); - assertEquals(Arrays.asList("baseminerals", "electricadvantage"), + assertEquals(Arrays.asList("baseminerals", "baseelectric", "electricadvantage"), MmdOreConflictCatalog.priority("lithium")); assertEquals(Collections.emptyList(), MmdOreConflictCatalog.priority("copper")); assertTrue(MmdOreConflictCatalog.netherOnly("nethermetals")); diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/NativeOreHostTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/NativeOreHostTest.java index f3507943..985d53a1 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/NativeOreHostTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/NativeOreHostTest.java @@ -5,6 +5,8 @@ import java.lang.reflect.Method; import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; import java.util.Set; import org.junit.jupiter.api.AfterEach; @@ -12,6 +14,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import net.minecraft.block.Block; @@ -42,6 +46,17 @@ void restoreTags() { BlockTags.setCollection(previousTags); } + @Test + void plainStoneHostDoesNotAcceptTheFlattenedIgneousBlocks() throws Exception { + Method add = OreSpawnOreGeneration.class.getDeclaredMethod("addBlocks", Map.class, JsonElement.class); + add.setAccessible(true); + Map hosts = new IdentityHashMap<>(); + JsonArray blocks = new JsonArray(); + blocks.add("minecraft:stone"); + add.invoke(null, hosts, blocks); + assertEquals(Collections.singletonMap(Blocks.STONE, 1.0D), hosts); + } + @Test void missingLegacyNetherHostStillResolvesNativeNetherrack() throws Exception { assertEquals(Collections.singleton(Blocks.NETHERRACK), resolve(NETHERRACK)); diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSourcePoliciesTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSourcePoliciesTest.java index 44d62954..82671fe8 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSourcePoliciesTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSourcePoliciesTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import net.minecraft.util.ResourceLocation; @@ -16,6 +17,47 @@ class OreSourcePoliciesTest { private static final ResourceLocation SULFUR = id("orespawn:sulfur"); private static final ResourceLocation OVERWORLD = id("minecraft:overworld"); + @Test + void importedBuiltInPatternsShareTheStandardChannelButExplicitChannelsStaySeparate() { + JsonObject rule = new JsonParser().parse("{\"pattern\":{\"type\":\"orespawn:precision\"}}").getAsJsonObject(); + assertEquals(OreSourcePolicies.STANDARD, OreSourcePolicies.placementChannel(rule)); + rule.addProperty("placement_channel", "orespawn:precision"); + assertEquals(id("orespawn:precision"), OreSourcePolicies.placementChannel(rule)); + rule.remove("placement_channel"); + rule.addProperty("pattern", "example:precision"); + assertEquals(id("example:precision"), OreSourcePolicies.placementChannel(rule)); + } + + @Test + void retainedChannelMustMatchItsOwnPatternWithoutChangingTheProfile() { + JsonObject profile = new JsonParser().parse("{\"ores\":{\"example:ore\":{\"dimensions\":{" + + "\"minecraft:overworld\":{\"pattern\":{\"type\":\"orespawn:precision\"}}}}}}").getAsJsonObject(); + String before = profile.toString(); + assertTrue(OreSourcePolicies.legacyChannelSelection(profile, id("example:ore"), OVERWORLD, + id("orespawn:precision"), OreSourcePolicies.STANDARD)); + assertFalse(OreSourcePolicies.legacyChannelSelection(profile, id("example:ore"), OVERWORLD, + id("orespawn:clusters"), OreSourcePolicies.STANDARD)); + assertEquals(before, profile.toString()); + profile.getAsJsonObject("ores").getAsJsonObject("example:ore").getAsJsonObject("dimensions") + .getAsJsonObject("minecraft:overworld").addProperty("placement_channel", "orespawn:standard"); + assertFalse(OreSourcePolicies.legacyChannelSelection(profile, id("example:ore"), OVERWORLD, + id("orespawn:precision"), OreSourcePolicies.STANDARD)); + } + + @Test + void baseElectricIsRecognisedWithoutChangingAnExistingWorldsPolicy() { + OreSourcePolicies.Group shared = group(); + shared.add(candidate("mineralogy:sulfur", "mineralogy", "mineralogy:sulfur_ore", + "forge:ores/sulfur", true, true, false, false)); + shared.add(candidate("baseelectric:sulfur", "baseelectric", "baseelectric:sulfur_ore", + "forge:ores/sulfur", true, true, false, false)); + JsonObject fresh = OreSourcePolicies.createPolicy(shared, false); + assertEquals("consolidated", fresh.get("mode").getAsString()); + assertEquals("mineralogy:sulfur", fresh.getAsJsonObject("placement_sources") + .get("orespawn:standard").getAsString()); + assertSeparate(OreSourcePolicies.createPolicy(shared, true)); + } + @Test void newWorldBalancesOnlyTwoLoadedOrdinaryOwnersWithAnExactSharedTag() { OreSourcePolicies.Group one = group(); diff --git a/src/worldHashTest/java/zone/moddev/mc/orespawn/testmod/WorldHashProbeTestMod.java b/src/worldHashTest/java/zone/moddev/mc/orespawn/testmod/WorldHashProbeTestMod.java index 4acf674b..d6c29973 100644 --- a/src/worldHashTest/java/zone/moddev/mc/orespawn/testmod/WorldHashProbeTestMod.java +++ b/src/worldHashTest/java/zone/moddev/mc/orespawn/testmod/WorldHashProbeTestMod.java @@ -9,11 +9,14 @@ import java.util.Properties; import net.minecraft.block.Block; +import net.minecraft.init.Biomes; import net.minecraft.util.ResourceLocation; import net.minecraft.util.math.BlockPos; import net.minecraft.world.WorldServer; +import net.minecraft.world.biome.provider.BiomeProvider; import net.minecraft.world.chunk.Chunk; import net.minecraft.world.dimension.DimensionType; +import net.minecraftforge.common.BiomeDictionary; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.EventPriority; import net.minecraftforge.fml.common.Mod; @@ -30,6 +33,11 @@ public WorldHashProbeTestMod() { private void audit(FMLServerStartedEvent event) { WorldServer world = event.getServer().getWorld(DimensionType.OVERWORLD); if (world == null) throw new IllegalStateException("Overworld is unavailable"); + if (Boolean.getBoolean("worldhashprobe.findBadlands")) { + locateBadlands(world); + event.getServer().initiateShutdown(); + return; + } // Adjacent veins can reach the outer row. Finish the surrounding chunks before hashing the 81-chunk area. for (int x = 251; x <= 261; x++) { world.getChunk(x, 251); @@ -90,6 +98,33 @@ private void audit(FMLServerStartedEvent event) { } } + private static void locateBadlands(WorldServer world) { + BiomeProvider biomes = world.getChunkProvider().getChunkGenerator().getBiomeProvider(); + BiomeDictionary.Type mesa = BiomeDictionary.Type.getType("MESA"); + for (int radius = 0; radius <= 512; radius += 8) { + for (int z = -radius; z <= radius; z += 8) { + for (int x = -radius; x <= radius; x += 8) { + if (Math.abs(x) != radius && Math.abs(z) != radius) continue; + boolean covered = true; + for (int dz = -3; dz <= 3 && covered; dz++) { + for (int dx = -3; dx <= 3; dx++) { + BlockPos center = new BlockPos(((x + dx) << 4) + 8, 64, ((z + dz) << 4) + 8); + if (!BiomeDictionary.hasType(biomes.getBiome(center, Biomes.PLAINS), mesa)) { + covered = false; + break; + } + } + } + if (covered) { + System.out.println("BADLANDS_CENTER " + x + " " + z); + return; + } + } + } + } + throw new IllegalStateException("No 49-chunk Badlands sample near spawn"); + } + private static MessageDigest digest() { try { return MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException failure) { throw new IllegalStateException(failure); }