Skip to content

Commit cd85241

Browse files
authored
Merge pull request #230 from SkyBlade1978/master-1.21.11
Preserve legacy Mineralogy upgrades on Forge 1.21.11
2 parents 7bee6b3 + 84c5f5b commit cd85241

21 files changed

Lines changed: 1879 additions & 19 deletions

CHANGELOG.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
Version 4.0.6
1+
Version 4.0.6.121111
22

3+
* Adopt target-qualified four-component versions so Minecraft and loader compatibility can be identified from the mod version.
4+
* Preserve generated worlds made with Mineralogy 1.10, 1.12, or 5.x by reading their saved mod metadata and exact legacy configuration before creating the OreSpawn world profile.
5+
* Write human-readable, idempotent upgrade reports for legacy OreSpawn and Mineralogy imports while retaining source files and existing chunks unchanged.
6+
* Audit automated runtime logs and dynamic-fluid generation so logged worldgen failures cannot pass merely because the process exits normally.
7+
* Qualify existing OreSpawn 4.0.4 global and per-world profiles without changing explicit Custom values or provider definitions.
38
* Fix provider top and filler materials being generated one block below exposed ground.
49
* Apply underwater materials from the corrected ground and ceiling materials to roof undersides.
510
* Preserve trees, vegetation, structures and block entities by running surface replacement before late features.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,23 @@ Important files:
3939
| `<world>/serverconfig/orespawn-worldgen.json` | Complete settings snapshot for one world |
4040
| `config/<modid>-orespawn.json` | Optional modpack override for one provider |
4141
| `config/orespawn-guide/README.md` | Guide exported automatically on first load |
42+
| `config/orespawn-upgrade-report.txt` | Human summary produced when legacy OreSpawn rules are imported |
43+
| `<world>/serverconfig/orespawn-upgrade-report.txt` | Human summary produced when a generated legacy Mineralogy world is pinned to its saved settings |
4244

4345
Profile edits affect newly generated chunks. Ore and flat-bedrock retrogen are
4446
separate opt-in features; OreSpawn never retro-generates rock strata.
4547
Stable Layers honours exact biome-ID geome influences on dynamic biome
4648
registries and spreads close geome transitions across layers rather than
4749
changing an entire vertical rock column at one boundary.
4850

51+
When an already-generated world has saved Mineralogy 1.10, 1.12, or 5.x mod
52+
metadata but no OreSpawn world profile, OreSpawn reads the matching published
53+
configuration contract and records the exact engine, numeric settings, rock
54+
order, and white/blacklists in the new world profile. Saved-world identity
55+
wins over stale files in the installation. A fresh world is never reclassified
56+
merely because an old `mineralogy.cfg` or `mineralogy-common.toml` remains in
57+
the instance.
58+
4959
To move a configured single-player world to a dedicated server, copy the
5060
world's `serverconfig/orespawn-worldgen.json` with the world and install the
5161
same provider mods on the server.

build.gradle

