Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
Version 4.0.6

* 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.
* Write human-readable, idempotent upgrade reports for legacy OreSpawn and Mineralogy imports while retaining source files and existing chunks unchanged.
* Audit automated runtime logs and dynamic-fluid generation so logged worldgen failures cannot pass merely because the process exits normally.
* Port OreSpawn 4.0.6 to Minecraft 1.13.2 and Forge 25.0.223 using Java 8.
* Fix provider top and filler materials being generated one block below exposed ground.
* Apply underwater materials from the corrected ground and ceiling materials to roof undersides.
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,20 @@ Important files:
| `<world>/serverconfig/orespawn-worldgen.json` | Complete settings snapshot for one world |
| `config/<modid>-orespawn.json` | Optional modpack override for one provider |
| `config/orespawn-guide/README.md` | Guide exported automatically on first load |
| `config/orespawn-upgrade-report.txt` | Human summary produced when legacy OreSpawn rules are imported |
| `<world>/serverconfig/orespawn-upgrade-report.txt` | Human summary produced when a generated legacy Mineralogy world is pinned to its saved settings |

Profile edits affect newly generated chunks. Ore and flat-bedrock retrogen are
separate opt-in features; OreSpawn never retro-generates rock strata.

When an already-generated world has saved Mineralogy 1.10, 1.12, or 5.x mod
metadata but no OreSpawn world profile, OreSpawn reads the matching published
configuration contract and records the exact engine, numeric settings, rock
order, and white/blacklists in the new world profile. Saved-world identity
wins over stale files in the installation. A fresh world is never reclassified
merely because an old `mineralogy.cfg` or `mineralogy-common.toml` remains in
the instance.

To move a configured single-player world to a dedicated server, copy the
world's `serverconfig/orespawn-worldgen.json` with the world and install the
same provider mods on the server.
Expand Down
154 changes: 154 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,133 @@ javadoc {

test {
useJUnitPlatform()
// Loaded only through an isolated URLClassLoader by the parity test. This
// is deliberately not a Gradle dependency and cannot leak into Eclipse or
// a published OreSpawn jar.
File mineralogy5Oracle = file('../../MinecraftMineralogy 113/MinecraftMineralogy/build/libs/Mineralogy-1.13.2-5.0.1.jar')
if (mineralogy5Oracle.isFile()) {
systemProperty 'orespawn.mineralogy5Oracle', mineralogy5Oracle.absolutePath
}
}

// A Forge process is not green merely because it returns exit code zero. The
// loader can log a worldgen/linkage failure and still shut down normally.
def acceptedForge25LogNoise = [
~/FML appears to be missing any signature data/,
~/Found multiple arguments for option fml\.mcVersion/,
~/Found multiple arguments for option fml\.forgeVersion/,
~/\/FATAL\] \[net\.minecraftforge\.common\.ForgeConfig\/CORE\]: Forge config just got changed on the file system!$/,
~/\/FATAL\] \[net\.minecraftforge\.fml\.packs\.ModFileResourcePack\/\]: Failed to clean up tempdir /
]

def runtimeCrashSnapshot = { File runDirectory ->
File crashDirectory = new File(runDirectory, 'crash-reports')
if (!crashDirectory.isDirectory()) return [] as Set
return fileTree(crashDirectory) { include '**/*' }.files
.findAll { it.isFile() }.collect { it.absolutePath } as Set
}

def assertRuntimeLogsClean = { File runDirectory, String context, Set priorCrashes ->
File crashDirectory = new File(runDirectory, 'crash-reports')
if (crashDirectory.isDirectory()) {
def crashes = fileTree(crashDirectory) { include '**/*' }.files
.findAll { it.isFile() && !priorCrashes.contains(it.absolutePath) }
if (!crashes.isEmpty()) {
throw new GradleException("${context} produced crash report ${crashes.first()}")
}
}

File logsDirectory = new File(runDirectory, 'logs')
if (!logsDirectory.isDirectory()) return
def failures = []
[new File(logsDirectory, 'latest.log'), new File(logsDirectory, 'debug.log')]
.findAll { it.isFile() }.each { File log ->
int lineNumber = 0
log.eachLine('UTF-8') { String line ->
lineNumber++
boolean unexpectedSeverity = line ==~ /.*\/(?:ERROR|FATAL)\].*/
boolean knownNoise = acceptedForge25LogNoise.any { line =~ it }
boolean fatalText = line.contains('Encountered an unexpected exception') ||
line.contains('Exception stopping the server') ||
line.contains('Migration audit failed') ||
line.contains('java.lang.Error:') ||
line.contains('NoSuchMethodError') ||
line.contains('NoClassDefFoundError') ||
line.contains('ExceptionInInitializerError') ||
line.contains('Tried to assign a mutable BlockPos') ||
line.contains('causing cascading worldgen lag')
if ((unexpectedSeverity && !knownNoise) || fatalText) {
failures.add("${log.name}:${lineNumber}: ${line}")
}
}
}
if (!failures.isEmpty()) {
throw new GradleException("${context} logged unexpected errors:\n"
+ failures.take(20).join('\n'))
}
}

