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
2 changes: 2 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
Version 4.1.0.2601021

* Leave imported vanilla ores inactive while Manage Vanilla Ores is off.
* Use active imported settings instead of adding a second default placement pass when it is on.
* Keep biome and ore-list text visible with Minecraft's ARGB renderer.
* Move Help & Guide beside Recommended Defaults and fit both main menus above the footer.
* Reject invalid geome IDs without changing their spelling.
Expand Down
6 changes: 6 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ report for the exact decision.
| `flat_bedrock` | object | Opt-in flat bedrock controls |
| `worldgen_aliases` | ID-to-ID object | Replacement output aliases resolved while baking |

Imported vanilla ore rules obey `manage_vanilla_ores`. With it off, their
settings remain saved but vanilla placement is unchanged. With it on, a valid,
enabled import replaces the matching native default in its eligible dimensions.
Saved Ore Sources placement choices still take precedence. This does not change
custom non-imported ore rules or enable retrogen.

## Enumerated Values

| Setting | Accepted values |
Expand Down
3 changes: 3 additions & 0 deletions docs/PLAYER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ safely instead of breaking world creation.
region.
- **Manage Vanilla Ores** lets OreSpawn replace vanilla ore features with the
configured OreSpawn rules. Leave it off to keep normal Minecraft placement.
Imported vanilla rules stay saved but inactive while it is off. When it is
on, an active import replaces the matching shipped default rather than
adding another placement pass. Existing chunks are not rewritten.
- **Fluid Deposits** appears after strata are enabled or when a mod or pack
supplies a rule. Press **Add** to choose water, lava, or a fluid block from an
installed mod. These are covered underground deposits, not exposed vanilla
Expand Down
2 changes: 1 addition & 1 deletion gradle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ responsibility so maintainers can find the relevant task quickly.
| `verification/ore-sources.gradle` | Test-only future MMD providers and missing-provider reloads. |
| `verification/performance.gradle` | Matched world-generation checks with optional sibling mods. |
| `verification/biome-override.gradle` | New-terrain biome replacement and no-retrogen probe. |
| `verification/native-ores.gradle` | Managed Nether quartz, tag/block hosts, reloads and wrapper cleanup. |
| `verification/native-ores.gradle` | Managed and imported Nether quartz, vanilla OFF parity, tag/block hosts, reloads and wrapper cleanup. |
| `release/artifacts.gradle` | Release jars, bundled guides, artifact audits and checksums. |
| `release/publishing.gradle` | Maven coordinates, credentials and opt-in publication. |
| `ide/eclipse.gradle` | Generated launches, Buildship setup and production-classpath isolation. |
Expand Down
24 changes: 22 additions & 2 deletions gradle/verification/native-ores.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ def packagedJar = jar
def serverRuntime = providers.gradleProperty('packagedForgeServerRuntime')
def comparisonJar = providers.gradleProperty('nativeOreSpawnJar')
def finalTasks = []
['Default', 'Explicit'].each { scenario ->
['Default', 'Explicit', 'Imported', 'OffControl', 'OffImported'].each { scenario ->
boolean imported = scenario in ['Imported', 'OffImported']
boolean managed = !scenario.startsWith('Off')
File runDirectory = file("${buildDir}/native-ore-${scenario.toLowerCase()}-run")
def prepare = tasks.register("prepareNativeOre${scenario}") {
dependsOn shared.releaseJar, packagedJar
Expand Down Expand Up @@ -71,6 +73,8 @@ max-tick-time=-1
commandLine shared.forge64ServerCommand(file(serverRuntime.get()), runDirectory,
['-XX:ActiveProcessorCount=2',
"-Dnativeoreprobe.phase=${phase.toLowerCase()}",
"-Dnativeoreprobe.manage=${managed}",
"-Dnativeoreprobe.imported=${imported}",
"-Dnativeoreprobe.host=${scenario.toLowerCase()}"])
}
doLast {
Expand All @@ -97,7 +101,7 @@ max-tick-time=-1
}
tasks.register('nativeOreIntegrationTest') {
group = 'verification'
description = 'Checks quartz defaults, explicit hosts, exact-save reload and new-terrain-only edits.'
description = 'Checks quartz defaults and imports, vanilla OFF parity, reloads and new-terrain-only edits.'
dependsOn finalTasks
doLast {
['fresh', 'reload', 'edited'].each { phase ->
Expand All @@ -114,5 +118,21 @@ tasks.register('nativeOreIntegrationTest') {
}
}
logger.lifecycle('Tag and explicit quartz hosts produce identical terrain, fresh/reload/edited')
[['default', 'imported'], ['offcontrol', 'offimported']].each { pair ->
['fresh', 'reload', 'edited'].each { phase ->
def audits = pair.collect { scenario ->
Properties result = new Properties()
file("${buildDir}/native-ore-${scenario}-run/audit-logs/${phase}.properties")
.withInputStream { result.load(it) }
result
}
['quartz', 'blocks_sha256', 'biomes_sha256', 'new_terrain_quartz', 'water', 'new_terrain_water'].each { key ->
if (audits[0].getProperty(key) != audits[1].getProperty(key)) {
throw new GradleException("Imported quartz changed ${pair}, ${phase}: ${key}")
}
}
}
}
logger.lifecycle('Imported quartz: OFF matches native terrain; ON matches one configured pass; reload and no-retrogen verified')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,12 @@ private static Map<String, Group> discover(JsonObject root) {
for (Entry<String, JsonElement> oreEntry : ores.entrySet()) {
if (!oreEntry.getValue().isJsonObject()) continue;
JsonObject ore = oreEntry.getValue().getAsJsonObject();
boolean oreActive = bool(ore, "enabled", true)
&& (!bool(ore, "native_generation", false) || manageVanillaOres);
Identifier blockId = resource(string(ore, "block", oreEntry.getKey()));
Block block = blockId == null ? null : ForgeRegistries.BLOCKS.getValue(blockId);
if (block == null || block == Blocks.AIR) continue;
boolean vanillaRule = bool(ore, "native_generation", false)
|| OreSpawnOreGeneration.isImportedVanillaOre(resource(oreEntry.getKey()), block, ore);
boolean oreActive = bool(ore, "enabled", true) && (!vanillaRule || manageVanillaOres);
int metadata = integer(ore, "metadata", 0);
Inference inferred = inferMaterial(root, blockTags(block));
Identifier material = resource(string(ore, "material", ""));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,12 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) {
if (ruleId == null) ruleId = oreId;
outputBundles.put(ruleId.toString(), new BakedOutputBundle(
output.defaultBlockState(), deepOutput, deepOutputMaxY, outputs));
if (!oreEnabled || (nativeGeneration && !manageVanillaOres)) continue;
boolean importedVanilla = !nativeGeneration && isImportedVanillaOre(ruleId, output, oreJson);
// Keep imported settings, but let vanilla place its own ores while management is off.
if (!oreEnabled || (!manageVanillaOres && (nativeGeneration || importedVanilla))) continue;
Identifier material = OreSourcePolicies.material(profile, oreJson, output);
BakedOreRule rule = new BakedOreRule(output, manageVanillaOres && suppressVanilla);
BakedOreRule rule = new BakedOreRule(ruleId, output,
manageVanillaOres && (importedVanilla || suppressVanilla), nativeGeneration, importedVanilla);

for (Entry<String, JsonElement> dimensionEntry : dimensions.entrySet()) {
Identifier dimensionId = resource(dimensionEntry.getKey());
Expand Down Expand Up @@ -333,10 +336,11 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) {
rule.selector, dimension);
if (selected != null) {
combined.add(selected);
if (rule.suppressVanilla) suppressed.add(rule.output);
if (rule.suppressVanilla) rule.addSuppressedOutputs(suppressed);
}
}
combined = arbitrate(profile, combined, outputBundles);
combined = preferImportedVanilla(rules, combined);
result.put(dimension, combined.toArray(new BakedOre[combined.size()]));
vanillaOutputs.put(dimension, Collections.unmodifiableSet(suppressed));
}
Expand All @@ -345,9 +349,10 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) {
for (BakedOreRule rule : rules) {
if (rule.selector == null) continue;
selectorList.add(rule.selector);
if (rule.suppressVanilla) selectorOutputs.add(rule.output);
if (rule.suppressVanilla) rule.addSuppressedOutputs(selectorOutputs);
}
selectorList = arbitrate(profile, selectorList, outputBundles);
selectorList = preferImportedVanilla(rules, selectorList);
BakedOre[] selectorResult = selectorList.toArray(new BakedOre[selectorList.size()]);
LOGGER.info("Baked {} OreSpawn-managed ore definitions across {} dimensions",
rules.size(), result.size());
Expand All @@ -360,6 +365,35 @@ private static BakedOres bakeOres(JsonObject profile, BakedGeomeConfig config) {
Collections.unmodifiableSet(selectorOutputs));
}

static boolean isImportedVanillaOre(Identifier ruleId, Block output, JsonObject ore) {
return (ruleId != null && ruleId.getPath().startsWith("legacy/")
|| "legacy".equals(string(ore, "source_mod", "")))
&& VanillaOreFeatureGate.vanillaOreFamily(output) != null;
}

private static List<BakedOre> preferImportedVanilla(List<BakedOreRule> rules, List<BakedOre> selected) {
Map<Identifier, BakedOreRule> byId = new HashMap<>();
for (BakedOreRule rule : rules) byId.put(rule.ruleId, rule);
Set<Block> importedFamilies = Collections.newSetFromMap(new IdentityHashMap<>());
for (BakedOre ore : selected) {
BakedOreRule rule = byId.get(ore.ruleId);
if (rule != null && rule.importedVanilla) {
importedFamilies.add(VanillaOreFeatureGate.vanillaOreFamily(rule.output));
}
}
if (importedFamilies.isEmpty()) return selected;
List<BakedOre> result = new ArrayList<>();
for (BakedOre ore : selected) {
BakedOreRule rule = byId.get(ore.ruleId);
// Apply this after Ore Sources so an explicit saved placement choice wins.
if (rule == null || !rule.nativeGeneration
|| !importedFamilies.contains(VanillaOreFeatureGate.vanillaOreFamily(rule.output))) {
result.add(ore);
}
}
return result;
}

private static List<BakedOre> arbitrate(JsonObject profile, List<BakedOre> original,
Map<String, BakedOutputBundle> outputBundles) {
if (!profile.has(OreSourcePolicies.SECTION)
Expand Down Expand Up @@ -842,15 +876,27 @@ boolean acceptsBiome(ResourceKey<Biome> biome) {
}

private static final class BakedOreRule {
final Identifier ruleId;
final Block output;
final boolean suppressVanilla;
final boolean nativeGeneration;
final boolean importedVanilla;
final Map<ResourceKey<Level>, BakedOre> explicit = new HashMap<>();
final Set<ResourceKey<Level>> explicitDimensions = new HashSet<>();
BakedOre selector;

BakedOreRule(Block output, boolean suppressVanilla) {
BakedOreRule(Identifier ruleId, Block output, boolean suppressVanilla,
boolean nativeGeneration, boolean importedVanilla) {
this.ruleId = ruleId;
this.output = output;
this.suppressVanilla = suppressVanilla;
this.nativeGeneration = nativeGeneration;
this.importedVanilla = importedVanilla;
}

void addSuppressedOutputs(Set<Block> outputs) {
outputs.add(output);
if (importedVanilla) outputs.add(VanillaOreFeatureGate.vanillaOreFamily(output));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ static Block nativeOutput(ConfiguredFeature<?, ?> feature) {
return output;
}

private static Block vanillaOreFamily(Block block) {
static Block vanillaOreFamily(Block block) {
// Stone and deepslate variants share one managed placement rule.
if (block == Blocks.COAL_ORE || block == Blocks.DEEPSLATE_COAL_ORE) return Blocks.COAL_ORE;
if (block == Blocks.IRON_ORE || block == Blocks.DEEPSLATE_IRON_ORE) return Blocks.IRON_ORE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@ static void verifyStoppedCaches() {
}
}

static void verifyQuartzPasses(boolean managed, boolean imported) {
try {
Class<?> generation = Class.forName("zone.moddev.mc.orespawn.worldgen.OreSpawnOreGeneration");
Field cache = generation.getDeclaredField("oresByDimension");
cache.setAccessible(true);
Object[] ores = (Object[]) ((Map<?, ?>) cache.get(null)).get(Level.NETHER);
int count = ores == null ? 0 : ores.length;
if (count != (managed ? 1 : 0)) {
throw new IllegalStateException("Unexpected managed quartz pass count: " + count);
}
if (managed) {
Field id = ores[0].getClass().getDeclaredField("ruleId");
id.setAccessible(true);
String expected = imported ? "orespawn:legacy/quartz" : "minecraft:nether_quartz_ore";
if (!expected.equals(id.get(ores[0]).toString())) {
throw new IllegalStateException("Wrong quartz placement source: " + id.get(ores[0]));
}
}
System.out.println("NATIVE_ORE_PROBE managed=" + managed + " imported=" + imported + " passes=" + count);
} catch (ReflectiveOperationException failure) {
throw new IllegalStateException("Could not inspect imported quartz placement", failure);
}
}

static boolean controlsFluid() {
try {
Class<?> generation = Class.forName("zone.moddev.mc.orespawn.worldgen.FluidDepositFeature");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ private void prepare(ServerAboutToStartEvent event) {
JsonObject onlyQuartz = new JsonObject();
onlyQuartz.add(QUARTZ, ores.get(QUARTZ));
root.add("ores", onlyQuartz);
root.addProperty("manage_vanilla_ores", true);
root.addProperty("manage_vanilla_ores", managed());
root.addProperty("place_fluid_deposits", true);
root.add("fluid_deposits", fluidProfile());
root.add("terrain_dimensions", new JsonObject());
Expand All @@ -82,7 +82,16 @@ private void prepare(ServerAboutToStartEvent event) {
retrogen.addProperty("revision", 0);
root.add("retrogen", retrogen);
}
JsonObject rule = root.getAsJsonObject("ores").getAsJsonObject(QUARTZ)
JsonObject ores = root.getAsJsonObject("ores");
if (imported() && "fresh".equals(phase)) {
JsonObject legacy = ores.getAsJsonObject(QUARTZ).deepCopy();
legacy.addProperty("block", QUARTZ);
legacy.addProperty("source_mod", "legacy");
legacy.addProperty("native_generation", false);
legacy.addProperty("retrogen", false);
ores.add("orespawn:legacy/quartz", legacy);
}
JsonObject rule = ores.getAsJsonObject(imported() ? "orespawn:legacy/quartz" : QUARTZ)
.getAsJsonObject("dimensions").getAsJsonObject(NETHER);
rule.addProperty("min_y", "edited".equals(phase) ? 100 : 24);
rule.addProperty("max_y", "edited".equals(phase) ? 104 : 28);
Expand Down Expand Up @@ -114,9 +123,10 @@ private void prepare(ServerAboutToStartEvent event) {
private void audit(ServerStartedEvent event) {
MinecraftServer server = event.getServer();
String phase = System.getProperty("nativeoreprobe.phase", "fresh");
if (!NativeOreProbeBridge.controlsQuartz()) {
throw new IllegalStateException("Quartz is not controlled by OreSpawn before generation");
if (NativeOreProbeBridge.controlsQuartz() != managed()) {
throw new IllegalStateException("Quartz takeover disagrees with Manage Vanilla Ores");
}
NativeOreProbeBridge.verifyQuartzPasses(managed(), imported());
if (!NativeOreProbeBridge.controlsFluid()) {
throw new IllegalStateException("Tag-only fluid was not baked before generation");
}
Expand Down Expand Up @@ -183,7 +193,9 @@ private static Properties inspect(ServerLevel world, int origin, int minY, int m
blocks.update((byte) id);
if (state.getBlock() == Blocks.WATER) water++;
if (state.getBlock() == Blocks.NETHER_QUARTZ_ORE) {
if (y < minY || y > maxY) throw new IllegalStateException("Quartz outside managed range at " + cursor);
if (managed() && (y < minY || y > maxY)) {
throw new IllegalStateException("Quartz outside managed range at " + cursor);
}
quartz++;
}
}
Expand All @@ -201,6 +213,14 @@ private static Properties inspect(ServerLevel world, int origin, int minY, int m
return result;
}

private static boolean managed() {
return Boolean.parseBoolean(System.getProperty("nativeoreprobe.manage", "true"));
}

private static boolean imported() {
return Boolean.getBoolean("nativeoreprobe.imported");
}

private static JsonObject fluidProfile() {
JsonObject rule = new JsonObject();
rule.addProperty("enabled", true);
Expand Down
Loading
Loading