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
7 changes: 7 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ Version 4.0.6
deduplicated OS4 scheduler with atomic backups and an idempotent report.
* Preserve 1.12 metadata block states, legacy dimension/biome selectors,
weighted outputs, retrogen flags, vanilla suppression, and flat bedrock.
* Keep existing Mineralogy 3 worlds on the matching Cyano engine contract,
distinguishing carried 1.10 configurations from native 1.12 configurations,
exact rock order, disabled geology, family lists, and realistic coal behavior.
* Write deterministic human-readable upgrade reports for consumed OS3 and
Mineralogy configuration alongside the detailed machine-readable report.
* 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.
* Preserve trees, vegetation, structures and block entities by running surface replacement before late features.
Expand All @@ -15,6 +20,8 @@ Version 4.0.6
later ordinary mod listeners cannot re-enable claimed vanilla ore features.
* Keep air-exposure inspection inside the active chunk so edge candidates do
not load neighbouring chunks or depend on neighbour-generation order.
* Place dynamic fluid deposits through Forge's world-write path so scheduled
liquid ticks cannot retain OreSpawn's reusable generation cursor.
* Adapt geology, surfaces, ores, fluids, bedrock and static biome overlays to
Forge 1.12 terrain events and one deduplicated IWorldGenerator fallback.
* Register the early surface/geology coordinator on Forge 1.12's actual event
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ Important files:
Profile edits affect newly generated chunks. Ore and flat-bedrock retrogen are
separate opt-in features; OreSpawn never retro-generates rock strata.

When an existing world records Mineralogy 3 or earlier and has no OreSpawn 4
world profile, OreSpawn preserves that world's Cyano geology contract before
new chunks generate. It distinguishes carried Mineralogy 1.10 configuration
from native Mineralogy 1.12 configuration, including the different ordered
rock families, `REALISTIC_COAL_LAYERS`, and `PLACE_MINERALOGY_ROCK`. A hybrid
file created while upgrading is interpreted using the Mineralogy version saved
with the world. Fresh worlds still use the installed provider's recommended
engine; selecting Sky for an upgraded world is an explicit choice which may
create an old/new terrain seam.

After an upgrade, read `config/orespawn-upgrade-report.txt` for translated OS3
rules and `<world>/serverconfig/orespawn-upgrade-report.txt` for the Mineralogy
handoff. The reports list the sources, selected lineage, preserved values and
anything needing review without rewriting existing chunks.

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
213 changes: 205 additions & 8 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,92 @@ javadoc {

test {
useJUnitPlatform()
// The parity test opens each published engine in its own URLClassLoader.
// Keeping these jars out of Gradle configurations also keeps Forge's
// ordinary Eclipse launch from discovering two Mineralogy mods.
systemProperty 'orespawn.mineralogy110Oracle',
file('../migration-fixtures/sources/artifacts/Mineralogy-1.10.2-3.3.8.26.jar').absolutePath
systemProperty 'orespawn.mineralogy112Oracle',
file('../migration-fixtures/sources/artifacts/Mineralogy-1.12.2-3.8.0.53.jar').absolutePath
}

// Runtime processes are not green merely because they exit with code zero.
// Forge 14 can log a fatal worldgen/callback error and still shut down cleanly.
def acceptedForge14LogNoise = [
~/Apache Maven library folder was not in the format expected/,
~/\[FML\]: Full: .*maven-artifact-/,
~/\[FML\]: Trimmed: .*maven-artifact/,
~/FML appears to be missing any signature data/,
~/Unable to read a class file correctly/,
~/There was a problem reading the entry (?:META-INF\/versions\/9\/)?module-info\.class .*probably a corrupt zip/
]

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

File logsDirectory = new File(runDirectory, 'logs')
if (!logsDirectory.isDirectory()) return
def failures = []
fileTree(logsDirectory) { include '**/*.log'; include '**/*.txt' }.files.each { File log ->
int lineNumber = 0
log.eachLine('UTF-8') { String line ->
lineNumber++
boolean unexpectedSeverity = line ==~ /.*\/(?:ERROR|FATAL)\].*/
boolean knownNoise = acceptedForge14LogNoise.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'))
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. This is not a good thing\n'
+ '[Server thread/INFO] [FML]: Done\n', 'UTF-8')
assertRuntimeLogsClean(probe, 'scanner-accepted-noise-probe')
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-rejection-probe') }
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/ERROR] [example]: Unexpected fixture failure\n', 'UTF-8')
rejected = false
try { assertRuntimeLogsClean(probe, 'scanner-error-severity-probe') }
catch (GradleException expected) { rejected = true }
if (!rejected) throw new GradleException('Runtime log scanner accepted an unexpected ERROR line')
delete probe
}
}

