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 @@ -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

Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions docs/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions docs/PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/VERSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions gradle/release/artifacts.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
}
Expand Down Expand Up @@ -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)')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1027,7 +1029,7 @@ private static void writeHumanUpgradeReport(Path destination) throws IOException
}
}
List<String> 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.");
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/cyano/orespawn/OreSpawn.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Path> oreSpawnConfigFiles = new ArrayList<>();
public static final List<String> additionalStoneBlocks = new ArrayList<>();
public static boolean disableVanillaOreGen;
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/zone/moddev/mc/orespawn/OreSpawn.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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();
Expand All @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> suppressed = new HashSet<>();
boolean changed;
}

private LegacyOreReplacements() {}

static Result reconcile(JsonObject profile, String provider, JsonObject ores, JsonObject defaults,
JsonObject active, JsonObject manifest, Set<String> known) {
Result result = new Result();
JsonObject previous = object(manifest, REPLACEMENTS);
JsonObject replacements = JsonCopies.copy(previous);
JsonObject review = new JsonObject();
Map<String, List<String>> 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<String, Integer> countByOutput = new HashMap<>();
for (Map.Entry<String, JsonElement> 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<String, JsonElement> 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<String> 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<String> 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<String> currentDefaults) {
JsonObject replacements = object(manifest, REPLACEMENTS);
for (Map.Entry<String, JsonElement> 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<String> stockPolicyReferences(JsonObject profile, String nativeId, String legacyId) {
List<String> disposable = new ArrayList<>();
JsonObject imported = object(object(profile, "ores"), legacyId);
for (Map.Entry<String, JsonElement> 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() : "";
}
}
Loading
Loading