task runtimeLogScannerTest {
group = 'verification'
description = 'Proves runtime log validation accepts documented Forge noise and rejects real failures.'
doLast {
File probe = file("${buildDir}/runtime-log-scanner-test")
delete probe
File logs = new File(probe, 'logs'); logs.mkdirs()
new File(logs, 'latest.log').setText(
'[main/ERROR] [FML]: FML appears to be missing any signature data\n'
+ '[Server thread/INFO] [FML]: Done\n', 'UTF-8')
assertRuntimeLogsClean(probe, 'scanner-accepted-noise-probe', [] as Set)
new File(logs, 'latest.log').setText(
'[Server thread/WARN]: Tried to assign a mutable BlockPos to tick data...\n', 'UTF-8')
boolean rejected = false
try { assertRuntimeLogsClean(probe, 'scanner-mutable-position-probe', [] as Set) }
catch (GradleException expected) { rejected = true }
if (!rejected) throw new GradleException('Runtime log scanner accepted a mutable BlockPos leak')
new File(logs, 'latest.log').setText(
'[Server thread/DEBUG] [FML]: Minecraft loaded a new chunk while populating another, causing cascading worldgen lag.\n', 'UTF-8')
rejected = false
try { assertRuntimeLogsClean(probe, 'scanner-cascading-probe', [] as Set) }
catch (GradleException expected) { rejected = true }
if (!rejected) throw new GradleException('Runtime log scanner accepted cascading worldgen')
new File(logs, 'latest.log').setText(
'[Server thread/ERROR] [example]: Unexpected fixture failure\n', 'UTF-8')
rejected = false
try { assertRuntimeLogsClean(probe, 'scanner-severity-probe', [] as Set) }
catch (GradleException expected) { rejected = true }
if (!rejected) throw new GradleException('Runtime log scanner accepted an unexpected ERROR line')
delete probe
}
}

check.dependsOn runtimeLogScannerTest

task verifyMineralogyOracleIsolation {
group = 'verification'
description = 'Prevents published Mineralogy engines from leaking into Gradle configurations or ordinary Eclipse launches.'
doLast {
configurations.each { configuration ->
if (configuration.canBeResolved &&
configuration.files.any { it.name ==~ /Mineralogy-.*\.jar/ }) {
throw new GradleException("Mineralogy oracle leaked into Gradle configuration ${configuration.name}")
}
}
}
}

check.dependsOn verifyMineralogyOracleIsolation

['runClient', 'runServer', 'runData'].each { String taskName ->
tasks.matching { it.name == taskName }.all { JavaExec runTask ->
doFirst {
new File(runTask.workingDir, 'mods').mkdirs()
runTask.ext.oreSpawnCrashSnapshot = runtimeCrashSnapshot(runTask.workingDir)
}
doLast {
assertRuntimeLogsClean(runTask.workingDir, taskName,
runTask.ext.oreSpawnCrashSnapshot as Set)
}
}
}

def surfaceIntegrationClasses = file("${buildDir}/surface-integration-fixture/classes")
Expand Down Expand Up @@ -291,8 +418,14 @@ surfaceIntegrationFreshProcess.doLast {
if (!marker.isFile()) {
throw new GradleException("Fresh surface integration completion marker is missing: ${marker}")
}
assertRuntimeLogsClean(surfaceIntegrationRunDirectory,
'surface integration fresh phase', [] as Set)
}
def surfaceIntegrationReloadProcess = createSurfaceProcess('Reload', surfaceIntegrationFreshProcess)
surfaceIntegrationReloadProcess.doLast {
assertRuntimeLogsClean(surfaceIntegrationRunDirectory,
'surface integration reload phase', [] as Set)
}

task surfaceIntegrationTest(dependsOn: surfaceIntegrationReloadProcess) {
group = 'verification'
Expand Down Expand Up @@ -348,6 +481,11 @@ task syncForge25EclipseLaunches(dependsOn: compileSurfaceIntegrationTestMod) {
text = text.replace(' <mapEntry key="MOD_CLASSES"',
environmentEntries(launchTarget) + ' <mapEntry key="MOD_CLASSES"')
}
if (!text.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE')) {
text = text.replace('</launchConfiguration>',
' <booleanAttribute key="org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE" value="true"/>\r\n' +
'</launchConfiguration>')
}
launch.setText(text, 'UTF-8')
}
['Fresh', 'Reload'].each { String phase ->
Expand All @@ -372,6 +510,22 @@ task syncForge25EclipseLaunches(dependsOn: compileSurfaceIntegrationTestMod) {
}
}