check.dependsOn runtimeLogScannerTest

def surfaceIntegrationClasses = file("${buildDir}/surface-integration-fixture/classes")
task compileSurfaceIntegrationTestMod(type: JavaCompile, dependsOn: classes) {
source fileTree('src/biomeIntegrationTest/java')
Expand Down Expand Up @@ -295,9 +379,13 @@ surfaceIntegrationFreshProcess.doLast {
if (!marker.isFile()) {
throw new GradleException("Fresh surface integration completion marker is missing: ${marker}")
}
assertRuntimeLogsClean(surfaceIntegrationRunDirectory, 'surface integration fresh phase')
}
def surfaceIntegrationReloadProcess = createSurfaceProcess('Reload', surfaceIntegrationFreshProcess)
useForge14Runtime(surfaceIntegrationReloadProcess)
surfaceIntegrationReloadProcess.doLast {
assertRuntimeLogsClean(surfaceIntegrationRunDirectory, 'surface integration reload phase')
}

task surfaceIntegrationTest(dependsOn: surfaceIntegrationReloadProcess) {
group = 'verification'
Expand Down Expand Up @@ -375,10 +463,7 @@ if (project.hasProperty('migrationRunDir')) {
throw new GradleException("Migration ${phase} phase did not complete: ${marker}")
}
def latest = new File(migrationRunDirectory, 'logs/latest.log')
if (latest.isFile() && (latest.text.contains('Migration audit failed')
|| latest.text.contains('Encountered an unexpected exception'))) {
throw new GradleException("Migration ${phase} phase logged a server failure: ${latest}")
}
assertRuntimeLogsClean(migrationRunDirectory, "migration ${phase} phase")
if (latest.isFile()
&& (project.findProperty('migrationAllowMissingMappings') ?: 'false') == 'true') {
def protectedMissing = latest.text =~ /(?m)^(?:Missing (?:basemetals|orespawn):|\s+(?:basemetals|mmdlib|orespawn):)/
Expand All @@ -391,6 +476,98 @@ if (project.hasProperty('migrationRunDir')) {
useForge14Runtime(migrationIntegrationProcess)
}

// Two sealed existing-world upgrades exercise the distinct Mineralogy configs
// that can reach Forge 1.12: a carried 1.10 file and the native 1.12 file.
def legacyMineralogyArchive = file("${rootDir}/../migration-fixtures/sources/worlds/os3-331-default-source.zip")
def legacyMineralogyJar = file("${rootDir}/../migration-fixtures/sources/artifacts/Mineralogy-1.12.2-3.8.0.53.jar")
def legacyMineralogyGates = []
[
'110': [version: '3.3.8.26', config: '''\
world-gen {
I:GEOME_SIZE=144
B:REALISTIC_COAL_LAYERS=true
S:ROCK_LAYER_NOISE=41.5
I:ROCK_LAYER_THICKNESS=11
}
'''],
'112': [version: '3.8.0.53', config: '''\
world-gen {
B:PLACE_MINERALOGY_ROCK=false
I:GEOME_SIZE=128
S:ROCK_LAYER_NOISE=37.25
I:ROCK_LAYER_THICKNESS=9
}
''']
].each { String lineage, Map fixture ->
String label = lineage == '110' ? '110' : '112'
File runDirectory = file("${buildDir}/legacy-mineralogy-${lineage}-run")
File mainOutput = file("${buildDir}/legacy-mineralogy-${lineage}-fixture/orespawn-main")
def prepareTask = task("prepareLegacyMineralogy${label}Run",
dependsOn: migrationIntegrationTestModJar) {
doLast {
if (!legacyMineralogyArchive.isFile() || !legacyMineralogyJar.isFile()) {
throw new GradleException('Sealed legacy Mineralogy fixtures are missing')
}
delete runDirectory
delete mainOutput
copy { from zipTree(legacyMineralogyArchive); into runDirectory }
mainOutput.mkdirs()
copy { from sourceSets.main.output; into mainOutput }
File mods = new File(runDirectory, 'mods'); mods.mkdirs()
copy { from migrationIntegrationTestModJar.archivePath; from legacyMineralogyJar; into mods }
File config = new File(runDirectory, 'config/mineralogy.cfg')
config.parentFile.mkdirs(); config.setText(fixture.config as String, 'UTF-8')
new File(runDirectory, 'eula.txt').setText('eula=true\n', 'UTF-8')
project.javaexec {
main = 'zone.moddev.mc.orespawn.migrationtest.LegacyMineralogyMetadataFixture'
classpath = files(migrationIntegrationClasses, sourceSets.main.runtimeClasspath)
args new File(runDirectory, 'world').absolutePath, fixture.version
}
}
}

def createLegacyMineralogyProcess = { String phase, Object dependency ->
def process = task("legacyMineralogy${label}${phase.capitalize()}", type: JavaExec,
dependsOn: [dependency, 'createSrgToMcp']) {
group = 'verification'
main = 'net.minecraftforge.legacydev.MainServer'
classpath = files(sourceSets.main.runtimeClasspath, legacyDevRuntime)
workingDir runDirectory
environment 'mainClass', 'net.minecraft.launchwrapper.Launch'
environment 'MCP_TO_SRG', file("${buildDir}/createSrgToMcp/output.srg").absolutePath
environment 'MOD_CLASSES', mainOutput.absolutePath
environment 'tweakClass', 'net.minecraftforge.fml.common.launcher.FMLServerTweaker'
systemProperty 'forge.logging.console.level', 'info'
systemProperty 'orespawn.migrationFamily', "legacy-mineralogy-${lineage}"
systemProperty 'orespawn.migrationPhase', phase
args '--nogui'
doLast {
File marker = new File(runDirectory, 'world/orespawn4-migration-probe.properties')
if (!marker.isFile()) {
throw new GradleException("Legacy Mineralogy ${lineage} ${phase} marker is missing")
}
Properties values = new Properties(); marker.withInputStream { values.load(it) }
if (values.getProperty("${phase}_complete") != 'true') {
throw new GradleException("Legacy Mineralogy ${lineage} ${phase} did not complete")
}
assertRuntimeLogsClean(runDirectory, "legacy Mineralogy ${lineage} ${phase} phase")
}
}
useForge14Runtime(process)
process
}

def freshTask = createLegacyMineralogyProcess('fresh', prepareTask)
def reloadTask = createLegacyMineralogyProcess('reload', freshTask)
def gate = task("legacyMineralogy${label}MigrationTest", dependsOn: reloadTask) {
group = 'verification'
description = "Proves Mineralogy ${lineage} settings remain exact across an OreSpawn 4 upgrade and reload."
}
legacyMineralogyGates.add(gate)
}

check.dependsOn legacyMineralogyGates

if (project.hasProperty('os3AbiFixtureJar')) {
def os3AbiFixture = file(project.property('os3AbiFixtureJar'))
def os3AbiRunDirectory = file(project.findProperty('os3AbiRunDir')
Expand Down Expand Up @@ -439,10 +616,7 @@ max-tick-time=-1
throw new GradleException("Published OS3 API fixture did not pass: ${marker}")
}
File latest = new File(os3AbiRunDirectory, 'logs/latest.log')
if (latest.isFile() && (latest.text.contains('Encountered an unexpected exception')
|| latest.text.contains('Exception stopping the server'))) {
throw new GradleException("Published OS3 API fixture logged a server failure: ${latest}")
}
assertRuntimeLogsClean(os3AbiRunDirectory, 'published OS3 API fixture')
}
}
useForge14Runtime(os3AbiCompatibilityProcess)
Expand All @@ -468,6 +642,23 @@ task syncForge14EclipseLaunches(dependsOn: compileSurfaceIntegrationTestMod) {
throw new GradleException("ForgeGradle generated an unexpected ${runName} launcher")
}
text = text.replace('value="${MC_VERSION}"', "value=\"${minecraft_version}\"")
String excludeTestKey = 'org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE'
String excludeTestAttribute =
"<booleanAttribute key=\"${excludeTestKey}\" value=\"true\"/>"
if (text.contains("key=\"${excludeTestKey}\"")) {
text = text.replaceFirst(
/<booleanAttribute key="org\.eclipse\.jdt\.launching\.ATTR_EXCLUDE_TEST_CODE" value="[^"]*"\/>/,
excludeTestAttribute)
} else {
int launchHeaderEnd = text.indexOf('\n', text.indexOf('<launchConfiguration'))
if (launchHeaderEnd < 0) {
throw new GradleException("Malformed Eclipse Java launch configuration: ${launch}")
}
String lineSeparator = text.contains('\r\n') ? '\r\n' : '\n'
text = "${text.substring(0, launchHeaderEnd + 1)}" +
" ${excludeTestAttribute}${lineSeparator}" +
text.substring(launchHeaderEnd + 1)
}
launch.setText(text, 'UTF-8')
}
File serverLaunch = file('runServer.launch')
Expand Down Expand Up @@ -505,6 +696,12 @@ task syncForge14EclipseClasspath(dependsOn: forge14RuntimeJar) {
throw new GradleException("Eclipse classpath does not contain mapped Forge 14 runtime: ${mappedPath}")
}
text = text.replace(mappedEntry, "kind=\"lib\" path=\"${runtimePath}\"")
for (String forbidden : ['Mineralogy-1.10.2-3.3.8.26.jar',
'Mineralogy-1.12.2-3.8.0.53.jar']) {
if (text.contains(forbidden)) {
throw new GradleException("Test-only Mineralogy oracle leaked into Eclipse classpath: ${forbidden}")
}
}
classpathFile.setText(text, 'UTF-8')
}
}
Expand Down
13 changes: 12 additions & 1 deletion docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,18 @@ named presets and as defaults for new Custom settings.

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.
Migrated Mineralogy worlds also store `cyano.enabled`, the exact ordered
`cyano.igneous_rocks`, `cyano.metamorphic_rocks`, and
`cyano.sedimentary_rocks` arrays, plus `cyano.realistic_coal_layers` for the
Mineralogy 1.10 lineage. Native Mineralogy 1.12 did not have realistic coal;
its `PLACE_MINERALOGY_ROCK=false` is preserved as `cyano.enabled=false`.
These values and the old family white/blacklists are snapshotted per world and
are ignored by Sky.

