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
15 changes: 15 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
Version 4.0.6.2602002

* Adopt target-qualified four-component versions so Minecraft and loader compatibility can be identified from the mod version.
* 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.
* Qualify existing OreSpawn 4.0.5 global and per-world profiles without changing explicit Custom values or provider definitions.
* 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.
* Honour exact biome-to-geome weights on dynamic biome registries.
* Stagger close Stable Layers geome transitions by layer instead of changing a whole rock column at one boundary.
* Recalibrate Stable Layers edge-detail presets so Average retains natural variation at later rock contacts.
* Existing chunks are not rewritten; the correction applies while generating new chunks.

Version 3.3.1

* Fix several bugs
Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,22 @@ 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.
Stable Layers honours exact biome-ID geome influences on dynamic biome
registries and spreads close geome transitions across layers rather than
changing an entire vertical rock column at one boundary.

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
Expand Down Expand Up @@ -84,9 +97,12 @@ Use Java 25 from the repository root:
```

`build` runs the standard `check` lifecycle. In addition to the JUnit suite,
that lifecycle packages a test-only provider mod, loads its custom biome in
normal noise terrain, and verifies both fresh generation and reopening the
same saved world. The fixture is not included in OreSpawn's published jars.
that lifecycle packages a test-only provider mod and verifies exposed,
underwater, filler, and ceiling surfaces in open and ceiling normal-noise
dimensions. It also proves later vegetation, structures, and block entities
survive, verifies identifier-weighted geology in a dynamic custom biome, then
reopens and checks the exact saved world. The fixture is not included in
OreSpawn's published jars.

Import or refresh the project with Eclipse Buildship. NeoGradle supplies the
Eclipse model and run configurations through the `eclipse` task; this branch
Expand Down
240 changes: 205 additions & 35 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,12 @@ runs {
}

['Fresh', 'Reload'].each { String phase ->
register("biomeIntegration${phase}") {
register("surfaceIntegration${phase}") {
runType 'gameTestServer'
workingDirectory layout.buildDirectory.dir('biome-integration-run')
systemProperty 'neoforge.enabledGameTestNamespaces', 'cakeworldprobe'
workingDirectory layout.buildDirectory.dir('surface-integration-run')
systemProperty 'neoforge.enabledGameTestNamespaces', 'surfaceprobe'
systemProperty 'forge.logging.console.level', 'info'
systemProperty 'cakeworld.biomeIntegrationPhase', phase.toLowerCase(Locale.ROOT)
systemProperty 'surfaceprobe.integrationPhase', phase.toLowerCase(Locale.ROOT)
modSource project.sourceSets.main
}
}
Expand Down Expand Up @@ -201,79 +201,249 @@ tasks.withType(JavaCompile).configureEach {
tasks.named('javadoc', Javadoc).configure {
options.encoding = 'UTF-8'
options.addStringOption('Xdoclint:none', '-quiet')
options.addBooleanOption('-no-fonts', true)
}

tasks.named('test', Test).configure {
useJUnitPlatform()
// Unit tests inspect target files relative to the checkout, but they do
// not need NeoForge's rolling runtime files. A console-only logger keeps
// them from contending with Eclipse/client logs in this working directory.
systemProperty 'log4j.configurationFile', file('src/test/resources/log4j2-test.xml').absolutePath
// 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 118/MinecraftMineralogy/build/libs/Mineralogy-1.18.2-5.4.0.jar')
if (mineralogy5Oracle.isFile()) {
systemProperty 'orespawn.mineralogy5Oracle', mineralogy5Oracle.absolutePath
}
}

// Several registry-focused tests initialize the real global config singleton.
// Keep that target-native coverage without creating or changing a developer's
// checkout config as a side effect of `test` or `build`.
def unitTestWorldgenConfig = file('config/orespawn-worldgen.json')
def unitTestWorldgenConfigWasPresent = false
byte[] unitTestWorldgenConfigBytes = null
tasks.named('test', Test).configure {
doFirst {
unitTestWorldgenConfigWasPresent = unitTestWorldgenConfig.isFile()
unitTestWorldgenConfigBytes = unitTestWorldgenConfigWasPresent
? unitTestWorldgenConfig.bytes : null
}
}
def preserveDeveloperWorldgenConfig = tasks.register('preserveDeveloperWorldgenConfig') {
doLast {
if (unitTestWorldgenConfigWasPresent) {
byte[] after = unitTestWorldgenConfig.isFile() ? unitTestWorldgenConfig.bytes : null
if (after == null || !java.util.Arrays.equals(unitTestWorldgenConfigBytes, after)) {
unitTestWorldgenConfig.parentFile.mkdirs()
unitTestWorldgenConfig.bytes = unitTestWorldgenConfigBytes
throw new GradleException('Unit tests changed config/orespawn-worldgen.json; the original was restored')
}
} else if (unitTestWorldgenConfig.isFile()) {
delete unitTestWorldgenConfig
}
}
}
tasks.named('test') {
finalizedBy preserveDeveloperWorldgenConfig
}

// A NeoForge process is not green merely because it returns exit code zero.
// The loader can log a worldgen/linkage failure and still shut down normally.
// No ERROR/FATAL line has been accepted as harmless on NeoForge 26.2; add an
// exception only after reproducing it in the matching loader control.
def acceptedNeoForge262LogNoise = []

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 = acceptedNeoForge262LogNoise.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'))
}
}

tasks.register('runtimeLogScannerTest') {
group = 'verification'
description = 'Proves runtime log validation rejects real NeoForge 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(
'[Server thread/INFO] [neoforge]: Done\n', 'UTF-8')
assertRuntimeLogsClean(probe, 'scanner-clean-log-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] [neoforge]: 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
}
}

tasks.named('check') {
dependsOn 'runtimeLogScannerTest'
}

tasks.register('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}")
}
}
}
}

tasks.named('check') {
dependsOn 'verifyMineralogyOracleIsolation'
}

['runClient', 'runServer', 'runGameTestServer', 'runClientData'].each { String taskName ->
tasks.matching { it.name == taskName }.configureEach { 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 biomeIntegrationClasses = layout.buildDirectory.dir('biome-integration-fixture/classes')
def compileBiomeIntegrationTestMod = tasks.register('compileBiomeIntegrationTestMod', JavaCompile) {
def surfaceIntegrationClasses = layout.buildDirectory.dir('surface-integration-fixture/classes')
def compileSurfaceIntegrationTestMod = tasks.register('compileSurfaceIntegrationTestMod', JavaCompile) {
dependsOn tasks.named('classes')
source fileTree('src/biomeIntegrationTest/java')
classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath)
destinationDirectory.set(biomeIntegrationClasses)
destinationDirectory.set(surfaceIntegrationClasses)
javaCompiler.set(javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(25)
})
options.release = 16
options.release = 25
options.encoding = 'UTF-8'
}

def biomeIntegrationTestModJar = tasks.register('biomeIntegrationTestModJar', Jar) {
dependsOn compileBiomeIntegrationTestMod
archiveFileName = 'cakeworldprobe.jar'
destinationDirectory = layout.buildDirectory.dir('biome-integration-fixture')
def surfaceIntegrationTestModJar = tasks.register('surfaceIntegrationTestModJar', Jar) {
dependsOn compileSurfaceIntegrationTestMod
archiveFileName = 'surfaceprobe.jar'
destinationDirectory = layout.buildDirectory.dir('surface-integration-fixture')
manifest {
attributes 'MixinConfigs': 'cakeworldprobe.mixins.json'
attributes 'MixinConfigs': 'surfaceprobe.mixins.json'
}
from biomeIntegrationClasses
from surfaceIntegrationClasses
from 'src/biomeIntegrationTest/resources'
}

def biomeIntegrationRunDirectory = layout.buildDirectory.dir('biome-integration-run')
def prepareBiomeIntegrationTest = tasks.register('prepareBiomeIntegrationTest') {
dependsOn biomeIntegrationTestModJar
def surfaceIntegrationRunDirectory = layout.buildDirectory.dir('surface-integration-run')
def prepareSurfaceIntegrationTest = tasks.register('prepareSurfaceIntegrationTest') {
dependsOn surfaceIntegrationTestModJar
doLast {
delete biomeIntegrationRunDirectory
delete surfaceIntegrationRunDirectory
copy {
from biomeIntegrationTestModJar.flatMap { it.archiveFile }
into biomeIntegrationRunDirectory.map { it.dir('mods') }
from surfaceIntegrationTestModJar.flatMap { it.archiveFile }
into surfaceIntegrationRunDirectory.map { it.dir('mods') }
}
}
}

tasks.configureEach {
if (name == 'runBiomeIntegrationFresh') {
dependsOn prepareBiomeIntegrationTest
} else if (name == 'runBiomeIntegrationReload') {
dependsOn 'runBiomeIntegrationFresh'
if (name == 'runSurfaceIntegrationFresh') {
dependsOn prepareSurfaceIntegrationTest
} else if (name == 'runSurfaceIntegrationReload') {
dependsOn 'runSurfaceIntegrationFresh'
}
if (name == 'runSurfaceIntegrationFresh' || name == 'runSurfaceIntegrationReload') {
doFirst {
ext.oreSpawnCrashSnapshot = runtimeCrashSnapshot(workingDir)
}
doLast {
assertRuntimeLogsClean(workingDir, "NeoForge 26.2 ${name}",
ext.oreSpawnCrashSnapshot as Set)
}
}
}

def biomeIntegrationTest = tasks.register('biomeIntegrationTest') {
def surfaceIntegrationTest = tasks.register('surfaceIntegrationTest') {
group = 'verification'
description = 'Verifies a provider-owned custom biome in fresh and reloaded normal terrain.'
dependsOn 'runBiomeIntegrationReload'
description = 'Verifies provider surfaces and dynamic-biome geology across fresh and reloaded normal terrain.'
dependsOn 'runSurfaceIntegrationReload'
doLast {
File marker = biomeIntegrationRunDirectory.get().file(
'gametestserver/gametestworld/cakeworld-biome-integration.properties').asFile
File marker = surfaceIntegrationRunDirectory.get().file(
'gametestserver/gametestworld/surfaceprobe-integration.properties').asFile
if (!marker.isFile()) {
throw new GradleException("Biome integration completion marker is missing: ${marker}")
throw new GradleException("Surface integration completion marker is missing: ${marker}")
}
Properties result = new Properties()
marker.withInputStream { result.load(it) }
if (result.getProperty('reload_verified') != 'true') {
throw new GradleException("Biome integration reload was not verified: ${marker}")
throw new GradleException("Surface integration reload was not verified: ${marker}")
}
logger.lifecycle('Custom-biome integration verified: {} chunks, {} top blocks, {} filler blocks, fresh + reload',
result.getProperty('matching_chunks'), result.getProperty('pink_surface'),
result.getProperty('white_filler'))
logger.lifecycle('Provider surfaces and dynamic-biome geology verified: {} dimensions, {} columns each, fresh + reload',
result.getProperty('dimensions'), result.getProperty('columns_per_dimension'))
}
}

tasks.named('check') {
dependsOn biomeIntegrationTest
dependsOn surfaceIntegrationTest
}

idea {
Expand Down
5 changes: 4 additions & 1 deletion docs/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# OreSpawn Documentation Map

This index is for navigating the documentation to learn how to integrate with and to use OreSpawn with a mod or modpack.
This index is for navigating the documentation to learn how to integrate with
and use OreSpawn with a mod or modpack.
Start with [DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md).

Use the focused guides for implementation details:
Expand All @@ -11,4 +12,6 @@ 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 four-component target-qualified
versioning and branch-release convention;
- [README.md](README.md) for schemas, examples, and the complete documentation index.
8 changes: 5 additions & 3 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,11 @@ OreSpawnApi.createSampler(server.overworld()).ifPresent(sampler -> {
});
```

`sampleColumn` performs one biome/geome classification and reuses it for every
Y query. Sampling is read-only and is intended for gameplay decisions,
diagnostics, and compatible generation outside OreSpawn's block loops.
`sampleColumn` performs one biome/dominant-geome classification and reuses its
transition scores for every Y query. `rockAt` therefore matches Stable Layers
when a close geome transition is staggered by layer. Sampling is read-only and
is intended for gameplay decisions, diagnostics, and compatible generation
outside OreSpawn's block loops.
Callbacks inside OreSpawn generation loops are intentionally unsupported.

Custom pattern mods create a NeoForge `DeferredRegister<OrePatternType>` using
Expand Down
Loading
Loading