Skip to content

Commit cd1b61e

Browse files
authored
Merge pull request #224 from SkyBlade1978/master-1.17.1
Preserve legacy Mineralogy upgrades on Forge 1.17.1
2 parents 781f9b7 + da736d5 commit cd1b61e

20 files changed

Lines changed: 1810 additions & 30 deletions

‎CHANGELOG.txt‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
Version 4.0.6
1+
Version 4.0.6.117011
22

3-
* Port OreSpawn 4.0.6 to Minecraft 1.17.1 and Forge 37.1.1 using Java 16
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+
* Port OreSpawn 4.0.6.117011 to Minecraft 1.17.1 and Forge 37.1.1 using Java 16.
48
* Fix provider top and filler materials being generated one block below exposed ground.
59
* Apply underwater materials from the corrected ground and ceiling materials to roof undersides.
610
* 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,10 +39,20 @@ 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

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

‎build.gradle‎

Lines changed: 198 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,168 @@ tasks.named('javadoc', Javadoc).configure {
299299

300300
tasks.named('test', Test).configure {
301301
useJUnitPlatform()
302+
// Unit tests inspect target files relative to the checkout, but they do
303+
// not need Forge's rolling runtime files. A console-only test logger keeps
304+
// them from contending with Eclipse/client logs in this working directory.
305+
systemProperty 'log4j.configurationFile', file('src/test/resources/log4j2-test.xml').absolutePath
306+
// Loaded only through an isolated URLClassLoader by the parity test. This
307+
// is deliberately not a Gradle dependency and cannot leak into Eclipse or
308+
// a published OreSpawn jar.
309+
File mineralogy5Oracle = file('../../MinecraftMineralogy 117/MinecraftMineralogy/build/libs/Mineralogy-1.17.1-5.3.0.jar')
310+
if (mineralogy5Oracle.isFile()) {
311+
systemProperty 'orespawn.mineralogy5Oracle', mineralogy5Oracle.absolutePath
312+
}
313+
}
314+
315+
// Several registry-focused tests initialize the real global config singleton.
316+
// Keep that target-native coverage without creating or changing a developer's
317+
// checkout config as a side effect of `test` or `build`.
318+
def unitTestWorldgenConfig = file('config/orespawn-worldgen.json')
319+
def unitTestWorldgenConfigWasPresent = false
320+
byte[] unitTestWorldgenConfigBytes = null
321+
tasks.named('test', Test).configure {
322+
doFirst {
323+
unitTestWorldgenConfigWasPresent = unitTestWorldgenConfig.isFile()
324+
unitTestWorldgenConfigBytes = unitTestWorldgenConfigWasPresent
325+
? unitTestWorldgenConfig.bytes : null
326+
}
327+
}
328+
def preserveDeveloperWorldgenConfig = tasks.register('preserveDeveloperWorldgenConfig') {
329+
doLast {
330+
if (unitTestWorldgenConfigWasPresent) {
331+
byte[] after = unitTestWorldgenConfig.isFile() ? unitTestWorldgenConfig.bytes : null
332+
if (after == null || !java.util.Arrays.equals(unitTestWorldgenConfigBytes, after)) {
333+
unitTestWorldgenConfig.parentFile.mkdirs()
334+
unitTestWorldgenConfig.bytes = unitTestWorldgenConfigBytes
335+
throw new GradleException('Unit tests changed config/orespawn-worldgen.json; the original was restored')
336+
}
337+
} else if (unitTestWorldgenConfig.isFile()) {
338+
delete unitTestWorldgenConfig
339+
}
340+
}
341+
}
342+
tasks.named('test') {
343+
finalizedBy preserveDeveloperWorldgenConfig
344+
}
345+
346+
// A Forge process is not green merely because it returns exit code zero. The
347+
// loader can log a worldgen/linkage failure and still shut down normally.
348+
def acceptedForge37LogNoise = [
349+
~/FML appears to be missing any signature data/,
350+
~/Found multiple arguments for option fml\.mcVersion/,
351+
~/Found multiple arguments for option fml\.forgeVersion/,
352+
~/\/FATAL\] \[net\.minecraftforge\.common\.ForgeConfig\/CORE\]: Forge config just got changed on the file system!$/,
353+
~/\/FATAL\] \[net\.minecraftforge\.fml\.packs\.ModFileResourcePack\/\]: Failed to clean up tempdir /
354+
]
355+
356+
def runtimeCrashSnapshot = { File runDirectory ->
357+
File crashDirectory = new File(runDirectory, 'crash-reports')
358+
if (!crashDirectory.isDirectory()) return [] as Set
359+
return fileTree(crashDirectory) { include '**/*' }.files
360+
.findAll { it.isFile() }.collect { it.absolutePath } as Set
361+
}
362+
363+
def assertRuntimeLogsClean = { File runDirectory, String context, Set priorCrashes ->
364+
File crashDirectory = new File(runDirectory, 'crash-reports')
365+
if (crashDirectory.isDirectory()) {
366+
def crashes = fileTree(crashDirectory) { include '**/*' }.files
367+
.findAll { it.isFile() && !priorCrashes.contains(it.absolutePath) }
368+
if (!crashes.isEmpty()) {
369+
throw new GradleException("${context} produced crash report ${crashes.first()}")
370+
}
371+
}
372+
373+
File logsDirectory = new File(runDirectory, 'logs')
374+
if (!logsDirectory.isDirectory()) return
375+
def failures = []
376+
[new File(logsDirectory, 'latest.log'), new File(logsDirectory, 'debug.log')]
377+
.findAll { it.isFile() }.each { File log ->
378+
int lineNumber = 0
379+
log.eachLine('UTF-8') { String line ->
380+
lineNumber++
381+
boolean unexpectedSeverity = line ==~ /.*\/(?:ERROR|FATAL)\].*/
382+
boolean knownNoise = acceptedForge37LogNoise.any { line =~ it }
383+
boolean fatalText = line.contains('Encountered an unexpected exception') ||
384+
line.contains('Exception stopping the server') ||
385+
line.contains('Migration audit failed') ||
386+
line.contains('java.lang.Error:') ||
387+
line.contains('NoSuchMethodError') ||
388+
line.contains('NoClassDefFoundError') ||
389+
line.contains('ExceptionInInitializerError') ||
390+
line.contains('Tried to assign a mutable BlockPos') ||
391+
line.contains('causing cascading worldgen lag')
392+
if ((unexpectedSeverity && !knownNoise) || fatalText) {
393+
failures.add("${log.name}:${lineNumber}: ${line}")
394+
}
395+
}
396+
}
397+
if (!failures.isEmpty()) {
398+
throw new GradleException("${context} logged unexpected errors:\n"
399+
+ failures.take(20).join('\n'))
400+
}
401+
}
402+
403+
task runtimeLogScannerTest {
404+
group = 'verification'
405+
description = 'Proves runtime log validation accepts documented Forge noise and rejects real failures.'
406+
doLast {
407+
File probe = file("${buildDir}/runtime-log-scanner-test")
408+
delete probe
409+
File logs = new File(probe, 'logs'); logs.mkdirs()
410+
new File(logs, 'latest.log').setText(
411+
'[main/ERROR] [FML]: FML appears to be missing any signature data\n'
412+
+ '[Server thread/INFO] [FML]: Done\n', 'UTF-8')
413+
assertRuntimeLogsClean(probe, 'scanner-accepted-noise-probe', [] as Set)
414+
new File(logs, 'latest.log').setText(
415+
'[Server thread/WARN]: Tried to assign a mutable BlockPos to tick data...\n', 'UTF-8')
416+
boolean rejected = false
417+
try { assertRuntimeLogsClean(probe, 'scanner-mutable-position-probe', [] as Set) }
418+
catch (GradleException expected) { rejected = true }
419+
if (!rejected) throw new GradleException('Runtime log scanner accepted a mutable BlockPos leak')
420+
new File(logs, 'latest.log').setText(
421+
'[Server thread/DEBUG] [FML]: Minecraft loaded a new chunk while populating another, causing cascading worldgen lag.\n', 'UTF-8')
422+
rejected = false
423+
try { assertRuntimeLogsClean(probe, 'scanner-cascading-probe', [] as Set) }
424+
catch (GradleException expected) { rejected = true }
425+
if (!rejected) throw new GradleException('Runtime log scanner accepted cascading worldgen')
426+
new File(logs, 'latest.log').setText(
427+
'[Server thread/ERROR] [example]: Unexpected fixture failure\n', 'UTF-8')
428+
rejected = false
429+
try { assertRuntimeLogsClean(probe, 'scanner-severity-probe', [] as Set) }
430+
catch (GradleException expected) { rejected = true }
431+
if (!rejected) throw new GradleException('Runtime log scanner accepted an unexpected ERROR line')
432+
delete probe
433+
}
434+
}
435+
436+
check.dependsOn runtimeLogScannerTest
437+
438+
task verifyMineralogyOracleIsolation {
439+
group = 'verification'
440+
description = 'Prevents published Mineralogy engines from leaking into Gradle configurations or ordinary Eclipse launches.'
441+
doLast {
442+
configurations.each { configuration ->
443+
if (configuration.canBeResolved &&
444+
configuration.files.any { it.name ==~ /Mineralogy-.*\.jar/ }) {
445+
throw new GradleException("Mineralogy oracle leaked into Gradle configuration ${configuration.name}")
446+
}
447+
}
448+
}
449+
}
450+
451+
check.dependsOn verifyMineralogyOracleIsolation
452+
453+
['runClient', 'runServer', 'runData'].each { String taskName ->
454+
tasks.matching { it.name == taskName }.all { JavaExec runTask ->
455+
doFirst {
456+
new File(runTask.workingDir, 'mods').mkdirs()
457+
runTask.ext.oreSpawnCrashSnapshot = runtimeCrashSnapshot(runTask.workingDir)
458+
}
459+
doLast {
460+
assertRuntimeLogsClean(runTask.workingDir, taskName,
461+
runTask.ext.oreSpawnCrashSnapshot as Set)
462+
}
463+
}
302464
}
303465