The resulting values and missing registry IDs are recorded in
`<world>/serverconfig/orespawn-upgrade-report.txt`. OS3 rule and global-switch
imports are summarized in `config/orespawn-upgrade-report.txt` and retained in
machine-readable form at `config/orespawn-os3-migration-report.json`.

## Rocks And Geomes

Expand Down
40 changes: 40 additions & 0 deletions docs/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,46 @@ use stable provider identities when their outputs match uniquely. Mineralogy
3 replacement rocks are accepted as ore hosts, but Mineralogy remains the
authoritative geology engine when that legacy stack is installed.

## Mineralogy 1.10 And 1.12 Geology Handoff

An existing world must not silently switch geology engines when Mineralogy is
updated to integrate with OreSpawn 4. If a generated world has no OreSpawn
world profile and its saved Forge mod list records Mineralogy 3 or earlier,
OreSpawn creates the first world profile in `legacy` mode before generating
new chunks.

Mineralogy 1.10 and 1.12 used similar configuration files but not identical
contracts. OreSpawn therefore selects a lineage from the Mineralogy version in
`level.dat` (or the recoverable `level.dat_old`) and then consumes:

- `GEOME_SIZE`, `ROCK_LAYER_NOISE`, and `ROCK_LAYER_THICKNESS` in both lines;
- every family whitelist and blacklist with the exact historical rock order;
- `REALISTIC_COAL_LAYERS` only for the 1.10 lineage;
- `PLACE_MINERALOGY_ROCK` only for native 1.12, including preserving `false`.

