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.2602001.jar
build/libs/OreSpawn-4.1.0.2602001-sources.jar
build/libs/OreSpawn-4.1.0.2602001-javadoc.jar
build/libs/OreSpawn-4.1.1.2602001.jar
build/libs/OreSpawn-4.1.1.2602001-sources.jar
build/libs/OreSpawn-4.1.1.2602001-javadoc.jar
build/release/SHA256SUMS
CHANGELOG.txt

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
Version 4.1.1.2602001 (local candidate)

* Keep saved ore settings when a provider renames a recognised rule. Add the
matching modern rule as a disabled alternative rather than another ore pass.

Version 4.1.0.2602001

* Leave imported vanilla ores inactive while Manage Vanilla Ores is off.
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ 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.

Recognised provider rule renames keep the saved ore settings and add the modern
rule as a disabled, editable alternative, avoiding an extra generation pass.
See [retained provider rules](docs/MIGRATION.md#retained-provider-rules).

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

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

Expand Down
33 changes: 33 additions & 0 deletions docs/MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# Migration

## Retained Provider Rules

An older profile can keep an ore rule after its provider renames that rule.
OreSpawn must not add a second generation pass just because the ID changed.
The merge recognises OreSpawn's imported `orespawn:legacy/<provider>/...`
rules and the following exact Base Metals replacements:

- `basemetals:legacy/<material>_ore` to `basemetals:ore/<material>` for
coldiron, adamantine, starsteel, tin, lead, zinc, silver, mercury, nickel
and platinum.

A match needs one saved rule and one provider candidate with the same single
output, default metadata, and dimension/selector and effective placement-channel
footprint. Conflicting provider ownership, mixed outputs, different deep outputs
and ambiguous matches are not treated as replacements. A generic mod's
`legacy/` name is not enough evidence.

For a recognised match, the missing modern rule is added as a complete,
disabled alternative. The saved rule keeps its rates, hosts, heights, pattern,
enablement and source choices, including disabled and zero-rate settings.
Fresh worlds still use the provider's normal defaults.

You can enable the alternative in the ore settings. Later merges retain that
choice, existing modern rules and recorded deletions. A recorded deletion of
one of the historical Base Metals IDs also leaves its mapped replacement
disabled. Deleted imports without an exact mapping are not guessed from their
names.

This repair does not automatically remove an already-saved historical
Base Metals/modern pair: both may have been selected deliberately. Review
ambiguous or previously affected profiles before changing their choices.
It does not alter generated chunks, public APIs or saved formats.

## Provider-Aware OS3 Imports

When an OreSpawn 2/3 file is named for an installed provider, the migrator now
Expand Down
2 changes: 1 addition & 1 deletion docs/VERSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Examples:
| 1.21.1 | Forge | `121011` | `4.0.16.121011` |
| 1.21.11 | Forge | `121111` | `4.0.16.121111` |
| 26.1.2 | Forge | `2601021` | `4.0.16.2601021` |
| 26.2 | Forge | `2602001` | `4.1.0.2602001` |
| 26.2 | Forge | `2602001` | `4.1.1.2602001` |
| 26.2 | NeoForge | `2602002` | `4.0.6.2602002` |

Historical MMD releases may also have four numeric components but may have used
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.2
mod_id=orespawn
mod_name=MMD OreSpawn
mod_license=LGPL-2.1
mod_version=4.1.0.2602001
mod_version=4.1.1.2602001
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.2602001'
if (project.mod_version != '4.1.1.2602001'
|| project.mod_group != expectedMavenGroup
|| project.minecraft_version != '26.2'
|| project.forge_version != '65.1.0'
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.2602001.jar',
'OreSpawn-4.1.0.2602001-sources.jar',
'OreSpawn-4.1.0.2602001-javadoc.jar'
'OreSpawn-4.1.1.2602001.jar',
'OreSpawn-4.1.1.2602001-sources.jar',
'OreSpawn-4.1.1.2602001-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.2602001')) {
if (!file(path).getText('UTF-8').contains('4.1.1.2602001')) {
throw new GradleException("Release identity missing from ${path}")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package zone.moddev.mc.orespawn.integration;

import java.util.HashSet;
import java.util.Arrays;
import java.util.Map;
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 (Map.Entry<String, JsonElement> entry : defaults.entrySet()) {
if (equivalentOutputAndScope(entry.getKey(), entry.getValue(), output, scope)) replacements++;
}
if (replacements != 1) return null;

String retained = null;
for (Map.Entry<String, JsonElement> 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 : parseIdentifier(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 : Arrays.asList("dimensions", "dimension_selectors")) {
JsonElement table = rule.get(kind);
if (table == null) continue;
if (!table.isJsonObject()) return null;
for (Map.Entry<String, JsonElement> entry : table.getAsJsonObject().entrySet()) {
if (parseIdentifier(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 = parseIdentifier(legacy);
return type == null ? "orespawn:standard" : type.toString();
}

private static Identifier parseIdentifier(String value) {
try { return value == null || value.trim().isEmpty() ? null : Identifier.parse(value); }
catch (RuntimeException ignored) { return null; }
}

private static Identifier identifier(JsonElement value) {
String name = text(value);
return name == null ? null : parseIdentifier(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 saved 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.2602001 Upgrade Report");
lines.add("OreSpawn 4.1.1.2602001 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.2602001 Upgrade Report");
lines.add("OreSpawn 4.1.1.2602001 Upgrade Report");
lines.add("================================");
lines.add("");
lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected.");
Expand Down
Loading
Loading