diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36022ff7..e64d8113 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.5.110021.jar - build/libs/OreSpawn-4.1.5.110021-sources.jar - build/libs/OreSpawn-4.1.5.110021-javadoc.jar + build/libs/OreSpawn-4.1.6.110021.jar + build/libs/OreSpawn-4.1.6.110021-sources.jar + build/libs/OreSpawn-4.1.6.110021-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 6fd2b880..489a0829 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,12 @@ +Version 4.1.6.110021 + +* Fix duplicate ore generation when an OreSpawn 1 world is moved without its + converted provider file. Keep imported rules and saved choices, and back + up the profile before removing a duplicate. Leave edited rules for review. +* Pick the default sulfur and lithium sources after all mods have loaded. + New setups can share those ores with Base Electric without adding extra + veins. Existing worlds keep their choices and existing chunks stay as they are. + Version 4.1.5.110021 * Apply the normal gold-generation controls to Mesa's extra gold pass too. diff --git a/README.md b/README.md index eea6f9fb..4e3071ab 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.5.110021`: the OreSpawn 4.1.5 +This branch builds target-qualified version `4.1.6.110021`: the OreSpawn 4.1.6 feature set for Minecraft 1.10.2 and Forge. It adds stable generation identity, non-loading geology sampling, per-resource background control, existing-world provider merge policy, an Ore Sources arbiter for equivalent provider ores and diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index d6ac17d9..878b9df2 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -77,6 +77,15 @@ remain unchanged. ## Provider-Aware OS3 Imports +An OreSpawn 1 import keeps its saved rule ID and settings even if you move +the world without its converted provider file. If an older build added a +second rule, OreSpawn removes it only when the saved rule still matches the +provider default and no custom ore-source selection depends on it. The +original profile is backed up before that repair. Edited or ambiguous +overlaps are left in place and listed under `legacy_ore_review` in the +provider's saved manifest and reported in the log. This does not change +ore blocks in chunks already generated. + When an OreSpawn 2/3 file is named for an installed provider, the migrator now compares each converted primary output with that provider's ore declarations. A unique output match is written under the provider's stable rule ID, allowing diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 46ee066c..ace4314d 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -120,11 +120,12 @@ unambiguous. NetherMetals and EndMetals stay in their respective dimension domains. DenseMetals is enrichment, not an interchangeable output, and BaseSciences has no ordinary ore-source role. -Reviewed fresh-world conflicts start Balanced with all eligible output blocks. -Mineralogy, then BaseMinerals, then ElectricAdvantage is the initial placement -and Single-mode priority for sulfur; BaseMinerals, then ElectricAdvantage is the -corresponding lithium priority. No other conflict is automatically consolidated -without an explicit catalog row. Exact Ore Dictionary `oreX` inference recognizes the curated +Once mods have finished registering their ores, reviewed fresh-world conflicts +start Balanced with all eligible output blocks. Sulfur prefers Mineralogy, +then Base Minerals, then Base Electric (`baseelectric` or the older +`electricadvantage` ID) for placement. Lithium prefers Base Minerals, then +Base Electric. Existing choices are kept. Other conflicts need an explicit +catalog entry for automatic consolidation. Exact Ore Dictionary `oreX` inference recognizes the curated sulfur/sulphur and aluminum/aluminium aliases but deliberately keeps Niter and Saltpeter distinct. Multiple unrelated entries are marked for review rather than fuzzy-matched from block names. diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 4c3f70c3..e3fa6b79 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.5.110021` | +| 1.10.2 | Forge | `110021` | `4.1.6.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 febbf260..58fe7950 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.5.110021 +mod_version=4.1.6.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 746ae826..9f61621d 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.5.110021' + if (project.mod_version != '4.1.6.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.5.110021')) { - throw new GradleException("Authoritative release location does not contain 4.1.5.110021: ${path}") + if (!file(path).getText('UTF-8').contains('4.1.6.110021')) { + throw new GradleException("Authoritative release location does not contain 4.1.6.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 d43e6931..29c0d244 100644 --- a/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java +++ b/src/main/java/com/mcmoddev/orespawn/compat/LegacyOs3Bridge.java @@ -488,6 +488,7 @@ private static JsonObject convertOs1Source(String owner, JsonObject source) { continue; } JsonObject spawn = new JsonObject(); spawn.addProperty("enabled", true); + spawn.addProperty("legacy_import", "os1"); spawn.addProperty("feature", "default"); spawn.addProperty("replaces", "default"); JsonArray blocks = new JsonArray(); JsonObject block = new JsonObject(); block.addProperty("name", output); block.addProperty("chance", 100); @@ -620,6 +621,7 @@ private static JsonObject translateSpawn(String modId, String name, JsonObject s ore.addProperty("block", output); ore.addProperty("source_mod", modId); ore.addProperty("native_generation", false); ore.addProperty("retrogen", bool(spawn, "retrogen", false)); + if ("os1".equals(text(spawn, "legacy_import", ""))) ore.addProperty("legacy_import", "os1"); copyMetadata(first, ore); if (flags.replaceVanilla && "minecraft".equals(new ResourceLocation(output).getResourceDomain())) { ore.addProperty("suppress_vanilla", true); @@ -1027,7 +1029,7 @@ private static void writeHumanUpgradeReport(Path destination) throws IOException } } List lines = new ArrayList<>(); - lines.add("OreSpawn 4.1.5.110021 Upgrade Report"); + lines.add("OreSpawn 4.1.6.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 cdbfd49b..69af7ac5 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.5.110021"; + public static final String VERSION = "4.1.6.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 aa8e913d..e9a0f50b 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.5.110021"; + public static final String VERSION = "4.1.6.110021"; private static final Logger LOGGER = LogManager.getLogger(); @@ -90,7 +90,7 @@ public void preInit(FMLPreInitializationEvent event) { public void init(FMLInitializationEvent event) { LegacyOs3Bridge.completeInitialization(); WorldgenIntegrationManager.initialize(); - GeomeConfig.bake(); + GeomeConfig.bakeBeforeLoadComplete(); logGeomeSampler(); DocumentationExporter.exportBundledGuide(); StoneReplacer.registerConfiguredFeature(); @@ -109,7 +109,7 @@ public void processInterMod(FMLInterModComms.IMCEvent event) { public void postInit(FMLPostInitializationEvent event) { WorldgenIntegrationManager.processImcMessages(); WorldgenIntegrationManager.freeze(); - GeomeConfig.bake(); + GeomeConfig.bakeBeforeLoadComplete(); refreshGenerationConfig(); WorldgenIntegrationManager.markFeatureReady(); } 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..eea84399 --- /dev/null +++ b/src/main/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacements.java @@ -0,0 +1,203 @@ +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 OS1 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) { + Result result = new Result(); + JsonObject previous = object(manifest, REPLACEMENTS); + JsonObject replacements = JsonCopies.copy(previous); + JsonObject review = new JsonObject(); + Map> imports = new HashMap<>(); + for (String id : known) { + JsonObject ore = object(ores, id); + String output = output(ore); + if (output != null && isOs1Import(provider, id, ore)) { + 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 (!isOs1Import(provider, entry.getKey(), ore)) { + 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 (isOs1Import(provider, id, nativeOre)) 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) || !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)) { + // 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"); + 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); + // 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; + } + + 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 isOs1Import(String provider, String id, JsonObject ore) { + String block = text(ore, "block"); + if (!block.startsWith(provider + ":") || !provider.equals(text(ore, "source_mod")) + || !"false".equals(text(ore, "native_generation"))) return false; + if ("os1".equals(text(ore, "legacy_import"))) return true; + // Older OS1 imports used the namespaced output plus an ordinal. OS3 and + // authored rules do not qualify by block name alone. + 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("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"); + return meta.matches("[0-9]|1[0-5]") ? 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 82cfb426..07d83bb2 100644 --- a/src/main/java/zone/moddev/mc/orespawn/integration/WorldgenIntegrationManager.java +++ b/src/main/java/zone/moddev/mc/orespawn/integration/WorldgenIntegrationManager.java @@ -280,9 +280,15 @@ private static boolean mergeProviderDefinitions(JsonObject target, boolean exist String knownKey = "known_" + sectionName; Set known = stringSet(manifest.get(knownKey)); Set current = JsonCopies.keys(providerSection); + LegacyOreReplacements.Result replacements = existingWorld && "ores".equals(sectionName) + ? LegacyOreReplacements.reconcile(target, provider.modId, targetSection, + nativeOreDefaults(provider.modId, providerSection), providerSection, manifest, known) + : new LegacyOreReplacements.Result(); + changed |= replacements.changed; for (Entry entry : providerSection.entrySet()) { String id = entry.getKey(); + if (replacements.suppressed.contains(id)) continue; if (mergeNewEntries && !targetSection.has(id) && !known.contains(id)) { JsonObject value = JsonCopies.copy(entry.getValue().getAsJsonObject()); if (!"biome_rules".equals(sectionName)) { @@ -301,7 +307,9 @@ private static boolean mergeProviderDefinitions(JsonObject target, boolean exist 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; } @@ -520,9 +528,21 @@ private static void rebuildActiveProviders() { } } + private static JsonObject nativeOreDefaults(String providerId, JsonObject activeOres) { + if (!FILE_PROVIDERS.containsKey(providerId)) return activeOres; + // A converted file can hide the native catalogue. Use the shipped rule to + // recognise an old duplicate; never treat an authored override as stock. + JsonObject shipped = installedProviderRoot(providerId); + return shipped == null ? new JsonObject() : optionalObject(shipped, "ores"); + } + private static JsonObject selectedProviderRoot(String providerId) { JsonObject root = FILE_PROVIDERS.get(providerId); - if (root == null) root = RESOURCE_PROVIDERS.get(providerId); + return root == null ? installedProviderRoot(providerId) : root; + } + + private static JsonObject installedProviderRoot(String providerId) { + JsonObject root = RESOURCE_PROVIDERS.get(providerId); if (root == null) { WorldgenProvider apiProvider = API_PROVIDERS.get(providerId); root = apiProvider == null ? null : apiProvider.toJson(); 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 a906dc4f..1bb7f283 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/GeomeConfig.java @@ -99,21 +99,31 @@ private GeomeConfig() { } public static synchronized BakedGeomeConfig bake() { + return bakeGlobal(true); + } + + public static synchronized BakedGeomeConfig bakeBeforeLoadComplete() { + // Features need their settings during startup, but other mods can still + // register ore providers. Wait for loadComplete before choosing defaults. + 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/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index b94ae147..4befc702 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.5.110021 Upgrade Report"); + lines.add("OreSpawn 4.1.6.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 232c72b0..985b3550 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.5.110021 Upgrade Report" + newline + String text = "OreSpawn 4.1.6.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 68ba9040..eae9f8ea 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/OreSourcePolicies.java @@ -487,9 +487,9 @@ private static Candidate preferred(Group group, ResourceLocation channel) { private static Map> priorities() { Map> result = new HashMap<>(); 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/WorldGeologyProfile.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfile.java index c07a7f2e..50ed7954 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 94cd5b18..bec6f1e0 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/WorldGeologyProfileManager.java @@ -106,7 +106,7 @@ public static synchronized boolean reloadActiveProfile() { JsonObject merged = profile.rootCopy(); boolean changed = WorldgenIntegrationManager.mergeProviderDefinitionsIntoExistingWorld(merged); changed |= OreSourcePolicies.initialize(merged, true); - if (changed) { + if (changed && preserveBeforeLegacyOreRepair(profilePath, profile, merged)) { profile = profile.withRoot(merged); writeProfile(profilePath, profile); } @@ -130,7 +130,7 @@ public static void onServerAboutToStart(FMLServerAboutToStartEvent event) { String beforeMerge = merged.toString(); WorldgenIntegrationManager.mergeProviderDefinitionsIntoExistingWorld(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); @@ -310,6 +310,24 @@ static WorldGeologyProfile readProfile(Path path, WorldGeologyProfile fallback) } } + private static boolean preserveBeforeLegacyOreRepair(Path path, WorldGeologyProfile before, JsonObject merged) { + JsonObject beforeRoot = before.rootCopy(); + if (!beforeRoot.has("ores") || !beforeRoot.get("ores").isJsonObject()) return true; + JsonObject newOres = merged.getAsJsonObject("ores"); + for (java.util.Map.Entry rule : beforeRoot.getAsJsonObject("ores").entrySet()) { + if (newOres != null && newOres.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/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..a11c331a --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/integration/LegacyOreReplacementsTest.java @@ -0,0 +1,155 @@ +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 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 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) + .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) + .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); + } + + private static JsonObject parse(String source) { + return new JsonParser().parse(source).getAsJsonObject(); + } +} 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 4240a42a..65a0e96e 100644 --- a/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSourcePoliciesTest.java +++ b/src/test/java/zone/moddev/mc/orespawn/worldgen/OreSourcePoliciesTest.java @@ -373,6 +373,26 @@ void separateLegacyPolicyForOneLoadedOutputIsHiddenWithoutBeingDeleted() { "All Groups must retain harmless single-output material groups"); } + @Test + void baseElectricIsARecognizedOutputWithoutAddingAPlacementBudget() { + JsonObject root = rootWithSulfurConflict(); + addOre(root, "baseelectric:sulfur", "baseelectric", "minecraft:coal_ore", + "orespawn:sulfur", "minecraft:overworld", null); + root.getAsJsonObject("ores").getAsJsonObject("baseelectric:sulfur") + .addProperty("enabled", false); + + OreSourcePolicies.initialize(root, false); + + JsonObject policy = policy(root); + assertEquals("consolidated", policy.get("mode").getAsString()); + assertEquals("balanced", policy.get("output_mode").getAsString()); + assertEquals(3, policy.getAsJsonObject("outputs").entrySet().size()); + assertTrue(policy.getAsJsonObject("outputs").has("baseelectric:sulfur")); + JsonObject sources = policy.getAsJsonObject("placement_sources"); + assertEquals(1, sources.entrySet().size()); + assertEquals("mineralogy:sulfur", sources.get("orespawn:standard").getAsString()); + } + @Test void inactiveManagedProviderRemainsAnOutputWithoutOwningPlacement() { JsonObject root = root();