A carried 1.10 file can become hybrid after Mineralogy 1.12 normalizes its own
Forge configuration. In that case the saved world version takes precedence:
1.10 retains its realistic-coal behavior and does not invent a later enable
flag, while 1.12 respects its native enable flag and does not enable realistic
coal. If the config was not copied, the published defaults for the selected
lineage are recorded instead. An existing OS4 profile is never overwritten.
Fresh worlds do not select legacy mode merely because a legacy config is still
installed.

The snapshot is stored at `<world>/serverconfig/orespawn-worldgen.json` and a
human-readable explanation is written to
`<world>/serverconfig/orespawn-upgrade-report.txt`. The report has no timestamp
and is byte-stable across reload. OreSpawn does not rewrite the source
Mineralogy config or generated chunks during this handoff; the installed
Mineralogy version may independently normalize its own Forge config. Moving an
upgraded world to Sky later remains possible as an explicit choice, with an
expected old/new chunk seam.

Legacy OreSpawn conversion also writes `config/orespawn-upgrade-report.txt`.
It summarizes consumed resources, translated providers, preserved global
flags, and warnings; `config/orespawn-os3-migration-report.json` remains the
deterministic machine-readable detail.

## Provider-Aware OS3 Imports

When an OreSpawn 2/3 file is named for an installed provider, the migrator now
Expand Down
Loading
Loading