Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ jobs:
if-no-files-found: error
retention-days: 30
path: |
build/libs/OreSpawn-4.1.0.2601021.jar
build/libs/OreSpawn-4.1.0.2601021-sources.jar
build/libs/OreSpawn-4.1.0.2601021-javadoc.jar
build/libs/OreSpawn-4.1.1.2601021.jar
build/libs/OreSpawn-4.1.1.2601021-sources.jar
build/libs/OreSpawn-4.1.1.2601021-javadoc.jar
build/release/SHA256SUMS
CHANGELOG.txt

Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Version 4.1.1.2601021

* Stop recognised ore-rule renames from adding a second generation pass to existing profiles.
* Keep saved ore settings and show matching modern rules as disabled, editable alternatives.
* Preserve explicit choices, deleted rules, and deliberately separate deposits.

Version 4.1.0.2601021

* Leave imported vanilla ores inactive while Manage Vanilla Ores is off.
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ Its OS3 compatibility layer preserves ranged legacy block budgets, exclusive
legacy height ceilings, and the historical "all dimensions except Nether and
End" policy used by mods such as Base Metals.

The [provider-merge repair](docs/MIGRATION.md#retained-provider-rules) keeps
recognised legacy ore settings when modern provider IDs are first added.
Matching alternatives remain visible but disabled, so upgrading does not add
a second generation pass for the same deposit.

This is not the unrelated mod that adds mobs and dimensions under the same
name.

This branch builds target-qualified version `4.1.0.2601021`: the OreSpawn 4.1
This branch builds target-qualified version `4.1.1.2601021`: the OreSpawn 4.1
feature set for Minecraft 26.1.2 and Forge. See the
[versioning policy](docs/VERSIONS.md) for the encoding and release convention.

Expand Down
43 changes: 43 additions & 0 deletions docs/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,49 @@ When legacy OreSpawn rules are translated, a concise player-facing summary is
also written atomically to `config/orespawn-upgrade-report.txt`; the existing
detailed rule report remains at `config/orespawn-migration/migration-report.txt`.

## Retained Provider Rules

Some existing OreSpawn 4 profiles still use rule IDs declared by older
providers. A changed rule ID must not silently add another generation pass
for the same deposit. The provider-merge repair recognises two positive
lineages:

- OreSpawn's own imported `orespawn:legacy/<provider>/...` rules.
- Base Metals' historical `basemetals:legacy/<material>_ore` to
`basemetals:ore/<material>` replacements for coldiron, adamantine, starsteel,
tin, lead, zinc, silver, mercury, nickel and platinum. Base Metals itself
declared the old IDs in its maintained 1.10/1.12 providers; this is an exact
compatibility list, not a guess based on the word `legacy`.

The merge requires one saved rule and one current candidate with the same
single output, default metadata, and exact dimension/selector and effective
placement-channel footprint. It does not infer equivalence from overlapping
dimensions, a shared material name, or an `orphaned_provider` flag. Mixed
outputs, different deep outputs, and ambiguous matches retain the existing
merge behaviour rather than being guessed away.
Explicit conflicting `source_provider` or fallback `source_mod` ownership is
not treated as a replacement. The importer's neutral `legacy` owner marker is
accepted only with OreSpawn's own imported IDs.

When that relationship is unambiguous, OreSpawn adds the missing modern rule
as a complete **disabled** alternative. The saved rule's rates, hosts, heights,
pattern, enablement and policy choices remain authoritative. Disabled and
zero-frequency settings count as saved choices too. Fresh installations keep
the provider's ordinary defaults.

Players can enable the alternative in the normal ore settings. Later merges
preserve that explicit choice; they also preserve existing modern rules and
intentional deletions recorded in `known_ores`. Already-present old/new rule
combinations are not automatically cleaned up, since the player may have
chosen both. A deleted historical Base Metals rule recorded in `known_ores`
also keeps its mapped modern replacement disabled; that exact ID relationship
is known even though the old placement payload has been removed. Other custom
deposits do not cancel that recorded deletion and are not changed. Deleted
imports without an explicit historical mapping are not matched by guessing an
output from their filename.
Review ambiguous profiles explicitly. This repair does not change API
versions, schemas, or already-generated chunks.

## Existing Mineralogy Worlds

An already-generated world without an OreSpawn per-world profile is inspected
Expand Down
2 changes: 1 addition & 1 deletion docs/VERSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Examples:
| 1.20.6 | Forge | `120061` | `4.0.16.120061` |
| 1.21.1 | Forge | `121011` | `4.0.16.121011` |
| 1.21.11 | Forge | `121111` | `4.0.16.121111` |
| 26.1.2 | Forge | `2601021` | `4.1.0.2601021` |
| 26.1.2 | Forge | `2601021` | `4.1.1.2601021` |
| 26.2 | Forge | `2602001` | `4.0.6.2602001` |
| 26.2 | NeoForge | `2602002` | `4.0.6.2602002` |

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ mapping_version=26.1.2
mod_id=orespawn
mod_name=MMD OreSpawn
mod_license=LGPL-2.1
mod_version=4.1.0.2601021
mod_version=4.1.1.2601021
mod_group=zone.moddev.mc.orespawn
mod_authors=SkyBlade1978, dshadowwolf, the MMD Team
mod_vendor=Minecraft Mod Development
Expand Down
10 changes: 5 additions & 5 deletions gradle/release/artifacts.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir')
tasks.register('verifyReleaseConfiguration') {
group = 'verification'
doLast {
if (project.mod_version != '4.1.0.2601021'
if (project.mod_version != '4.1.1.2601021'
|| project.mod_group != expectedMavenGroup
|| project.minecraft_version != '26.1.2'
|| project.forge_version != '64.0.9'
Expand All @@ -93,9 +93,9 @@ tasks.register('verifyReleaseConfiguration') {
throw new GradleException('Unexpected dispatcher or Java target metadata')
}
List<String> expectedPublicArtifacts = [
'OreSpawn-4.1.0.2601021.jar',
'OreSpawn-4.1.0.2601021-sources.jar',
'OreSpawn-4.1.0.2601021-javadoc.jar'
'OreSpawn-4.1.1.2601021.jar',
'OreSpawn-4.1.1.2601021-sources.jar',
'OreSpawn-4.1.1.2601021-javadoc.jar'
]
if (base.archivesName.get() != expectedMavenArtifact
|| expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) {
Expand All @@ -112,7 +112,7 @@ tasks.register('verifyReleaseConfiguration') {
'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java',
'README.md', 'CHANGELOG.txt'
].each { path ->
if (!file(path).getText('UTF-8').contains('4.1.0.2601021')) {
if (!file(path).getText('UTF-8').contains('4.1.1.2601021')) {
throw new GradleException("Release identity missing from ${path}")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package zone.moddev.mc.orespawn.integration;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import net.minecraft.resources.Identifier;

/** Conservative matching at provider merge time, never in chunk generation. */
final class RetainedProviderOreDefaults {

private static final Set<String> BASE_METALS_LEGACY_MATERIALS = Set.of(
"coldiron", "adamantine", "starsteel", "tin", "lead", "zinc",
"silver", "mercury", "nickel", "platinum");

private record Placement(String kind, String dimension, String channel) {}

private RetainedProviderOreDefaults() {}

static String predecessor(String owner, String currentId, JsonObject current,
JsonObject defaults, JsonObject saved, Set<String> known) {
String output = singletonOutput(currentId, current);
Set<Placement> scope = placements(current);
if (output == null || scope == null || scope.isEmpty()) return null;
String historical = historicalPredecessor(owner, currentId, output);
if (historical != null && known.contains(historical) && !saved.has(historical)) return historical;

int replacements = 0;
for (var entry : defaults.entrySet()) {
if (equivalentOutputAndScope(entry.getKey(), entry.getValue(), output, scope)) replacements++;
}
if (replacements != 1) return null;

String retained = null;
for (var entry : saved.entrySet()) {
if (!equivalentOutputAndScope(entry.getKey(), entry.getValue(), output, scope)) continue;
if (retained != null) return null;
retained = entry.getKey();
}
return retained != null && declaredPredecessor(owner, currentId, output, retained, saved.getAsJsonObject(retained))
? retained : null;
}

private static boolean declaredPredecessor(String owner, String currentId, String output, String savedId, JsonObject saved) {
String importedPrefix = "orespawn:legacy/" + owner + "/";
boolean imported = savedId.startsWith(importedPrefix) && savedId.length() > importedPrefix.length();
if (saved.has("source_provider")) {
if (!owner.equals(text(saved.get("source_provider")))) return false;
} else if (saved.has("source_mod")) {
String fallbackOwner = text(saved.get("source_mod"));
if (!owner.equals(fallbackOwner) && !(imported && "legacy".equals(fallbackOwner))) return false;
}
if (imported) return true;
return savedId.equals(historicalPredecessor(owner, currentId, output));
}

private static String historicalPredecessor(String owner, String currentId, String output) {
// Base Metals' maintained 1.10/1.12 providers declared these IDs themselves.
// A generic provider's "legacy/" prefix is not evidence of a replacement.
if (!owner.equals("basemetals")) return null;
for (String material : BASE_METALS_LEGACY_MATERIALS) {
if (currentId.equals("basemetals:ore/" + material)
&& output.equals("basemetals:" + material + "_ore")) return "basemetals:legacy/" + material + "_ore";
}
return null;
}

private static boolean equivalentOutputAndScope(String id, JsonElement rule, String output, Set<Placement> scope) {
return rule.isJsonObject() && output.equals(singletonOutput(id, rule.getAsJsonObject()))
&& scope.equals(placements(rule.getAsJsonObject()));
}

private static String singletonOutput(String id, JsonObject rule) {
String output = rule.has("block") ? text(rule.get("block")) : id;
Identifier parsed = output == null ? null : Identifier.tryParse(output);
if (parsed == null || !defaultMetadata(rule)) return null;
output = parsed.toString();
if (rule.has("deep_output") && !output.equals(text(rule.get("deep_output")))) return null;
if (rule.has("outputs")) {
JsonElement values = rule.get("outputs");
if (!values.isJsonArray() || values.getAsJsonArray().size() != 1
|| !values.getAsJsonArray().get(0).isJsonObject()) return null;
JsonObject only = values.getAsJsonArray().get(0).getAsJsonObject();
if (!output.equals(text(only.get("block"))) || !defaultMetadata(only)) return null;
}
return output;
}

private static boolean defaultMetadata(JsonObject rule) {
JsonElement metadata = rule.get("metadata");
return metadata == null || metadata.isJsonPrimitive() && metadata.getAsJsonPrimitive().isNumber()
&& metadata.getAsBigDecimal().signum() == 0;
}

private static Set<Placement> placements(JsonObject rule) {
Set<Placement> result = new HashSet<>();
for (String kind : List.of("dimensions", "dimension_selectors")) {
JsonElement table = rule.get(kind);
if (table == null) continue;
if (!table.isJsonObject()) return null;
for (var entry : table.getAsJsonObject().entrySet()) {
if (Identifier.tryParse(entry.getKey()) == null || !entry.getValue().isJsonObject()) return null;
// Disabled and zero-rate placements are saved choices too.
result.add(new Placement(kind, entry.getKey(), channel(entry.getValue().getAsJsonObject())));
}
}
return result;
}

private static String channel(JsonObject placement) {
Identifier explicit = identifier(placement.get("placement_channel"));
if (explicit != null) return explicit.toString();
JsonElement pattern = placement.get("pattern");
Identifier type = pattern != null && pattern.isJsonObject()
? identifier(pattern.getAsJsonObject().get("type")) : null;
String legacy = text(pattern);
if (type == null && legacy != null && legacy.indexOf(':') >= 0) type = Identifier.tryParse(legacy);
return type == null ? "orespawn:standard" : type.toString();
}

private static Identifier identifier(JsonElement value) {
String name = text(value);
return name == null ? null : Identifier.tryParse(name);
}

private static String text(JsonElement value) {
return value != null && value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()
? value.getAsString() : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,14 @@ public static synchronized boolean mergeProviderDefinitions(JsonObject target) {
String id = entry.getKey();
if (!targetSection.has(id) && !known.contains(id)) {
JsonObject value = entry.getValue().getAsJsonObject().deepCopy();
if ("ores".equals(sectionName)) {
String retained = RetainedProviderOreDefaults.predecessor(
provider.modId, id, value, providerSection, targetSection, known);
if (retained != null) {
value.addProperty("enabled", false);
LOGGER.info("Preserved legacy ore choice '{}'; added provider alternative '{}' disabled", retained, id);
}
}
if (!"biome_rules".equals(sectionName)) {
value.addProperty("source_provider", provider.modId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ private static void writeReport(Path config, List<String> lines) {

private static void writeUpgradeReport(Path config, int imported, List<String> detail) {
List<String> lines = new ArrayList<>();
lines.add("OreSpawn 4.1.0.2601021 Upgrade Report");
lines.add("OreSpawn 4.1.1.2601021 Upgrade Report");
lines.add("================================");
lines.add("");
lines.add("RESULT: Legacy OreSpawn settings were imported into the OS4 profile.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath,
Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt");
List<String> missing = missingBlocks(igneous, metamorphic, sedimentary);
List<String> lines = new ArrayList<>();
lines.add("OreSpawn 4.1.0.2601021 Upgrade Report");
lines.add("OreSpawn 4.1.1.2601021 Upgrade Report");
lines.add("================================");
lines.add("");
lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected.");
Expand Down
Loading
Loading