task verifyForge25EclipseLaunchIsolation {
group = 'verification'
doLast {
['runClient.launch', 'runServer.launch', 'runData.launch'].each { String launchName ->
File launch = file(launchName)
String launchText = launch.getText('UTF-8')
if (launchText.contains('Mineralogy-') ||
!launchText.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE')) {
throw new GradleException("Ordinary Eclipse launch is not isolated from test oracles: ${launch}")
}
}
}
}

syncForge25EclipseLaunches.finalizedBy verifyForge25EclipseLaunchIsolation

tasks.matching { it.name == 'genEclipseRuns' }.all {
finalizedBy syncForge25EclipseLaunches
}
1 change: 1 addition & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ Use the focused guides for implementation details:
- [BIOMES.md](BIOMES.md) and [DIMENSIONS.md](DIMENSIONS.md) for world integration;
- [TEMPLATES.md](TEMPLATES.md) for selectable world styles;
- [CONFIGURATION.md](CONFIGURATION.md) for configuration behavior;
- [VERSIONS.md](VERSIONS.md) for the shared mod versioning and branch-release convention;
- [README.md](README.md) for schemas, examples, and the complete documentation index.
14 changes: 14 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ packaged or API providers, provider override files, the global configuration,
the selected template, and Create World edits. The result is saved with the
world. Restart after editing JSON by hand.

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

## Top-Level Fields

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

Profiles created from a legacy Mineralogy world also retain
`cyano.enabled`, `cyano.realistic_coal_layers`, the three effective
`*_rocks` arrays, the six original `*_whitelist`/`*_blacklist` arrays, and
source/version fields. These are migration snapshots, not new settings that a
fresh pack needs to author.

## Rocks And Geomes

A rock requires `enabled`, `family`, `depth_peak`, `depth_spread`, `min_y`,
Expand Down
31 changes: 31 additions & 0 deletions docs/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,37 @@ legacy-ID behaviour.

Migration is non-destructive. OreSpawn writes `config/orespawn-worldgen.json`
only when that target does not already exist and retains every source file.
When legacy OreSpawn rules are translated, a concise player-facing summary is
also written atomically to `config/orespawn-upgrade-report.txt`; the existing
detailed rule report remains at `config/orespawn-migration/migration-report.txt`.

## Existing Mineralogy Worlds

An already-generated world without an OreSpawn per-world profile is inspected
before OreSpawn chooses any installed-pack or Create World default. OreSpawn
uses saved mod metadata from `level.dat`, with a valid `level.dat_old` as a
fallback, to distinguish these published contracts:

- Mineralogy 1.10.2 `3.3.8.26` and its `mineralogy.cfg`;
- Mineralogy 1.12.2 `3.8.0.53` and its distinct `mineralogy.cfg`;
- Mineralogy 5.0.1 through 5.4.0 and `mineralogy-common.toml`.

The resulting world profile preserves enablement, selected legacy/geome
engine, geome size, layer noise and thickness, realistic-coal behavior where
supported, exact effective rock order (including historical duplicates), and
all six white/blacklists. Saved-world identity chooses the lineage even when a
different stale config is present. Missing or malformed values use that
lineage's published defaults and are reported rather than silently broadening
the world configuration.

The human-readable result is written atomically to
`<world>/serverconfig/orespawn-upgrade-report.txt`. It identifies the saved
version and metadata source, config source, selected engine and lineage,
effective settings and outputs, missing IDs, fallbacks, and warnings. Source
configuration and existing chunks are not rewritten. Once
`orespawn-worldgen.json` exists it is authoritative and the import is not run
again. A fresh world containing stale legacy files remains on its explicit
OreSpawn/Create World settings.

When `config/mineralogy-geomes.json` exists, OreSpawn imports the Mineralogy 6
profile directly, updates its schema marker, and records `migrated_from`.
Expand Down
17 changes: 17 additions & 0 deletions docs/PLAYER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,20 @@ the same mods. Alternatively, place a prepared global profile at

The server console commands `/orespawn status`, `/orespawn reload`, and
`/orespawn dump-biomes` help pack authors diagnose active providers and IDs.

### Upgrading a Mineralogy world

If the world was already generated with Mineralogy 1.10, 1.12, or 5.x and has
no OreSpawn world profile yet, OreSpawn reads the Mineralogy version saved in
the world and the matching old configuration. It preserves the selected
engine, numeric settings, rock order, and lists rather than silently applying
new-world defaults. Look for:

```text
<world>/serverconfig/orespawn-upgrade-report.txt
```

The report explains what was detected and retained, including any missing rock
IDs or fallback values. OreSpawn leaves the old configuration and generated
chunks untouched. A fresh world does not inherit this behavior merely because
an old Mineralogy config is still present in the instance.
Loading
Loading