Skip to content

Commit 5777543

Browse files
authored
Merge pull request #222 from SkyBlade1978/master-1.15.2
Preserve legacy Mineralogy upgrades on Forge 1.15.2
2 parents ecc6c53 + b3a0831 commit 5777543

20 files changed

Lines changed: 1766 additions & 21 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.115021
22

3-
* Port OreSpawn 4.0.6 to Minecraft 1.15.2 and Forge 31.2.57 using Java 8.
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.115021 to Minecraft 1.15.2 and Forge 31.2.57 using Java 8.
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: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,137 @@ javadoc {
200200

201201
test {
202202
useJUnitPlatform()
203+
// Unit tests inspect target files relative to the checkout, but they do
204+
// not need Forge's rolling runtime files. A console-only test logger keeps
205+
// them from contending with Eclipse/client logs in this working directory.
206+
systemProperty 'log4j.configurationFile', file('src/test/resources/log4j2-test.xml').absolutePath
207+
// Loaded only through an isolated URLClassLoader by the parity test. This
208+
// is deliberately not a Gradle dependency and cannot leak into Eclipse or
209+
// a published OreSpawn jar.
210+
File mineralogy5Oracle = file('../../MinecraftMineralogy 115/MinecraftMineralogy/build/libs/Mineralogy-1.15.2-5.1.1.jar')
211+
if (mineralogy5Oracle.isFile()) {
212+
systemProperty 'orespawn.mineralogy5Oracle', mineralogy5Oracle.absolutePath
213+
}
214+
}
215+
216+
// A Forge process is not green merely because it returns exit code zero. The
217+
// loader can log a worldgen/linkage failure and still shut down normally.
218+
def acceptedForge31LogNoise = [
219+
~/FML appears to be missing any signature data/,
220+
~/Found multiple arguments for option fml\.mcVersion/,
221+
~/Found multiple arguments for option fml\.forgeVersion/,
222+
~/\/FATAL\] \[net\.minecraftforge\.common\.ForgeConfig\/CORE\]: Forge config just got changed on the file system!$/,
223+
~/\/FATAL\] \[net\.minecraftforge\.fml\.packs\.ModFileResourcePack\/\]: Failed to clean up tempdir /
224+
]
225+
226+
def runtimeCrashSnapshot = { File runDirectory ->
227+
File crashDirectory = new File(runDirectory, 'crash-reports')
228+
if (!crashDirectory.isDirectory()) return [] as Set
229+
return fileTree(crashDirectory) { include '**/*' }.files
230+
.findAll { it.isFile() }.collect { it.absolutePath } as Set
231+
}
232+
233+
def assertRuntimeLogsClean = { File runDirectory, String context, Set priorCrashes ->
234+
File crashDirectory = new File(runDirectory, 'crash-reports')
235+
if (crashDirectory.isDirectory()) {
236+
def crashes = fileTree(crashDirectory) { include '**/*' }.files
237+
.findAll { it.isFile() && !priorCrashes.contains(it.absolutePath) }
238+
if (!crashes.isEmpty()) {
239+
throw new GradleException("${context} produced crash report ${crashes.first()}")
240+
}
241+
}
242+
243+
File logsDirectory = new File(runDirectory, 'logs')
244+
if (!logsDirectory.isDirectory()) return
245+
def failures = []
246+
[new File(logsDirectory, 'latest.log'), new File(logsDirectory, 'debug.log')]
247+
.findAll { it.isFile() }.each { File log ->
248+
int lineNumber = 0
249+
log.eachLine('UTF-8') { String line ->
250+
lineNumber++
251+
boolean unexpectedSeverity = line ==~ /.*\/(?:ERROR|FATAL)\].*/
252+
boolean knownNoise = acceptedForge31LogNoise.any { line =~ it }
253+
boolean fatalText = line.contains('Encountered an unexpected exception') ||
254+
line.contains('Exception stopping the server') ||
255+
line.contains('Migration audit failed') ||
256+
line.contains('java.lang.Error:') ||
257+
line.contains('NoSuchMethodError') ||
258+
line.contains('NoClassDefFoundError') ||
259+
line.contains('ExceptionInInitializerError') ||
260+
line.contains('Tried to assign a mutable BlockPos') ||
261+
line.contains('causing cascading worldgen lag')
262+
if ((unexpectedSeverity && !knownNoise) || fatalText) {
263+
failures.add("${log.name}:${lineNumber}: ${line}")
264+
}
265+
}
266+
}
267+
if (!failures.isEmpty()) {
268+
throw new GradleException("${context} logged unexpected errors:\n"
269+
+ failures.take(20).join('\n'))
270+
}
271+
}
272+
273+
task runtimeLogScannerTest {
274+
group = 'verification'
275+
description = 'Proves runtime log validation accepts documented Forge noise and rejects real failures.'
276+
doLast {
277+
File probe = file("${buildDir}/runtime-log-scanner-test")
278+
delete probe
279+
File logs = new File(probe, 'logs'); logs.mkdirs()
280+
new File(logs, 'latest.log').setText(
281+
'[main/ERROR] [FML]: FML appears to be missing any signature data\n'
282+
+ '[Server thread/INFO] [FML]: Done\n', 'UTF-8')
283+
assertRuntimeLogsClean(probe, 'scanner-accepted-noise-probe', [] as Set)
284+
new File(logs, 'latest.log').setText(
285+
'[Server thread/WARN]: Tried to assign a mutable BlockPos to tick data...\n', 'UTF-8')
286+
boolean rejected = false
287+
try { assertRuntimeLogsClean(probe, 'scanner-mutable-position-probe', [] as Set) }
288+
catch (GradleException expected) { rejected = true }
289+
if (!rejected) throw new GradleException('Runtime log scanner accepted a mutable BlockPos leak')
290+
new File(logs, 'latest.log').setText(
291+
'[Server thread/DEBUG] [FML]: Minecraft loaded a new chunk while populating another, causing cascading worldgen lag.\n', 'UTF-8')
292+
rejected = false
293+
try { assertRuntimeLogsClean(probe, 'scanner-cascading-probe', [] as Set) }
294+
catch (GradleException expected) { rejected = true }
295+
if (!rejected) throw new GradleException('Runtime log scanner accepted cascading worldgen')
296+
new File(logs, 'latest.log').setText(
297+
'[Server thread/ERROR] [example]: Unexpected fixture failure\n', 'UTF-8')
298+
rejected = false
299+
try { assertRuntimeLogsClean(probe, 'scanner-severity-probe', [] as Set) }
300+
catch (GradleException expected) { rejected = true }
301+
if (!rejected) throw new GradleException('Runtime log scanner accepted an unexpected ERROR line')
302+
delete probe
303+
}
304+
}
305+
306+
check.dependsOn runtimeLogScannerTest
307+
308+
task verifyMineralogyOracleIsolation {
309+
group = 'verification'
310+
description = 'Prevents published Mineralogy engines from leaking into Gradle configurations or ordinary Eclipse launches.'
311+
doLast {
312+
configurations.each { configuration ->
313+
if (configuration.canBeResolved &&
314+
configuration.files.any { it.name ==~ /Mineralogy-.*\.jar/ }) {
315+
throw new GradleException("Mineralogy oracle leaked into Gradle configuration ${configuration.name}")
316+
}
317+
}
318+
}
319+
}
320+
321+
check.dependsOn verifyMineralogyOracleIsolation
322+
323+
['runClient', 'runServer', 'runData'].each { String taskName ->
324+
tasks.matching { it.name == taskName }.all { JavaExec runTask ->
325+
doFirst {
326+
new File(runTask.workingDir, 'mods').mkdirs()
327+
runTask.ext.oreSpawnCrashSnapshot = runtimeCrashSnapshot(runTask.workingDir)
328+
}
329+
doLast {
330+
assertRuntimeLogsClean(runTask.workingDir, taskName,
331+
runTask.ext.oreSpawnCrashSnapshot as Set)
332+
}
333+
}
203334
}
204335