Lines changed: 171 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,168 @@ tasks.named('javadoc', Javadoc).configure {
223223

224224
tasks.named('test', Test).configure {
225225
useJUnitPlatform()
226+
// Unit tests inspect target files relative to the checkout, but they do
227+
// not need Forge's rolling runtime files. A console-only test logger keeps
228+
// them from contending with Eclipse/client logs in this working directory.
229+
systemProperty 'log4j.configurationFile', file('src/test/resources/log4j2-test.xml').absolutePath
230+
// Loaded only through an isolated URLClassLoader by the parity test. This
231+
// is deliberately not a Gradle dependency and cannot leak into Eclipse or
232+
// a published OreSpawn jar.
233+
File mineralogy5Oracle = file('../../MinecraftMineralogy 118/MinecraftMineralogy/build/libs/Mineralogy-1.18.2-5.4.0.jar')
234+
if (mineralogy5Oracle.isFile()) {
235+
systemProperty 'orespawn.mineralogy5Oracle', mineralogy5Oracle.absolutePath
236+
}
237+
}
238+
239+
// Several registry-focused tests initialize the real global config singleton.
240+
// Keep that target-native coverage without creating or changing a developer's
241+
// checkout config as a side effect of `test` or `build`.
242+
def unitTestWorldgenConfig = file('config/orespawn-worldgen.json')
243+
def unitTestWorldgenConfigWasPresent = false
244+
byte[] unitTestWorldgenConfigBytes = null
245+
tasks.named('test', Test).configure {
246+
doFirst {
247+
unitTestWorldgenConfigWasPresent = unitTestWorldgenConfig.isFile()
248+
unitTestWorldgenConfigBytes = unitTestWorldgenConfigWasPresent
249+
? unitTestWorldgenConfig.bytes : null
250+
}
251+
}
252+
def preserveDeveloperWorldgenConfig = tasks.register('preserveDeveloperWorldgenConfig') {
253+
doLast {
254+
if (unitTestWorldgenConfigWasPresent) {
255+
byte[] after = unitTestWorldgenConfig.isFile() ? unitTestWorldgenConfig.bytes : null
256+
if (after == null || !java.util.Arrays.equals(unitTestWorldgenConfigBytes, after)) {
257+
unitTestWorldgenConfig.parentFile.mkdirs()
258+
unitTestWorldgenConfig.bytes = unitTestWorldgenConfigBytes
259+
throw new GradleException('Unit tests changed config/orespawn-worldgen.json; the original was restored')
260+
}
261+
} else if (unitTestWorldgenConfig.isFile()) {
262+
delete unitTestWorldgenConfig
263+
}
264+
}
265+
}
266+
tasks.named('test') {
267+
finalizedBy preserveDeveloperWorldgenConfig
268+
}
269+
270+
// A Forge process is not green merely because it returns exit code zero. The
271+
// loader can log a worldgen/linkage failure and still shut down normally.
272+
def acceptedForge40LogNoise = [
273+
~/FML appears to be missing any signature data/,
274+
~/Found multiple arguments for option fml\.mcVersion/,
275+
~/Found multiple arguments for option fml\.forgeVersion/,
276+
~/\/FATAL\] \[net\.minecraftforge\.common\.ForgeConfig\/CORE\]: Forge config just got changed on the file system!$/,
277+
~/\/FATAL\] \[net\.minecraftforge\.fml\.packs\.ModFileResourcePack\/\]: Failed to clean up tempdir /
278+
]
279+
280+
def runtimeCrashSnapshot = { File runDirectory ->
281+
File crashDirectory = new File(runDirectory, 'crash-reports')
282+
if (!crashDirectory.isDirectory()) return [] as Set
283+
return fileTree(crashDirectory) { include '**/*' }.files
284+
.findAll { it.isFile() }.collect { it.absolutePath } as Set
285+
}
286+
287+
def assertRuntimeLogsClean = { File runDirectory, String context, Set priorCrashes ->
288+
File crashDirectory = new File(runDirectory, 'crash-reports')
289+
if (crashDirectory.isDirectory()) {
290+
def crashes = fileTree(crashDirectory) { include '**/*' }.files
291+
.findAll { it.isFile() && !priorCrashes.contains(it.absolutePath) }
292+
if (!crashes.isEmpty()) {
293+
throw new GradleException("${context} produced crash report ${crashes.first()}")
294+
}
295+
}
296+
297+
File logsDirectory = new File(runDirectory, 'logs')
298+
if (!logsDirectory.isDirectory()) return
299+
def failures = []
300+
[new File(logsDirectory, 'latest.log'), new File(logsDirectory, 'debug.log')]
301+
.findAll { it.isFile() }.each { File log ->
302+
int lineNumber = 0
303+
log.eachLine('UTF-8') { String line ->
304+
lineNumber++
305+
boolean unexpectedSeverity = line ==~ /.*\/(?:ERROR|FATAL)\].*/
306+
boolean knownNoise = acceptedForge40LogNoise.any { line =~ it }
307+
boolean fatalText = line.contains('Encountered an unexpected exception') ||
308+
line.contains('Exception stopping the server') ||
309+
line.contains('Migration audit failed') ||
310+
line.contains('java.lang.Error:') ||
311+
line.contains('NoSuchMethodError') ||
312+
line.contains('NoClassDefFoundError') ||
313+
line.contains('ExceptionInInitializerError') ||
314+
line.contains('Tried to assign a mutable BlockPos') ||
315+
line.contains('causing cascading worldgen lag')
316+
if ((unexpectedSeverity && !knownNoise) || fatalText) {
317+
failures.add("${log.name}:${lineNumber}: ${line}")
318+
}
319+
}
320+
}
321+
if (!failures.isEmpty()) {
322+
throw new GradleException("${context} logged unexpected errors:\n"
323+
+ failures.take(20).join('\n'))
324+
}
325+
}
326+
327+
task runtimeLogScannerTest {
328+
group = 'verification'
329+
description = 'Proves runtime log validation accepts documented Forge noise and rejects real failures.'
330+
doLast {
331+
File probe = file("${buildDir}/runtime-log-scanner-test")
332+
delete probe
333+
File logs = new File(probe, 'logs'); logs.mkdirs()
334+
new File(logs, 'latest.log').setText(
335+
'[main/ERROR] [FML]: FML appears to be missing any signature data\n'
336+
+ '[Server thread/INFO] [FML]: Done\n', 'UTF-8')
337+
assertRuntimeLogsClean(probe, 'scanner-accepted-noise-probe', [] as Set)
338+
new File(logs, 'latest.log').setText(
339+
'[Server thread/WARN]: Tried to assign a mutable BlockPos to tick data...\n', 'UTF-8')
340+
boolean rejected = false
341+
try { assertRuntimeLogsClean(probe, 'scanner-mutable-position-probe', [] as Set) }
342+
catch (GradleException expected) { rejected = true }
343+
if (!rejected) throw new GradleException('Runtime log scanner accepted a mutable BlockPos leak')
344+
new File(logs, 'latest.log').setText(
345+
'[Server thread/DEBUG] [FML]: Minecraft loaded a new chunk while populating another, causing cascading worldgen lag.\n', 'UTF-8')
346+
rejected = false
347+
try { assertRuntimeLogsClean(probe, 'scanner-cascading-probe', [] as Set) }
348+
catch (GradleException expected) { rejected = true }
349+
if (!rejected) throw new GradleException('Runtime log scanner accepted cascading worldgen')
350+
new File(logs, 'latest.log').setText(
351+
'[Server thread/ERROR] [example]: Unexpected fixture failure\n', 'UTF-8')
352+
rejected = false
353+
try { assertRuntimeLogsClean(probe, 'scanner-severity-probe', [] as Set) }
354+
catch (GradleException expected) { rejected = true }
355+
if (!rejected) throw new GradleException('Runtime log scanner accepted an unexpected ERROR line')
356+
delete probe
357+
}
358+
}
359+
360+
check.dependsOn runtimeLogScannerTest
361+
362+
task verifyMineralogyOracleIsolation {
363+
group = 'verification'
364+
description = 'Prevents published Mineralogy engines from leaking into Gradle configurations or ordinary Eclipse launches.'
365+
doLast {
366+
configurations.each { configuration ->
367+
if (configuration.canBeResolved &&
368+
configuration.files.any { it.name ==~ /Mineralogy-.*\.jar/ }) {
369+
throw new GradleException("Mineralogy oracle leaked into Gradle configuration ${configuration.name}")
370+
}
371+
}
372+
}
373+
}
374+
375+
check.dependsOn verifyMineralogyOracleIsolation
376+
377+
['runClient', 'runServer', 'runData'].each { String taskName ->
378+
tasks.matching { it.name == taskName }.all { JavaExec runTask ->
379+
doFirst {
380+
new File(runTask.workingDir, 'mods').mkdirs()
381+
runTask.ext.oreSpawnCrashSnapshot = runtimeCrashSnapshot(runTask.workingDir)
382+
}
383+
doLast {
384+
assertRuntimeLogsClean(runTask.workingDir, taskName,
385+
runTask.ext.oreSpawnCrashSnapshot as Set)
386+
}
387+
}
226388
}
227389

