From 340278d7d3901ff504780fa230c82406c0ac74e9 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Mon, 26 Oct 2026 16:36:25 +0000 Subject: [PATCH] Fix imported ore placement limits on 1.10.2 --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 12 + README.md | 2 +- docs/CONFIGURATION.md | 14 + docs/MIGRATION.md | 7 + docs/VERSIONS.md | 2 +- gradle.properties | 2 +- gradle/release/artifacts.gradle | 6 +- .../orespawn/compat/LegacyOs3Bridge.java | 2 +- src/main/java/cyano/orespawn/OreSpawn.java | 2 +- .../zone/moddev/mc/orespawn/OreSpawn.java | 2 +- .../orespawn/client/GeologyEditorSession.java | 18 +- .../orespawn/client/OreSourceEditorView.java | 120 +++++++ .../client/OreSourceGroupSettingsScreen.java | 26 +- .../orespawn/client/OreSourceListScreen.java | 8 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../worldgen/LegacyOs3ProfileMigration.java | 2 +- .../orespawn/worldgen/OreSourcePolicies.java | 35 +- .../worldgen/OreSpawnOreGeneration.java | 43 ++- .../client/LegacyOreSourceEditorTest.java | 170 ++++++++++ .../worldgen/LegacyPlacementChannelTest.java | 160 +++++++++ .../worldgen/LegacyVanillaOreTest.java | 21 ++ .../ore-source-editor/custom-channel.json | 128 +++++++ .../ore-source-editor/fresh-standard.json | 125 +++++++ .../ore-source-editor/os1-import.json | 142 ++++++++ .../ore-source-editor/os3-import.json | 142 ++++++++ .../editor/EditorFixtureMod.java | 319 ++++++++++++++++++ tools/qualification/editor/probe.init.gradle | 31 ++ 28 files changed, 1513 insertions(+), 36 deletions(-) create mode 100644 src/main/java/zone/moddev/mc/orespawn/client/OreSourceEditorView.java create mode 100644 src/test/java/zone/moddev/mc/orespawn/client/LegacyOreSourceEditorTest.java create mode 100644 src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyPlacementChannelTest.java create mode 100644 src/test/resources/ore-source-editor/custom-channel.json create mode 100644 src/test/resources/ore-source-editor/fresh-standard.json create mode 100644 src/test/resources/ore-source-editor/os1-import.json create mode 100644 src/test/resources/ore-source-editor/os3-import.json create mode 100644 tools/qualification/editor/EditorFixtureMod.java create mode 100644 tools/qualification/editor/probe.init.gradle diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c89a18ea..41af3ba2 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.2.110021.jar - build/libs/OreSpawn-4.1.2.110021-sources.jar - build/libs/OreSpawn-4.1.2.110021-javadoc.jar + build/libs/OreSpawn-4.1.3.110021.jar + build/libs/OreSpawn-4.1.3.110021-sources.jar + build/libs/OreSpawn-4.1.3.110021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0ef74c03..0990b261 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,15 @@ +Version 4.1.3.110021 + +* Apply hybrid and replacement background limits to imported built-in ore + patterns, just as for ordinary native rules. Keep explicit channels and + third-party deposit patterns independent. +* Retain older Ore Sources selections under their saved pattern names without + rewriting imported settings or regenerating existing chunks. +* Show imported placement choices in the editor without duplicate old-channel + rows. Distinguish an inactive source from a missing mod, and keep custom + channels and saved choices intact. +* Keep the empty Ore Sources message inside its pane in smaller windows. + Version 4.1.2.110021 * Respect Manage Vanilla Ores for imported OS1/OS3 rules. With it off, leave diff --git a/README.md b/README.md index e78cb932..4066a3b5 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It gives mods and modpacks one place to configure ores, deposit shapes, optional rock strata and geomes, provider-owned underground fluid deposits, biome palettes and world materials, flat bedrock, and bounded ore retrogen. -This branch builds target-qualified version `4.1.2.110021`: the OreSpawn 4.1.2 +This branch builds target-qualified version `4.1.3.110021`: the OreSpawn 4.1.3 feature set for Minecraft 1.10.2 and Forge. It adds stable generation identity, non-loading geology sampling, per-resource background control, existing-world provider merge policy, an Ore Sources arbiter for equivalent provider ores and diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 8ce98181..e13af100 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -308,6 +308,14 @@ informational because the original rules remain independent. The compact help and row tooltips explain fixed, selectable, missing and inactive states without reducing the alias-list height. +Built-in patterns use the standard channel whether their names are short +(`default`, `vein`) or fully qualified (`orespawn:default`, `orespawn:vein`). +This lets a child's hybrid or replacement policy reduce imported ordinary +veins too. An explicit `placement_channel` always wins, and third-party pattern +channels remain independent. Older saved source selections still select the +same rule; changing this classification does not rewrite their ore settings +or existing terrain. + Native vanilla ore rules are active placement sources only when `manage_vanilla_ores` is enabled. Changing that option refreshes the pending Ore Sources snapshot before the editor opens. While it is disabled, Group @@ -337,6 +345,12 @@ BaseSciences has no ordinary ore-source role. An Ore Dictionary member whose generator is not controlled by OreSpawn is reported as external generation and is never suppressed, so total abundance may still be increased by that mod. +Older imported profiles may still store a built-in pattern name as their +placement channel. The Ore Sources editor shows a single ordinary placement +row for that choice; opening the screen does not rewrite the saved profile. +Explicit custom channels stay separate. A source whose placement is switched +off remains listed as output-only, rather than being reported as a missing mod. + ## Fluid Deposits, Retrogen, And Bedrock Each `fluid_deposits` entry has a stable provider-namespaced rule ID, an diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 81ab0f81..d6ac17d9 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -167,6 +167,13 @@ Original), preserving prior placement frequency, ordering and output behaviour. Missing selected sources and dormant policies remain recorded. Policy changes affect new chunks only and do not add retrogen. +Legacy imports may spell built-in patterns as `orespawn:default` or another +fully qualified built-in name. These are ordinary generation, so hybrid and +replacement background limits apply to them. Saved Ore Sources choices under +those older inferred channel names remain valid without changing the saved +ore rule. Explicit channels, custom patterns, imported quantities and heights, +and already generated chunks are left alone. + The old `place_crude_oil` and singleton `oil` fields migrate to `place_fluid_deposits` and a provider-owned rule. A valid block becomes `:fluid_deposit/`; `minecraft:air` creates no rule. Mineralogy diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 6684c80e..a89ad8fe 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -51,7 +51,7 @@ Examples: | Minecraft | Loader | Target | Example full version | | --- | --- | ---: | --- | -| 1.10.2 | Forge | `110021` | `4.1.2.110021` | +| 1.10.2 | Forge | `110021` | `4.1.3.110021` | | 1.12.2 | Forge | `112021` | `4.0.6.112021` | | 1.13.2 | Forge | `113021` | `4.0.6.113021` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | diff --git a/gradle.properties b/gradle.properties index 9e05572c..42c6e12b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -30,7 +30,7 @@ mod_vendor=Minecraft Mod Development mod_license=LGPL-2.1 mod_license_name=GNU Lesser General Public License, Version 2.1 mod_license_url=https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt -mod_version=4.1.2.110021 +mod_version=4.1.3.110021 mod_group=zone.moddev.mc.orespawn mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/gradle/release/artifacts.gradle b/gradle/release/artifacts.gradle index f2580a87..aa28aa24 100644 --- a/gradle/release/artifacts.gradle +++ b/gradle/release/artifacts.gradle @@ -77,7 +77,7 @@ tasks.register('verifyReleaseConfiguration') { description = 'Validates the target-qualified release, API, schemas, reports, and publishing identity.' doLast { - if (project.mod_version != '4.1.2.110021' + if (project.mod_version != '4.1.3.110021' || project.mod_group != expectedMavenGroup) { throw new GradleException("Unexpected OreSpawn release version: ${project.mod_version}") } @@ -106,8 +106,8 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.1.2.110021')) { - throw new GradleException("Authoritative release location does not contain 4.1.2.110021: ${path}") + if (!file(path).getText('UTF-8').contains('4.1.3.110021')) { + throw new GradleException("Authoritative release location does not contain 4.1.3.110021: ${path}") } } if (!file('docs/API.md').getText('UTF-8').contains('orespawn@[4.0.6,5.0.0)')) { diff --git a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java index 654f7b3a..c7363018 100644 --- a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java +++ b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java @@ -1027,7 +1027,7 @@ private static void writeHumanUpgradeReport(Path destination) throws IOException } } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.1.2.110021 Upgrade Report"); + lines.add("OreSpawn 4.1.3.110021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn configuration was consumed and translated for OS4."); diff --git a/src/main/java/cyano/orespawn/OreSpawn.java b/src/main/java/cyano/orespawn/OreSpawn.java index 95cdc0a4..02b6d108 100644 --- a/src/main/java/cyano/orespawn/OreSpawn.java +++ b/src/main/java/cyano/orespawn/OreSpawn.java @@ -19,7 +19,7 @@ public class OreSpawn { public static OreSpawn INSTANCE; public static final String MODID = "orespawn"; public static final String NAME = "OreSpawn"; - public static final String VERSION = "4.1.2.110021"; + public static final String VERSION = "4.1.3.110021"; public static final List oreSpawnConfigFiles = new ArrayList<>(); public static final List additionalStoneBlocks = new ArrayList<>(); public static boolean disableVanillaOreGen; diff --git a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java index 33524512..5758c852 100644 --- a/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java +++ b/src/main/java/zone/moddev/mc/orespawn/OreSpawn.java @@ -52,7 +52,7 @@ public class OreSpawn { public static final String MODID = "orespawn"; public static final String NAME = "OreSpawn"; - public static final String VERSION = "4.1.2.110021"; + public static final String VERSION = "4.1.3.110021"; private static final Logger LOGGER = LogManager.getLogger(); 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 5bf6f88b..1fbf6d4f 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/GeologyEditorSession.java @@ -409,7 +409,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; @@ -983,8 +983,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()) { @@ -992,8 +992,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); } } @@ -1005,10 +1007,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; } } 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..0bb26129 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/client/OreSourceEditorView.java @@ -0,0 +1,120 @@ +package zone.moddev.mc.orespawn.client; + +import java.util.ArrayList; +import java.util.Arrays; +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 zone.moddev.mc.orespawn.util.JsonCopies; + +/** Presents old inferred channel names 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 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 5f00dc9b..8b05f7e9 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/OreSourceGroupSettingsScreen.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/OreSourceGroupSettingsScreen.java @@ -152,11 +152,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()); } @@ -189,6 +188,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) { @@ -325,6 +327,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)) { @@ -333,7 +345,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 281cb6e8..eba9479e 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/OreSourceListScreen.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/OreSourceListScreen.java @@ -507,8 +507,12 @@ public void render(int mouseX, int mouseY, float partialTick) { new TextComponentString(error), width - 24)), width / 2, height - 39, 0xFF5555); } if (selected(orderedGroups()) == null) { - drawCenteredString(font, new TextComponentTranslation("label.orespawn.ore_source.none"), - leftPaneX + (leftPaneWidth / 2), CONTENT_TOP + 10, 0xA0A0A0); + int y = CONTENT_TOP + 10; + for (String line : font.listFormattedStringToWidth( + I18n.format("label.orespawn.ore_source.none"), leftPaneWidth - 12)) { + drawString(font, new TextComponentString(line), leftPaneX + 6, y, 0xA0A0A0); + y += font.FONT_HEIGHT + 2; + } } updateWeightBounds(); super.render(mouseX, mouseY, partialTick); 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 ac40f223..1845ca8e 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -125,7 +125,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configDirectory, } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.1.2.110021 Upgrade Report"); + lines.add("OreSpawn 4.1.3.110021 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + mineralogyVersion diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java index d29b0d5f..7b05e71f 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java @@ -65,7 +65,7 @@ private static void writeInitialUpgradeReport(Path configDirectory, boolean forceRetrogen, boolean flatBedrock, boolean retrogenBedrock, int bedrockLayers) throws IOException { String newline = System.lineSeparator(); - String text = "OreSpawn 4.1.2.110021 Upgrade Report" + newline + String text = "OreSpawn 4.1.3.110021 Upgrade Report" + newline + "================================" + newline + newline + "RESULT: Legacy OreSpawn settings were imported into the OS4 profile." + newline + "- Manage vanilla ores: " + manageVanilla + newline diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java index 440ddc9c..68ba9040 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java @@ -167,16 +167,45 @@ static ResourceLocation material(JsonObject root, JsonObject ore, ItemStack outp static ResourceLocation placementChannel(JsonObject rule) { ResourceLocation explicit = resource(string(rule, "placement_channel", "")); - if (explicit != null) return explicit; + + if (explicit != null) { + return explicit; + } + JsonObject pattern = rule.has("pattern") && rule.get("pattern").isJsonObject() ? 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 type) { + // Imports name built-in patterns in full, but they still place ordinary veins. + // Only these known patterns share a channel; custom patterns stay independent. + if ("orespawn".equals(type.getResourceDomain())) { + switch (type.getResourcePath()) { + case "default": + case "vein": + case "normal_cloud": + case "precision": + case "clusters": + case "underfluids": + return STANDARD; + default: + break; + } + } + + return type; } static Inference inferMaterial(Iterable oreNames) { 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 1259e511..d12544da 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSpawnOreGeneration.java @@ -533,8 +533,8 @@ private static List arbitrate(JsonObject profile, List origi profile, first.material, first.policyDomain, available); List selected = new ArrayList<>(); for (Entry placement : placements.entrySet()) { - ResourceLocation channel = placement.getKey(); ResourceLocation source = placement.getValue(); + ResourceLocation channel = available.get(source); for (BakedOre candidate : entry.getValue()) { if (channel.equals(candidate.channel) && source.equals(candidate.ruleId)) { selected.add(candidate.withSources(sources)); @@ -569,19 +569,54 @@ static Map selectedPlacementRules(JsonObject JsonObject policy = policies.getAsJsonObject(key); if (!"consolidated".equals(string(policy, "mode", "keep_separate"))) return null; Map result = new LinkedHashMap<>(); - for (Entry placement : objectOrEmpty( - policy, "placement_sources").entrySet()) { + JsonObject placements = objectOrEmpty(policy, "placement_sources"); + + for (Entry placement : placements.entrySet()) { ResourceLocation channel = resource(placement.getKey()); ResourceLocation source; try { source = resource(placement.getValue().getAsString()); } catch (RuntimeException invalid) { continue; } - if (channel != null && source != null && channel.equals(availableSourceChannels.get(source))) { + + if (channel == null || source == null) { + continue; + } + + ResourceLocation currentChannel = availableSourceChannels.get(source); + boolean retainedAlias = !placements.has(OreSourcePolicies.STANDARD.toString()) + && legacyChannelSelection(profile, source, domain, channel, currentChannel); + + if (channel.equals(currentChannel) || retainedAlias) { result.put(channel, source); } } return Collections.unmodifiableMap(result); } + private static boolean legacyChannelSelection(JsonObject profile, ResourceLocation source, + ResourceLocation domain, ResourceLocation savedChannel, ResourceLocation currentChannel) { + if (!OreSourcePolicies.STANDARD.equals(currentChannel) + || !OreSourcePolicies.STANDARD.equals(OreSourcePolicies.patternChannel(savedChannel))) { + return false; + } + + JsonObject ore = objectOrEmpty(objectOrEmpty(profile, "ores"), source.toString()); + JsonObject dimensions = objectOrEmpty(ore, "dimensions"); + JsonObject placement = dimensions.has(domain.toString()) + ? objectOrEmpty(dimensions, domain.toString()) + : objectOrEmpty(objectOrEmpty(ore, "dimension_selectors"), domain.toString()); + + // Honour the old inferred name without changing a saved selection. A newer + // explicit channel or standard-channel choice must not fall back to this alias. + if (placement.has("placement_channel")) { + return false; + } + + JsonObject pattern = objectOrEmpty(placement, "pattern"); + ResourceLocation oldChannel = resource(pattern.has("type") + ? string(pattern, "type", "") : string(placement, "pattern", "")); + return savedChannel.equals(oldChannel); + } + static int weightedIndex(double sample, double... weights) { double total = 0.0D; for (double weight : weights) if (Double.isFinite(weight) && weight > 0.0D) total += weight; diff --git a/src/test/java/zone/moddev/mc/orespawn/client/LegacyOreSourceEditorTest.java b/src/test/java/zone/moddev/mc/orespawn/client/LegacyOreSourceEditorTest.java new file mode 100644 index 00000000..976c6832 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/client/LegacyOreSourceEditorTest.java @@ -0,0 +1,170 @@ +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.assertNotNull; + +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import zone.moddev.mc.orespawn.client.GeologyEditorSession.OreSourceGroup; +import zone.moddev.mc.orespawn.test.Forge12TestBootstrap; +import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfile; + +class LegacyOreSourceEditorTest { + private static final String GROUP = "orespawn:iron|minecraft:overworld"; + + @BeforeAll + static void bootstrap() { + Forge12TestBootstrap.registerVanilla(); + } + + @Test + void importedSelectionsHaveOneLiveStandardRowWithoutChangingTheSavedProfile() throws Exception { + for (String name : Arrays.asList("os1-import", "os3-import")) { + JsonObject fixture = fixture(name); + GeologyEditorSession session = session(fixture); + String before = session.root().toString(); + OreSourceGroup group = session.oreSourceGroups().get(0); + + assertEquals(Collections.singletonList("orespawn:standard"), + OreSourceGroupSettingsScreen.placementChannels(group), name); + assertEquals("consolidated", group.status, name); + assertEquals(2, group.candidates.size(), name); + assertEquals(2, OreSourceListScreen.placementCandidates(group, "orespawn:standard").size()); + assertEquals(before, session.root().toString(), "Viewing must not rewrite saved choices"); + } + } + + @Test + void explicitCustomChannelsAndFreshStandardSelectionsStaySeparate() throws Exception { + for (String name : Arrays.asList("custom-channel", "fresh-standard")) { + GeologyEditorSession session = session(fixture(name)); + String before = session.root().toString(); + OreSourceGroup group = session.oreSourceGroups().get(0); + String expected = name.equals("custom-channel") ? "example:custom_iron" : "orespawn:standard"; + + assertEquals(Collections.singletonList(expected), + OreSourceGroupSettingsScreen.placementChannels(group)); + assertEquals("consolidated", group.status); + assertEquals(before, session.root().toString()); + } + } + + @Test + void anExplicitOldDefaultChannelIsNotAnInferredAlias() throws Exception { + JsonObject fixture = fixture("custom-channel"); + fixture.getAsJsonObject("ores").getAsJsonObject("example:iron") + .getAsJsonObject("dimensions").getAsJsonObject("minecraft:overworld") + .addProperty("placement_channel", "orespawn:default"); + JsonObject policy = policy(fixture); + policy.getAsJsonObject("placement_sources").remove("example:custom_iron"); + policy.getAsJsonObject("placement_sources").addProperty("orespawn:default", "example:iron"); + for (com.google.gson.JsonElement entry : policy.getAsJsonArray("candidates")) { + entry.getAsJsonObject().addProperty("placement_channel", "orespawn:default"); + } + + assertEquals(Collections.singletonList("orespawn:default"), + OreSourceGroupSettingsScreen.placementChannels(session(fixture).oreSourceGroups().get(0))); + } + + @Test + void aNewStandardChoiceWinsButAnActuallyMissingSourceStillNeedsAttention() throws Exception { + JsonObject fixture = fixture("os3-import"); + policy(fixture).getAsJsonObject("placement_sources") + .addProperty("orespawn:standard", "example:alternative_iron"); + OreSourceGroup group = session(fixture).oreSourceGroups().get(0); + + assertEquals(Collections.singletonList("orespawn:standard"), + OreSourceGroupSettingsScreen.placementChannels(group)); + assertEquals("example:alternative_iron", group.placements.get("orespawn:standard")); + assertEquals("consolidated", group.status); + + policy(fixture).getAsJsonObject("placement_sources").addProperty("orespawn:standard", "missing:iron"); + assertEquals("missing_source", session(fixture).oreSourceGroups().get(0).status); + } + + @Test + void editsRoundTripWithoutChangingRulesAndCancelLeavesTheOriginalUntouched() throws Exception { + JsonObject fixture = fixture("os3-import"); + String original = fixture.toString(); + GeologyEditorSession session = session(fixture); + String rules = session.root().get("ores").toString(); + + session.setOreSourcePlacement(GROUP, "orespawn:standard", "example:alternative_iron"); + assertEquals("example:alternative_iron", + session(session.profile().toJson()).oreSourceGroups().get(0).placements.get("orespawn:standard")); + assertEquals(rules, session.root().get("ores").toString()); + assertEquals(original, fixture.toString(), "Discarding a session must leave its input intact"); + } + + @Test + void loadedInactiveSourcesDoNotLookMissing() throws Exception { + JsonObject fixture = fixture("os1-import"); + for (com.google.gson.JsonElement entry : policy(fixture).getAsJsonArray("candidates")) { + entry.getAsJsonObject().addProperty("placement_active", false); + } + + OreSourceGroup group = session(fixture).oreSourceGroups().get(0); + assertEquals("consolidated", group.status); + assertNotNull(OreSourceGroupSettingsScreen.selectedPlacementCandidate(group, "orespawn:standard")); + assertFalse(OreSourceGroupSettingsScreen.placementSelectable(group, "orespawn:standard")); + } + + @Test + void manageVanillaSwitchKeepsImportedChoicesAndCustomRules() throws Exception { + for (String name : Arrays.asList("os1-import", "os3-import", "custom-channel", "fresh-standard")) { + GeologyEditorSession session = session(fixture(name)); + String rules = session.root().get("ores").toString(); + String choices = policy(session.root()).get("placement_sources").toString(); + + session.setManageVanillaOres(false); + assertEquals(choices, policy(session.root()).get("placement_sources").toString(), name); + session.setManageVanillaOres(true); + assertEquals(choices, policy(session.root()).get("placement_sources").toString(), name); + assertEquals(rules, session.root().get("ores").toString(), name); + } + } + + @Test + void unrelatedAliasesAndMultipleSavedPatternChoicesAreNotSilentlyMerged() throws Exception { + JsonObject fixture = fixture("os3-import"); + JsonObject placements = policy(fixture).getAsJsonObject("placement_sources"); + placements.addProperty("orespawn:vein", "example:alternative_iron"); + fixture.getAsJsonObject("ores").getAsJsonObject("example:alternative_iron") + .getAsJsonObject("dimensions").getAsJsonObject("minecraft:overworld") + .addProperty("pattern", "orespawn:vein"); + String before = fixture.toString(); + OreSourceGroup group = session(fixture).oreSourceGroups().get(0); + + assertEquals(placements.toString(), new com.google.gson.Gson().toJsonTree(group.placements).toString()); + assertNotNull(OreSourceGroupSettingsScreen.selectedPlacementCandidate(group, "orespawn:default")); + assertNotNull(OreSourceGroupSettingsScreen.selectedPlacementCandidate(group, "orespawn:vein")); + assertEquals(Arrays.asList("orespawn:default", "orespawn:vein"), + OreSourceGroupSettingsScreen.placementChannels(group)); + assertEquals(before, fixture.toString()); + } + + static JsonObject fixture(String name) throws Exception { + try (InputStreamReader reader = new InputStreamReader( + LegacyOreSourceEditorTest.class.getResourceAsStream("/ore-source-editor/" + name + ".json"), + StandardCharsets.UTF_8)) { + return new JsonParser().parse(reader).getAsJsonObject(); + } + } + + private static GeologyEditorSession session(JsonObject root) { + return new GeologyEditorSession(WorldGeologyProfile.fromJson(root, WorldGeologyProfile.recommended(false))); + } + + private static JsonObject policy(JsonObject root) { + return root.getAsJsonObject("ore_source_policies").getAsJsonObject(GROUP); + } +} diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyPlacementChannelTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyPlacementChannelTest.java new file mode 100644 index 00000000..8cb934e0 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyPlacementChannelTest.java @@ -0,0 +1,160 @@ +package zone.moddev.mc.orespawn.worldgen; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.LinkedHashMap; +import java.util.Map; + +import com.google.gson.JsonObject; +import net.minecraft.util.ResourceLocation; +import org.junit.jupiter.api.Test; + +class LegacyPlacementChannelTest { + + @Test + void namespacedBuiltInPatternsUseTheOrdinaryChannel() { + for (String name : new String[] { + "default", "vein", "normal_cloud", "precision", "clusters", "underfluids" }) { + JsonObject rule = rule("orespawn:" + name); + String before = rule.toString(); + + assertEquals(OreSourcePolicies.STANDARD, OreSourcePolicies.placementChannel(rule), name); + assertEquals(before, rule.toString(), "Channel inference must not rewrite imported settings"); + } + } + + @Test + void builtInPatternObjectsUseTheSameOrdinaryChannel() { + for (String name : new String[] { + "default", "vein", "normal_cloud", "precision", "clusters", "underfluids" }) { + JsonObject rule = objectRule("orespawn:" + name); + String before = rule.toString(); + + assertEquals(OreSourcePolicies.STANDARD, OreSourcePolicies.placementChannel(rule), name); + assertEquals(before, rule.toString()); + } + } + + @Test + void thirdPartyAndUnrecognisedOreSpawnPatternsKeepTheirOwnChannels() { + for (String pattern : new String[] { + "realisticdeposits:stratiform_seam", "example:default", "example:vein", + "orespawn:registered_custom" }) { + ResourceLocation expected = new ResourceLocation(pattern); + + assertEquals(expected, OreSourcePolicies.placementChannel(rule(pattern))); + assertEquals(expected, OreSourcePolicies.placementChannel(objectRule(pattern))); + } + } + + @Test + void explicitChannelsAlwaysWinIncludingTheOldDefaultName() { + for (String channel : new String[] { + "example:custom", "orespawn:default", "orespawn:standard" }) { + for (JsonObject rule : new JsonObject[] { + rule("orespawn:default"), objectRule("orespawn:vein"), + rule("realisticdeposits:stratiform_seam") }) { + rule.addProperty("placement_channel", channel); + String before = rule.toString(); + + assertEquals(new ResourceLocation(channel), OreSourcePolicies.placementChannel(rule)); + assertEquals(before, rule.toString()); + } + } + } + + @Test + void bareLegacyNamesAndMissingPatternsKeepTheirExistingFallback() { + assertEquals(OreSourcePolicies.STANDARD, OreSourcePolicies.placementChannel(new JsonObject())); + + for (String name : new String[] { "default", "vein", "cloud", "cluster", "under-fluid" }) { + assertEquals(OreSourcePolicies.STANDARD, OreSourcePolicies.placementChannel(rule(name))); + } + } + + @Test + void oldSourceSelectionsOnlyMatchTheirOriginalPattern() { + JsonObject profile = selectionProfile("orespawn:vein"); + String before = profile.toString(); + + assertTrue(selected(profile).isEmpty(), "An unrelated old channel must not activate a source"); + assertEquals(before, profile.toString()); + } + + @Test + void aNewExplicitChannelDoesNotResurrectAnOldSelection() { + JsonObject profile = selectionProfile("orespawn:default"); + profile.getAsJsonObject("ores").getAsJsonObject("orespawn:legacy/iron") + .getAsJsonObject("dimensions").getAsJsonObject("minecraft:overworld") + .addProperty("placement_channel", "orespawn:standard"); + + assertTrue(selected(profile).isEmpty()); + } + + @Test + void aNewStandardSelectionTakesPrecedenceWithoutFallingBack() { + JsonObject profile = selectionProfile("orespawn:default"); + profile.getAsJsonObject(OreSourcePolicies.SECTION).getAsJsonObject("orespawn:iron|minecraft:overworld") + .getAsJsonObject("placement_sources").addProperty("orespawn:standard", "missing:iron"); + + assertTrue(selected(profile).isEmpty()); + } + + @Test + void aSavedInferredChannelStillSelectsTheSameSourceWithoutRewritingIt() { + JsonObject profile = selectionProfile("orespawn:default"); + String before = profile.toString(); + Map choices = selected(profile); + + assertEquals(1, choices.size()); + assertEquals(new ResourceLocation("orespawn:legacy/iron"), + choices.get(new ResourceLocation("orespawn:default"))); + assertEquals(before, profile.toString()); + } + + private static JsonObject selectionProfile(String savedChannel) { + JsonObject dimension = rule("orespawn:default"); + JsonObject dimensions = new JsonObject(); + dimensions.add("minecraft:overworld", dimension); + JsonObject ore = new JsonObject(); + ore.add("dimensions", dimensions); + JsonObject ores = new JsonObject(); + ores.add("orespawn:legacy/iron", ore); + + JsonObject placements = new JsonObject(); + placements.addProperty(savedChannel, "orespawn:legacy/iron"); + JsonObject policy = new JsonObject(); + policy.addProperty("mode", "consolidated"); + policy.add("placement_sources", placements); + JsonObject policies = new JsonObject(); + policies.add("orespawn:iron|minecraft:overworld", policy); + + JsonObject profile = new JsonObject(); + profile.add("ores", ores); + profile.add(OreSourcePolicies.SECTION, policies); + return profile; + } + + private static Map selected(JsonObject profile) { + Map available = new LinkedHashMap<>(); + available.put(new ResourceLocation("orespawn:legacy/iron"), OreSourcePolicies.STANDARD); + return OreSpawnOreGeneration.selectedPlacementRules(profile, + new ResourceLocation("orespawn:iron"), WorldIds.OVERWORLD, available); + } + + private static JsonObject rule(String pattern) { + JsonObject rule = new JsonObject(); + rule.addProperty("pattern", pattern); + return rule; + } + + private static JsonObject objectRule(String type) { + JsonObject pattern = new JsonObject(); + pattern.addProperty("type", type); + pattern.add("settings", new JsonObject()); + JsonObject rule = new JsonObject(); + rule.add("pattern", pattern); + return rule; + } +} diff --git a/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyVanillaOreTest.java b/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyVanillaOreTest.java index fe0ba515..21fbff5a 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyVanillaOreTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/LegacyVanillaOreTest.java @@ -375,6 +375,27 @@ void intentionalLegacyDeclarationsAreNotCollapsedIntoOneRule() throws Exception assertTrue(takeover(baked, WorldIds.OVERWORLD, Blocks.DIAMOND_ORE)); } + @Test + void importedDefaultPatternReceivesHybridAndReplacementScalingWithoutChangingItsSettings() throws Exception { + JsonObject profile = profile(true); + profile.getAsJsonObject("ores").add("orespawn:legacy/diamond_ore", rule(false, true)); + String before = profile.toString(); + Object[] imported = ores(bake(profile), WorldIds.OVERWORLD); + + assertEquals(1, imported.length); + ResourceLocation channel = (ResourceLocation) field(imported[0], "channel"); + ResourceLocation material = new ResourceLocation("orespawn:diamond"); + + assertEquals(OreSourcePolicies.STANDARD, channel); + assertEquals(0.15D, OreSpawnOreGeneration.channelBackgroundScale(material, channel, 0.15D, 1.0D)); + assertEquals(0.0D, OreSpawnOreGeneration.channelBackgroundScale(material, channel, 0.0D, 1.0D)); + assertEquals(0.375D, field(imported[0], "frequency")); + assertEquals(7, field(imported[0], "minY")); + assertEquals(11, field(imported[0], "maxY")); + assertEquals(false, field(imported[0], "retrogen")); + assertEquals(before, profile.toString()); + } + private static JsonObject profile(boolean manageVanilla) { JsonObject profile = new JsonObject(); profile.addProperty("manage_vanilla_ores", manageVanilla); diff --git a/src/test/resources/ore-source-editor/custom-channel.json b/src/test/resources/ore-source-editor/custom-channel.json new file mode 100644 index 00000000..9e277afd --- /dev/null +++ b/src/test/resources/ore-source-editor/custom-channel.json @@ -0,0 +1,128 @@ +{ + "schema_version": 7, + "manage_vanilla_ores": true, + "ore_material_groups": { + "orespawn:iron": { + "display_name": "Iron", + "ore_dictionary_entries": [ + "oreIron" + ] + } + }, + "ores": { + "example:iron": { + "enabled": true, + "dimensions": { + "minecraft:overworld": { + "enabled": true, + "frequency": 3.25, + "quantity": 5, + "retrogen": false, + "host_blocks": [ + "minecraft:stone" + ], + "pattern": "orespawn:default", + "placement_channel": "example:custom_iron", + "min_y": 12, + "max_y": 18 + } + }, + "source_mod": "minecraft", + "block": "minecraft:iron_ore", + "dimension_selectors": {} + }, + "example:alternative_iron": { + "enabled": true, + "dimensions": { + "minecraft:overworld": { + "enabled": true, + "frequency": 1.75, + "quantity": 5, + "retrogen": false, + "host_blocks": [ + "minecraft:stone" + ], + "pattern": "orespawn:default", + "placement_channel": "example:custom_iron", + "min_y": 12, + "max_y": 18 + } + }, + "source_mod": "orespawn", + "block": "minecraft:iron_ore", + "dimension_selectors": {} + }, + "example:disabled_gold": { + "enabled": false, + "dimensions": { + "minecraft:overworld": { + "enabled": false, + "frequency": 0.5, + "quantity": 5, + "retrogen": false, + "host_blocks": [ + "minecraft:stone" + ], + "pattern": "orespawn:default", + "placement_channel": "example:custom_iron", + "min_y": 12, + "max_y": 18 + } + }, + "source_mod": "minecraft", + "block": "minecraft:gold_ore", + "dimension_selectors": {} + } + }, + "ore_source_policies": { + "orespawn:iron|minecraft:overworld": { + "material": "orespawn:iron", + "domain": "minecraft:overworld", + "mode": "consolidated", + "output_mode": "balanced", + "review_required": false, + "outputs": { + "example:iron": 1 + }, + "placement_sources": { + "example:custom_iron": "example:iron" + }, + "candidates": [ + { + "source_id": "example:iron", + "owner": "minecraft", + "owner_name": "Minecraft", + "owner_version": "4.1.2.110021", + "registry_id": "minecraft:iron_ore", + "metadata": 0, + "placement_channel": "example:custom_iron", + "ore_dictionary": [ + "oreIron" + ], + "loaded": true, + "placement_active": true, + "external": false, + "enrichment": false, + "review_required": false + }, + { + "source_id": "example:alternative_iron", + "owner": "orespawn", + "owner_name": "OreSpawn", + "owner_version": "4.1.2.110021", + "registry_id": "minecraft:iron_ore", + "metadata": 0, + "placement_channel": "example:custom_iron", + "ore_dictionary": [ + "oreIron" + ], + "loaded": true, + "placement_active": true, + "external": false, + "enrichment": false, + "review_required": false + } + ] + } + } +} diff --git a/src/test/resources/ore-source-editor/fresh-standard.json b/src/test/resources/ore-source-editor/fresh-standard.json new file mode 100644 index 00000000..dc91f836 --- /dev/null +++ b/src/test/resources/ore-source-editor/fresh-standard.json @@ -0,0 +1,125 @@ +{ + "schema_version": 7, + "manage_vanilla_ores": true, + "ore_material_groups": { + "orespawn:iron": { + "display_name": "Iron", + "ore_dictionary_entries": [ + "oreIron" + ] + } + }, + "ores": { + "example:iron": { + "enabled": true, + "dimensions": { + "minecraft:overworld": { + "enabled": true, + "frequency": 3.25, + "quantity": 5, + "retrogen": false, + "host_blocks": [ + "minecraft:stone" + ], + "pattern": "orespawn:default", + "min_y": 12, + "max_y": 18 + } + }, + "source_mod": "minecraft", + "block": "minecraft:iron_ore", + "dimension_selectors": {} + }, + "example:alternative_iron": { + "enabled": true, + "dimensions": { + "minecraft:overworld": { + "enabled": true, + "frequency": 1.75, + "quantity": 5, + "retrogen": false, + "host_blocks": [ + "minecraft:stone" + ], + "pattern": "orespawn:default", + "min_y": 12, + "max_y": 18 + } + }, + "source_mod": "orespawn", + "block": "minecraft:iron_ore", + "dimension_selectors": {} + }, + "example:disabled_gold": { + "enabled": false, + "dimensions": { + "minecraft:overworld": { + "enabled": false, + "frequency": 0.5, + "quantity": 5, + "retrogen": false, + "host_blocks": [ + "minecraft:stone" + ], + "pattern": "orespawn:default", + "min_y": 12, + "max_y": 18 + } + }, + "source_mod": "minecraft", + "block": "minecraft:gold_ore", + "dimension_selectors": {} + } + }, + "ore_source_policies": { + "orespawn:iron|minecraft:overworld": { + "material": "orespawn:iron", + "domain": "minecraft:overworld", + "mode": "consolidated", + "output_mode": "balanced", + "review_required": false, + "outputs": { + "example:iron": 1 + }, + "placement_sources": { + "orespawn:standard": "example:iron" + }, + "candidates": [ + { + "source_id": "example:iron", + "owner": "minecraft", + "owner_name": "Minecraft", + "owner_version": "4.1.2.110021", + "registry_id": "minecraft:iron_ore", + "metadata": 0, + "placement_channel": "orespawn:standard", + "ore_dictionary": [ + "oreIron" + ], + "loaded": true, + "placement_active": true, + "external": false, + "enrichment": false, + "review_required": false + }, + { + "source_id": "example:alternative_iron", + "owner": "orespawn", + "owner_name": "OreSpawn", + "owner_version": "4.1.2.110021", + "registry_id": "minecraft:iron_ore", + "metadata": 0, + "placement_channel": "orespawn:standard", + "ore_dictionary": [ + "oreIron" + ], + "loaded": true, + "placement_active": true, + "external": false, + "enrichment": false, + "review_required": false + } + ] + } + } +} diff --git a/src/test/resources/ore-source-editor/os1-import.json b/src/test/resources/ore-source-editor/os1-import.json new file mode 100644 index 00000000..2f9db537 --- /dev/null +++ b/src/test/resources/ore-source-editor/os1-import.json @@ -0,0 +1,142 @@ +{ + "schema_version": 7, + "manage_vanilla_ores": true, + "ore_material_groups": { + "orespawn:iron": { + "display_name": "Iron", + "ore_dictionary_entries": [ + "oreIron" + ] + } + }, + "ores": { + "orespawn:legacy/minecraft_iron_ore_0": { + "enabled": true, + "dimensions": { + "minecraft:overworld": { + "enabled": true, + "frequency": 3.25, + "quantity": 5, + "retrogen": false, + "host_blocks": [ + "minecraft:stone" + ], + "pattern": "orespawn:default", + "min_y": 12, + "max_y": 18 + } + }, + "source_mod": "minecraft", + "block": "minecraft:iron_ore", + "dimension_selectors": {} + }, + "example:alternative_iron": { + "enabled": true, + "dimensions": { + "minecraft:overworld": { + "enabled": true, + "frequency": 1.75, + "quantity": 5, + "retrogen": false, + "host_blocks": [ + "minecraft:stone" + ], + "pattern": "orespawn:default", + "min_y": 12, + "max_y": 18 + } + }, + "source_mod": "orespawn", + "block": "minecraft:iron_ore", + "dimension_selectors": {} + }, + "example:disabled_gold": { + "enabled": false, + "dimensions": { + "minecraft:overworld": { + "enabled": false, + "frequency": 0.5, + "quantity": 5, + "retrogen": false, + "host_blocks": [ + "minecraft:stone" + ], + "pattern": "orespawn:default", + "min_y": 12, + "max_y": 18 + } + }, + "source_mod": "minecraft", + "block": "minecraft:gold_ore", + "dimension_selectors": {} + } + }, + "ore_source_policies": { + "orespawn:iron|minecraft:overworld": { + "material": "orespawn:iron", + "domain": "minecraft:overworld", + "mode": "consolidated", + "output_mode": "balanced", + "review_required": false, + "outputs": { + "orespawn:legacy/minecraft_iron_ore_0": 1 + }, + "placement_sources": { + "orespawn:default": "orespawn:legacy/minecraft_iron_ore_0" + }, + "candidates": [ + { + "source_id": "orespawn:legacy/minecraft_iron_ore_0", + "owner": "minecraft", + "owner_name": "Minecraft", + "owner_version": "4.1.2.110021", + "registry_id": "minecraft:iron_ore", + "metadata": 0, + "placement_channel": "orespawn:default", + "ore_dictionary": [ + "oreIron" + ], + "loaded": false, + "placement_active": true, + "external": false, + "enrichment": false, + "review_required": false + }, + { + "source_id": "orespawn:legacy/minecraft_iron_ore_0", + "owner": "minecraft", + "owner_name": "Minecraft", + "owner_version": "4.1.2.110021", + "registry_id": "minecraft:iron_ore", + "metadata": 0, + "placement_channel": "orespawn:standard", + "ore_dictionary": [ + "oreIron" + ], + "loaded": true, + "placement_active": true, + "external": false, + "enrichment": false, + "review_required": false + }, + { + "source_id": "example:alternative_iron", + "owner": "orespawn", + "owner_name": "OreSpawn", + "owner_version": "4.1.2.110021", + "registry_id": "minecraft:iron_ore", + "metadata": 0, + "placement_channel": "orespawn:standard", + "ore_dictionary": [ + "oreIron" + ], + "loaded": true, + "placement_active": true, + "external": false, + "enrichment": false, + "review_required": false + } + ] + } + } +} diff --git a/src/test/resources/ore-source-editor/os3-import.json b/src/test/resources/ore-source-editor/os3-import.json new file mode 100644 index 00000000..d7971c0b --- /dev/null +++ b/src/test/resources/ore-source-editor/os3-import.json @@ -0,0 +1,142 @@ +{ + "schema_version": 7, + "manage_vanilla_ores": true, + "ore_material_groups": { + "orespawn:iron": { + "display_name": "Iron", + "ore_dictionary_entries": [ + "oreIron" + ] + } + }, + "ores": { + "orespawn:legacy/iron_ore": { + "enabled": true, + "dimensions": { + "minecraft:overworld": { + "enabled": true, + "frequency": 3.25, + "quantity": 5, + "retrogen": false, + "host_blocks": [ + "minecraft:stone" + ], + "pattern": "orespawn:default", + "min_y": 12, + "max_y": 18 + } + }, + "source_mod": "minecraft", + "block": "minecraft:iron_ore", + "dimension_selectors": {} + }, + "example:alternative_iron": { + "enabled": true, + "dimensions": { + "minecraft:overworld": { + "enabled": true, + "frequency": 1.75, + "quantity": 5, + "retrogen": false, + "host_blocks": [ + "minecraft:stone" + ], + "pattern": "orespawn:default", + "min_y": 12, + "max_y": 18 + } + }, + "source_mod": "orespawn", + "block": "minecraft:iron_ore", + "dimension_selectors": {} + }, + "example:disabled_gold": { + "enabled": false, + "dimensions": { + "minecraft:overworld": { + "enabled": false, + "frequency": 0.5, + "quantity": 5, + "retrogen": false, + "host_blocks": [ + "minecraft:stone" + ], + "pattern": "orespawn:default", + "min_y": 12, + "max_y": 18 + } + }, + "source_mod": "minecraft", + "block": "minecraft:gold_ore", + "dimension_selectors": {} + } + }, + "ore_source_policies": { + "orespawn:iron|minecraft:overworld": { + "material": "orespawn:iron", + "domain": "minecraft:overworld", + "mode": "consolidated", + "output_mode": "balanced", + "review_required": false, + "outputs": { + "orespawn:legacy/iron_ore": 1 + }, + "placement_sources": { + "orespawn:default": "orespawn:legacy/iron_ore" + }, + "candidates": [ + { + "source_id": "orespawn:legacy/iron_ore", + "owner": "minecraft", + "owner_name": "Minecraft", + "owner_version": "4.1.2.110021", + "registry_id": "minecraft:iron_ore", + "metadata": 0, + "placement_channel": "orespawn:default", + "ore_dictionary": [ + "oreIron" + ], + "loaded": false, + "placement_active": true, + "external": false, + "enrichment": false, + "review_required": false + }, + { + "source_id": "orespawn:legacy/iron_ore", + "owner": "minecraft", + "owner_name": "Minecraft", + "owner_version": "4.1.2.110021", + "registry_id": "minecraft:iron_ore", + "metadata": 0, + "placement_channel": "orespawn:standard", + "ore_dictionary": [ + "oreIron" + ], + "loaded": true, + "placement_active": true, + "external": false, + "enrichment": false, + "review_required": false + }, + { + "source_id": "example:alternative_iron", + "owner": "orespawn", + "owner_name": "OreSpawn", + "owner_version": "4.1.2.110021", + "registry_id": "minecraft:iron_ore", + "metadata": 0, + "placement_channel": "orespawn:standard", + "ore_dictionary": [ + "oreIron" + ], + "loaded": true, + "placement_active": true, + "external": false, + "enrichment": false, + "review_required": false + } + ] + } + } +} diff --git a/tools/qualification/editor/EditorFixtureMod.java b/tools/qualification/editor/EditorFixtureMod.java new file mode 100644 index 00000000..c6796fcc --- /dev/null +++ b/tools/qualification/editor/EditorFixtureMod.java @@ -0,0 +1,319 @@ +package zone.moddev.mc.orespawn.client; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.resources.I18n; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.util.ScreenShotHelper; +import net.minecraft.util.text.TextFormatting; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.client.event.GuiScreenEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; +import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfile; +import zone.moddev.mc.orespawn.worldgen.WorldGeologyProfileManager; +import zone.moddev.mc.orespawn.client.GeologyEditorSession.OreSourceGroup; + +/** Disposable editor fixtures; this mod is never part of an OreSpawn release. */ +@Mod(modid = "editorfixtures", name = "Ore Sources visual test", version = "1", + acceptedMinecraftVersions = "[1.10.2]", dependencies = "required-after:orespawn;required-after:realisticdeposits") +public final class EditorFixtureMod { + private static final List FIXTURES = Arrays.asList( + "os1-import", "os3-import", "custom-channel", "fresh-standard"); + private static final String GROUP = "orespawn:iron|minecraft:overworld"; + private final String requestedFixture = System.getProperty("editorfixtures.fixture", ""); + private final FixtureMenu menu = new FixtureMenu(); + private OreSpawnWorldSettingsScreen root; + private GeologyEditorSession session; + private WorldGeologyProfile starting; + private String selected; + private int fixtureIndex; + private int stage; + private int ticks; + private boolean opened; + private boolean editing; + private boolean finished; + private String pendingScreenshot; + private int captureFrames; + private String result = "Choose a profile. All changes stay in this disposable test folder."; + + @Mod.EventHandler + public void initialize(FMLInitializationEvent event) { + MinecraftForge.EVENT_BUS.register(this); + } + + @SubscribeEvent + public void capture(GuiScreenEvent.DrawScreenEvent.Post event) { + if (pendingScreenshot == null || !(event.getGui() instanceof OreSourceGroupSettingsScreen)) return; + if (++captureFrames < 5) return; + Minecraft mc = Minecraft.getMinecraft(); + + // Capture after the screen has drawn, not between client ticks. + int texture = org.lwjgl.opengl.GL11.glGetInteger(org.lwjgl.opengl.GL11.GL_TEXTURE_BINDING_2D); + try { + ScreenShotHelper.saveScreenshot(mc.mcDataDir, pendingScreenshot, + mc.displayWidth, mc.displayHeight, mc.getFramebuffer()); + } finally { + GlStateManager.bindTexture(texture); + } + pendingScreenshot = null; + captureFrames = 0; + } + + @SubscribeEvent + public void tick(TickEvent.ClientTickEvent event) { + if (event.phase != TickEvent.Phase.END || finished) return; + Minecraft mc = Minecraft.getMinecraft(); + + try { + if (!opened && mc.currentScreen instanceof GuiMainMenu) { + opened = true; + mc.displayGuiScreen(menu); + } + if (!opened || !Boolean.getBoolean("editorfixtures.automated")) return; + require(ticks < 3600, "The editor test timed out"); + if (++ticks % 12 != 0) return; + + switch (stage) { + case 0: + if (!requestedFixture.isEmpty()) { + fixtureIndex = FIXTURES.indexOf(requestedFixture); + require(fixtureIndex >= 0, "Unknown editor fixture " + requestedFixture); + } + open(FIXTURES.get(fixtureIndex)); + showPlacementScreen(); + pendingScreenshot = selected + "-before.png"; + stage = 1; + break; + case 1: + if (pendingScreenshot != null) return; + inspectAndEdit(mc); + mc.displayGuiScreen(root); + press(root, "gui.cancel"); + require(starting.toJson().equals(WorldGeologyProfileManager.pendingNewWorldProfile().toJson()), + "Cancel changed the accepted profile"); + stage = 2; + break; + case 2: + open(FIXTURES.get(fixtureIndex)); + showPlacementScreen(); + pendingScreenshot = selected + "-save.png"; + stage = 3; + break; + case 3: + if (pendingScreenshot != null) return; + inspectAndEdit(mc); + mc.displayGuiScreen(root); + press(root, "gui.done"); + require(mc.currentScreen == menu, "Done did not return to the fixture menu"); + WorldGeologyProfile accepted = WorldGeologyProfileManager.pendingNewWorldProfile(); + GeologyEditorSession reloaded = new GeologyEditorSession(accepted); + String channel = selected.equals("custom-channel") ? "example:custom_iron" : "orespawn:standard"; + require("example:alternative_iron".equals(ironGroup(reloaded).placements.get(channel)), + "Done did not retain the chosen source"); + require(starting.toJson().get("ores").equals(accepted.toJson().get("ores")), + "Editing a placement source changed customised ore rules"); + write(selected + "-accepted.json", accepted.toJson()); + JsonObject disk = new JsonParser().parse(new String(Files.readAllBytes( + output().resolve(selected + "-accepted.json")), StandardCharsets.UTF_8)).getAsJsonObject(); + require(accepted.toJson().equals(WorldGeologyProfile.fromJson(disk, accepted).toJson()), + "The accepted profile changed on reload"); + checkManagementSwitch(accepted, channel); + fixtureIndex++; + if (!requestedFixture.isEmpty() || fixtureIndex == FIXTURES.size()) { + Files.write(output().resolve("PASS.txt"), + "Editor checks passed: screen, selection, Cancel, Done and file reload.\n" + .getBytes(StandardCharsets.UTF_8)); + finished = true; + mc.shutdown(); + } else { + stage = 0; + } + break; + default: + throw new IllegalStateException("Unexpected fixture stage " + stage); + } + } catch (Throwable failure) { + failure.printStackTrace(); + try { + Files.write(output().resolve("FAIL.txt"), failure.toString().getBytes(StandardCharsets.UTF_8)); + } catch (IOException writing) { + failure.addSuppressed(writing); + } + finished = true; + mc.shutdown(); + } + } + + private void open(String name) throws Exception { + selected = name; + Path saved = output().resolve(name + "-manual.json"); + JsonObject json; + if (!Boolean.getBoolean("editorfixtures.automated") && Files.exists(saved)) { + json = new JsonParser().parse(new String(Files.readAllBytes(saved), StandardCharsets.UTF_8)).getAsJsonObject(); + } else { + try (InputStreamReader reader = new InputStreamReader(getClass().getResourceAsStream( + "/ore-source-editor/" + name + ".json"), StandardCharsets.UTF_8)) { + json = new JsonParser().parse(reader).getAsJsonObject(); + } + } + + starting = WorldGeologyProfile.fromJson(json, WorldGeologyProfile.recommended(false)); + write(name + "-input.json", starting.toJson()); + WorldGeologyProfileManager.setPendingNewWorldProfile(starting); + root = new OreSpawnWorldSettingsScreen(menu, starting); + session = (GeologyEditorSession) field(root, "session"); + editing = true; + Minecraft.getMinecraft().displayGuiScreen(root); + } + + private void showPlacementScreen() { + OreSourceListScreen sources = new OreSourceListScreen(root, session); + Minecraft.getMinecraft().displayGuiScreen(sources); + Minecraft.getMinecraft().displayGuiScreen(new OreSourceGroupSettingsScreen(sources, session, GROUP)); + } + + private void inspectAndEdit(Minecraft mc) throws Exception { + require(mc.currentScreen instanceof OreSourceGroupSettingsScreen, "Placement screen did not open"); + OreSourceGroupSettingsScreen screen = (OreSourceGroupSettingsScreen) mc.currentScreen; + CompactScrollList rows = (CompactScrollList) field(screen, "placementList"); + require(rows.size() == 1, "An old inferred channel left a duplicate row"); + require(!rows.rowText(0).contains(I18n.format("label.orespawn.ore_source.missing")), + "A loaded placement source was labelled missing"); + require(rows.rowEnabled(0), "The two-source fixture should allow a selection"); + rows.onRowPressed(0); + } + + private static void press(OreSpawnScreen screen, String translation) { + for (GuiButton button : screen.buttons) { + if (button instanceof Button && I18n.format(translation).equals( + TextFormatting.getTextWithoutFormattingCodes(button.displayString))) { + ((Button) button).press(); + return; + } + } + throw new IllegalStateException("Could not find " + translation); + } + + private void checkManagementSwitch(WorldGeologyProfile profile, String channel) throws Exception { + Minecraft mc = Minecraft.getMinecraft(); + OreSpawnWorldSettingsScreen settings = new OreSpawnWorldSettingsScreen(menu, profile); + GeologyEditorSession working = (GeologyEditorSession) field(settings, "session"); + String choices = working.root().getAsJsonObject("ore_source_policies") + .getAsJsonObject(GROUP).get("placement_sources").toString(); + + for (int i = 0; i < 2; i++) { + mc.displayGuiScreen(settings); + ((Button) field(settings, "vanillaOresButton")).press(); + press(settings, "button.orespawn.materials"); + require(choices.equals(working.root().getAsJsonObject("ore_source_policies") + .getAsJsonObject(GROUP).get("placement_sources").toString()), + "Manage Vanilla Ores changed a saved choice"); + require("example:alternative_iron".equals(ironGroup(working).placements.get(channel)), + "Manage Vanilla Ores lost the displayed choice"); + } + mc.displayGuiScreen(menu); + } + + private static OreSourceGroup ironGroup(GeologyEditorSession session) { + for (OreSourceGroup group : session.oreSourceGroups()) { + if (GROUP.equals(group.key)) return group; + } + throw new IllegalStateException("The Iron group disappeared"); + } + + private static Object field(Object owner, String name) throws ReflectiveOperationException { + Field field = owner.getClass().getDeclaredField(name); + field.setAccessible(true); + return field.get(owner); + } + + private static void require(boolean condition, String message) { + if (!condition) throw new IllegalStateException(message); + } + + private static Path output() throws IOException { + Path directory = Minecraft.getMinecraft().mcDataDir.toPath().resolve("editor-results"); + Files.createDirectories(directory); + return directory; + } + + private static void write(String name, JsonObject json) throws IOException { + String text = new GsonBuilder().setPrettyPrinting().create().toJson(json) + "\n"; + Files.write(output().resolve(name), text.getBytes(StandardCharsets.UTF_8)); + } + + private final class FixtureMenu extends GuiScreen { + @Override + public void initGui() { + if (editing) { + editing = false; + WorldGeologyProfile accepted = WorldGeologyProfileManager.pendingNewWorldProfile(); + boolean changed = !starting.toJson().equals(accepted.toJson()); + result = changed ? "Saved test changes for " + selected + "." : "No changes saved for " + selected + "."; + if (changed) { + try { + write(selected + "-manual.json", accepted.toJson()); + } catch (IOException failure) { + throw new IllegalStateException("Could not save the disposable profile", failure); + } + } + } + + String[] labels = { "OS1 imported profile", "OS3 imported profile", "Explicit custom channel", "Fresh standard channel" }; + for (int i = 0; i < FIXTURES.size(); i++) { + buttonList.add(new GuiButton(i, width / 2 - 155, 60 + i * 26, 310, 20, labels[i])); + } + buttonList.add(new GuiButton(10, width / 2 - 155, height - 28, 310, 20, "Quit test")); + } + + @Override + protected void actionPerformed(GuiButton button) throws IOException { + if (button.id == 10) { + mc.shutdown(); + return; + } + + try { + open(FIXTURES.get(button.id)); + } catch (Exception failure) { + throw new IOException("Could not open the fixture", failure); + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + drawDefaultBackground(); + drawCenteredString(fontRendererObj, "Ore Sources visual test", width / 2, 18, 0xFFFFFF); + List lines = fontRendererObj.listFormattedStringToWidth(result, width - 40); + int y = 174; + for (String line : lines) { + drawCenteredString(fontRendererObj, line, width / 2, y, 0xCCCCCC); + y += 11; + } + super.drawScreen(mouseX, mouseY, partialTicks); + } + + @Override + protected void keyTyped(char character, int keyCode) throws IOException { + if (keyCode == org.lwjgl.input.Keyboard.KEY_ESCAPE) mc.shutdown(); + else super.keyTyped(character, keyCode); + } + } +} diff --git a/tools/qualification/editor/probe.init.gradle b/tools/qualification/editor/probe.init.gradle new file mode 100644 index 00000000..99b93a71 --- /dev/null +++ b/tools/qualification/editor/probe.init.gradle @@ -0,0 +1,31 @@ +// Opt-in editor fixtures. Normal builds and release jars do not include them. +gradle.afterProject { project, state -> + if (project != project.rootProject || state.failure != null) return + + def classes = project.tasks.register('compileOreSourceEditorFixture', JavaCompile) { + dependsOn project.tasks.named('classes') + source project.file('tools/qualification/editor/EditorFixtureMod.java') + classpath = project.files(project.sourceSets.main.output, project.sourceSets.main.compileClasspath) + destinationDirectory = project.layout.buildDirectory.dir('editor-fixtures/classes') + options.encoding = 'UTF-8' + } + + def input = project.tasks.register('oreSourceEditorFixtureInput', Jar) { + dependsOn classes + from classes.flatMap { it.destinationDirectory } + from(project.file('src/test/resources')) { include 'ore-source-editor/**' } + archiveFileName = 'editor-fixtures-deobf.jar' + destinationDirectory = project.layout.buildDirectory.dir('editor-fixtures') + } + + def release = project.renamer.classes(input) { + map.from project.minecraft.dependency.toSrgFile + output = project.layout.buildDirectory.file('editor-fixtures/editor-fixtures.jar') + } + + project.tasks.register('prepareOreSourceEditorFixture') { + dependsOn release + group = 'verification' + description = 'Builds the optional, packaged Ore Sources visual-test mod.' + } +}