304466
def surfaceIntegrationClasses = layout.buildDirectory.dir('surface-integration-fixture/classes')
@@ -360,14 +522,25 @@ def surfaceIntegrationProcess = { String phase, dependency ->
360522
group = 'verification'
361523
description = "Runs the ${phase.toLowerCase(Locale.ROOT)} surface-probe server behind a process boundary."
362524
dependsOn dependency, "prepareRunSurfaceIntegration${phase}"
363-
doFirst {
364-
JavaExec runTask = tasks.getByName("runSurfaceIntegration${phase}") as JavaExec
365-
File argumentFile = layout.buildDirectory.file(
366-
"surface-integration-fixture/${phase.toLowerCase(Locale.ROOT)}.args").get().asFile
367-
argumentFile.parentFile.mkdirs()
368-
List<String> arguments = []
369-
arguments.addAll(runTask.allJvmArgs)
370-
arguments.add('-cp')
525+
doFirst {
526+
JavaExec runTask = tasks.getByName("runSurfaceIntegration${phase}") as JavaExec
527+
File argumentFile = layout.buildDirectory.file(
528+
"surface-integration-fixture/${phase.toLowerCase(Locale.ROOT)}.args").get().asFile
529+
argumentFile.parentFile.mkdirs()
530+
List<String> arguments = []
531+
arguments.addAll(runTask.allJvmArgs)
532+
String legacyClassPathArgument = arguments.find {
533+
it.startsWith('-DlegacyClassPath.file=')
534+
}
535+
if (legacyClassPathArgument != null) {
536+
File legacyClassPath = file(legacyClassPathArgument.substring(
537+
'-DlegacyClassPath.file='.length()))
538+
legacyClassPath.parentFile.mkdirs()
539+
legacyClassPath.setText(runTask.classpath.files.findAll { it.isFile() }
540+
.collect { it.absolutePath }
541+
.join(System.lineSeparator()) + System.lineSeparator(), 'UTF-8')
542+
}
543+
arguments.add('-cp')
371544
arguments.add(runTask.classpath.asPath)
372545
arguments.add(runTask.main)
373546
arguments.addAll(runTask.args)
@@ -393,7 +566,24 @@ def surfaceIntegrationProcess = { String phase, dependency ->
393566
}
394567

395568
def surfaceIntegrationFreshProcess = surfaceIntegrationProcess('Fresh', prepareSurfaceIntegrationTest)
569+
surfaceIntegrationFreshProcess.configure {
570+
doLast {
571+
File marker = surfaceIntegrationRunDirectory.get().file(
572+
'surface-integration-world/surfaceprobe-integration.properties').asFile
573+
if (!marker.isFile()) {
574+
throw new GradleException("Fresh surface integration completion marker is missing: ${marker}")
575+
}
576+
assertRuntimeLogsClean(surfaceIntegrationRunDirectory.get().asFile,
577+
'Forge 37 surface integration fresh phase', [] as Set)
578+
}
579+
}
396580
def surfaceIntegrationReloadProcess = surfaceIntegrationProcess('Reload', surfaceIntegrationFreshProcess)
581+
surfaceIntegrationReloadProcess.configure {
582+
doLast {
583+
assertRuntimeLogsClean(surfaceIntegrationRunDirectory.get().asFile,
584+
'Forge 37 surface integration reload phase', [] as Set)
585+
}
586+
}
397587

398588
def surfaceIntegrationTest = tasks.register('surfaceIntegrationTest') {
399589
group = 'verification'

‎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)