228390
def surfaceIntegrationClasses = layout.buildDirectory.dir('surface-integration-fixture/classes')
@@ -267,8 +429,16 @@ tasks.configureEach {
267429
} else if (name == 'runSurfaceIntegrationReload') {
268430
dependsOn 'runSurfaceIntegrationFresh'
269431
}
432+
if (name == 'runSurfaceIntegrationFresh' || name == 'runSurfaceIntegrationReload') {
433+
doFirst {
434+
ext.oreSpawnCrashSnapshot = runtimeCrashSnapshot(workingDir)
435+
}
436+
doLast {
437+
assertRuntimeLogsClean(workingDir, "Forge 61 ${name}",
438+
ext.oreSpawnCrashSnapshot as Set)
439+
}
440+
}
270441
}
271-
272442
def surfaceIntegrationTest = tasks.register('surfaceIntegrationTest') {
273443
group = 'verification'
274444
description = 'Verifies provider surfaces and dynamic-biome geology across fresh and reloaded normal terrain.'

docs/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ Use the focused guides for implementation details:
1212
- [BIOMES.md](BIOMES.md) and [DIMENSIONS.md](DIMENSIONS.md) for world integration;
1313
- [TEMPLATES.md](TEMPLATES.md) for selectable world styles;
1414
- [CONFIGURATION.md](CONFIGURATION.md) for configuration behavior;
15+
- [VERSIONS.md](VERSIONS.md) for the shared four-component target-qualified versioning and branch-release convention;
1516
- [README.md](README.md) for schemas, examples, and the complete documentation index.

docs/CONFIGURATION.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ packaged or API providers, provider override files, the global configuration,
1717
the selected template, and Create World edits. The result is saved with the
1818
world. Restart after editing JSON by hand.
1919

20+
An existing generated world follows a stricter safety order. Its existing
21+
`serverconfig/orespawn-worldgen.json` always wins. If none exists, saved legacy
22+
Mineralogy mod metadata may select the matching 1.10, 1.12, or 5.x config
23+
contract before the world profile is first written. Installed-pack defaults,
24+
Create World choices, and unrelated stale legacy files cannot override that
25+
saved-world identity. See `MIGRATION.md` and the generated per-world upgrade
26+
report for the exact decision.
27+
2028
## Top-Level Fields
2129

2230
| Field | Values | Meaning |
@@ -94,6 +102,12 @@ Cyano settings use `cyano.geome_size` (4-32767),
94102
`cyano.rock_layer_noise` (1-32767), and `cyano.rock_layer_thickness` (1-255).
95103
They are ignored by Sky.
96104

105+
Profiles created from a legacy Mineralogy world also retain
106+
`cyano.enabled`, `cyano.realistic_coal_layers`, the three effective
107+
`*_rocks` arrays, the six original `*_whitelist`/`*_blacklist` arrays, and
108+
source/version fields. These are migration snapshots, not new settings that a
109+
fresh pack needs to author.
110+
97111
## Rocks And Geomes
98112

99113
A rock requires `enabled`, `family`, `depth_peak`, `depth_spread`, `min_y`,

docs/MIGRATION.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,37 @@ legacy-ID behaviour.
1313

1414
Migration is non-destructive. OreSpawn writes `config/orespawn-worldgen.json`
1515
only when that target does not already exist and retains every source file.
16+
When legacy OreSpawn rules are translated, a concise player-facing summary is
17+
also written atomically to `config/orespawn-upgrade-report.txt`; the existing
18+
detailed rule report remains at `config/orespawn-migration/migration-report.txt`.
19+
20+
## Existing Mineralogy Worlds
21+
22+
An already-generated world without an OreSpawn per-world profile is inspected
23+
before OreSpawn chooses any installed-pack or Create World default. OreSpawn
24+
uses saved mod metadata from `level.dat`, with a valid `level.dat_old` as a
25+
fallback, to distinguish these published contracts:
26+
27+
- Mineralogy 1.10.2 `3.3.8.26` and its `mineralogy.cfg`;
28+
- Mineralogy 1.12.2 `3.8.0.53` and its distinct `mineralogy.cfg`;
29+
- Mineralogy 5.0.1 through 5.4.0 and `mineralogy-common.toml`.
30+
31+
The resulting world profile preserves enablement, selected legacy/geome
32+
engine, geome size, layer noise and thickness, realistic-coal behavior where
33+
supported, exact effective rock order (including historical duplicates), and
34+
all six white/blacklists. Saved-world identity chooses the lineage even when a
35+
different stale config is present. Missing or malformed values use that
36+
lineage's published defaults and are reported rather than silently broadening
37+
the world configuration.
38+
39+
The human-readable result is written atomically to
40+
`<world>/serverconfig/orespawn-upgrade-report.txt`. It identifies the saved
41+
version and metadata source, config source, selected engine and lineage,
42+
effective settings and outputs, missing IDs, fallbacks, and warnings. Source
43+
configuration and existing chunks are not rewritten. Once
44+
`orespawn-worldgen.json` exists it is authoritative and the import is not run
45+
again. A fresh world containing stale legacy files remains on its explicit
46+
OreSpawn/Create World settings.
1647

1748
When `config/mineralogy-geomes.json` exists, OreSpawn imports the Mineralogy 6
1849
profile directly, updates its schema marker, and records `migrated_from`.

docs/PLAYER_GUIDE.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,20 @@ the same mods. Alternatively, place a prepared global profile at
9393

9494
The server console commands `/orespawn status`, `/orespawn reload`, and
9595
`/orespawn dump-biomes` help pack authors diagnose active providers and IDs.
96+
97+
### Upgrading a Mineralogy world
98+
99+
If the world was already generated with Mineralogy 1.10, 1.12, or 5.x and has
100+
no OreSpawn world profile yet, OreSpawn reads the Mineralogy version saved in
101+
the world and the matching old configuration. It preserves the selected
102+
engine, numeric settings, rock order, and lists rather than silently applying
103+
new-world defaults. Look for:
104+
105+
```text
106+
<world>/serverconfig/orespawn-upgrade-report.txt
107+
```
108+
109+
The report explains what was detected and retained, including any missing rock
110+
IDs or fallback values. OreSpawn leaves the old configuration and generated
111+
chunks untouched. A fresh world does not inherit this behavior merely because
112+
an old Mineralogy config is still present in the instance.

0 commit comments

Comments
 (0)