205336
def surfaceIntegrationClasses = file("${buildDir}/surface-integration-fixture/classes")
@@ -291,8 +422,14 @@ surfaceIntegrationFreshProcess.doLast {
291422
if (!marker.isFile()) {
292423
throw new GradleException("Fresh surface integration completion marker is missing: ${marker}")
293424
}
425+
assertRuntimeLogsClean(surfaceIntegrationRunDirectory,
426+
'surface integration fresh phase', [] as Set)
294427
}
295428
def surfaceIntegrationReloadProcess = createSurfaceProcess('Reload', surfaceIntegrationFreshProcess)
429+
surfaceIntegrationReloadProcess.doLast {
430+
assertRuntimeLogsClean(surfaceIntegrationRunDirectory,
431+
'surface integration reload phase', [] as Set)
432+
}
296433

297434
task surfaceIntegrationTest(dependsOn: surfaceIntegrationReloadProcess) {
298435
group = 'verification'
@@ -327,6 +464,19 @@ task syncForge31EclipseLaunches(dependsOn: compileSurfaceIntegrationTestMod) {
327464
" <mapEntry key=\"FORGE_GROUP\" value=\"net.minecraftforge\"/>\r\n" +
328465
" <mapEntry key=\"target\" value=\"fmluserdevserver\"/>\r\n" +
329466
" <mapEntry key=\"MC_VERSION\" value=\"${minecraft_version}\"/>\r\n"
467+
['Client', 'Server', 'Data'].each { String runName ->
468+
File launch = file("run${runName}.launch")
469+
if (!launch.isFile()) {
470+
throw new GradleException("Missing generated Eclipse launch: ${launch}")
471+
}
472+
String text = launch.getText('UTF-8')
473+
if (!text.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE')) {
474+
text = text.replace('</launchConfiguration>',
475+
' <booleanAttribute key="org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE" value="true"/>\r\n' +
476+
'</launchConfiguration>')
477+
}
478+
launch.setText(text, 'UTF-8')
479+
}
330480
['Fresh', 'Reload'].each { String phase ->
331481
File launch = file("runSurfaceIntegration${phase}.launch")
332482
if (!launch.isFile()) {
@@ -349,6 +499,22 @@ task syncForge31EclipseLaunches(dependsOn: compileSurfaceIntegrationTestMod) {
349499
}
350500
}
351501

502+
task verifyForge31EclipseLaunchIsolation {
503+
group = 'verification'
504+
doLast {
505+
['runClient.launch', 'runServer.launch', 'runData.launch'].each { String launchName ->
506+
File launch = file(launchName)
507+
String launchText = launch.getText('UTF-8')
508+
if (launchText.contains('Mineralogy-') || launchText.contains('biomeIntegrationTest') ||
509+
!launchText.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE')) {
510+
throw new GradleException("Ordinary Eclipse launch is not isolated from test oracles: ${launch}")
511+
}
512+
}
513+
}
514+
}
515+
516+
syncForge31EclipseLaunches.finalizedBy verifyForge31EclipseLaunchIsolation
517+
352518
tasks.matching { it.name == 'genEclipseRuns' }.all {
353519
finalizedBy syncForge31EclipseLaunches
354520
}

‎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
@@ -94,3 +94,20 @@ the same mods. Alternatively, place a prepared global profile at
9494

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

0 commit comments

Comments
 (0)