From 09a6f2af2b7643c39b7301c651e8c815c48b98ed Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Fri, 30 Oct 2026 12:47:06 +0000 Subject: [PATCH] Add diamond deposits and stepped mine entrances --- .github/workflows/ci.yml | 8 +- .github/workflows/release-on-tag.yml | 2 +- CHANGELOG.md | 24 ++ README.md | 37 +- build.gradle | 1 + docs/ABANDONED_MINES.md | 37 +- docs/COALFIELDS.md | 3 +- docs/DIAMONDS.md | 80 ++++ docs/RELEASING.md | 2 +- gradle.properties | 2 +- gradle/diamond-manual.gradle | 66 ++++ gradle/release.gradle | 2 +- .../realisticdeposits/RealisticDeposits.java | 13 +- .../command/RealisticDepositsCommand.java | 8 + .../config/DepositCatalogLoader.java | 19 + .../config/RealisticDepositsRuntime.java | 5 +- .../content/DiamondGravelBlock.java | 43 +++ .../realisticdeposits/content/ModContent.java | 17 + .../generation/CoalMeasuresPattern.java | 4 +- .../generation/DiamondPattern.java | 353 ++++++++++++++++++ .../generation/MineTerrainSampler.java | 8 +- .../generation/MineWorkingsGenerator.java | 35 +- .../generation/StratiformSeamPattern.java | 5 +- .../geometry/AbandonedMinePlanner.java | 80 ++-- .../geometry/DiamondDistrict.java | 172 +++++++++ .../geometry/DiamondPipeWorkingsBody.java | 58 +++ .../realisticdeposits/geometry/MineBlock.java | 3 +- .../geometry/MineDecline.java | 158 ++++++++ .../integration/OreSpawnIntegration.java | 57 ++- .../model/DepositDefinition.java | 14 +- .../model/DiamondPipeSettings.java | 72 ++++ .../model/DiamondPlacerSettings.java | 41 ++ .../realisticdeposits/proxy/ClientProxy.java | 12 + .../realisticdeposits/proxy/CommonProxy.java | 1 + .../blockstates/diamond_gravel.json | 5 + .../realisticdeposits/defaults/coal.json | 2 +- .../realisticdeposits/defaults/diamonds.json | 54 +++ .../realisticdeposits/defaults/vanilla.json | 4 +- .../assets/realisticdeposits/lang/en_US.lang | 3 +- .../models/block/diamond_gravel.json | 32 ++ .../models/item/diamond_gravel.json | 3 + .../blocks/diamond_gravel_overlay.png | Bin 0 -> 217 bytes .../mc/realisticdeposits/DiamondFixtures.java | 25 ++ .../ProjectIdentityTest.java | 2 +- .../config/CoalCatalogTest.java | 4 +- .../config/DiamondCatalogTest.java | 100 +++++ .../config/MineDefaultsTest.java | 72 ++++ .../content/DiamondGravelTest.java | 29 ++ .../generation/CoalRuntimeProbe.java | 13 +- .../DiamondGravelRuntimeChecks.java | 115 ++++++ .../generation/DiamondRuleSwitchTest.java | 27 ++ .../generation/DiamondRuntimeProbe.java | 247 ++++++++++++ .../generation/DiamondSurveyProbe.java | 266 +++++++++++++ .../generation/FrozenMineRuntimeProbe.java | 69 ++++ .../generation/MineNaturalHostsTest.java | 45 +++ .../generation/MineWorkingsRuntimeProbe.java | 91 ++++- .../generation/MineWorldDataTest.java | 21 ++ .../generation/PipeWorkingsRuntimeChecks.java | 226 +++++++++++ .../generation/PolicyQualificationProbe.java | 40 +- .../geometry/AbandonedMinePlannerTest.java | 11 +- .../geometry/DiamondDistrictTest.java | 167 +++++++++ .../geometry/DiamondPipeWorkingsTest.java | 96 +++++ .../geometry/GradualMineAccessTest.java | 60 +++ tools/qualification/DIAMOND_TESTING.md | 64 ++++ tools/qualification/MINES_TESTING.md | 41 +- tools/qualification/README.md | 57 ++- tools/qualification/ci_runtime.py | 29 +- tools/qualification/coal_upgrade_110.py | 26 +- tools/qualification/diamond_balance_110.py | 80 ++++ tools/qualification/diamond_survey_110.py | 85 +++++ tools/qualification/diamonds_110.py | 162 ++++++++ tools/qualification/frozen_mines_110.py | 94 +++++ tools/qualification/mines_110.py | 26 +- tools/qualification/mines_upgrade_110.py | 14 +- tools/qualification/pipe_mines_110.py | 100 +++++ tools/qualification/probes.init.gradle | 49 +++ tools/qualification/qualify_110.py | 28 +- tools/qualification/render_diamonds.py | 53 +++ tools/qualification/test_upgrade_profile.py | 29 ++ tools/release/test_release.py | 2 +- 80 files changed, 4052 insertions(+), 158 deletions(-) create mode 100644 docs/DIAMONDS.md create mode 100644 gradle/diamond-manual.gradle create mode 100644 src/main/java/zone/moddev/mc/realisticdeposits/content/DiamondGravelBlock.java create mode 100644 src/main/java/zone/moddev/mc/realisticdeposits/content/ModContent.java create mode 100644 src/main/java/zone/moddev/mc/realisticdeposits/generation/DiamondPattern.java create mode 100644 src/main/java/zone/moddev/mc/realisticdeposits/geometry/DiamondDistrict.java create mode 100644 src/main/java/zone/moddev/mc/realisticdeposits/geometry/DiamondPipeWorkingsBody.java create mode 100644 src/main/java/zone/moddev/mc/realisticdeposits/geometry/MineDecline.java create mode 100644 src/main/java/zone/moddev/mc/realisticdeposits/model/DiamondPipeSettings.java create mode 100644 src/main/java/zone/moddev/mc/realisticdeposits/model/DiamondPlacerSettings.java create mode 100644 src/main/resources/assets/realisticdeposits/blockstates/diamond_gravel.json create mode 100644 src/main/resources/assets/realisticdeposits/defaults/diamonds.json create mode 100644 src/main/resources/assets/realisticdeposits/models/block/diamond_gravel.json create mode 100644 src/main/resources/assets/realisticdeposits/models/item/diamond_gravel.json create mode 100644 src/main/resources/assets/realisticdeposits/textures/blocks/diamond_gravel_overlay.png create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/DiamondFixtures.java create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/config/DiamondCatalogTest.java create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/config/MineDefaultsTest.java create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/content/DiamondGravelTest.java create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondGravelRuntimeChecks.java create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondRuleSwitchTest.java create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondRuntimeProbe.java create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondSurveyProbe.java create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/generation/FrozenMineRuntimeProbe.java create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/generation/MineNaturalHostsTest.java create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/generation/PipeWorkingsRuntimeChecks.java create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/geometry/DiamondDistrictTest.java create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/geometry/DiamondPipeWorkingsTest.java create mode 100644 src/test/java/zone/moddev/mc/realisticdeposits/geometry/GradualMineAccessTest.java create mode 100644 tools/qualification/DIAMOND_TESTING.md create mode 100644 tools/qualification/diamond_balance_110.py create mode 100644 tools/qualification/diamond_survey_110.py create mode 100644 tools/qualification/diamonds_110.py create mode 100644 tools/qualification/frozen_mines_110.py create mode 100644 tools/qualification/pipe_mines_110.py create mode 100644 tools/qualification/render_diamonds.py create mode 100644 tools/qualification/test_upgrade_profile.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4136ddf..5de485e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,7 +138,7 @@ jobs: - name: Build the isolated packaged-server probe run: >- - ./gradlew preparePolicyQualification --init-script tools/qualification/probes.init.gradle + ./gradlew preparePolicyQualification prepareDiamondQualification prepareMineQualification --init-script tools/qualification/probes.init.gradle --offline --no-daemon --max-workers=2 -Dorg.gradle.java.installations.paths="$JAVA_HOME,$JAVA_HOME_8_X64,$JAVA_HOME_25_X64" -Dorg.gradle.java.installations.auto-detect=false @@ -156,9 +156,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/RealisticDeposits-0.2.0.110021.jar - build/libs/RealisticDeposits-0.2.0.110021-sources.jar - build/libs/RealisticDeposits-0.2.0.110021-javadoc.jar + build/libs/RealisticDeposits-0.3.0.110021.jar + build/libs/RealisticDeposits-0.3.0.110021-sources.jar + build/libs/RealisticDeposits-0.3.0.110021-javadoc.jar build/release/SHA256SUMS CHANGELOG.md diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index ad93c7f..f9b852d 100644 --- a/.github/workflows/release-on-tag.yml +++ b/.github/workflows/release-on-tag.yml @@ -88,7 +88,7 @@ jobs: echo echo "- mode: \`publish\`" echo "- release_version: \`$GITHUB_REF_NAME\`" - echo "- curseforge_release_level: \`alpha\`" + echo "- curseforge_release_level: \`beta\`" echo "- confirm_live_publication: \`true\`" echo echo "The dispatcher checks the release bundle and requires explicit publication confirmation." diff --git a/CHANGELOG.md b/CHANGELOG.md index f27c239..2aca3f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 0.3.0.110021 + +Beta for Minecraft 1.10.2. + +- Add rare, irregular diamond pipes and linked river placers. Keep the existing + iron and coal deposit shapes and ore settings. +- Add diamond-bearing gravel with a vanilla gravel background and subtle mineral + flecks. Mining leaves ordinary gravel and has a 1-in-16 diamond chance; Fortune + increases successful drops. Silk Touch and falling preserve the rich block. +- Keep diamond settings in their own catalogue. Preserve existing world profiles, + background generation and terrain rather than adding new rules to old saves. +- Keep 15% ordinary background diamonds and the 1-in-16 gravel roll. Tune pipes + to supply most diamonds, with river pockets as an occasional bonus. +- Honour explicit world-rule switches even when their catalogue default is off. +- Add separate diamond previews, packaged-server checks and an isolated test launch. +- Attempt abandoned workings at every new iron formation, coalfield and diamond + pipe where safe. Stepped access tunnels lead to short underground crosscuts; + river placers stay untouched. Keep existing world settings and opt-outs unchanged. +- Let workings pass through the natural rock blocks declared by Mineralogy's + OreSpawn profile, without confusing rock-definition names with block names. +- Replace long ladder shafts in new workings with winding stair runs, level + turning landings, timber supports and entrance guardrails. Keep already saved + mine layouts intact, including their unfinished sections. + ## 0.2.0.110021 Beta for Minecraft 1.10.2. diff --git a/README.md b/README.md index c0524aa..86a9e94 100644 --- a/README.md +++ b/README.md @@ -5,21 +5,19 @@ ![Realistic Deposits logo](src/main/resources/assets/realisticdeposits/textures/logo.png) -Realistic Deposits is an extension for -[MMD OreSpawn](https://github.com/MinecraftModDevelopmentMods/OreSpawn) that -is being built to make valuable minerals feel like geological discoveries. -Instead of scattering a steady supply of small, isolated ore clusters -throughout the world, it plans rare formations large enough to reward -exploration and support sprawling mines, rail networks, and long-term resource -projects. +Realistic Deposits makes finding ore a reason to explore, then build a proper +mine. Rare iron districts, layered coalfields and diamond pipes give you +deposits worth returning to, with old workings leading into some of them. +Small pockets of diamond-bearing river gravel offer another way to prospect. +It runs on [MMD OreSpawn](https://github.com/MinecraftModDevelopmentMods/OreSpawn). The project starts with Minecraft 1.10.2 and is intended to follow the maintained OreSpawn branches across Forge and NeoForge. -This beta adds coalfields and optional abandoned workings -alongside the existing iron districts. Back up your world before updating. - -Coalfields use the exact-host API in the published OreSpawn 4.1.4.110021 release. +Version 0.3.0.110021 is a beta for Minecraft 1.10.2. It adds diamond deposits +and replaces deep ladder shafts in new workings with stepped entrances. +Existing worlds keep their settings and saved mine layouts. Back up important +worlds before updating. ## Current features @@ -37,14 +35,17 @@ Coalfields use the exact-host API in the published OreSpawn 4.1.4.110021 release Mineralogy is not required. See [coalfields](docs/COALFIELDS.md). - Additive, hybrid, and replacement policies for controlling interaction with ordinary ore generation. +- Rare diamond pipes and linked river placers, without adding new host rocks + or reshaping rivers. See [diamond deposits](docs/DIAMONDS.md). - Configurable support for vanilla and modded resources. - Safe generation that avoids air, fluids, bedrock, block entities, structures, and undeclared host blocks. -- Live reload for geometry and alignment tuning, with structural changes - clearly deferred until restart. +- Live reload for iron and coal geometry. Diamond geometry and other structural + changes wait until restart. - An entry in both Forge's Mods configuration and OreSpawn's World Settings. -- Optional abandoned workings, with surface access and short seam-following - galleries. They are off by default; see the [mine settings](docs/ABANDONED_MINES.md). +- Abandoned workings with sloping, stepped entrances and short underground galleries. + New defaults attempt them at every iron formation, coalfield and diamond + pipe where safe. River placers stay untouched. See the [mine settings](docs/ABANDONED_MINES.md). Coalfields can already use Mineralogy's existing shale, siltstone and conglomerate. Wider catalogues for Mineralogy, Base Metals, Modern Metals, @@ -71,6 +72,7 @@ The first launch creates: ```text config/realisticdeposits/deposits/vanilla.json config/realisticdeposits/deposits/coal.json +config/realisticdeposits/deposits/diamonds.json ``` The supplied definition describes one rare iron district using a 640–960 block @@ -132,8 +134,9 @@ formations: /realisticdeposits reload ``` -`reload` requires operator permission. It immediately applies `enabled`, -selection, shape, height, geome, family, and alignment changes. Resource, +`reload` requires operator permission. For iron and coal it applies `enabled`, +selection, shape, height, geome, family, and alignment changes. Diamond geometry +and source links require a restart. Resource, dimension, host, biome-filter, interaction-policy, scale, added, and removed definition changes are structural and take effect after restart. Abandoned-mine settings are also captured with the world's deposit definition. New provider diff --git a/build.gradle b/build.gradle index fffd572..7bf31bb 100644 --- a/build.gradle +++ b/build.gradle @@ -647,3 +647,4 @@ tasks.named('check') { apply from: 'gradle/release.gradle' apply from: 'gradle/coal-manual.gradle' +apply from: 'gradle/diamond-manual.gradle' diff --git a/docs/ABANDONED_MINES.md b/docs/ABANDONED_MINES.md index d60310a..be18a0a 100644 --- a/docs/ABANDONED_MINES.md +++ b/docs/ABANDONED_MINES.md @@ -1,39 +1,49 @@ # Abandoned mines -Some deposits can have old workings: a timber-framed surface shaft, a ladder -down to the seam, a short main gallery and a couple of side passages. Broken +Underground deposits can have old workings: a timber-framed entrance, a winding +stepped tunnel, a short main gallery and a couple of side passages. Broken rails, cobwebs and small piles of fallen rock give them an abandoned look. Most of the ore is left for you to mine. These workings contain no loot or spawners, and they do not replace Minecraft's own mineshafts. -The feature is experimental and **off by default**. Older configuration files -remain off without needing any changes. +Fresh catalogues enable workings for iron formations, coalfields and diamond +pipes, with an attempt at every deposit where safe. Short stair runs and level +turning landings make the descent walkable in both directions, without a long +open drop. Guardrails protect the exposed approach. Iron and coal galleries +follow a seam; diamond workings make short crosscuts through one pipe. River +placers have no shafts or surface diggings. -## Enable it +Existing catalogues and world settings are not overwritten. An older world +with workings disabled keeps that choice. -Before creating a world, add this section to a deposit in -`config/realisticdeposits/deposits/vanilla.json`: +## Settings + +Before creating a world, check the deposit's `abandoned_mine` section in +`config/realisticdeposits/deposits/vanilla.json`, `coal.json` or `diamonds.json`. +The new defaults are: ```json "abandoned_mine": { "enabled": true, - "chance": 0.15 + "chance": 1.0 } ``` `chance` is the chance of attempting workings for each selected deposit, not for each chunk. `0.15` means 15%; `0` disables attempts and `1` attempts every -deposit. Different deposit definitions can use different values. +deposit. Set `enabled` to `false` to turn workings off. Different deposit +definitions can use different values. Older definitions without this section +remain off. -Not every attempt finds a suitable site. This first version looks for dry, +Not every attempt finds a suitable site. The planner looks for dry, reasonably level, open ground in the Overworld. It skips wooded entrances, -oceans, rivers, exposed galleries, excessive shaft depths and known structure +oceans, rivers, exposed galleries, excessive depths and known structure collisions. It currently supports the normal and superflat terrain generators; unrecognised terrain generators are left alone. The seed and deposit settings choose the layout. Site safety still takes priority: if custom deposits overlap, an already accepted mine keeps its space. -This first version does not join neighbouring mines into a larger complex. +It does not join neighbouring mines into a larger complex. ## Saved worlds @@ -48,6 +58,9 @@ are protected, even if they had not yet been populated. A raw chunk first created for a new mine can finish its initial population later, but only while the saved plan still records that section as unbuilt. +Older ladder shafts are not rebuilt when you update. Their saved sections keep +the original layout; the stepped entrances apply only to newly planned mines. + If later generation places liquid or another structure in a planned section, the mine leaves that section sealed rather than cutting into it. The server log reports the obstruction. Clearing tunnels does not create item drops. diff --git a/docs/COALFIELDS.md b/docs/COALFIELDS.md index a83d4d3..df4c567 100644 --- a/docs/COALFIELDS.md +++ b/docs/COALFIELDS.md @@ -36,7 +36,8 @@ shows every setting. - 384-768 blocks along strike and 128-256 blocks across. - Two to four beds, 1-3 blocks thick, separated by 4-12 blocks of rock. - Hybrid generation with 15% ordinary background coal. -- Abandoned workings off by default, with a 15% attempt chance when enabled. +- Abandoned workings attempted at every new coalfield where safe. Existing + catalogues and saved-world settings keep their choices. Candidate selection is not a promise that every region has a coalfield. Geology, available height, caves and safe hosts can reduce or reject a deposit. diff --git a/docs/DIAMONDS.md b/docs/DIAMONDS.md new file mode 100644 index 0000000..a8817ae --- /dev/null +++ b/docs/DIAMONDS.md @@ -0,0 +1,80 @@ +# Diamond deposits + +There are two linked deposits: steep diamond pipes and small pockets of +diamond-bearing river gravel. The 1-in-16 gravel roll stays fixed; pipes make +up most of the supply, with river pockets as an occasional bonus. Ordinary +background generation stays at 15%. + +The six-seed ore survey estimates 3.57 diamonds per chunk against 3.70 in +the matched vanilla controls, before Fortune. Calibration and separate holdout +averages both fall within 20% of their controls. The estimated split is about +83% pipes, 2% placers and 15% background diamonds. Fortune increases all three +sources by the same vanilla multiplier. + +These are regional estimates, not a promise for each seed or mining trip. +District clustering gives a wide statistical range, and other ores, caves, +structures and excavated workings can reduce the ore left to mine. The survey +counts pipe reserves before those losses. Some areas will be much richer +than others; exploration is part of finding a productive district. + +## Pipes + +Pipes occur in rare districts, normally beneath OreSpawn's stable cratons. Each +district contains one to three irregular bodies, wider near the top and narrower +at depth. Diamonds occur in patches rather than filling the whole body. A few +can occur close to the surface, but the upper parts are deliberately sparse. + +Pipes cut across existing rocks. Without Mineralogy, their hosts are natural +vanilla stone, granite, diorite and andesite. With physical Mineralogy strata, +they can cut through the declared natural rocks. They do not require peridotite. +Kimberlite and lamproite are not represented by new blocks yet, so barren pipe +rock looks like the surrounding country rock. No craters or replacement terrain +are added. + +Older Cyano geology has no stable-craton classification. In those worlds, the +same seed-based district planner uses an additional rarity check instead. It +does not change the world's geology settings. + +## River gravel + +Placers need submerged natural sand or gravel in a river, within 1,024 blocks +of a planned productive pipe district. The source is the saved district plan, +not the diamonds left in the ground: mining the pipe does not remove its +placers, and exploring the river first does not prevent them. + +Mining diamond-bearing gravel leaves one ordinary gravel block as an item. +There is a 1-in-16 chance of also receiving a diamond. Fortune multiplies a +successful diamond drop in the same way as vanilla diamond ore; it does not +increase the initial chance. Silk Touch returns only the rich block. Falling +gravel keeps its identity, and ordinary gravel never becomes diamond-bearing +when placed again. There are no panning or processing recipes. + +The rich block uses the vanilla gravel texture beneath a subtle mineral overlay, +so resource packs can replace its background. + +## Settings and existing worlds + +`config/realisticdeposits/deposits/diamonds.json` contains both definitions. It +is created only if absent; existing catalogues are never overwritten. The pipe +rule alone controls ordinary background diamonds. The placer rule adds sediment +and never applies a second background reduction or substitutes gravel for ore. + +Disabling the source pipe rule also disables its placers. Disabling only the +placer rule leaves the pipes available. The geometry and source relationship +are saved with the world's OreSpawn profile and require a restart to change. + +Existing world profiles do not gain these rules automatically. Updating the mod +does not remove their background diamonds or change old chunks. The new block +and its item stay registered even when generation is disabled. + +Diamond pipes can have a stepped surface entrance and short underground crosscuts. New +defaults attempt these at every pipe where safe, as they do for iron and coal. +River placers stay untouched. Existing settings are preserved; see +[abandoned mines](ABANDONED_MINES.md). Mineralogy +is optional; its proposed new rocks and the wider mod integrations are separate +future work. + +The current terrain adapters support normal and flat Overworld generation. +Unsupported generators are reported and skipped rather than loading nearby +chunks to guess a site. If that warning appears, disable the pipe rule in +OreSpawn to keep the ordinary diamond supply. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 07387e0..45d309e 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -5,7 +5,7 @@ The publication guard still rejects local OreSpawn candidates; a release must use the public dependency, not an experimental jar. The **Release Realistic Deposits** action runs from the repository's default -branch. Enter the full numeric version, such as `0.2.0.110021`; the final +branch. Enter the full numeric version, such as `0.3.0.110021`; the final component identifies the Minecraft version and loader. CurseForge's alpha, beta or release label is separate from the numeric version. diff --git a/gradle.properties b/gradle.properties index bb6af0c..dd69f08 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,7 +22,7 @@ gradle_java_version=17 mod_id=realisticdeposits mod_name=Realistic Deposits mod_license=LGPL-2.1 -mod_version=0.2.0.110021 +mod_version=0.3.0.110021 mod_group=zone.moddev.mc.realisticdeposits mod_authors=SkyBlade1978, the MMD Team mod_description=Rare, massive, and geologically aware ore deposits powered by MMD OreSpawn. diff --git a/gradle/diamond-manual.gradle b/gradle/diamond-manual.gradle new file mode 100644 index 0000000..941fead --- /dev/null +++ b/gradle/diamond-manual.gradle @@ -0,0 +1,66 @@ +// This profile has separate saves and settings from ordinary development runs. +tasks.named('isolateEclipseProductionRuns') { + doLast { + [['runClient.launch', 'runDiamondDepositsClient.launch'], + ['runServer.launch', 'runDiamondDepositsServer.launch'], + ['runClient.launch', 'runUndergroundMinesClient.launch'], + ['runServer.launch', 'runUndergroundMinesServer.launch']].each { pair -> + String contents = file(pair[0]).getText('UTF-8') + String directory = pair[1].contains('UndergroundMines') ? 'run-underground-mines' : 'run-diamond-deposits' + contents = contents.replaceAll( + /()/, + '$1' + file(directory).absolutePath.replace('\\', '\\\\') + '$2') + file(pair[1]).setText(contents.replace('\r\n', '\n'), 'UTF-8') + } + } +} + +tasks.named('verifyEclipseProductionClasspath') { + doLast { + ['runDiamondDepositsClient.launch', 'runDiamondDepositsServer.launch', + 'runUndergroundMinesClient.launch', 'runUndergroundMinesServer.launch'].each { name -> + String contents = file(name).getText('UTF-8').replace('\\', '/') + String directory = name.contains('UndergroundMines') ? 'run-underground-mines' : 'run-diamond-deposits' + if (!contents.contains(directory) || !contents.contains('ATTR_EXCLUDE_TEST_CODE')) { + throw new GradleException("${name} must use separate saves and production-only output") + } + ['src/test', 'bin/test', 'junit-', 'diamond-probe', 'diamond-survey'].each { forbidden -> + if (contents.contains(forbidden)) throw new GradleException("${name} exposes ${forbidden}") + } + } + } +} + +tasks.register('prepareUndergroundMinesManual') { + group = 'verification' + description = 'Prepares fresh underground-mine defaults without changing existing test saves.' + dependsOn tasks.named('verifyEclipseProductionClasspath') + doLast { + ['vanilla', 'coal', 'diamonds'].each { name -> + File target = file("run-underground-mines/config/realisticdeposits/deposits/${name}.json") + if (!target.exists()) { + target.parentFile.mkdirs() + target.setText(file("src/main/resources/assets/realisticdeposits/defaults/${name}.json") + .getText('UTF-8').replace('\r\n', '\n'), 'UTF-8') + } + } + logger.lifecycle('Use runUndergroundMinesClient.launch and tools/qualification/DIAMOND_TESTING.md.') + logger.lifecycle('Other profiles and existing saves were left alone.') + } +} + +tasks.register('prepareDiamondDepositsManual') { + group = 'verification' + description = 'Prepares a separate diamond test and its Eclipse launches.' + dependsOn tasks.named('verifyEclipseProductionClasspath') + doLast { + File target = file('run-diamond-deposits/config/realisticdeposits/deposits/diamonds.json') + if (!target.exists()) { + target.parentFile.mkdirs() + target.setText(file('src/main/resources/assets/realisticdeposits/defaults/diamonds.json') + .getText('UTF-8').replace('\r\n', '\n'), 'UTF-8') + } + logger.lifecycle('Use runDiamondDepositsClient.launch and tools/qualification/DIAMOND_TESTING.md.') + logger.lifecycle('Existing settings and saves were left alone. Use a new world for the fixed-seed walkthrough.') + } +} diff --git a/gradle/release.gradle b/gradle/release.gradle index e28882e..63f4e76 100644 --- a/gradle/release.gradle +++ b/gradle/release.gradle @@ -81,7 +81,7 @@ tasks.register('verifyJavadocOrdering') { tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '0.2.0.110021' + if (project.mod_version != '0.3.0.110021' || project.curseforge_project_id != '1689703' || project.mod_artifact != 'RealisticDeposits' || project.java_version != '8' || project.gradle_java_version != '17' diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/RealisticDeposits.java b/src/main/java/zone/moddev/mc/realisticdeposits/RealisticDeposits.java index 8d4041a..2c46c99 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/RealisticDeposits.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/RealisticDeposits.java @@ -5,6 +5,8 @@ import zone.moddev.mc.realisticdeposits.generation.StratiformSeamPattern; import zone.moddev.mc.realisticdeposits.generation.CoalMeasuresPattern; import zone.moddev.mc.realisticdeposits.generation.MineWorkingsGenerator; +import zone.moddev.mc.realisticdeposits.generation.DiamondPattern; +import zone.moddev.mc.realisticdeposits.content.ModContent; import zone.moddev.mc.realisticdeposits.integration.OreSpawnIntegration; import zone.moddev.mc.realisticdeposits.proxy.CommonProxy; @@ -35,11 +37,15 @@ public final class RealisticDeposits { public static final String MOD_ID = "realisticdeposits"; public static final String NAME = "Realistic Deposits"; - public static final String VERSION = "0.2.0.110021"; + public static final String VERSION = "0.3.0.110021"; private static final OrePatternType STRATIFORM_SEAM = StratiformSeamPattern.type() .setRegistryName(OreSpawnIntegration.PATTERN_ID); private static final OrePatternType COAL_MEASURES = CoalMeasuresPattern.type() .setRegistryName(OreSpawnIntegration.COAL_PATTERN_ID); + private static final OrePatternType DIAMOND_PIPE = DiamondPattern.type(false) + .setRegistryName(OreSpawnIntegration.DIAMOND_PIPE_ID); + private static final OrePatternType DIAMOND_PLACER = DiamondPattern.type(true) + .setRegistryName(OreSpawnIntegration.DIAMOND_PLACER_ID); @SidedProxy(clientSide = "zone.moddev.mc.realisticdeposits.proxy.ClientProxy", serverSide = "zone.moddev.mc.realisticdeposits.proxy.CommonProxy") @@ -54,10 +60,14 @@ public RealisticDeposits() { public void registerPatterns(RegistryEvent.Register event) { event.getRegistry().register(STRATIFORM_SEAM); event.getRegistry().register(COAL_MEASURES); + event.getRegistry().register(DIAMOND_PIPE); + event.getRegistry().register(DIAMOND_PLACER); } @Mod.EventHandler public void preInitialize(FMLPreInitializationEvent event) { + ModContent.register(); + proxy.registerClientModels(); RealisticDepositsRuntime.initialize(event.getModConfigurationDirectory()); GameRegistry.registerWorldGenerator(MineWorkingsGenerator.INSTANCE, Integer.MAX_VALUE); } @@ -73,6 +83,7 @@ public void initialize(FMLInitializationEvent event) { @Mod.EventHandler public void serverAboutToStart(FMLServerAboutToStartEvent event) { CoalMeasuresPattern.newServerSession(); + DiamondPattern.newServerSession(); } @Mod.EventHandler diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/command/RealisticDepositsCommand.java b/src/main/java/zone/moddev/mc/realisticdeposits/command/RealisticDepositsCommand.java index 3f3c7d5..9eed4e8 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/command/RealisticDepositsCommand.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/command/RealisticDepositsCommand.java @@ -69,6 +69,14 @@ private static void preview(ICommandSender sender, String[] args) throws Command DepositDefinition definition = selectDefinition(state, args.length == 4 ? args[3] : null); World world = sender.getEntityWorld(); String dimension = dimensionId(world.provider.getDimension()); + if (definition.isDiamond()) { + if (!(world instanceof net.minecraft.world.WorldServer)) { + throw new CommandException("Diamond previews require a server world"); + } + sender.sendMessage(new TextComponentString(zone.moddev.mc.realisticdeposits.generation.DiamondPattern + .preview(definition, (net.minecraft.world.WorldServer) world, regionX, regionZ))); + return; + } Optional planned = definition.isCoal() ? CoalMeasuresBody.plan(definition, world.getSeed(), dimension, regionX, regionZ) : RegionDepositPlanner.plan(definition, world.getSeed(), dimension, regionX, regionZ); diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/config/DepositCatalogLoader.java b/src/main/java/zone/moddev/mc/realisticdeposits/config/DepositCatalogLoader.java index b420e98..f5c0ba8 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/config/DepositCatalogLoader.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/config/DepositCatalogLoader.java @@ -20,6 +20,8 @@ import zone.moddev.mc.realisticdeposits.model.DepositDefinition; import zone.moddev.mc.realisticdeposits.model.DepositShapeSettings; import zone.moddev.mc.realisticdeposits.model.CoalMeasuresSettings; +import zone.moddev.mc.realisticdeposits.model.DiamondPipeSettings; +import zone.moddev.mc.realisticdeposits.model.DiamondPlacerSettings; import zone.moddev.mc.realisticdeposits.model.IntRange; import zone.moddev.mc.realisticdeposits.model.InteractionPolicy; import zone.moddev.mc.realisticdeposits.model.StratiformSeamSettings; @@ -109,6 +111,23 @@ private static DepositDefinition definition(JsonObject json, String path, List warnings) { String type = string(shape, "type", path + " shape"); + if ("diamond_pipe".equals(type)) { + unknownKeys(shape, keys("type", "pipes", "upper_diameter", "root_y", "surface_cover", + "district_radius", "ore_density", "legacy_selection_scale"), path + " shape", warnings); + return new DiamondPipeSettings(integerRange(shape, "pipes", path), + integerRange(shape, "upper_diameter", path), integerRange(shape, "root_y", path), + integerRange(shape, "surface_cover", path), integer(shape, "district_radius", path), + number(shape, "ore_density", path), number(shape, "legacy_selection_scale", path)); + } + + if ("diamond_placer".equals(type)) { + unknownKeys(shape, keys("type", "source_deposit", "maximum_source_distance", "thickness", + "coverage"), path + " shape", warnings); + return new DiamondPlacerSettings(string(shape, "source_deposit", path), + integer(shape, "maximum_source_distance", path), integerRange(shape, "thickness", path), + number(shape, "coverage", path)); + } + if ("coal_measures".equals(type)) { unknownKeys(shape, keys("type", "strike_length", "down_dip_width", "beds", "thickness", "rock_interval", "maximum_dip_degrees", "fold_amplitude", "maximum_fault_throw"), diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/config/RealisticDepositsRuntime.java b/src/main/java/zone/moddev/mc/realisticdeposits/config/RealisticDepositsRuntime.java index 8d3e1e2..3e0c43f 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/config/RealisticDepositsRuntime.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/config/RealisticDepositsRuntime.java @@ -78,6 +78,7 @@ private static void reloadCatalogs(boolean diagnostics) { File defaultCatalog = new File(deposits, "vanilla.json"); createDefault(defaultCatalog); createDefault(new File(deposits, "coal.json"), "/assets/realisticdeposits/defaults/coal.json"); + createDefault(new File(deposits, "diamonds.json"), "/assets/realisticdeposits/defaults/diamonds.json"); List files = jsonFiles(deposits); List catalogs = new ArrayList<>(); List definitions = new ArrayList<>(); @@ -187,6 +188,7 @@ private static synchronized void warnStructuralReload(String definitionId) { } private static void validateResourcePolicies(List definitions) { + zone.moddev.mc.realisticdeposits.integration.OreSpawnIntegration.validateDiamondLinks(definitions); Map contracts = new LinkedHashMap<>(); for (DepositDefinition definition : definitions) { if (!definition.enabled()) continue; @@ -250,7 +252,8 @@ private static List jsonFiles(File directory) { List result = new ArrayList<>(); collectJson(directory, result); // Keep existing catalogue order; the new default must not become the first deposit. - result.sort(Comparator.comparingInt((File file) -> "coal.json".equals(file.getName()) ? 1 : 0) + result.sort(Comparator.comparingInt((File file) -> "diamonds.json".equals(file.getName()) ? 2 + : "coal.json".equals(file.getName()) ? 1 : 0) .thenComparing(File::getPath)); return result; } diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/content/DiamondGravelBlock.java b/src/main/java/zone/moddev/mc/realisticdeposits/content/DiamondGravelBlock.java new file mode 100644 index 0000000..0ae2962 --- /dev/null +++ b/src/main/java/zone/moddev/mc/realisticdeposits/content/DiamondGravelBlock.java @@ -0,0 +1,43 @@ +package zone.moddev.mc.realisticdeposits.content; + +import java.util.List; +import java.util.Random; + +import net.minecraft.block.BlockGravel; +import net.minecraft.block.state.IBlockState; +import net.minecraft.creativetab.CreativeTabs; +import net.minecraft.init.Blocks; +import net.minecraft.init.Items; +import net.minecraft.item.ItemStack; +import net.minecraft.util.BlockRenderLayer; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IBlockAccess; + +/** Rich sediment is consumed once; the ordinary gravel left behind is spent. */ +public final class DiamondGravelBlock extends BlockGravel { + public DiamondGravelBlock() { + setRegistryName("realisticdeposits", "diamond_gravel"); + setUnlocalizedName("realisticdeposits.diamond_gravel"); + setCreativeTab(CreativeTabs.BUILDING_BLOCKS); + } + + @Override + public List getDrops(IBlockAccess world, BlockPos pos, IBlockState state, int fortune) { + List drops = new java.util.ArrayList<>(); + drops.add(new ItemStack(Blocks.GRAVEL)); + int diamonds = diamondYield(RANDOM, fortune); + if (diamonds > 0) drops.add(new ItemStack(Items.DIAMOND, diamonds)); + return drops; + } + + public static int diamondYield(Random random, int fortune) { + if (random.nextInt(16) != 0) return 0; + int bonus = fortune > 0 ? Math.max(0, random.nextInt(Math.min(fortune, 255) + 2) - 1) : 0; + return bonus + 1; + } + + @Override + public BlockRenderLayer getBlockLayer() { + return BlockRenderLayer.CUTOUT_MIPPED; + } +} diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/content/ModContent.java b/src/main/java/zone/moddev/mc/realisticdeposits/content/ModContent.java new file mode 100644 index 0000000..28bcc3c --- /dev/null +++ b/src/main/java/zone/moddev/mc/realisticdeposits/content/ModContent.java @@ -0,0 +1,17 @@ +package zone.moddev.mc.realisticdeposits.content; + +import net.minecraft.item.ItemBlock; +import net.minecraftforge.fml.common.registry.GameRegistry; + +/** Registry identity stays the same even when every diamond rule is disabled. */ +public final class ModContent { + public static final DiamondGravelBlock DIAMOND_GRAVEL = new DiamondGravelBlock(); + + private ModContent() { } + + public static void register() { + GameRegistry.register(DIAMOND_GRAVEL); + GameRegistry.register(new ItemBlock(DIAMOND_GRAVEL).setRegistryName(DIAMOND_GRAVEL.getRegistryName())); + } + +} diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/generation/CoalMeasuresPattern.java b/src/main/java/zone/moddev/mc/realisticdeposits/generation/CoalMeasuresPattern.java index 69da1da..725b18a 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/generation/CoalMeasuresPattern.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/generation/CoalMeasuresPattern.java @@ -91,6 +91,7 @@ boolean place(DepositDefinition definition, OrePlacementContext base, CoalGeolog int chunkZ = context.chunkZ() << 4; int regionSize = definition.regionSpacingChunks() * 16; int radius = (definition.coal().strikeLength().maximum() + definition.coal().downDipWidth().maximum()) / 2 + 3; + if (definition.abandonedMine().enabled() && definition.abandonedMine().chance() > 0) radius += 96; int count = 0; java.util.List structures = CoalStructureProtection.inChunk(context.chunkX(), context.chunkZ()); GeologyColumn[] columns = new GeologyColumn[256]; @@ -111,9 +112,10 @@ boolean place(DepositDefinition definition, OrePlacementContext base, CoalGeolog } bodies.put(key, selected); } - if (!selected.isPresent() || !selected.get().bounds().intersectsChunk(context.chunkX(), context.chunkZ())) continue; + if (!selected.isPresent()) continue; CoalMeasuresBody body = selected.get(); MineWorkingsGenerator.offer(context, definition, body); + if (!body.bounds().intersectsChunk(context.chunkX(), context.chunkZ())) continue; for (int x = chunkX; x < chunkX + 16; x++) { for (int z = chunkZ; z < chunkZ + 16; z++) { diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/generation/DiamondPattern.java b/src/main/java/zone/moddev/mc/realisticdeposits/generation/DiamondPattern.java new file mode 100644 index 0000000..568fe30 --- /dev/null +++ b/src/main/java/zone/moddev/mc/realisticdeposits/generation/DiamondPattern.java @@ -0,0 +1,353 @@ +package zone.moddev.mc.realisticdeposits.generation; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import com.google.gson.JsonObject; +import com.mojang.serialization.Codec; +import org.apache.logging.log4j.LogManager; + +import net.minecraft.block.material.Material; +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.WorldServer; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.chunk.Chunk; +import net.minecraftforge.common.BiomeDictionary; +import net.minecraftforge.fml.common.FMLCommonHandler; + +import zone.moddev.mc.orespawn.api.GeologyColumn; +import zone.moddev.mc.orespawn.api.GeologySampler; +import zone.moddev.mc.orespawn.api.OreGenerationContext; +import zone.moddev.mc.orespawn.api.OrePatternType; +import zone.moddev.mc.orespawn.api.OreSpawnApi; +import zone.moddev.mc.realisticdeposits.config.DepositCatalogLoader; +import zone.moddev.mc.realisticdeposits.config.RealisticDepositsRuntime; +import zone.moddev.mc.realisticdeposits.geometry.BlockBounds; +import zone.moddev.mc.realisticdeposits.geometry.DiamondDistrict; +import zone.moddev.mc.realisticdeposits.geometry.DiamondPipeWorkingsBody; +import zone.moddev.mc.realisticdeposits.model.DepositDefinition; +import zone.moddev.mc.realisticdeposits.model.AlignmentMode; + +/** OreSpawn owns the pass; these patterns only place their current chunk's slice. */ +public final class DiamondPattern { + private static int epoch; + private static boolean warnedTerrain; + private static boolean warnedGeology; + + private DiamondPattern() { } + + public static OrePatternType type(boolean placer) { + return OrePatternType.create(Codec.of(input -> { + if (input == null || !input.isJsonObject()) { + throw new IllegalArgumentException("Diamond settings need a captured definition"); + } + JsonObject json = input.getAsJsonObject(); + DepositDefinition definition = DepositCatalogLoader.loadDefinition( + json.getAsJsonObject("definition"), "OreSpawn diamond profile"); + if (placer != definition.isDiamondPlacer() || !definition.isDiamond()) { + throw new IllegalArgumentException("Diamond pattern and shape do not match"); + } + DepositDefinition source = placer ? DepositCatalogLoader.loadDefinition( + json.getAsJsonObject("source_definition"), "OreSpawn placer source") : definition; + if (!source.isDiamondPipe() || (placer && !source.id().equals(definition.placer().sourceDeposit()))) { + throw new IllegalArgumentException("The placer needs its captured diamond-pipe source"); + } + return new Captured(enabledForCallback(definition), enabledForCallback(source)); + }), captured -> { + Renderer renderer = new Renderer(captured); + return base -> { + if (!(base instanceof OreGenerationContext)) { + throw new IllegalStateException("Diamonds require OreSpawn's extended generation context"); + } + return renderer.place((OreGenerationContext) base); + }; + }); + } + + public static void newServerSession() { + epoch++; + warnedTerrain = false; + warnedGeology = false; + } + + static DepositDefinition enabledForCallback(DepositDefinition definition) { + // OreSpawn calls the pattern only for an enabled rule. Its saved switch + // takes precedence over the catalogue default captured inside the settings. + if (definition.enabled()) return definition; + JsonObject json = definition.toJson(); + json.addProperty("enabled", true); + return DepositCatalogLoader.loadDefinition(json, "enabled world diamond rule"); + } + + private static final class Captured { + final DepositDefinition definition; + final DepositDefinition source; + + Captured(DepositDefinition definition, DepositDefinition source) { + this.definition = definition; + this.source = source; + } + } + + private static final class Renderer { + private final Captured captured; + private final Map> districts = + new LinkedHashMap>() { + @Override + protected boolean removeEldestEntry(Map.Entry> entry) { + return size() > 128; + } + }; + private int observedEpoch = -1; + private long observedSeed; + private DepositDefinition source; + + Renderer(Captured captured) { + this.captured = captured; + } + + boolean place(OreGenerationContext context) { + DepositDefinition definition = captured.definition; + if (!definition.enabled() || !definition.dimensions().contains(context.dimension().toString()) + || !"minecraft:overworld".equals(context.dimension().toString())) return false; + + MinecraftServer server = FMLCommonHandler.instance().getMinecraftServerInstance(); + WorldServer world = server == null ? null : server.worldServerForDimension(0); + if (world == null) throw new IllegalStateException("Diamond placement has no active Overworld"); + Chunk chunk = world.getChunkProvider().getLoadedChunk(context.chunkX(), context.chunkZ()); + if (chunk == null) throw new IllegalStateException("Diamond placement may only inspect its loaded chunk"); + + if (observedEpoch != epoch || observedSeed != context.worldSeed()) { + districts.clear(); + observedEpoch = epoch; + observedSeed = context.worldSeed(); + source = definition.isDiamondPlacer() ? activeSource(server, captured.source) : captured.source; + } + if (source == null || !source.enabled()) return false; + + List structures = CoalStructureProtection.inChunk(context.chunkX(), context.chunkZ()); + int count = definition.isDiamondPipe() ? pipes(context, world, chunk, structures) + : placer(context, world, chunk, structures); + if (count > 0) RealisticDepositsRuntime.reportFirstGeneration(definition.id(), + context.dimension().toString(), context.chunkX(), context.chunkZ(), count); + return count > 0; + } + + private Optional district(OreGenerationContext context, WorldServer world, int rx, int rz) { + long key = ((long) rx << 32) ^ (rz & 0xffffffffL); + Optional result = districts.get(key); + if (result != null) return result; + + MineTerrainSampler terrain; + try { + terrain = new MineTerrainSampler(world, world.getChunkProvider().chunkGenerator); + } catch (IllegalArgumentException unsupported) { + if (!warnedTerrain) { + warnedTerrain = true; + LogManager.getLogger("Realistic Deposits").warn("Diamond deposits cannot safely sample this terrain generator. " + + "No neighbouring chunks will be generated to guess a site. " + + "Disable the pipe rule in OreSpawn if you want to keep the ordinary diamond supply."); + } + result = Optional.empty(); + districts.put(key, result); + return result; + } + + Optional sampler = context.geologySampler(); + result = DiamondDistrict.plan(source, context.worldSeed(), context.dimension().toString(), rx, rz, + new DiamondDistrict.Site() { + @Override public int surfaceY(int x, int z) { return terrain.naturalSurfaceY(x, z); } + + @Override + public String geome(int x, int z, int surface) { + String id = sampler.isPresent() ? sampler.get().sampleColumn(x, z, surface).geome().toString() : null; + if ((id == null || "orespawn:cyano".equals(id)) && !warnedGeology) { + warnedGeology = true; + LogManager.getLogger("Realistic Deposits").warn("This world has no modern craton classification. " + + "Diamond districts use the configured rarity-only approximation; geology settings are unchanged."); + } + return id; + } + }); + districts.put(key, result); + return result; + } + + private int pipes(OreGenerationContext context, WorldServer world, Chunk chunk, List structures) { + int minX = context.chunkX() << 4; + int minZ = context.chunkZ() << 4; + int radius = source.pipe().districtRadius() + source.pipe().upperDiameter().maximum(); + if (source.abandonedMine().enabled() && source.abandonedMine().chance() > 0) radius += 96; + int size = source.regionSpacingChunks() * 16; + int count = 0; + boolean physical = CoalGeology.active().physicalStrata; + GeologyColumn[] columns = new GeologyColumn[256]; + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + + for (int rx = Math.floorDiv(minX - radius, size); rx <= Math.floorDiv(minX + 15 + radius, size); rx++) { + for (int rz = Math.floorDiv(minZ - radius, size); rz <= Math.floorDiv(minZ + 15 + radius, size); rz++) { + Optional selected = district(context, world, rx, rz); + if (!selected.isPresent()) continue; + DiamondDistrict district = selected.get(); + for (int pipeIndex = 0; pipeIndex < district.pipes().size(); pipeIndex++) { + DiamondDistrict.Pipe pipe = district.pipes().get(pipeIndex); + BlockBounds bounds = pipe.bounds(); + + if (source.abandonedMine().enabled()) { + MineWorkingsGenerator.offer(context, source, new DiamondPipeWorkingsBody(district, pipeIndex)); + } + if (!bounds.intersectsChunk(context.chunkX(), context.chunkZ())) continue; + + for (int x = Math.max(minX, bounds.minimumX()); x <= Math.min(minX + 15, bounds.maximumX()); x++) { + for (int z = Math.max(minZ, bounds.minimumZ()); z <= Math.min(minZ + 15, bounds.maximumZ()); z++) { + int index = (x & 15) + (z & 15) * 16; + int top = Math.min(pipe.topY(), chunk.getHeightValue(x & 15, z & 15) - 4); + for (int y = pipe.bottomY(); y <= top; y++) { + if (!pipe.isOreAt(x, y, z) || !context.inside(x, y, z) + || CoalStructureProtection.contains(structures, x, y, z)) continue; + IBlockState state = chunk.getBlockState(position.setPos(x, y, z)); + if (!naturalRock(source, state, physical, columns, index, context, x, y, z, top)) continue; + if (context.tryPlace(x, y, z, pipe.seed())) count++; + } + } + } + } + } + } + return count; + } + + private int placer(OreGenerationContext context, WorldServer world, Chunk chunk, List structures) { + DepositDefinition definition = captured.definition; + int minX = context.chunkX() << 4; + int minZ = context.chunkZ() << 4; + int reach = definition.placer().maximumSourceDistance(); + int size = source.regionSpacingChunks() * 16; + int search = reach + source.pipe().districtRadius(); + java.util.ArrayList nearby = new java.util.ArrayList<>(); + + Biome[] biomes = world.provider.getBiomeProvider().getBiomes(null, minX, minZ, 16, 16); + boolean possibleRiver = false; + for (Biome biome : biomes) { + if (BiomeDictionary.isBiomeOfType(biome, BiomeDictionary.Type.RIVER)) { + possibleRiver = true; + break; + } + } + if (!possibleRiver) return 0; + + for (int rx = Math.floorDiv(minX - search, size); rx <= Math.floorDiv(minX + 15 + search, size); rx++) { + for (int rz = Math.floorDiv(minZ - search, size); rz <= Math.floorDiv(minZ + 15 + search, size); rz++) { + Optional candidate = district(context, world, rx, rz); + if (candidate.isPresent()) nearby.add(candidate.get()); + } + } + if (nearby.isEmpty()) return 0; + + int count = 0; + BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos(); + for (int x = minX; x < minX + 16; x++) { + for (int z = minZ; z < minZ + 16; z++) { + Biome biome = biomes[(x & 15) + (z & 15) * 16]; + if (!BiomeDictionary.isBiomeOfType(biome, BiomeDictionary.Type.RIVER)) continue; + DiamondDistrict nearest = null; + double distance = (double) reach * reach; + for (DiamondDistrict candidate : nearby) { + double current = candidate.distanceSquared(x, z); + if (current < distance) { nearest = candidate; distance = current; } + } + if (nearest == null) continue; + + int y = Math.min(255, chunk.getHeightValue(x & 15, z & 15) - 1); + IBlockState state = chunk.getBlockState(position.setPos(x, y, z)); + if (state.getMaterial() != Material.WATER) continue; + while (y > definition.minimumY() && state.getMaterial() == Material.WATER) { + state = chunk.getBlockState(position.setPos(x, --y, z)); + } + int thickness = definition.placer().thickness().minimum() + + (int) Math.floorMod(nearest.identity().seed() ^ x ^ ((long) z << 32), + definition.placer().thickness().maximum() - definition.placer().thickness().minimum() + 1); + for (int depth = 0; depth < thickness && y - depth >= definition.minimumY(); depth++) { + int py = y - depth; + if (py > definition.maximumY()) continue; + state = chunk.getBlockState(position.setPos(x, py, z)); + if (state.getBlock() != Blocks.GRAVEL && state.getBlock() != Blocks.SAND) break; + IBlockState below = chunk.getBlockState(position.setPos(x, py - 1, z)); + if (!below.getMaterial().isSolid() || CoalStructureProtection.contains(structures, x, py, z)) continue; + if (DiamondDistrict.placerPocket(nearest.identity().seed() ^ definition.salt(), x, z, + depth, definition.placer().coverage()) && context.inside(x, py, z) + && context.tryPlace(x, py, z, nearest.identity().seed())) count++; + } + } + } + return count; + } + } + + private static boolean naturalRock(DepositDefinition definition, IBlockState state, boolean physical, GeologyColumn[] columns, + int index, OreGenerationContext context, int x, int y, int z, int surface) { + if (state.getMaterial() != Material.ROCK || state.getBlock().hasTileEntity(state)) return false; + if (physical && context.geologySampler().isPresent()) { + if (columns[index] == null) columns[index] = context.geologySampler().get().sampleColumn(x, z, surface); + if (!columns[index].familyAt(y).isPresent() || !state.equals(columns[index].rockAt(y))) return false; + return definition.alignment() != AlignmentMode.ROCK_ALIGNED + || definition.rockFamilies().contains(columns[index].familyAt(y).get()); + } + if (state.getBlock() != Blocks.STONE) return false; + int meta = Blocks.STONE.getMetaFromState(state); + return meta == 0 || meta == 1 || meta == 3 || meta == 5; + } + + private static DepositDefinition activeSource(MinecraftServer server, DepositDefinition fallback) { + if (fallback == null) return null; + Optional profile = OreSpawnApi.getActiveProfile(server); + if (!profile.isPresent()) return null; + JsonObject root = profile.get().toJson(); + if (!root.has("ores")) return null; + JsonObject ores = root.getAsJsonObject("ores"); + String key = "realisticdeposits:deposit/" + fallback.id().replace(':', '/'); + if (!ores.has(key)) return null; + JsonObject rule = ores.getAsJsonObject(key); + if (rule.has("enabled") && !rule.get("enabled").getAsBoolean()) return null; + JsonObject dimensions = rule.getAsJsonObject("dimensions"); + if (dimensions == null || !dimensions.has("minecraft:overworld")) return null; + JsonObject dimension = dimensions.getAsJsonObject("minecraft:overworld"); + if (dimension.has("enabled") && !dimension.get("enabled").getAsBoolean()) return null; + if (!dimension.has("pattern") || !dimension.get("pattern").isJsonObject()) return null; + JsonObject pattern = dimension.getAsJsonObject("pattern"); + if (!"realisticdeposits:diamond_pipe".equals(pattern.get("type").getAsString())) return null; + DepositDefinition source = DepositCatalogLoader.loadDefinition(pattern.getAsJsonObject("settings") + .getAsJsonObject("definition"), "world-captured diamond source"); + return source.isDiamondPipe() ? enabledForCallback(source) : null; + } + + public static String preview(DepositDefinition definition, WorldServer world, int rx, int rz) { + DepositDefinition source = definition.isDiamondPipe() ? definition + : activeSource(world.getMinecraftServer(), RealisticDepositsRuntime.state() + .definition(definition.placer().sourceDeposit())); + if (source == null) return "No active captured pipe source is available for this placer."; + MineTerrainSampler terrain; + try { + terrain = new MineTerrainSampler(world, world.getChunkProvider().chunkGenerator); + } catch (IllegalArgumentException unsupported) { + return "This terrain generator does not support safe diamond-site previews."; + } + Optional sampler = OreSpawnApi.createSampler(world); + Optional district = DiamondDistrict.plan(source, world.getSeed(), "minecraft:overworld", rx, rz, + new DiamondDistrict.Site() { + @Override public int surfaceY(int x, int z) { return terrain.naturalSurfaceY(x, z); } + @Override public String geome(int x, int z, int surface) { + return sampler.isPresent() ? sampler.get().sampleColumn(x, z, surface).geome().toString() : null; + } + }); + return district.isPresent() ? "Diamond district at " + district.get().centerX() + "," + + district.get().centerZ() + "; pipes=" + district.get().pipes().size() + + ". River placers also require suitable submerged sediment nearby. Preview changes no blocks." + : "No productive diamond district is selected in this region."; + } +} diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/generation/MineTerrainSampler.java b/src/main/java/zone/moddev/mc/realisticdeposits/generation/MineTerrainSampler.java index fa3b7c7..f5a807f 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/generation/MineTerrainSampler.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/generation/MineTerrainSampler.java @@ -67,8 +67,8 @@ boolean dry(AbandonedMinePlan plan) { boolean covered(AbandonedMinePlan plan) { for (Map.Entry cell : plan.blocks().entrySet()) { MinePoint point = MinePoint.unpack(cell.getKey()); - if (Math.abs(point.x() - plan.landing().x()) <= 2 - && Math.abs(point.z() - plan.landing().z()) <= 2) continue; + int portalDistance = Math.abs(point.x() - plan.entrance().x()) + Math.abs(point.z() - plan.entrance().z()); + if (portalDistance <= 20 && point.y() >= plan.entrance().y() - 6) continue; if (heightAt(point.x(), point.z()) < point.y() + 2) return false; } @@ -122,4 +122,8 @@ private int heightAt(int x, int z) { heights.put(column, -1); return -1; } + + int naturalSurfaceY(int x, int z) { + return heightAt(x, z); + } } diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/generation/MineWorkingsGenerator.java b/src/main/java/zone/moddev/mc/realisticdeposits/generation/MineWorkingsGenerator.java index c8295b5..c5609a1 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/generation/MineWorkingsGenerator.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/generation/MineWorkingsGenerator.java @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -14,6 +15,8 @@ import java.util.Set; import java.util.WeakHashMap; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -21,6 +24,7 @@ import net.minecraft.block.BlockLadder; import net.minecraft.block.BlockRail; import net.minecraft.block.BlockRailBase; +import net.minecraft.block.BlockStairs; import net.minecraft.block.state.IBlockState; import net.minecraft.init.Blocks; import net.minecraft.nbt.NBTTagCompound; @@ -159,9 +163,8 @@ public void generate(Random ignored, int chunkX, int chunkZ, World baseWorld, private static void decide(Candidate candidate, MineWorldData data, Session session, WorldServer world, IChunkGenerator generator, long currentChunk) { MineTerrainSampler terrain = new MineTerrainSampler(world, generator); - MinePoint landing = AbandonedMinePlanner.landing(candidate.body); Optional proposed = AbandonedMinePlanner.plan(candidate.body, - candidate.definition.abandonedMine(), terrain.surfaceY(landing)); + candidate.definition.abandonedMine(), terrain::naturalSurfaceY); if (!proposed.isPresent()) { data.reject(candidate.key, "No safe surface access"); return; @@ -173,7 +176,8 @@ private static void decide(Candidate candidate, MineWorldData data, Session sess } else if (!world.getWorldBorder().contains(new BlockPos(plan.bounds().minimumX(), 64, plan.bounds().minimumZ())) || !world.getWorldBorder().contains(new BlockPos(plan.bounds().maximumX(), 64, plan.bounds().maximumZ()))) { data.reject(candidate.key, "World border"); - } else if (!terrain.dry(plan) || !terrain.covered(plan) || !biomesAllowed(plan, candidate.definition, world)) { + } else if (terrain.surfaceY(plan.entrance()) != plan.entrance().y() + || !terrain.dry(plan) || !terrain.covered(plan) || !biomesAllowed(plan, candidate.definition, world)) { data.reject(candidate.key, "Water, insufficient cover or an excluded biome"); } else if (intersectsStructure(plan.bounds(), world)) { data.reject(candidate.key, "Another structure"); @@ -328,7 +332,7 @@ private static Set naturalHosts(DepositDefinition definition, WorldServer addHost(blocks, new ResourceLocation(definition.resource())); } OreSpawnApi.getActiveProfile(world.getMinecraftServer()).ifPresent(profile -> { - for (ResourceLocation rock : profile.rockIds()) addHost(blocks, rock); + for (ResourceLocation rock : naturalRockBlocks(profile.toJson())) addHost(blocks, rock); }); // These may be natural-looking, but they also make up villages and ruins. @@ -340,6 +344,25 @@ private static Set naturalHosts(DepositDefinition definition, WorldServer return blocks; } + static Set naturalRockBlocks(JsonObject profile) { + Set blocks = new LinkedHashSet<>(); + if (!profile.has("rocks") || !profile.get("rocks").isJsonObject()) return blocks; + + // Rock IDs name geology definitions, not registered blocks. Read the + // declared block instead, just as OreSpawn does when building its strata. + for (Map.Entry entry : profile.getAsJsonObject("rocks").entrySet()) { + if (!entry.getValue().isJsonObject()) continue; + JsonObject rock = entry.getValue().getAsJsonObject(); + if (rock.has("enabled") && !rock.get("enabled").getAsBoolean()) continue; + if (!rock.has("block") || !rock.get("block").isJsonPrimitive() + || !rock.getAsJsonPrimitive("block").isString()) continue; + + blocks.add(new ResourceLocation(rock.get("block").getAsString())); + } + + return blocks; + } + private static void addHost(Set hosts, ResourceLocation id) { if (Block.REGISTRY.containsKey(id)) hosts.add(Block.REGISTRY.getObject(id)); } @@ -351,6 +374,10 @@ private static IBlockState state(MineBlock block) { case FENCE: return Blocks.OAK_FENCE.getDefaultState(); case COBWEB: return Blocks.WEB.getDefaultState(); case COBBLESTONE: return Blocks.COBBLESTONE.getDefaultState(); + case STAIRS_NORTH: return Blocks.OAK_STAIRS.getDefaultState().withProperty(BlockStairs.FACING, EnumFacing.NORTH); + case STAIRS_SOUTH: return Blocks.OAK_STAIRS.getDefaultState().withProperty(BlockStairs.FACING, EnumFacing.SOUTH); + case STAIRS_EAST: return Blocks.OAK_STAIRS.getDefaultState().withProperty(BlockStairs.FACING, EnumFacing.EAST); + case STAIRS_WEST: return Blocks.OAK_STAIRS.getDefaultState().withProperty(BlockStairs.FACING, EnumFacing.WEST); case LADDER_WEST: return Blocks.LADDER.getDefaultState().withProperty(BlockLadder.FACING, EnumFacing.WEST); case RAIL_EAST_WEST: return Blocks.RAIL.getDefaultState().withProperty( BlockRail.SHAPE, BlockRailBase.EnumRailDirection.EAST_WEST); diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/generation/StratiformSeamPattern.java b/src/main/java/zone/moddev/mc/realisticdeposits/generation/StratiformSeamPattern.java index 18a41a0..9951ca6 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/generation/StratiformSeamPattern.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/generation/StratiformSeamPattern.java @@ -72,6 +72,7 @@ private static boolean placeResolved(DepositDefinition definition, int chunkMaxZ = chunkMinZ + 15; int regionBlocks = definition.regionSpacingChunks() * 16; int radius = maximumHorizontalRadius(definition); + if (definition.abandonedMine().enabled() && definition.abandonedMine().chance() > 0) radius += 96; int minRegionX = Math.floorDiv(chunkMinX - radius, regionBlocks); int maxRegionX = Math.floorDiv(chunkMaxX + radius, regionBlocks); int minRegionZ = Math.floorDiv(chunkMinZ - radius, regionBlocks); @@ -85,9 +86,9 @@ private static boolean placeResolved(DepositDefinition definition, context.worldSeed(), context.dimension().toString(), regionX, regionZ); if (!planned.isPresent()) continue; StratiformSeamBody body = planned.get(); - if (!body.bounds().intersectsChunk(context.chunkX(), context.chunkZ()) - || !acceptsCenter(definition, body, sampler)) continue; + if (!acceptsCenter(definition, body, sampler)) continue; MineWorkingsGenerator.offer(context, definition, body); + if (!body.bounds().intersectsChunk(context.chunkX(), context.chunkZ())) continue; BlockBounds bounds = body.bounds(); for (int x = Math.max(chunkMinX, bounds.minimumX()); x <= Math.min(chunkMaxX, bounds.maximumX()); x++) { diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/geometry/AbandonedMinePlanner.java b/src/main/java/zone/moddev/mc/realisticdeposits/geometry/AbandonedMinePlanner.java index b45cd93..1ae0376 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/geometry/AbandonedMinePlanner.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/geometry/AbandonedMinePlanner.java @@ -1,5 +1,6 @@ package zone.moddev.mc.realisticdeposits.geometry; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -28,6 +29,25 @@ public static MinePoint landing(MineableDepositBody body) { return body.miningRoutePoint(anchorAlong(body), 0.0D); } + public static Optional plan(MineableDepositBody body, + AbandonedMineSettings settings, java.util.function.ToIntBiFunction surface) { + if (!selected(body, settings)) return Optional.empty(); + MinePoint landing = landing(body); + int initial = surface.applyAsInt(landing.x(), landing.z()); + if (initial < 0) return Optional.empty(); + + for (int offset = 0; offset <= 16; offset++) { + for (int sign : offset == 0 ? new int[] {1} : new int[] {1, -1}) { + int height = initial + offset * sign; + if (height < landing.y() + 6 || height - landing.y() > MAXIMUM_SHAFT_DEPTH || height > 245) continue; + MinePoint entrance = MineDecline.plan(landing, height, body.identity().seed()).entrance(); + if (surface.applyAsInt(entrance.x(), entrance.z()) == height) return plan(body, settings, height); + } + } + + return Optional.empty(); + } + public static Optional plan(MineableDepositBody body, AbandonedMineSettings settings, int surfaceY) { if (!selected(body, settings)) return Optional.empty(); @@ -92,10 +112,36 @@ public static Optional plan(MineableDepositBody body, if (air.contains(rubble)) blocks.put(rubble, MineBlock.COBBLESTONE); } } - shaft(blocks, landing, surfaceY); + MineDecline access = MineDecline.plan(landing, surfaceY, body.identity().seed()); + access.build(blocks); + if (!sealClosedPockets(blocks, access.entrance(), landing)) return Optional.empty(); return Optional.of(new AbandonedMinePlan(body.identity().seed(), - new MinePoint(landing.x(), surfaceY, landing.z()), landing, blocks)); + access.entrance(), landing, blocks)); + } + + private static boolean sealClosedPockets(Map blocks, MinePoint entrance, MinePoint landing) { + Set connected = new LinkedHashSet<>(); + ArrayDeque queue = new ArrayDeque<>(); + queue.add(entrance); + int[][] offsets = {{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}}; + + while (!queue.isEmpty()) { + MinePoint point = queue.remove(); + MineBlock block = blocks.get(point.packed()); + if (block == null || !block.passable() || !connected.add(point.packed())) continue; + + for (int[] offset : offsets) { + queue.add(new MinePoint(point.x() + offset[0], point.y() + offset[1], point.z() + offset[2])); + } + } + + if (!connected.contains(landing.packed())) return false; + + // Stair foundations can seal a small pocket where they meet a gallery. + // Leave that rock in place rather than carving an inaccessible void. + blocks.entrySet().removeIf(cell -> cell.getValue().passable() && !connected.contains(cell.getKey())); + return true; } private static double anchorAlong(MineableDepositBody body) { @@ -162,36 +208,6 @@ private static void decorate(Map blocks, Set air, } } - private static void shaft(Map blocks, MinePoint landing, int surfaceY) { - for (int y = landing.y(); y <= surfaceY + 2; y++) { - for (int dx = -1; dx <= 1; dx++) { - for (int dz = -1; dz <= 1; dz++) { - put(blocks, landing.x() + dx, y, landing.z() + dz, MineBlock.AIR); - } - } - - put(blocks, landing.x() + 2, y, landing.z(), MineBlock.PLANKS); - if (y <= surfaceY) put(blocks, landing.x() + 1, y, landing.z(), MineBlock.LADDER_WEST); - - if ((y - landing.y()) % 7 == 0 || y == surfaceY + 2) { - for (int offset = -2; offset <= 2; offset++) { - put(blocks, landing.x() - 2, y, landing.z() + offset, MineBlock.PLANKS); - put(blocks, landing.x() + 2, y, landing.z() + offset, MineBlock.PLANKS); - put(blocks, landing.x() + offset, y, landing.z() - 2, MineBlock.PLANKS); - put(blocks, landing.x() + offset, y, landing.z() + 2, MineBlock.PLANKS); - } - } - } - - for (int dx = -2; dx <= 2; dx++) { - for (int dz = -2; dz <= 2; dz++) { - if (Math.abs(dx) == 2 || Math.abs(dz) == 2) { - put(blocks, landing.x() + dx, surfaceY - 1, landing.z() + dz, MineBlock.PLANKS); - } - } - } - } - private static void put(Map blocks, int x, int y, int z, MineBlock block) { blocks.put(new MinePoint(x, y, z).packed(), block); } diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/geometry/DiamondDistrict.java b/src/main/java/zone/moddev/mc/realisticdeposits/geometry/DiamondDistrict.java new file mode 100644 index 0000000..9b1da41 --- /dev/null +++ b/src/main/java/zone/moddev/mc/realisticdeposits/geometry/DiamondDistrict.java @@ -0,0 +1,172 @@ +package zone.moddev.mc.realisticdeposits.geometry; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Random; + +import zone.moddev.mc.realisticdeposits.model.AlignmentMode; +import zone.moddev.mc.realisticdeposits.model.DepositDefinition; +import zone.moddev.mc.realisticdeposits.model.DiamondPipeSettings; + +/** A district is planned without reading generated ore or loading any chunks. */ +public final class DiamondDistrict { + public interface Site { + int surfaceY(int x, int z); + String geome(int x, int z, int surfaceY); + } + + private final DepositIdentity identity; + private final int x; + private final int z; + private final List pipes; + + private DiamondDistrict(DepositIdentity identity, int x, int z, List pipes) { + this.identity = identity; + this.x = x; + this.z = z; + this.pipes = Collections.unmodifiableList(new ArrayList<>(pipes)); + } + + public static Optional plan(DepositDefinition definition, long seed, String dimension, + int regionX, int regionZ, Site site) { + if (!definition.isDiamondPipe() || !definition.enabled() + || !definition.dimensions().contains(dimension)) return Optional.empty(); + + DepositIdentity identity = new DepositIdentity(seed, dimension, definition.id(), + regionX, regionZ, definition.salt()); + if (StableHash.unit(identity.seed() ^ 0x3BACB2B415F019D1L) >= definition.selectionChance()) { + return Optional.empty(); + } + + Random random = new Random(identity.seed()); + int size = definition.regionSpacingChunks() * 16; + int x = Math.toIntExact((long) regionX * size + random.nextInt(size)); + int z = Math.toIntExact((long) regionZ * size + random.nextInt(size)); + int surface = site.surfaceY(x, z); + if (surface < 0) return Optional.empty(); + + DiamondPipeSettings settings = definition.pipe(); + String geome = site.geome(x, z, surface); + if (definition.alignment() != AlignmentMode.MASSIVE_RARE) { + boolean approximate = geome == null || "orespawn:cyano".equals(geome); + if (approximate) { + if (StableHash.unit(identity.seed() ^ 0x50B195A874F34111L) + >= settings.legacySelectionScale()) return Optional.empty(); + } else if (!definition.geomes().contains(geome)) { + return Optional.empty(); + } + } + + List pipes = new ArrayList<>(); + int count = settings.pipeCount().sample(random); + for (int index = 0; index < count; index++) { + double angle = random.nextDouble() * Math.PI * 2; + double distance = index == 0 ? 0 : settings.districtRadius() * (0.35 + random.nextDouble() * 0.65); + int px = x + (int) Math.round(Math.cos(angle) * distance); + int pz = z + (int) Math.round(Math.sin(angle) * distance); + int localSurface = site.surfaceY(px, pz); + int bottom = Math.max(definition.minimumY(), settings.rootY().sample(random)); + int top = Math.min(definition.maximumY(), localSurface - settings.surfaceCover().sample(random)); + if (localSurface < 0 || top - bottom < 16) continue; + + pipes.add(new Pipe(StableHash.combine(identity.seed(), index), px, pz, bottom, top, + settings.upperDiameter().sample(random) * 0.5, settings.oreDensity())); + } + + return pipes.isEmpty() ? Optional.empty() : Optional.of(new DiamondDistrict(identity, x, z, pipes)); + } + + public DepositIdentity identity() { return identity; } + public int centerX() { return x; } + public int centerZ() { return z; } + public List pipes() { return pipes; } + + public double distanceSquared(int blockX, int blockZ) { + double nearest = Double.POSITIVE_INFINITY; + for (Pipe pipe : pipes) { + double dx = (double) blockX - pipe.x; + double dz = (double) blockZ - pipe.z; + nearest = Math.min(nearest, dx * dx + dz * dz); + } + return nearest; + } + + /** One irregular breccia envelope; only its mineralised cells become ore. */ + public static final class Pipe { + private final long seed; + private final int x; + private final int z; + private final int bottom; + private final int top; + private final double radius; + private final double density; + private final double phase; + private final double driftX; + private final double driftZ; + + private Pipe(long seed, int x, int z, int bottom, int top, double radius, double density) { + this.seed = seed; + this.x = x; + this.z = z; + this.bottom = bottom; + this.top = top; + this.radius = radius; + this.density = density; + phase = StableHash.unit(seed) * Math.PI * 2; + driftX = Math.cos(phase) * radius * 0.25; + driftZ = Math.sin(phase) * radius * 0.25; + } + + public long seed() { return seed; } + public int centerX() { return x; } + public int centerZ() { return z; } + public int bottomY() { return bottom; } + public int topY() { return top; } + public double upperRadius() { return radius; } + + public BlockBounds bounds() { + int reach = (int) Math.ceil(radius * 1.5 + 4); + return new BlockBounds(x - reach, bottom, z - reach, x + reach, top, z + reach); + } + + public boolean inside(int blockX, int y, int blockZ) { + if (y < bottom || y > top) return false; + double t = (double) (y - bottom) / (top - bottom); + double dx = blockX + 0.5 - x - driftX * (1 - t); + double dz = blockZ + 0.5 - z - driftZ * (1 - t); + double angle = Math.atan2(dz, dx); + double wall = 1 + 0.10 * Math.sin(angle * 3 + phase + t * 2) + + 0.06 * Math.sin(angle * 7 - phase + t * 4); + double r = (3 + (radius - 3) * Math.pow(t, 0.75)) * wall; + if (dx * dx + dz * dz <= r * r) return true; + + // A narrow oblique feeder joins the lower third of the main pipe. + if (t > 0.35) return false; + double along = dx * Math.cos(phase) + dz * Math.sin(phase); + double across = -dx * Math.sin(phase) + dz * Math.cos(phase); + return Math.abs(across) <= 1.25 && along >= 0 && along <= radius * (0.35 - t); + } + + public boolean isOreAt(int blockX, int y, int blockZ) { + if (!inside(blockX, y, blockZ)) return false; + double t = (double) (y - bottom) / (top - bottom); + long pocket = cellSeed(seed, Math.floorDiv(blockX, 8), Math.floorDiv(y, 6), Math.floorDiv(blockZ, 8)); + double grade = (0.2 + 1.6 * StableHash.unit(pocket)) * (1 - 0.65 * t); + return StableHash.unit(cellSeed(seed ^ 0x3C67B146EFA02491L, blockX, y, blockZ)) + < Math.min(1, density * grade); + } + } + + public static boolean placerPocket(long seed, int x, int z, int depth, double coverage) { + long pocket = cellSeed(seed, Math.floorDiv(x, 8), 0, Math.floorDiv(z, 8)); + double threshold = coverage * (0.7 + 0.6 * StableHash.unit(pocket)); + return StableHash.unit(cellSeed(seed ^ 0x2A1384DC931165B7L, + Math.floorDiv(x, 2), depth, Math.floorDiv(z, 2))) < threshold; + } + + private static long cellSeed(long seed, int x, int y, int z) { + return StableHash.combine(StableHash.combine(StableHash.combine(seed, x), y), z); + } +} diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/geometry/DiamondPipeWorkingsBody.java b/src/main/java/zone/moddev/mc/realisticdeposits/geometry/DiamondPipeWorkingsBody.java new file mode 100644 index 0000000..9c3e049 --- /dev/null +++ b/src/main/java/zone/moddev/mc/realisticdeposits/geometry/DiamondPipeWorkingsBody.java @@ -0,0 +1,58 @@ +package zone.moddev.mc.realisticdeposits.geometry; + +/** A shaft and short crosscuts through one pipe, without changing its ore plan. */ +public final class DiamondPipeWorkingsBody implements MineableDepositBody { + private final DepositIdentity identity; + private final DiamondDistrict.Pipe pipe; + private final int level; + private final double centerX; + private final double centerZ; + private final double radius; + private final double cosine; + private final double sine; + + public DiamondPipeWorkingsBody(DiamondDistrict district, int pipeIndex) { + pipe = district.pipes().get(pipeIndex); + DepositIdentity parent = district.identity(); + identity = new DepositIdentity(parent.worldSeed(), parent.dimension(), + parent.definition() + "/pipe_" + pipeIndex, parent.regionX(), parent.regionZ(), parent.salt()); + + level = Math.max(10, pipe.bottomY() + (int) ((pipe.topY() - pipe.bottomY()) * 0.4D)); + double height = (double) (level - pipe.bottomY()) / (pipe.topY() - pipe.bottomY()); + double phase = StableHash.unit(pipe.seed()) * Math.PI * 2; + centerX = pipe.centerX() + Math.cos(phase) * pipe.upperRadius() * 0.25D * (1 - height); + centerZ = pipe.centerZ() + Math.sin(phase) * pipe.upperRadius() * 0.25D * (1 - height); + radius = (3 + (pipe.upperRadius() - 3) * Math.pow(height, 0.75D)) * 0.65D; + + double direction = StableHash.unit(identity.seed()) * Math.PI * 2; + cosine = Math.cos(direction); + sine = Math.sin(direction); + } + + @Override public DepositIdentity identity() { return identity; } + @Override public int centerX() { return (int) Math.round(centerX); } + @Override public int centerY() { return level; } + @Override public int centerZ() { return (int) Math.round(centerZ); } + @Override public int strikeLength() { return (int) Math.ceil(radius * 2); } + @Override public BlockBounds bounds() { return pipe.bounds(); } + @Override public int minimumCandidateYAt(int x, int z) { return pipe.bottomY(); } + @Override public int maximumCandidateYAt(int x, int z) { return pipe.topY(); } + @Override public boolean isOreAt(int x, int y, int z) { return pipe.isOreAt(x, y, z); } + + @Override + public MinePoint miningRoutePoint(double along, double lateralOffset) { + // Keep the galleries inside the wider part of the pipe. The surface + // ladder reaches this level; miners can then follow the ore downwards. + double forward = along * radius / 36.0D; + double side = lateralOffset * radius / 24.0D; + double length = Math.hypot(forward, side); + + if (length > radius) { + forward *= radius / length; + side *= radius / length; + } + + return new MinePoint((int) Math.round(centerX + forward * cosine - side * sine), level, + (int) Math.round(centerZ + forward * sine + side * cosine)); + } +} diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/geometry/MineBlock.java b/src/main/java/zone/moddev/mc/realisticdeposits/geometry/MineBlock.java index e5d9c50..2c51026 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/geometry/MineBlock.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/geometry/MineBlock.java @@ -3,7 +3,8 @@ /** Stable palette used by the world save; numbers must not be reassigned. */ public enum MineBlock { AIR(0), PLANKS(1), FENCE(2), RAIL_NORTH_SOUTH(3), RAIL_EAST_WEST(4), - COBWEB(5), LADDER_WEST(6), COBBLESTONE(7); + COBWEB(5), LADDER_WEST(6), COBBLESTONE(7), + STAIRS_NORTH(8), STAIRS_SOUTH(9), STAIRS_EAST(10), STAIRS_WEST(11); private final int savedId; diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/geometry/MineDecline.java b/src/main/java/zone/moddev/mc/realisticdeposits/geometry/MineDecline.java new file mode 100644 index 0000000..5c00dc5 --- /dev/null +++ b/src/main/java/zone/moddev/mc/realisticdeposits/geometry/MineDecline.java @@ -0,0 +1,158 @@ +package zone.moddev.mc.realisticdeposits.geometry; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +/** Stepped access with turning landings instead of an open vertical shaft. */ +public final class MineDecline { + private static final long ACCESS_SALT = 0x6A09E667F3BCC909L; + private static final int[] DX = {1, 0, -1, 0}; + private static final int[] DZ = {0, 1, 0, -1}; + private final List steps; + + private MineDecline(List steps) { + this.steps = Collections.unmodifiableList(steps); + } + + public static MineDecline plan(MinePoint landing, int surfaceY, long identity) { + if (surfaceY <= landing.y() || surfaceY - landing.y() > AbandonedMinePlanner.MAXIMUM_SHAFT_DEPTH) { + throw new IllegalArgumentException("Mine access depth is outside the supported range"); + } + + Random random = new Random(StableHash.mix(identity ^ ACCESS_SALT)); + int forward = random.nextInt(4); + int across = (forward + (random.nextBoolean() ? 1 : 3)) % 4; + int x = landing.x(); + int y = landing.y(); + int z = landing.z(); + List steps = new ArrayList<>(); + steps.add(new Step(new MinePoint(x, y, z), forward, false)); + + while (y < surfaceY) { + int rise = Math.min(10 + random.nextInt(5), surfaceY - y); + + for (int level = 0; level < rise; level++) { + x += DX[forward]; + z += DZ[forward]; + steps.add(new Step(new MinePoint(x, y, z), forward, true)); + x += DX[forward]; + z += DZ[forward]; + y++; + steps.add(new Step(new MinePoint(x, y, z), forward, false)); + } + + if (y < surfaceY) { + for (int turn = 0; turn < 8; turn++) { + x += DX[across]; + z += DZ[across]; + steps.add(new Step(new MinePoint(x, y, z), across, false)); + } + forward = (forward + 2) % 4; + } + } + + return new MineDecline(steps); + } + + public List steps() { + return steps; + } + + public MinePoint entrance() { + return steps.get(steps.size() - 1).point(); + } + + void build(Map blocks) { + Map floors = new LinkedHashMap<>(); + Set air = new LinkedHashSet<>(); + + for (Step step : steps) { + MinePoint point = step.point; + int side = (step.direction + 1) % 4; + int floorY = point.y() - (step.stairs ? 0 : 1); + int firstAir = point.y() + (step.stairs ? 1 : 0); + + for (int width = -1; width <= 1; width++) { + int x = point.x() + DX[side] * width; + int z = point.z() + DZ[side] * width; + floors.put(new MinePoint(x, floorY, z).packed(), step.stairs ? stair(step.direction) : MineBlock.PLANKS); + if (step.stairs) floors.put(new MinePoint(x, floorY - 1, z).packed(), MineBlock.PLANKS); + + for (int y = firstAir; y <= point.y() + 3; y++) { + air.add(new MinePoint(x, y, z).packed()); + } + } + } + + // Lay the footing last so a bend cannot cut away its own steps. + for (long position : air) blocks.put(position, MineBlock.AIR); + blocks.putAll(floors); + + for (int index = 0; index < steps.size(); index++) { + Step step = steps.get(index); + if (index % 12 == 0 || index == steps.size() - 1) frame(blocks, air, floors.keySet(), step); + + if (step.point.y() >= entrance().y() - 4) { + int side = (step.direction + 1) % 4; + for (int edge : new int[] {-2, 2}) { + long position = new MinePoint(step.point.x() + DX[side] * edge, step.point.y(), + step.point.z() + DZ[side] * edge).packed(); + if (!air.contains(position) && !floors.containsKey(position)) blocks.put(position, MineBlock.FENCE); + } + } + } + } + + private static void frame(Map blocks, Set air, Set floors, Step step) { + int side = (step.direction + 1) % 4; + for (int width = -2; width <= 2; width++) { + int x = step.point.x() + DX[side] * width; + int z = step.point.z() + DZ[side] * width; + long beam = new MinePoint(x, step.point.y() + 4, z).packed(); + if (!air.contains(beam) && !floors.contains(beam)) blocks.put(beam, MineBlock.PLANKS); + + if (Math.abs(width) == 2) { + for (int y = step.point.y(); y < step.point.y() + 4; y++) { + long post = new MinePoint(x, y, z).packed(); + if (!air.contains(post) && !floors.contains(post)) blocks.put(post, MineBlock.FENCE); + } + } + } + } + + private static MineBlock stair(int direction) { + switch (direction) { + case 0: return MineBlock.STAIRS_EAST; + case 1: return MineBlock.STAIRS_SOUTH; + case 2: return MineBlock.STAIRS_WEST; + case 3: return MineBlock.STAIRS_NORTH; + default: throw new IllegalArgumentException("Unknown mine direction"); + } + } + + public static final class Step { + private final MinePoint point; + private final int direction; + private final boolean stairs; + + private Step(MinePoint point, int direction, boolean stairs) { + this.point = point; + this.direction = direction; + this.stairs = stairs; + } + + public MinePoint point() { + return point; + } + + public boolean stairs() { + return stairs; + } + } +} diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/integration/OreSpawnIntegration.java b/src/main/java/zone/moddev/mc/realisticdeposits/integration/OreSpawnIntegration.java index 7b38d33..b3fdac1 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/integration/OreSpawnIntegration.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/integration/OreSpawnIntegration.java @@ -15,48 +15,93 @@ public final class OreSpawnIntegration { new ResourceLocation("realisticdeposits", "stratiform_seam"); public static final ResourceLocation COAL_PATTERN_ID = new ResourceLocation("realisticdeposits", "coal_measures"); + public static final ResourceLocation DIAMOND_PIPE_ID = + new ResourceLocation("realisticdeposits", "diamond_pipe"); + public static final ResourceLocation DIAMOND_PLACER_ID = + new ResourceLocation("realisticdeposits", "diamond_placer"); private OreSpawnIntegration() { } public static WorldgenProvider provider(Iterable definitions) { + java.util.Map indexed = new java.util.LinkedHashMap<>(); + for (DepositDefinition definition : definitions) indexed.put(definition.id(), definition); + validateDiamondLinks(indexed.values()); WorldgenProvider.Builder provider = WorldgenProvider.builder("realisticdeposits", 1) .mergeNewEntriesIntoExistingWorlds(false); - for (DepositDefinition definition : definitions) add(provider, definition); + for (DepositDefinition definition : indexed.values()) add(provider, definition, indexed); return provider.build(); } + public static void validateDiamondLinks(Iterable definitions) { + java.util.Map indexed = new java.util.LinkedHashMap<>(); + for (DepositDefinition definition : definitions) indexed.put(definition.id(), definition); + for (DepositDefinition definition : indexed.values()) { + if (!definition.isDiamondPlacer()) continue; + DepositDefinition source = indexed.get(definition.placer().sourceDeposit()); + if (source == null || !source.isDiamondPipe() + || !source.dimensions().containsAll(definition.dimensions())) { + throw new IllegalArgumentException("Diamond placer '" + definition.id() + + "' needs a pipe source in the same dimensions: " + definition.placer().sourceDeposit()); + } + } + } + + private static ResourceLocation patternId(DepositDefinition definition) { + switch (definition.shape().type()) { + case "stratiform_seam": return PATTERN_ID; + case "coal_measures": return COAL_PATTERN_ID; + case "diamond_pipe": return DIAMOND_PIPE_ID; + case "diamond_placer": return DIAMOND_PLACER_ID; + default: throw new IllegalArgumentException("Unsupported shape: " + definition.shape().type()); + } + } + public static boolean enqueue(Iterable definitions) { return OreSpawnApi.enqueue(provider(definitions)); } - private static void add(WorldgenProvider.Builder provider, DepositDefinition definition) { + private static void add(WorldgenProvider.Builder provider, DepositDefinition definition, + java.util.Map indexed) { ResourceLocation output = new ResourceLocation(definition.resource()); ResourceLocation oreId = new ResourceLocation("realisticdeposits", "deposit/" + new ResourceLocation(definition.id()).getResourceDomain() + "/" + new ResourceLocation(definition.id()).getResourcePath()); JsonObject patternSettings = new JsonObject(); patternSettings.add("definition", definition.toJson()); + DepositDefinition source = definition.isDiamondPlacer() ? indexed.get(definition.placer().sourceDeposit()) : null; + if (source != null) patternSettings.add("source_definition", source.toJson()); provider.ore(oreId, output, ore -> { ore.retrogen(false).suppressVanilla(false); - if (definition.isCoal()) ore.enabled(definition.enabled()); + if (definition.isCoal() || definition.isDiamond()) { + ore.enabled(definition.enabled() && (source == null || source.enabled())); + } + if (definition.isDiamondPlacer()) { + // Sediment is not an interchangeable output for diamond-bearing rock. + ore.material(new ResourceLocation("realisticdeposits", "diamond_gravel")); + } for (String dimensionId : definition.dimensions()) { ore.dimension(new ResourceLocation(dimensionId), dimension -> { // Keep the enclosing OreSpawn attempt broad: live geometry reload may // change the deposit's own height envelope without mutating a saved profile. dimension.yRange(0, 255) .attempts(1.0D).quantity(1).spread(0, 0).nodeSize(1) - .pattern(definition.isCoal() ? COAL_PATTERN_ID : PATTERN_ID, patternSettings); + .pattern(patternId(definition), patternSettings); for (String host : definition.hosts()) { - if (definition.isCoal()) { + if (definition.isCoal() || definition.isDiamondPlacer()) { ResourceLocation id = new ResourceLocation(host); if (net.minecraft.block.Block.REGISTRY.containsKey(id)) dimension.hostState(id, 0); + if (definition.isDiamondPlacer() && "minecraft:sand".equals(host)) dimension.hostState(id, 1); + } else if (definition.isDiamondPipe() && "minecraft:stone".equals(host)) { + for (int metadata : new int[] {0, 1, 3, 5}) { + dimension.hostState(new ResourceLocation(host), metadata); + } } else { dimension.hostBlock(new ResourceLocation(host)); } } // Every geology family is a safe outer host. ROCK_ALIGNED applies the // current live family selection per output block inside the pattern. - if (!definition.isCoal()) { + if (!definition.isCoal() && !definition.isDiamondPlacer()) { for (GeologyFamily family : GeologyFamily.values()) dimension.hostFamily(family); } for (String biome : definition.biomeIds()) dimension.biome(new ResourceLocation(biome)); diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/model/DepositDefinition.java b/src/main/java/zone/moddev/mc/realisticdeposits/model/DepositDefinition.java index a9ffc1e..dd8ac2d 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/model/DepositDefinition.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/model/DepositDefinition.java @@ -113,6 +113,12 @@ public DepositDefinition(String id, boolean enabled, String resource, this.salt = salt; this.shape = Objects.requireNonNull(seam, "shape"); this.abandonedMine = Objects.requireNonNull(abandonedMine, "abandonedMine"); + if (isDiamondPlacer() && abandonedMine.enabled()) { + throw new IllegalArgumentException("River placers do not support underground workings: " + id); + } + if (isDiamondPlacer() && interaction != InteractionPolicy.ADDITIVE) { + throw new IllegalArgumentException("The source pipe, not its placer, controls background diamonds: " + id); + } if (alignment == AlignmentMode.MASSIVE_RARE && (!this.geomes.isEmpty() || !this.rockFamilies.isEmpty())) { throw new IllegalArgumentException("Massive-and-rare deposit cannot declare geology alignment: " + id); @@ -167,6 +173,11 @@ public OptionalDouble effectiveBackgroundGenerationScale() { public long salt() { return salt; } public DepositShapeSettings shape() { return shape; } public boolean isCoal() { return shape instanceof CoalMeasuresSettings; } + public boolean isDiamondPipe() { return shape instanceof DiamondPipeSettings; } + public boolean isDiamondPlacer() { return shape instanceof DiamondPlacerSettings; } + public boolean isDiamond() { return isDiamondPipe() || isDiamondPlacer(); } + public DiamondPipeSettings pipe() { return (DiamondPipeSettings) shape; } + public DiamondPlacerSettings placer() { return (DiamondPlacerSettings) shape; } public StratiformSeamSettings seam() { return (StratiformSeamSettings) shape; } public CoalMeasuresSettings coal() { return (CoalMeasuresSettings) shape; } @@ -183,7 +194,8 @@ public String structuralFingerprint() { + (effectiveBackgroundGenerationScale().isPresent() ? effectiveBackgroundGenerationScale().getAsDouble() : "additive") + (abandonedMine.enabled() ? "|abandoned_mine:" + abandonedMine.chance() : "") - + (isCoal() ? "|shape:coal_measures" : ""); + + (isCoal() ? "|shape:coal_measures" : "") + + (isDiamond() ? "|diamond:" + toJson() : ""); } /** Deterministic schema-1 representation embedded in OreSpawn pattern settings. */ diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/model/DiamondPipeSettings.java b/src/main/java/zone/moddev/mc/realisticdeposits/model/DiamondPipeSettings.java new file mode 100644 index 0000000..a5ea2b1 --- /dev/null +++ b/src/main/java/zone/moddev/mc/realisticdeposits/model/DiamondPipeSettings.java @@ -0,0 +1,72 @@ +package zone.moddev.mc.realisticdeposits.model; + +import java.util.Objects; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +/** Dimensions and grade of a small district of steep diamond pipes. */ +public final class DiamondPipeSettings implements DepositShapeSettings { + private final IntRange pipes; + private final IntRange diameter; + private final IntRange rootY; + private final IntRange cover; + private final int radius; + private final double density; + private final double legacyScale; + + public DiamondPipeSettings(IntRange pipes, IntRange diameter, IntRange rootY, + IntRange cover, int radius, double density, double legacyScale) { + this.pipes = checked(pipes, 1, 8, "pipe count"); + this.diameter = checked(diameter, 8, 256, "upper diameter"); + this.rootY = checked(rootY, 1, 128, "root height"); + this.cover = checked(cover, 1, 64, "surface cover"); + + if (radius < 32 || radius > 512 || !Double.isFinite(density) || density <= 0 || density > 1 + || !Double.isFinite(legacyScale) || legacyScale <= 0 || legacyScale > 1) { + throw new IllegalArgumentException("Invalid diamond district radius, density or legacy selection scale"); + } + + this.radius = radius; + this.density = density; + this.legacyScale = legacyScale; + } + + @Override public String type() { return "diamond_pipe"; } + public IntRange pipeCount() { return pipes; } + public IntRange upperDiameter() { return diameter; } + public IntRange rootY() { return rootY; } + public IntRange surfaceCover() { return cover; } + public int districtRadius() { return radius; } + public double oreDensity() { return density; } + public double legacySelectionScale() { return legacyScale; } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("type", type()); + json.add("pipes", array(pipes)); + json.add("upper_diameter", array(diameter)); + json.add("root_y", array(rootY)); + json.add("surface_cover", array(cover)); + json.addProperty("district_radius", radius); + json.addProperty("ore_density", density); + json.addProperty("legacy_selection_scale", legacyScale); + return json; + } + + private static IntRange checked(IntRange value, int min, int max, String name) { + Objects.requireNonNull(value, name); + if (value.minimum() < min || value.maximum() > max) { + throw new IllegalArgumentException("Invalid diamond " + name); + } + return value; + } + + static JsonArray array(IntRange value) { + JsonArray array = new JsonArray(); + array.add(new com.google.gson.JsonPrimitive(value.minimum())); + array.add(new com.google.gson.JsonPrimitive(value.maximum())); + return array; + } +} diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/model/DiamondPlacerSettings.java b/src/main/java/zone/moddev/mc/realisticdeposits/model/DiamondPlacerSettings.java new file mode 100644 index 0000000..3e2857d --- /dev/null +++ b/src/main/java/zone/moddev/mc/realisticdeposits/model/DiamondPlacerSettings.java @@ -0,0 +1,41 @@ +package zone.moddev.mc.realisticdeposits.model; + +import com.google.gson.JsonObject; + +/** River pockets supplied by one captured diamond-pipe definition. */ +public final class DiamondPlacerSettings implements DepositShapeSettings { + private final String source; + private final int distance; + private final IntRange thickness; + private final double coverage; + + public DiamondPlacerSettings(String source, int distance, IntRange thickness, double coverage) { + this.source = DepositDefinition.identifier(source, "placer source"); + if (distance < 32 || distance > 2048 || thickness == null + || thickness.minimum() < 1 || thickness.maximum() > 3 + || !Double.isFinite(coverage) || coverage <= 0 || coverage > 1) { + throw new IllegalArgumentException("Invalid diamond placer distance, thickness or coverage"); + } + + this.distance = distance; + this.thickness = thickness; + this.coverage = coverage; + } + + @Override public String type() { return "diamond_placer"; } + public String sourceDeposit() { return source; } + public int maximumSourceDistance() { return distance; } + public IntRange thickness() { return thickness; } + public double coverage() { return coverage; } + + @Override + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("type", type()); + json.addProperty("source_deposit", source); + json.addProperty("maximum_source_distance", distance); + json.add("thickness", DiamondPipeSettings.array(thickness)); + json.addProperty("coverage", coverage); + return json; + } +} diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/proxy/ClientProxy.java b/src/main/java/zone/moddev/mc/realisticdeposits/proxy/ClientProxy.java index 891921d..702339b 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/proxy/ClientProxy.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/proxy/ClientProxy.java @@ -4,12 +4,24 @@ import zone.moddev.mc.realisticdeposits.client.RealisticDepositsConfigScreen; import net.minecraft.util.ResourceLocation; +import net.minecraft.block.Block; +import net.minecraft.item.Item; +import net.minecraft.client.renderer.block.model.ModelResourceLocation; +import net.minecraftforge.client.model.ModelLoader; +import zone.moddev.mc.realisticdeposits.content.ModContent; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; /** Registers the optional action in OreSpawn's world-settings screen. */ @SideOnly(Side.CLIENT) public final class ClientProxy extends CommonProxy { + @Override + public void registerClientModels() { + Block block = ModContent.DIAMOND_GRAVEL; + ModelLoader.setCustomModelResourceLocation(Item.getItemFromBlock(block), 0, + new ModelResourceLocation(block.getRegistryName(), "inventory")); + } + @Override public void initializeClient() { WorldSettingsExtensionRegistry.register( diff --git a/src/main/java/zone/moddev/mc/realisticdeposits/proxy/CommonProxy.java b/src/main/java/zone/moddev/mc/realisticdeposits/proxy/CommonProxy.java index 55128e5..14e1a91 100644 --- a/src/main/java/zone/moddev/mc/realisticdeposits/proxy/CommonProxy.java +++ b/src/main/java/zone/moddev/mc/realisticdeposits/proxy/CommonProxy.java @@ -2,5 +2,6 @@ /** Server-safe loader proxy. */ public class CommonProxy { + public void registerClientModels() { } public void initializeClient() { } } diff --git a/src/main/resources/assets/realisticdeposits/blockstates/diamond_gravel.json b/src/main/resources/assets/realisticdeposits/blockstates/diamond_gravel.json new file mode 100644 index 0000000..90cd178 --- /dev/null +++ b/src/main/resources/assets/realisticdeposits/blockstates/diamond_gravel.json @@ -0,0 +1,5 @@ +{ + "variants": { + "normal": { "model": "realisticdeposits:diamond_gravel" } + } +} diff --git a/src/main/resources/assets/realisticdeposits/defaults/coal.json b/src/main/resources/assets/realisticdeposits/defaults/coal.json index 01dd950..4946762 100644 --- a/src/main/resources/assets/realisticdeposits/defaults/coal.json +++ b/src/main/resources/assets/realisticdeposits/defaults/coal.json @@ -17,7 +17,7 @@ "region_spacing_chunks": 32, "selection_chance": 0.5, "salt": 2685821657736338717, - "abandoned_mine": {"enabled": false, "chance": 0.15}, + "abandoned_mine": {"enabled": true, "chance": 1.0}, "shape": { "type": "coal_measures", "strike_length": [384, 768], diff --git a/src/main/resources/assets/realisticdeposits/defaults/diamonds.json b/src/main/resources/assets/realisticdeposits/defaults/diamonds.json new file mode 100644 index 0000000..4402e72 --- /dev/null +++ b/src/main/resources/assets/realisticdeposits/defaults/diamonds.json @@ -0,0 +1,54 @@ +{ + "schema_version": 1, + "catalog": "realisticdeposits:diamonds", + "deposits": [ + { + "id": "realisticdeposits:overworld_diamond_pipes", + "enabled": true, + "resource": "minecraft:diamond_ore", + "alignment": "geome_aligned", + "interaction": "hybrid", + "background_generation_scale": 0.15, + "dimensions": ["minecraft:overworld"], + "hosts": ["minecraft:stone"], + "geomes": ["orespawn:stable_craton"], + "min_y": 4, + "max_y": 128, + "region_spacing_chunks": 32, + "selection_chance": 0.5, + "salt": 7146057694168187206, + "abandoned_mine": {"enabled": true, "chance": 1.0}, + "shape": { + "type": "diamond_pipe", + "pipes": [1, 3], + "upper_diameter": [48, 96], + "root_y": [4, 12], + "surface_cover": [4, 12], + "district_radius": 192, + "ore_density": 0.28, + "legacy_selection_scale": 0.25 + } + }, + { + "id": "realisticdeposits:overworld_diamond_placers", + "enabled": true, + "resource": "realisticdeposits:diamond_gravel", + "alignment": "massive_rare", + "interaction": "additive", + "dimensions": ["minecraft:overworld"], + "hosts": ["minecraft:gravel", "minecraft:sand"], + "min_y": 8, + "max_y": 128, + "region_spacing_chunks": 32, + "selection_chance": 1.0, + "salt": 7146057694168187207, + "shape": { + "type": "diamond_placer", + "source_deposit": "realisticdeposits:overworld_diamond_pipes", + "maximum_source_distance": 1024, + "thickness": [1, 3], + "coverage": 0.35 + } + } + ] +} diff --git a/src/main/resources/assets/realisticdeposits/defaults/vanilla.json b/src/main/resources/assets/realisticdeposits/defaults/vanilla.json index 74dfd9f..5e3b950 100644 --- a/src/main/resources/assets/realisticdeposits/defaults/vanilla.json +++ b/src/main/resources/assets/realisticdeposits/defaults/vanilla.json @@ -21,8 +21,8 @@ "selection_chance": 0.25, "salt": 7146057694168187204, "abandoned_mine": { - "enabled": false, - "chance": 0.15 + "enabled": true, + "chance": 1.0 }, "shape": { "type": "stratiform_seam", diff --git a/src/main/resources/assets/realisticdeposits/lang/en_US.lang b/src/main/resources/assets/realisticdeposits/lang/en_US.lang index ab49c35..1da67c4 100644 --- a/src/main/resources/assets/realisticdeposits/lang/en_US.lang +++ b/src/main/resources/assets/realisticdeposits/lang/en_US.lang @@ -2,9 +2,10 @@ realisticdeposits.name=Realistic Deposits screen.realisticdeposits.title=Realistic Deposits screen.realisticdeposits.loaded=%s catalog(s), %s deposit definition(s) loaded screen.realisticdeposits.generation_status=World generation: %s -screen.realisticdeposits.reload_policy=Geometry/alignment reload live; structural edits require restart +screen.realisticdeposits.reload_policy=Iron and coal shapes reload live. Linked diamond settings and other structural edits require a restart. screen.realisticdeposits.orespawn_ui=Also available from OreSpawn World Settings button.realisticdeposits.settings=Realistic Deposits button.realisticdeposits.reload=Reload Definitions status.realisticdeposits.reload_ok=Definitions reloaded; structural edits, if any, are deferred. status.realisticdeposits.reload_failed=Reload failed: %s +tile.realisticdeposits.diamond_gravel.name=Diamond-bearing Gravel diff --git a/src/main/resources/assets/realisticdeposits/models/block/diamond_gravel.json b/src/main/resources/assets/realisticdeposits/models/block/diamond_gravel.json new file mode 100644 index 0000000..7ccd993 --- /dev/null +++ b/src/main/resources/assets/realisticdeposits/models/block/diamond_gravel.json @@ -0,0 +1,32 @@ +{ + "parent": "block/block", + "textures": { + "particle": "blocks/gravel", + "base": "blocks/gravel", + "overlay": "realisticdeposits:blocks/diamond_gravel_overlay" + }, + "elements": [ + { + "from": [0, 0, 0], "to": [16, 16, 16], + "faces": { + "down": { "texture": "#base", "cullface": "down" }, + "up": { "texture": "#base", "cullface": "up" }, + "north": { "texture": "#base", "cullface": "north" }, + "south": { "texture": "#base", "cullface": "south" }, + "west": { "texture": "#base", "cullface": "west" }, + "east": { "texture": "#base", "cullface": "east" } + } + }, + { + "from": [-0.05, -0.05, -0.05], "to": [16.05, 16.05, 16.05], + "faces": { + "down": { "texture": "#overlay", "cullface": "down" }, + "up": { "texture": "#overlay", "cullface": "up" }, + "north": { "texture": "#overlay", "cullface": "north" }, + "south": { "texture": "#overlay", "cullface": "south" }, + "west": { "texture": "#overlay", "cullface": "west" }, + "east": { "texture": "#overlay", "cullface": "east" } + } + } + ] +} diff --git a/src/main/resources/assets/realisticdeposits/models/item/diamond_gravel.json b/src/main/resources/assets/realisticdeposits/models/item/diamond_gravel.json new file mode 100644 index 0000000..99a5c21 --- /dev/null +++ b/src/main/resources/assets/realisticdeposits/models/item/diamond_gravel.json @@ -0,0 +1,3 @@ +{ + "parent": "realisticdeposits:block/diamond_gravel" +} diff --git a/src/main/resources/assets/realisticdeposits/textures/blocks/diamond_gravel_overlay.png b/src/main/resources/assets/realisticdeposits/textures/blocks/diamond_gravel_overlay.png new file mode 100644 index 0000000000000000000000000000000000000000..5bd04047446bb048dbc2494e11fff9c14578b2dc GIT binary patch literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V4kOoV@QO1a)Jcw;slYNra%XlihqxvhnE=c zYMNXAu4C=@)TBW z{r~>w{rZ0kyC?Ie?fd^xJhn$d;NB(gY5M{VxZDnEEYN0Pm|&y$KrCN=3(#f;Pgg&e IbxsLQ01E_AKL7v# literal 0 HcmV?d00001 diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/DiamondFixtures.java b/src/test/java/zone/moddev/mc/realisticdeposits/DiamondFixtures.java new file mode 100644 index 0000000..8bb53ee --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/DiamondFixtures.java @@ -0,0 +1,25 @@ +package zone.moddev.mc.realisticdeposits; + +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import zone.moddev.mc.realisticdeposits.config.DepositCatalogLoader; +import zone.moddev.mc.realisticdeposits.model.DepositDefinition; + +/** Shared defaults for diamond tests, not a second set of generation settings. */ +public final class DiamondFixtures { + private DiamondFixtures() { } + + public static List definitions() { + try (InputStreamReader source = new InputStreamReader(DiamondFixtures.class.getResourceAsStream( + "/assets/realisticdeposits/defaults/diamonds.json"), StandardCharsets.UTF_8)) { + return DepositCatalogLoader.load(source, "diamond fixture").definitions(); + } catch (java.io.IOException e) { + throw new IllegalStateException("Could not read the diamond fixture", e); + } + } + + public static DepositDefinition pipe() { return definitions().get(0); } + public static DepositDefinition placer() { return definitions().get(1); } +} diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/ProjectIdentityTest.java b/src/test/java/zone/moddev/mc/realisticdeposits/ProjectIdentityTest.java index 64072b9..81a9fa9 100644 --- a/src/test/java/zone/moddev/mc/realisticdeposits/ProjectIdentityTest.java +++ b/src/test/java/zone/moddev/mc/realisticdeposits/ProjectIdentityTest.java @@ -16,7 +16,7 @@ class ProjectIdentityTest { void usesStablePublicIdentity() { assertEquals("realisticdeposits", RealisticDeposits.MOD_ID); assertEquals("Realistic Deposits", RealisticDeposits.NAME); - assertEquals("0.2.0.110021", RealisticDeposits.VERSION); + assertEquals("0.3.0.110021", RealisticDeposits.VERSION); net.minecraftforge.fml.common.Mod annotation = RealisticDeposits.class.getAnnotation( net.minecraftforge.fml.common.Mod.class); assertTrue(annotation.guiFactory().endsWith("RealisticDepositsGuiFactory")); diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/config/CoalCatalogTest.java b/src/test/java/zone/moddev/mc/realisticdeposits/config/CoalCatalogTest.java index f61f6db..bc39eea 100644 --- a/src/test/java/zone/moddev/mc/realisticdeposits/config/CoalCatalogTest.java +++ b/src/test/java/zone/moddev/mc/realisticdeposits/config/CoalCatalogTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -28,7 +27,8 @@ void shipsCoalSeparatelyWithoutChangingTheIronCatalogue() { assertEquals(0.15D, catalogue.definitions().get(0).effectiveBackgroundGenerationScale().getAsDouble()); assertTrue(catalogue.definitions().get(0).isCoal()); assertEquals(32, catalogue.definitions().get(0).regionSpacingChunks()); - assertFalse(catalogue.definitions().get(0).abandonedMine().enabled()); + assertTrue(catalogue.definitions().get(0).abandonedMine().enabled()); + assertEquals(1.0D, catalogue.definitions().get(0).abandonedMine().chance()); } @Test diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/config/DiamondCatalogTest.java b/src/test/java/zone/moddev/mc/realisticdeposits/config/DiamondCatalogTest.java new file mode 100644 index 0000000..2de0e69 --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/config/DiamondCatalogTest.java @@ -0,0 +1,100 @@ +package zone.moddev.mc.realisticdeposits.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.StringReader; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Test; +import zone.moddev.mc.realisticdeposits.model.DepositDefinition; +import zone.moddev.mc.realisticdeposits.DiamondFixtures; +import zone.moddev.mc.realisticdeposits.integration.OreSpawnIntegration; + +class DiamondCatalogTest { + + static JsonObject pipe() { + return new JsonParser().parse("{\"id\":\"realisticdeposits:overworld_diamond_pipes\"," + + "\"enabled\":true,\"resource\":\"minecraft:diamond_ore\"," + + "\"alignment\":\"geome_aligned\",\"interaction\":\"hybrid\"," + + "\"background_generation_scale\":0.15,\"dimensions\":[\"minecraft:overworld\"]," + + "\"hosts\":[\"minecraft:stone\"],\"geomes\":[\"orespawn:stable_craton\"]," + + "\"min_y\":4,\"max_y\":128,\"region_spacing_chunks\":32," + + "\"selection_chance\":0.5,\"salt\":7146057694168187206," + + "\"shape\":{\"type\":\"diamond_pipe\",\"pipes\":[1,3]," + + "\"upper_diameter\":[48,96],\"root_y\":[4,12],\"surface_cover\":[4,12]," + + "\"district_radius\":192,\"ore_density\":0.05,\"legacy_selection_scale\":0.25}}") + .getAsJsonObject(); + } + + @Test + void understandsDiamondPipesAsTheirOwnShape() { + assertEquals("diamond_pipe", parsed(pipe()).shape().type()); + } + + @Test + void shipsLinkedPipesAndPlacersWithOnlyOneBackgroundController() { + net.minecraft.init.Bootstrap.register(); + java.util.List definitions = DiamondFixtures.definitions(); + assertEquals(2, definitions.size()); + assertTrue(definitions.get(0).isDiamondPipe()); + assertTrue(definitions.get(1).isDiamondPlacer()); + assertEquals(definitions.get(0).id(), definitions.get(1).placer().sourceDeposit()); + assertFalse(definitions.get(1).effectiveBackgroundGenerationScale().isPresent()); + + JsonObject ores = OreSpawnIntegration.provider(definitions).toJson().getAsJsonObject("ores"); + int controllers = 0; + for (java.util.Map.Entry entry : ores.entrySet()) { + JsonObject rule = entry.getValue().getAsJsonObject(); + JsonObject dimension = rule.getAsJsonObject("dimensions").getAsJsonObject("minecraft:overworld"); + if (dimension.has("background_generation_scale")) controllers++; + if (entry.getKey().endsWith("placers")) { + assertEquals("realisticdeposits:diamond_gravel", rule.get("material").getAsString()); + assertEquals(0, dimension.getAsJsonArray("host_families").size()); + assertTrue(dimension.getAsJsonObject("pattern").getAsJsonObject("settings").has("source_definition")); + } + } + assertEquals(1, controllers); + } + + @Test + void disabledSourcesDisablePlacersAndMissingSourcesAreRejected() { + net.minecraft.init.Bootstrap.register(); + JsonObject disabled = pipe(); + disabled.addProperty("enabled", false); + JsonObject ores = OreSpawnIntegration.provider(java.util.Arrays.asList(parsed(disabled), DiamondFixtures.placer())) + .toJson().getAsJsonObject("ores"); + for (com.google.gson.JsonElement rule : ores.entrySet().stream().map(java.util.Map.Entry::getValue) + .collect(java.util.stream.Collectors.toList())) { + assertFalse(rule.getAsJsonObject().get("enabled").getAsBoolean()); + } + assertThrows(IllegalArgumentException.class, () -> OreSpawnIntegration.provider( + java.util.Collections.singleton(DiamondFixtures.placer()))); + } + + @Test + void linkedGeometryIsStructuralAndInvalidGeometryIsRejected() { + JsonObject changed = pipe(); + changed.getAsJsonObject("shape").addProperty("ore_density", 0.07); + assertFalse(parsed(pipe()).structuralFingerprint().equals(parsed(changed).structuralFingerprint())); + changed.getAsJsonObject("shape").addProperty("ore_density", -1); + assertThrows(IllegalArgumentException.class, () -> parsed(changed)); + + JsonObject workings = pipe(); + workings.add("abandoned_mine", new JsonParser().parse("{\"enabled\":true,\"chance\":1}")); + assertTrue(parsed(workings).abandonedMine().enabled()); + JsonObject placer = DiamondFixtures.placer().toJson(); + placer.add("abandoned_mine", workings.get("abandoned_mine")); + assertThrows(IllegalArgumentException.class, () -> parsed(placer)); + } + + private static DepositDefinition parsed(JsonObject definition) { + String json = "{\"schema_version\":1,\"catalog\":\"realisticdeposits:diamonds\"," + + "\"deposits\":[" + definition + "]}"; + DepositCatalog catalogue = DepositCatalogLoader.load(new StringReader(json), "diamond test"); + return catalogue.definitions().get(0); + } +} diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/config/MineDefaultsTest.java b/src/test/java/zone/moddev/mc/realisticdeposits/config/MineDefaultsTest.java new file mode 100644 index 0000000..8c649b2 --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/config/MineDefaultsTest.java @@ -0,0 +1,72 @@ +package zone.moddev.mc.realisticdeposits.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +import com.google.gson.JsonObject; +import org.junit.jupiter.api.Test; + +import zone.moddev.mc.realisticdeposits.model.DepositDefinition; + +class MineDefaultsTest { + + @Test + void everyNewUndergroundDepositAttemptsWorkings() { + for (String name : new String[] {"vanilla", "coal", "diamonds"}) { + for (DepositDefinition definition : catalogue(name).definitions()) { + if (definition.isDiamondPlacer()) { + assertFalse(definition.abandonedMine().enabled()); + } else { + assertTrue(definition.abandonedMine().enabled(), definition.id()); + assertEquals(1.0D, definition.abandonedMine().chance(), definition.id()); + } + } + } + } + + @Test + void diamondPipesCanCaptureWorkingsWithoutMovingTheDeposit() { + DepositDefinition original = catalogue("diamonds").definitions().get(0); + JsonObject json = original.toJson(); + JsonObject mine = new JsonObject(); + mine.addProperty("enabled", true); + mine.addProperty("chance", 1.0D); + json.add("abandoned_mine", mine); + + DepositDefinition changed = DepositCatalogLoader.loadDefinition(json, "diamond workings"); + assertTrue(changed.abandonedMine().enabled()); + assertEquals(original.shape().toJson(), changed.shape().toJson()); + assertEquals(original.salt(), changed.salt()); + assertEquals(mine, changed.toJson().getAsJsonObject("abandoned_mine")); + } + + @Test + void oldProfilesAndExplicitOptOutsStayOff() { + for (String name : new String[] {"vanilla", "coal", "diamonds"}) { + for (DepositDefinition definition : catalogue(name).definitions()) { + JsonObject legacy = definition.toJson(); + legacy.remove("abandoned_mine"); + DepositDefinition parsed = DepositCatalogLoader.loadDefinition(legacy, "old world"); + assertFalse(parsed.abandonedMine().enabled()); + assertFalse(parsed.toJson().has("abandoned_mine")); + + JsonObject disabled = new JsonObject(); + disabled.addProperty("enabled", false); + disabled.addProperty("chance", 0.37D); + legacy.add("abandoned_mine", disabled); + parsed = DepositCatalogLoader.loadDefinition(legacy, "custom opt-out"); + assertFalse(parsed.abandonedMine().enabled()); + assertEquals(disabled, parsed.toJson().getAsJsonObject("abandoned_mine")); + } + } + } + + private static DepositCatalog catalogue(String name) { + return DepositCatalogLoader.load(new InputStreamReader(MineDefaultsTest.class.getResourceAsStream( + "/assets/realisticdeposits/defaults/" + name + ".json"), StandardCharsets.UTF_8), name); + } +} diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/content/DiamondGravelTest.java b/src/test/java/zone/moddev/mc/realisticdeposits/content/DiamondGravelTest.java new file mode 100644 index 0000000..515681e --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/content/DiamondGravelTest.java @@ -0,0 +1,29 @@ +package zone.moddev.mc.realisticdeposits.content; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Random; + +import org.junit.jupiter.api.Test; + +class DiamondGravelTest { + @Test + void plainAndFortuneRewardsFollowTheOneInSixteenContract() { + int trials = 320000; + for (int fortune : new int[] {0, 1, 2, 3}) { + Random random = new Random(7117); + int diamonds = 0; + int successes = 0; + for (int i = 0; i < trials; i++) { + int count = DiamondGravelBlock.diamondYield(random, fortune); + assertTrue(count >= 0 && count <= Math.max(1, fortune + 1)); + diamonds += count; + if (count > 0) successes++; + } + double multiplier = fortune == 0 ? 1 : (double) (1 + (fortune + 1) * (fortune + 2) / 2) / (fortune + 2); + assertEquals(1D / 16, (double) successes / trials, 0.002); + assertEquals(multiplier / 16, (double) diamonds / trials, 0.003); + } + } +} diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/generation/CoalRuntimeProbe.java b/src/test/java/zone/moddev/mc/realisticdeposits/generation/CoalRuntimeProbe.java index 73087b1..b6eca92 100644 --- a/src/test/java/zone/moddev/mc/realisticdeposits/generation/CoalRuntimeProbe.java +++ b/src/test/java/zone/moddev/mc/realisticdeposits/generation/CoalRuntimeProbe.java @@ -88,6 +88,16 @@ public void started(FMLServerStartedEvent event) throws Exception { } if ("first".equals(phase)) populate(world, first); if ("later".equals(phase)) populate(world, later); + if (Boolean.getBoolean("rd.coal.workings")) { + CoalMeasuresBody mineBody = "later".equals(phase) || "stable".equals(phase) ? later : first; + AbandonedMinePlan plan = MineWorldData.get(world).plan(mineBody.identity().toString()); + if (plan == null) throw new IllegalStateException("The selected coal workings were not built"); + for (long chunk : MineWorkingsRuntimeProbe.allChunks(plan)) { + MineWorkingsRuntimeProbe.populate(world, (int) (chunk >> 32), (int) chunk); + } + MineWorkingsRuntimeProbe.verify(world, plan, MineWorkingsRuntimeProbe.allChunks(plan), false); + MineWorkingsRuntimeProbe.verifyAccess(world, plan, false); + } JsonObject report = new JsonObject(); report.addProperty("phase", phase); @@ -128,8 +138,9 @@ private static void find(WorldServer world, DepositDefinition definition, Geolog if (workings) { MineTerrainSampler terrain = new MineTerrainSampler(world, world.getChunkProvider().chunkGenerator); Optional mine = AbandonedMinePlanner.plan(fitted.get(), definition.abandonedMine(), - terrain.surfaceY(AbandonedMinePlanner.landing(fitted.get()))); + terrain::naturalSurfaceY); if (!mine.isPresent()) { rejected[1]++; continue; } + if (terrain.surfaceY(mine.get().entrance()) != mine.get().entrance().y()) { rejected[1]++; continue; } if (!terrain.dry(mine.get())) { rejected[2]++; continue; } if (!terrain.covered(mine.get())) { rejected[3]++; continue; } if (!MineWorkingsGenerator.biomesAllowed(mine.get(), definition, world)) { rejected[4]++; continue; } diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondGravelRuntimeChecks.java b/src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondGravelRuntimeChecks.java new file mode 100644 index 0000000..b58d6ef --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondGravelRuntimeChecks.java @@ -0,0 +1,115 @@ +package zone.moddev.mc.realisticdeposits.generation; + +import java.util.List; + +import com.google.gson.JsonObject; + +import net.minecraft.block.Block; +import net.minecraft.entity.item.EntityFallingBlock; +import net.minecraft.entity.item.EntityItem; +import net.minecraft.init.Blocks; +import net.minecraft.init.Enchantments; +import net.minecraft.init.Items; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.WorldServer; +import net.minecraftforge.common.util.FakePlayer; +import net.minecraftforge.common.util.FakePlayerFactory; + +/** Native harvest and gravity paths, used only in the disposable server fixture. */ +final class DiamondGravelRuntimeChecks { + private static final BlockPos POS = new BlockPos(8, 200, 8); + private static final AxisAlignedBB AREA = new AxisAlignedBB(0, 170, 0, 16, 240, 16); + + private DiamondGravelRuntimeChecks() { } + + static JsonObject run(WorldServer world, Block rich) { + FakePlayer player = FakePlayerFactory.getMinecraft(world); + ItemStack shovel = new ItemStack(Items.DIAMOND_SHOVEL); + Item richItem = Item.getItemFromBlock(rich); + clearItems(world); + + for (int i = 0; i < 128; i++) { + world.setBlockState(POS, rich.getDefaultState(), 2); + world.setBlockToAir(POS); + rich.harvestBlock(world, player, POS, rich.getDefaultState(), null, shovel); + List drops = items(world); + require(count(drops, Item.getItemFromBlock(Blocks.GRAVEL)) == 1, "Mining must return one spent gravel"); + require(count(drops, richItem) == 0, "Mining must consume the rich block"); + require(drops.stream().allMatch(item -> item.getEntityItem().getItem() == Items.DIAMOND + || item.getEntityItem().getItem() == Item.getItemFromBlock(Blocks.GRAVEL)), "Unexpected mining drop"); + clearItems(world); + } + + shovel.addEnchantment(Enchantments.SILK_TOUCH, 1); + rich.harvestBlock(world, player, POS, rich.getDefaultState(), null, shovel); + require(count(items(world), richItem) == 1 && items(world).size() == 1, "Silk Touch must return only rich gravel"); + clearItems(world); + + for (int i = 0; i < 512; i++) { + for (ItemStack drop : Blocks.GRAVEL.getDrops(world, POS, Blocks.GRAVEL.getDefaultState(), 3)) { + require(drop.getItem() != Items.DIAMOND && drop.getItem() != richItem, "Ordinary gravel became rich"); + } + } + + world.setBlockState(POS.down(), Blocks.STONE.getDefaultState(), 2); + world.setBlockState(POS.up(8), rich.getDefaultState(), 2); + EntityFallingBlock falling = new EntityFallingBlock(world, POS.getX() + 0.5, + POS.getY() + 8.5, POS.getZ() + 0.5, rich.getDefaultState()); + for (int tick = 0; tick < 100 && !falling.isDead; tick++) falling.onUpdate(); + require(falling.isDead && world.getBlockState(POS).getBlock() == rich, "Falling gravel lost its identity"); + require(items(world).isEmpty(), "Landing extracted diamonds"); + world.setBlockToAir(POS); + + world.setBlockState(POS.up(8), rich.getDefaultState(), 2); + falling = new EntityFallingBlock(world, POS.getX() + 0.5, + POS.getY() + 8.5, POS.getZ() + 0.5, rich.getDefaultState()); + falling.onUpdate(); + NBTTagCompound saved = new NBTTagCompound(); + falling.writeToNBT(saved); + EntityFallingBlock restored = new EntityFallingBlock(world); + restored.readFromNBT(saved); + restored.fallTime = 600; + restored.onUpdate(); + require(restored.isDead && count(items(world), richItem) == 1 && items(world).size() == 1, + "Indirect falling drop must preserve rich gravel without extracting diamonds"); + clearItems(world); + + for (int i = 0; i < 32; i++) { + world.setBlockState(POS, rich.getDefaultState(), 2); + world.newExplosion(null, POS.getX() + 0.5, POS.getY() + 0.5, POS.getZ() + 0.5, 2, false, true); + require(world.getBlockState(POS).getBlock() != rich, "Explosion did not consume the test block"); + require(count(items(world), richItem) == 0, "Explosion returned an intact rich block"); + clearItems(world); + } + + JsonObject report = new JsonObject(); + report.addProperty("normal_harvests", 128); + report.addProperty("spent_gravel_checks", 512); + report.addProperty("explosions", 32); + report.addProperty("silk_touch", true); + report.addProperty("falling_landing", true); + report.addProperty("falling_save_reload_indirect_drop", true); + return report; + } + + private static List items(WorldServer world) { + return world.getEntitiesWithinAABB(EntityItem.class, AREA, item -> !item.isDead); + } + + private static int count(List items, Item item) { + return items.stream().filter(entity -> entity.getEntityItem().getItem() == item) + .mapToInt(entity -> entity.getEntityItem().stackSize).sum(); + } + + private static void clearItems(WorldServer world) { + for (EntityItem item : items(world)) item.setDead(); + } + + private static void require(boolean passed, String message) { + if (!passed) throw new IllegalStateException(message); + } +} diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondRuleSwitchTest.java b/src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondRuleSwitchTest.java new file mode 100644 index 0000000..765a9f6 --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondRuleSwitchTest.java @@ -0,0 +1,27 @@ +package zone.moddev.mc.realisticdeposits.generation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.JsonObject; +import org.junit.jupiter.api.Test; + +import zone.moddev.mc.realisticdeposits.DiamondFixtures; +import zone.moddev.mc.realisticdeposits.config.DepositCatalogLoader; +import zone.moddev.mc.realisticdeposits.model.DepositDefinition; + +class DiamondRuleSwitchTest { + @Test + void anEnabledOreSpawnCallbackHonoursTheWorldSwitchWithoutMutatingDefaults() { + JsonObject json = DiamondFixtures.pipe().toJson(); + json.addProperty("enabled", false); + DepositDefinition disabled = DepositCatalogLoader.loadDefinition(json, "disabled catalogue"); + DepositDefinition active = DiamondPattern.enabledForCallback(disabled); + assertTrue(active.enabled()); + assertFalse(disabled.enabled()); + JsonObject expected = disabled.toJson(); + expected.addProperty("enabled", true); + assertEquals(expected, active.toJson()); + } +} diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondRuntimeProbe.java b/src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondRuntimeProbe.java new file mode 100644 index 0000000..2c5078b --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondRuntimeProbe.java @@ -0,0 +1,247 @@ +package zone.moddev.mc.realisticdeposits.generation; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.security.MessageDigest; +import java.util.Optional; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.WorldServer; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.gen.ChunkProviderServer; +import net.minecraftforge.fml.common.FMLCommonHandler; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLServerStartedEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.event.terraingen.PopulateChunkEvent; + +import zone.moddev.mc.orespawn.api.GeologySampler; +import zone.moddev.mc.orespawn.api.OreSpawnApi; +import zone.moddev.mc.realisticdeposits.config.DepositCatalogLoader; +import zone.moddev.mc.realisticdeposits.config.RealisticDepositsRuntime; +import zone.moddev.mc.realisticdeposits.geometry.DiamondDistrict; +import zone.moddev.mc.realisticdeposits.model.DepositDefinition; + +/** Runs only in the separately packaged, disposable diamond-test server. */ +@Mod(modid = "rddiamondprobe", name = "Diamond Qualification", version = "1", + dependencies = "required-after:realisticdeposits") +public final class DiamondRuntimeProbe { + private static final int SIDE = 8; + private static int riverMinX = Integer.MAX_VALUE; + private static int riverMinZ; + private static final java.util.Set installedRiverChunks = new java.util.HashSet<>(); + + public DiamondRuntimeProbe() { + net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(this); + } + + @SubscribeEvent + public void beforePopulation(PopulateChunkEvent.Pre event) { + if (!Boolean.getBoolean("rd.diamond.enabled") || !Boolean.getBoolean("rd.diamond.fixture_river") + || event.getChunkX() < riverMinX || event.getChunkX() >= riverMinX + 3 + || event.getChunkZ() < riverMinZ || event.getChunkZ() >= riverMinZ + SIDE) return; + if (!(event.getWorld() instanceof WorldServer) || !"diamond-test-world".equals( + ((WorldServer) event.getWorld()).getSaveHandler().getWorldDirectory().getName())) { + throw new IllegalStateException("The river fixture cannot touch another world"); + } + + Chunk chunk = ((WorldServer) event.getWorld()).getChunkProvider() + .getLoadedChunk(event.getChunkX(), event.getChunkZ()); + long chunkKey = ((long) event.getChunkX() << 32) ^ (event.getChunkZ() & 0xffffffffL); + if (chunk == null || !installedRiverChunks.add(chunkKey)) { + throw new IllegalStateException("The river fixture requires one pre-population event per new chunk"); + } + for (int x = event.getChunkX() * 16; x < event.getChunkX() * 16 + 16; x++) { + for (int z = event.getChunkZ() * 16; z < event.getChunkZ() * 16 + 16; z++) { + for (int y = 58; y < 130; y++) { + IBlockState state = y <= 60 ? Blocks.GRAVEL.getDefaultState() + : y <= 63 ? Blocks.WATER.getDefaultState() : Blocks.AIR.getDefaultState(); + chunk.setBlockState(new BlockPos(x, y, z), state); + } + } + } + chunk.generateSkylightMap(); + } + + @Mod.EventHandler + public void started(FMLServerStartedEvent event) throws Exception { + if (!Boolean.getBoolean("rd.diamond.enabled")) return; + MinecraftServer server = FMLCommonHandler.instance().getMinecraftServerInstance(); + try { + WorldServer world = server.worldServerForDimension(0); + if (!"diamond-test-world".equals(world.getSaveHandler().getWorldDirectory().getName())) { + throw new IllegalStateException("Diamond checks require their disposable world"); + } + world.getGameRules().setOrCreateGameRule("randomTickSpeed", "0"); + Block gravel = Block.REGISTRY.getObject(new ResourceLocation("realisticdeposits:diamond_gravel")); + if (gravel == Blocks.AIR) throw new IllegalStateException("Diamond-bearing gravel is not registered"); + DepositDefinition original = RealisticDepositsRuntime.state().definitions().stream() + .filter(DepositDefinition::isDiamondPipe).findFirst().get(); + JsonObject enabled = original.toJson(); + enabled.addProperty("enabled", true); + DepositDefinition definition = DepositCatalogLoader.loadDefinition(enabled, "diamond fixture geometry"); + String phase = System.getProperty("rd.diamond.phase", "first"); + File target = new File("diamond-target.json"); + JsonObject coordinates; + if (target.isFile()) { + try (FileReader reader = new FileReader(target)) { + coordinates = new JsonParser().parse(reader).getAsJsonObject(); + } + } else { + coordinates = new JsonObject(); + find(world, definition, 32, "first", coordinates); + find(world, definition, 96, "later", coordinates); + write(target, coordinates); + } + + DiamondDistrict first = resolve(world, definition, coordinates, "first"); + DiamondDistrict later = resolve(world, definition, coordinates, "later"); + if ("first".equals(phase)) populate(world, first); + if ("later".equals(phase)) populate(world, later); + JsonObject report = new JsonObject(); + report.addProperty("phase", phase); + if ("first".equals(phase)) report.add("gravel_contract", DiamondGravelRuntimeChecks.run(world, gravel)); + report.add("first", sample(world, first, gravel)); + if ("later".equals(phase) || "stable".equals(phase)) report.add("later", sample(world, later, gravel)); + report.add("profile", OreSpawnApi.getActiveProfile(server).get().toJson()); + write(new File("diamond-" + phase + ".json"), report); + System.out.println("RD_DIAMOND_PASS " + phase); + } finally { + server.initiateShutdown(); + } + } + + private static DiamondDistrict.Site site(WorldServer world) { + MineTerrainSampler terrain = new MineTerrainSampler(world, world.getChunkProvider().chunkGenerator); + Optional sampler = OreSpawnApi.createSampler(world); + return new DiamondDistrict.Site() { + @Override public int surfaceY(int x, int z) { return terrain.naturalSurfaceY(x, z); } + @Override public String geome(int x, int z, int surface) { + return sampler.isPresent() ? sampler.get().sampleColumn(x, z, surface).geome().toString() : null; + } + }; + } + + private static void find(WorldServer world, DepositDefinition definition, int start, String prefix, JsonObject output) { + for (int x = start; x < start + 32; x++) { + for (int z = start; z < start + 32; z++) { + Optional planned = DiamondDistrict.plan(definition, world.getSeed(), + "minecraft:overworld", x, z, site(world)); + if (!planned.isPresent()) continue; + output.addProperty(prefix + "_rx", x); + output.addProperty(prefix + "_rz", z); + return; + } + } + throw new IllegalStateException("No diamond district in bounded fixture search"); + } + + private static DiamondDistrict resolve(WorldServer world, DepositDefinition definition, JsonObject coordinates, String prefix) { + return DiamondDistrict.plan(definition, world.getSeed(), "minecraft:overworld", + coordinates.get(prefix + "_rx").getAsInt(), coordinates.get(prefix + "_rz").getAsInt(), site(world)).get(); + } + + private static void populate(WorldServer world, DiamondDistrict district) { + ChunkProviderServer provider = world.getChunkProvider(); + int baseX = (district.centerX() >> 4) - SIDE / 2; + int baseZ = (district.centerZ() >> 4) - SIDE / 2; + riverMinX = baseX + 4; + riverMinZ = baseZ; + java.util.List halo = new java.util.ArrayList<>(); + for (int x = baseX - 2; x <= baseX + SIDE + 1; x++) { + for (int z = baseZ - 2; z <= baseZ + SIDE + 1; z++) halo.add(new int[] {x, z}); + } + if (Boolean.getBoolean("rd.diamond.reverse")) java.util.Collections.reverse(halo); + for (int[] cell : halo) provider.provideChunk(cell[0], cell[1]); + + java.util.List order = new java.util.ArrayList<>(); + for (int x = baseX - 1; x <= baseX + SIDE; x++) { + for (int z = baseZ - 1; z <= baseZ + SIDE; z++) order.add(new int[] {x, z}); + } + if (Boolean.getBoolean("rd.diamond.reverse")) java.util.Collections.reverse(order); + for (int[] cell : order) provider.provideChunk(cell[0], cell[1]).populateChunk(provider, provider.chunkGenerator); + } + + private static JsonObject sample(WorldServer world, DiamondDistrict district, Block gravel) throws Exception { + MessageDigest terrain = MessageDigest.getInstance("SHA-256"); + MessageDigest diamonds = MessageDigest.getInstance("SHA-256"); + int baseX = (district.centerX() >> 4) - SIDE / 2; + int baseZ = (district.centerZ() >> 4) - SIDE / 2; + int pipeOre = 0; + int background = 0; + int sediment = 0; + int shallow = 0; + JsonArray section = new JsonArray(); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int cx = baseX; cx < baseX + SIDE; cx++) { + for (int cz = baseZ; cz < baseZ + SIDE; cz++) { + Chunk chunk = world.getChunkProvider().loadChunk(cx, cz); + if (chunk == null || !chunk.isTerrainPopulated()) throw new IllegalStateException("Unopened sample chunk"); + for (int x = cx * 16; x < cx * 16 + 16; x++) { + for (int z = cz * 16; z < cz * 16 + 16; z++) { + for (int y = 0; y < 160; y++) { + IBlockState state = chunk.getBlockState(pos.setPos(x, y, z)); + update(terrain, Block.getStateId(state)); + if (state.getBlock() == Blocks.DIAMOND_ORE) { + boolean planned = district.pipes().stream().anyMatch(pipe -> pipe.isOreAt(pos.getX(), pos.getY(), pos.getZ())); + if (planned) pipeOre++; else background++; + if (y > 40) shallow++; + update(diamonds, x); update(diamonds, y); update(diamonds, z); + } + if (state.getBlock() == gravel) { + sediment++; + update(diamonds, x); update(diamonds, y); update(diamonds, z); + } + if (z == district.centerZ() && y <= 140) { + JsonArray cell = new JsonArray(); + cell.add(new com.google.gson.JsonPrimitive(x)); + cell.add(new com.google.gson.JsonPrimitive(y)); + cell.add(new com.google.gson.JsonPrimitive(state.getBlock().getRegistryName().toString())); + section.add(cell); + } + } + } + } + } + } + JsonObject report = new JsonObject(); + report.addProperty("centre_x", district.centerX()); + report.addProperty("centre_z", district.centerZ()); + report.addProperty("pipe_ore", pipeOre); + report.addProperty("background_ore", background); + report.addProperty("rich_gravel", sediment); + report.addProperty("shallow_ore", shallow); + report.addProperty("terrain_sha256", hex(terrain.digest())); + report.addProperty("diamonds_sha256", hex(diamonds.digest())); + report.add("section", section); + return report; + } + + private static void update(MessageDigest digest, int value) { + digest.update((byte) (value >>> 24)); digest.update((byte) (value >>> 16)); + digest.update((byte) (value >>> 8)); digest.update((byte) value); + } + + private static String hex(byte[] bytes) { + StringBuilder text = new StringBuilder(); + for (byte value : bytes) text.append(String.format("%02x", value & 255)); + return text.toString(); + } + + private static void write(File file, JsonObject json) throws Exception { + try (FileWriter writer = new FileWriter(file)) { + new GsonBuilder().setPrettyPrinting().create().toJson(json, writer); + } + } +} diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondSurveyProbe.java b/src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondSurveyProbe.java new file mode 100644 index 0000000..0ad7d87 --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/generation/DiamondSurveyProbe.java @@ -0,0 +1,266 @@ +package zone.moddev.mc.realisticdeposits.generation; + +import java.io.File; +import java.io.FileWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Random; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; + +import net.minecraft.block.material.Material; +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.WorldServer; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.gen.ChunkProviderOverworld; +import net.minecraftforge.common.BiomeDictionary; +import net.minecraftforge.fml.common.FMLCommonHandler; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLServerStartedEvent; + +import zone.moddev.mc.orespawn.api.GeologySampler; +import zone.moddev.mc.orespawn.api.OreSpawnApi; +import zone.moddev.mc.realisticdeposits.config.RealisticDepositsRuntime; +import zone.moddev.mc.realisticdeposits.geometry.BlockBounds; +import zone.moddev.mc.realisticdeposits.geometry.DiamondDistrict; +import zone.moddev.mc.realisticdeposits.model.DepositDefinition; + +/** Area-weighted native-terrain survey; no survey chunk is installed in the world. */ +@Mod(modid = "rddiamondsurvey", name = "Diamond Reward Survey", version = "1", + dependencies = "required-after:realisticdeposits") +public final class DiamondSurveyProbe { + private final Map> districts = new HashMap<>(); + private final Map terrain = new java.util.LinkedHashMap() { + @Override protected boolean removeEldestEntry(Map.Entry entry) { return size() > 128; } + }; + private WorldServer world; + private ChunkProviderOverworld generator; + private MineTerrainSampler heights; + private Optional geology; + private DepositDefinition pipe; + private DepositDefinition placer; + + @Mod.EventHandler + public void started(FMLServerStartedEvent event) throws Exception { + if (!Boolean.getBoolean("rd.diamond.survey")) return; + MinecraftServer server = FMLCommonHandler.instance().getMinecraftServerInstance(); + try { + world = server.worldServerForDimension(0); + if (!"diamond-test-world".equals(world.getSaveHandler().getWorldDirectory().getName())) { + throw new IllegalStateException("The survey requires its disposable world"); + } + if (Boolean.getBoolean("rd.diamond.survey.control")) { + control(); + return; + } + pipe = RealisticDepositsRuntime.state().definitions().stream().filter(DepositDefinition::isDiamondPipe).findFirst().get(); + placer = RealisticDepositsRuntime.state().definitions().stream().filter(DepositDefinition::isDiamondPlacer).findFirst().get(); + geology = OreSpawnApi.createSampler(world); + generator = new ChunkProviderOverworld(world, world.getSeed(), false, world.getWorldInfo().getGeneratorOptions()); + heights = new MineTerrainSampler(world, generator); + int loadedBefore = world.getChunkProvider().getLoadedChunkCount(); + int side = Integer.getInteger("rd.diamond.survey.regions", 16); + int regionStart = 256; + int size = pipe.regionSpacingChunks() * 16; + boolean nativeSediment = Boolean.getBoolean("rd.diamond.survey.native_sediment"); + long pipeOre = 0; + java.util.Set orePositions = new java.util.HashSet<>(); + int productive = 0; + int bodies = 0; + for (int rx = regionStart; !nativeSediment && rx < regionStart + side; rx++) { + for (int rz = regionStart; rz < regionStart + side; rz++) { + Optional selected = district(rx, rz); + if (!selected.isPresent()) continue; + productive++; + for (DiamondDistrict.Pipe body : selected.get().pipes()) { + bodies++; + BlockBounds bounds = body.bounds(); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int x = bounds.minimumX(); x <= bounds.maximumX(); x++) { + for (int z = bounds.minimumZ(); z <= bounds.maximumZ(); z++) { + Chunk chunk = terrain(x >> 4, z >> 4); + int top = Math.min(body.topY(), chunk.getHeightValue(x & 15, z & 15) - 4); + for (int y = body.bottomY(); y <= top; y++) { + if (body.isOreAt(x, y, z) && natural(chunk.getBlockState(pos.setPos(x, y, z))) + && orePositions.add(pos.toLong())) pipeOre++; + } + } + } + } + } + } + + int samples = Integer.getInteger("rd.diamond.survey.chunks", 4096); + Random random = new Random(world.getSeed() ^ 0x795AB7C3D012461FL); + long rich = 0; + long eligible = 0; + long riverColumns = 0; + long actualRich = 0; + for (int sample = 0; sample < samples; sample++) { + int cx = regionStart * pipe.regionSpacingChunks() + random.nextInt(side * pipe.regionSpacingChunks()); + int cz = regionStart * pipe.regionSpacingChunks() + random.nextInt(side * pipe.regionSpacingChunks()); + Biome[] biomes = world.getBiomeProvider().getBiomes(null, cx * 16, cz * 16, 16, 16); + boolean river = false; + for (Biome biome : biomes) river |= BiomeDictionary.isBiomeOfType(biome, BiomeDictionary.Type.RIVER); + if (!river) continue; + Chunk chunk = nativeSediment ? populated(cx, cz) : terrain(cx, cz); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int lx = 0; lx < 16; lx++) { + for (int lz = 0; lz < 16; lz++) { + if (!BiomeDictionary.isBiomeOfType(biomes[lx + lz * 16], BiomeDictionary.Type.RIVER)) continue; + riverColumns++; + int x = cx * 16 + lx; + int z = cz * 16 + lz; + DiamondDistrict source = nearest(x, z); + if (source == null) continue; + int y = chunk.getHeightValue(lx, lz) - 1; + IBlockState state = chunk.getBlockState(pos.setPos(x, y, z)); + if (state.getMaterial() != Material.WATER) continue; + while (y > placer.minimumY() && state.getMaterial() == Material.WATER) { + state = chunk.getBlockState(pos.setPos(x, --y, z)); + } + int thickness = placer.placer().thickness().minimum() + (int) Math.floorMod( + source.identity().seed() ^ x ^ ((long) z << 32), + placer.placer().thickness().maximum() - placer.placer().thickness().minimum() + 1); + for (int depth = 0; depth < thickness && y - depth >= placer.minimumY(); depth++) { + int py = y - depth; + state = chunk.getBlockState(pos.setPos(x, py, z)); + boolean placedRich = state.getBlock().getRegistryName().toString().equals("realisticdeposits:diamond_gravel"); + if (state.getBlock() != Blocks.SAND && state.getBlock() != Blocks.GRAVEL && !placedRich) break; + if (py > placer.maximumY() || !chunk.getBlockState(pos.setPos(x, py - 1, z)).getMaterial().isSolid()) continue; + eligible++; + if (placedRich) actualRich++; + if (DiamondDistrict.placerPocket(source.identity().seed() ^ placer.salt(), x, z, depth, + placer.placer().coverage())) rich++; + } + } + } + if (nativeSediment && world.getChunkProvider().getLoadedChunkCount() > 800) { + world.getChunkProvider().unloadAllChunks(); + for (int pass = 0; pass < 12; pass++) world.getChunkProvider().tick(); + } + } + if (!nativeSediment && world.getChunkProvider().getLoadedChunkCount() != loadedBefore) { + throw new IllegalStateException("The in-memory survey loaded real world chunks"); + } + JsonObject report = new JsonObject(); + report.addProperty("seed", world.getSeed()); + report.addProperty("survey_region_side", side); + report.addProperty("represented_chunks", (long) side * side * size * size / 256); + report.addProperty("productive_districts", productive); + report.addProperty("pipes", bodies); + report.addProperty("pipe_ore", pipeOre); + report.addProperty("placer_sample_chunks", samples); + report.addProperty("river_columns", riverColumns); + report.addProperty("eligible_sediment", eligible); + report.addProperty("rich_gravel", rich); + report.addProperty("actual_rich_gravel", actualRich); + report.addProperty("native_sediment", nativeSediment); + report.addProperty("loaded_chunks_unchanged", !nativeSediment); + try (FileWriter output = new FileWriter(new File("diamond-survey.json"))) { + new GsonBuilder().setPrettyPrinting().create().toJson(report, output); + } + System.out.println("RD_DIAMOND_SURVEY_PASS"); + } finally { + server.initiateShutdown(); + } + } + + private Optional district(int rx, int rz) { + long key = ((long) rx << 32) ^ (rz & 0xffffffffL); + return districts.computeIfAbsent(key, ignored -> DiamondDistrict.plan(pipe, world.getSeed(), "minecraft:overworld", rx, rz, + new DiamondDistrict.Site() { + @Override public int surfaceY(int x, int z) { return heights.naturalSurfaceY(x, z); } + @Override public String geome(int x, int z, int y) { + return geology.isPresent() ? geology.get().sampleColumn(x, z, y).geome().toString() : null; + } + })); + } + + private DiamondDistrict nearest(int x, int z) { + int reach = placer.placer().maximumSourceDistance(); + int search = reach + pipe.pipe().districtRadius(); + int size = pipe.regionSpacingChunks() * 16; + DiamondDistrict closest = null; + double distance = (double) reach * reach; + for (int rx = Math.floorDiv(x - search, size); rx <= Math.floorDiv(x + search, size); rx++) { + for (int rz = Math.floorDiv(z - search, size); rz <= Math.floorDiv(z + search, size); rz++) { + Optional candidate = district(rx, rz); + if (candidate.isPresent() && candidate.get().distanceSquared(x, z) < distance) { + closest = candidate.get(); + distance = closest.distanceSquared(x, z); + } + } + } + return closest; + } + + private Chunk terrain(int cx, int cz) { + long key = ((long) cx << 32) ^ (cz & 0xffffffffL); + return terrain.computeIfAbsent(key, ignored -> generator.provideChunk(cx, cz)); + } + + private Chunk populated(int cx, int cz) { + net.minecraft.world.gen.ChunkProviderServer provider = world.getChunkProvider(); + for (int dx = -2; dx <= 2; dx++) { + for (int dz = -2; dz <= 2; dz++) provider.provideChunk(cx + dx, cz + dz); + } + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + provider.provideChunk(cx + dx, cz + dz).populateChunk(provider, provider.chunkGenerator); + } + } + return provider.getLoadedChunk(cx, cz); + } + + private static boolean natural(IBlockState state) { + if (state.getBlock() != Blocks.STONE) return false; + int meta = Blocks.STONE.getMetaFromState(state); + return meta == 0 || meta == 1 || meta == 3 || meta == 5; + } + + private void control() throws Exception { + if (RealisticDepositsRuntime.state().definitions().stream().anyMatch(def -> def.isDiamond() && def.enabled())) { + throw new IllegalStateException("Vanilla controls must disable both diamond definitions before startup"); + } + int base = 8192; + int side = 12; + net.minecraft.world.gen.ChunkProviderServer provider = world.getChunkProvider(); + for (int cx = base - 2; cx <= base + side + 1; cx++) { + for (int cz = base - 2; cz <= base + side + 1; cz++) provider.provideChunk(cx, cz); + } + for (int cx = base - 1; cx <= base + side; cx++) { + for (int cz = base - 1; cz <= base + side; cz++) { + provider.provideChunk(cx, cz).populateChunk(provider, provider.chunkGenerator); + } + } + int diamonds = 0; + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + for (int cx = base; cx < base + side; cx++) { + for (int cz = base; cz < base + side; cz++) { + Chunk chunk = provider.getLoadedChunk(cx, cz); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < 128; y++) { + if (chunk.getBlockState(pos.setPos(cx * 16 + x, y, cz * 16 + z)).getBlock() == Blocks.DIAMOND_ORE) diamonds++; + } + } + } + } + } + JsonObject report = new JsonObject(); + report.addProperty("seed", world.getSeed()); + report.addProperty("chunks", side * side); + report.addProperty("diamond_ore", diamonds); + try (FileWriter output = new FileWriter(new File("diamond-survey.json"))) { + new GsonBuilder().setPrettyPrinting().create().toJson(report, output); + } + System.out.println("RD_DIAMOND_SURVEY_PASS"); + } +} diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/generation/FrozenMineRuntimeProbe.java b/src/test/java/zone/moddev/mc/realisticdeposits/generation/FrozenMineRuntimeProbe.java new file mode 100644 index 0000000..9de2c48 --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/generation/FrozenMineRuntimeProbe.java @@ -0,0 +1,69 @@ +package zone.moddev.mc.realisticdeposits.generation; + +import java.io.FileInputStream; +import java.io.FileWriter; +import java.util.List; +import java.util.Properties; + +import com.google.gson.JsonObject; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.WorldServer; +import net.minecraftforge.fml.common.FMLCommonHandler; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.event.FMLServerStartedEvent; + +import zone.moddev.mc.realisticdeposits.geometry.AbandonedMinePlan; +import zone.moddev.mc.realisticdeposits.geometry.MineBlock; + +/** Reads frozen cells directly, so an upgrade cannot quietly replan an old shaft. */ +@Mod(modid = "rdfrozenmineprobe", name = "Frozen Mine Qualification", version = "1", + dependencies = "required-after:realisticdeposits") +public final class FrozenMineRuntimeProbe { + + @Mod.EventHandler + public void started(FMLServerStartedEvent event) throws Exception { + if (!Boolean.getBoolean("rd.frozen.enabled")) return; + MinecraftServer server = FMLCommonHandler.instance().getMinecraftServerInstance(); + try { + WorldServer world = server.worldServerForDimension(0); + MineWorkingsRuntimeProbe.require("mine-test-world".equals(world.getSaveHandler().getWorldDirectory().getName()), + "The frozen-mine probe must use its disposable world"); + Properties first = new Properties(); + try (FileInputStream input = new FileInputStream("mine-first.properties")) { + first.load(input); + } + + MineWorldData data = MineWorldData.get(world); + AbandonedMinePlan plan = null; + for (AbandonedMinePlan candidate : data.plans().values()) { + if (first.getProperty("plan").equals(MineWorkingsRuntimeProbe.planHash(candidate))) plan = candidate; + } + MineWorkingsRuntimeProbe.require(plan != null && plan.blocks().containsValue(MineBlock.LADDER_WEST), + "The old vertical layout was lost or replaced"); + List old = MineWorkingsRuntimeProbe.split(first.getProperty("chunks")); + MineWorkingsRuntimeProbe.require(first.getProperty("terrain").equals(MineWorkingsRuntimeProbe.terrainHash(world, old)), + "The first loaded sections or player marker changed"); + + String phase = System.getProperty("rd.frozen.phase"); + List all = MineWorkingsRuntimeProbe.allChunks(plan); + if ("complete".equals(phase)) { + for (long chunk : all) MineWorkingsRuntimeProbe.populate(world, (int) (chunk >> 32), (int) chunk); + } + + MineWorkingsRuntimeProbe.require(first.getProperty("terrain").equals(MineWorkingsRuntimeProbe.terrainHash(world, old)), + "Later completion excavated previously finished sections"); + int cells = MineWorkingsRuntimeProbe.verify(world, plan, all, true); + MineWorkingsRuntimeProbe.verifyAccess(world, plan, true); + JsonObject report = new JsonObject(); + report.addProperty("plan", MineWorkingsRuntimeProbe.planHash(plan)); + report.addProperty("terrain", MineWorkingsRuntimeProbe.terrainHash(world, all)); + report.addProperty("cells", cells); + try (FileWriter output = new FileWriter("frozen-" + phase + ".json")) { + output.write(report.toString()); + } + System.out.println("RD_FROZEN_MINE_PASS " + phase); + } finally { + server.initiateShutdown(); + } + } +} diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/generation/MineNaturalHostsTest.java b/src/test/java/zone/moddev/mc/realisticdeposits/generation/MineNaturalHostsTest.java new file mode 100644 index 0000000..59f47f0 --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/generation/MineNaturalHostsTest.java @@ -0,0 +1,45 @@ +package zone.moddev.mc.realisticdeposits.generation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import com.google.gson.JsonObject; +import org.junit.jupiter.api.Test; + +import net.minecraft.util.ResourceLocation; + +class MineNaturalHostsTest { + + @Test + void geologyDefinitionIdsDoNotHaveToMatchBlockIds() { + JsonObject rocks = new JsonObject(); + rocks.add("mineralogy:rock/mineralogy/rock_salt", rock("mineralogy:rock_salt", true)); + rocks.add("pack:shale_layer", rock("mineralogy:shale", true)); + rocks.add("pack:disabled", rock("pack:authored_block", false)); + rocks.add("pack:no_block", new JsonObject()); + JsonObject profile = new JsonObject(); + profile.add("rocks", rocks); + + Set expected = new LinkedHashSet<>(Arrays.asList( + new ResourceLocation("mineralogy:rock_salt"), new ResourceLocation("mineralogy:shale"))); + assertEquals(expected, MineWorkingsGenerator.naturalRockBlocks(profile)); + assertFalse(MineWorkingsGenerator.naturalRockBlocks(profile) + .contains(new ResourceLocation("mineralogy:rock/mineralogy/rock_salt"))); + } + + @Test + void missingRockDeclarationsDoNotAddAnyHosts() { + assertEquals(java.util.Collections.emptySet(), MineWorkingsGenerator.naturalRockBlocks(new JsonObject())); + } + + private static JsonObject rock(String block, boolean enabled) { + JsonObject result = new JsonObject(); + result.addProperty("block", block); + result.addProperty("enabled", enabled); + return result; + } +} diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/generation/MineWorkingsRuntimeProbe.java b/src/test/java/zone/moddev/mc/realisticdeposits/generation/MineWorkingsRuntimeProbe.java index 04bc414..6ac4180 100644 --- a/src/test/java/zone/moddev/mc/realisticdeposits/generation/MineWorkingsRuntimeProbe.java +++ b/src/test/java/zone/moddev/mc/realisticdeposits/generation/MineWorkingsRuntimeProbe.java @@ -18,6 +18,8 @@ import org.apache.logging.log4j.Logger; import net.minecraft.block.BlockLadder; +import net.minecraft.block.BlockStairs; +import net.minecraft.entity.monster.EntityZombie; import net.minecraft.block.BlockRail; import net.minecraft.block.BlockRailBase; import net.minecraft.block.state.IBlockState; @@ -41,6 +43,7 @@ import zone.moddev.mc.realisticdeposits.geometry.AbandonedMinePlanner; import zone.moddev.mc.realisticdeposits.geometry.MineBlock; import zone.moddev.mc.realisticdeposits.geometry.MinePoint; +import zone.moddev.mc.realisticdeposits.geometry.MineDecline; import zone.moddev.mc.realisticdeposits.geometry.RegionDepositPlanner; import zone.moddev.mc.realisticdeposits.geometry.StratiformSeamBody; import zone.moddev.mc.realisticdeposits.model.AbandonedMineSettings; @@ -67,6 +70,12 @@ public void started(FMLServerStartedEvent event) { WorldServer world = server.worldServerForDimension(0); require(world.getSeed() == SEED, "This probe is restricted to its disposable fixed-seed world"); require(world.getSaveHandler().getWorldDirectory().getName().equals("mine-test-world"), "Unexpected world directory"); + if (Boolean.getBoolean("rd.mines.pipe")) { + PipeWorkingsRuntimeChecks.run(world, phase); + LOG.info("RD_MINES_PASS {}", phase); + return; + } + DepositDefinition definition = RealisticDepositsRuntime.state().definitions().get(0); if (phase.startsWith("default-")) { normalWorld(world, definition, phase); @@ -204,7 +213,7 @@ public void started(FMLServerStartedEvent event) { } } - private static int verify(WorldServer world, AbandonedMinePlan plan, List chunks, boolean hasMarker) { + static int verify(WorldServer world, AbandonedMinePlan plan, List chunks, boolean hasMarker) { int checked = 0; for (long key : chunks) { Chunk chunk = world.getChunkProvider().loadChunk((int) (key >> 32), (int) key); @@ -325,16 +334,26 @@ private static void normalWorld(WorldServer world, DepositDefinition definition, StratiformSeamBody chosen = null; plan = null; - search: for (int x = 8; x < 24; x++) { - for (int z = 8; z < 24; z++) { + search: for (int x = 8; x < 72; x++) { + for (int z = 8; z < 72; z++) { java.util.Optional body = RegionDepositPlanner.plan( definition, SEED, "minecraft:overworld", x, z); if (!body.isPresent()) continue; java.util.Optional proposed = AbandonedMinePlanner.plan(body.get(), - new AbandonedMineSettings(true, 1.0D), terrain.surfaceY(AbandonedMinePlanner.landing(body.get()))); + new AbandonedMineSettings(true, 1.0D), terrain::naturalSurfaceY); if (!proposed.isPresent()) continue; AbandonedMinePlan candidate = proposed.get(); - if (terrain.covered(candidate) && terrain.dry(candidate) + // Mountain decoration can add infested stone after the terrain + // preview. Use an ordinary-rock site for the walking fixture; + // obstruction handling has its own sealed-section checks. + boolean mountains = candidate.blocks().keySet().stream().map(MinePoint::unpack) + .anyMatch(point -> net.minecraftforge.common.BiomeDictionary.isBiomeOfType( + world.getBiomeProvider().getBiome(new BlockPos(point.x(), 0, point.z())), + net.minecraftforge.common.BiomeDictionary.Type.MOUNTAIN)); + if (mountains) continue; + + if (terrain.surfaceY(candidate.entrance()) == candidate.entrance().y() + && terrain.covered(candidate) && terrain.dry(candidate) && MineWorkingsGenerator.biomesAllowed(candidate, definition, world)) { chosen = body.get(); plan = candidate; @@ -379,7 +398,40 @@ private static void normalWorld(WorldServer world, DepositDefinition definition, } } - private static void verifyAccess(WorldServer world, AbandonedMinePlan plan, boolean hasMarker) { + static void verifyAccess(WorldServer world, AbandonedMinePlan plan, boolean hasMarker) { + if (!plan.blocks().containsValue(MineBlock.LADDER_WEST)) { + List steps = MineDecline.plan(plan.landing(), plan.entrance().y(), plan.identity()).steps(); + require(steps.get(steps.size() - 1).point().equals(plan.entrance()), "The access entrance moved"); + int loaded = world.getChunkProvider().getLoadedChunkCount(); + + for (int direction : new int[] {1, -1}) { + List route = new ArrayList<>(steps); + if (direction < 0) Collections.reverse(route); + MinePoint start = route.get(0).point(); + net.minecraft.entity.Entity walker = new EntityZombie(world); + walker.stepHeight = 0.6F; + walker.setPosition(start.x() + 0.5D, start.y(), start.z() + 0.5D); + + for (int index = 1; index < route.size(); index++) { + MinePoint target = route.get(index).point(); + for (int tick = 0; tick < 12; tick++) { + double dx = Math.max(-0.125D, Math.min(0.125D, target.x() + 0.5D - walker.posX)); + double dz = Math.max(-0.125D, Math.min(0.125D, target.z() + 0.5D - walker.posZ)); + walker.move(dx, -0.15D, dz); + } + + require(Math.abs(walker.posX - target.x() - 0.5D) < 0.2D + && Math.abs(walker.posZ - target.z() - 0.5D) < 0.2D, + "The walker could not pass the incline at " + target + ": " + walker.posX + "," + walker.posY + "," + walker.posZ); + require(walker.posY >= target.y() - 0.05D && walker.posY <= target.y() + 1.05D, + "Unsupported or obstructed stair at " + target); + } + } + + require(loaded == world.getChunkProvider().getLoadedChunkCount(), "Walking access loaded a neighbouring chunk"); + return; + } + MinePoint landing = plan.landing(); for (int y = landing.y(); y <= plan.entrance().y(); y++) { BlockPos ladder = new BlockPos(landing.x() + 1, y, landing.z()); @@ -407,7 +459,7 @@ private static StratiformSeamBody distantBody(DepositDefinition definition) { throw new IllegalStateException("The fixture has no suitable distant deposit"); } - private static void populate(WorldServer world, int x, int z) { + static void populate(WorldServer world, int x, int z) { ChunkProviderServer provider = world.getChunkProvider(); for (int dx = 0; dx <= 1; dx++) { for (int dz = 0; dz <= 1; dz++) provider.provideChunk(x + dx, z + dz); @@ -415,7 +467,7 @@ private static void populate(WorldServer world, int x, int z) { provider.provideChunk(x, z).populateChunk(provider, provider.chunkGenerator); } - private static List allChunks(AbandonedMinePlan plan) { + static List allChunks(AbandonedMinePlan plan) { List chunks = new ArrayList<>(); for (int x = Math.floorDiv(plan.bounds().minimumX(), 16); x <= Math.floorDiv(plan.bounds().maximumX(), 16); x++) { for (int z = Math.floorDiv(plan.bounds().minimumZ(), 16); z <= Math.floorDiv(plan.bounds().maximumZ(), 16); z++) { @@ -425,7 +477,7 @@ private static List allChunks(AbandonedMinePlan plan) { return chunks; } - private static List populatedChunks(WorldServer world, AbandonedMinePlan plan) { + static List populatedChunks(WorldServer world, AbandonedMinePlan plan) { List chunks = new ArrayList<>(); for (long key : allChunks(plan)) { Chunk chunk = world.getChunkProvider().getLoadedChunk((int) (key >> 32), (int) key); @@ -434,11 +486,14 @@ private static List populatedChunks(WorldServer world, AbandonedMinePlan p return chunks; } - private static BlockPos marker(AbandonedMinePlan plan) { + static BlockPos marker(AbandonedMinePlan plan) { + if (!plan.blocks().containsValue(MineBlock.LADDER_WEST)) { + return new BlockPos(plan.landing().x() + 3, plan.landing().y() + 1, plan.landing().z() + 3); + } return new BlockPos(plan.landing().x(), plan.landing().y() + 1, plan.landing().z()); } - private static String terrainHash(WorldServer world, List chunks) throws Exception { + static String terrainHash(WorldServer world, List chunks) throws Exception { MessageDigest digest = MessageDigest.getInstance("SHA-256"); ByteBuffer value = ByteBuffer.allocate(4); for (long key : chunks) { @@ -457,7 +512,7 @@ private static String terrainHash(WorldServer world, List chunks) throws E return hex(digest.digest()); } - private static String planHash(AbandonedMinePlan plan) throws Exception { + static String planHash(AbandonedMinePlan plan) throws Exception { require(plan != null, "Expected saved mine"); MessageDigest digest = MessageDigest.getInstance("SHA-256"); ByteBuffer value = ByteBuffer.allocate(12); @@ -469,7 +524,7 @@ private static String planHash(AbandonedMinePlan plan) throws Exception { return hex(digest.digest()); } - private static String join(List values) { + static String join(List values) { StringBuilder text = new StringBuilder(); for (long value : values) { if (text.length() > 0) text.append(','); @@ -478,7 +533,7 @@ private static String join(List values) { return text.toString(); } - private static List split(String text) { + static List split(String text) { List values = new ArrayList<>(); for (String value : text.split(",")) values.add(Long.parseLong(value)); return values; @@ -490,13 +545,17 @@ private static String hex(byte[] bytes) { return value.toString(); } - private static IBlockState expected(MineBlock block) { + static IBlockState expected(MineBlock block) { switch (block) { case AIR: return Blocks.AIR.getDefaultState(); case PLANKS: return Blocks.PLANKS.getDefaultState(); case FENCE: return Blocks.OAK_FENCE.getDefaultState(); case COBWEB: return Blocks.WEB.getDefaultState(); case COBBLESTONE: return Blocks.COBBLESTONE.getDefaultState(); + case STAIRS_NORTH: return Blocks.OAK_STAIRS.getDefaultState().withProperty(BlockStairs.FACING, EnumFacing.NORTH); + case STAIRS_SOUTH: return Blocks.OAK_STAIRS.getDefaultState().withProperty(BlockStairs.FACING, EnumFacing.SOUTH); + case STAIRS_EAST: return Blocks.OAK_STAIRS.getDefaultState().withProperty(BlockStairs.FACING, EnumFacing.EAST); + case STAIRS_WEST: return Blocks.OAK_STAIRS.getDefaultState().withProperty(BlockStairs.FACING, EnumFacing.WEST); case LADDER_WEST: return Blocks.LADDER.getDefaultState().withProperty(BlockLadder.FACING, EnumFacing.WEST); case RAIL_EAST_WEST: return Blocks.RAIL.getDefaultState().withProperty(BlockRail.SHAPE, BlockRailBase.EnumRailDirection.EAST_WEST); case RAIL_NORTH_SOUTH: return Blocks.RAIL.getDefaultState().withProperty(BlockRail.SHAPE, BlockRailBase.EnumRailDirection.NORTH_SOUTH); @@ -504,7 +563,7 @@ private static IBlockState expected(MineBlock block) { } } - private static void require(boolean condition, String message) { + static void require(boolean condition, String message) { if (!condition) throw new IllegalStateException(message); } } diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/generation/MineWorldDataTest.java b/src/test/java/zone/moddev/mc/realisticdeposits/generation/MineWorldDataTest.java index 454035d..68fb8d7 100644 --- a/src/test/java/zone/moddev/mc/realisticdeposits/generation/MineWorldDataTest.java +++ b/src/test/java/zone/moddev/mc/realisticdeposits/generation/MineWorldDataTest.java @@ -18,6 +18,27 @@ class MineWorldDataTest { + @Test + void theOldPaletteAndSavedVerticalLayoutsStayCompatible() { + assertEquals(MineBlock.LADDER_WEST, MineBlock.fromSavedId(6)); + assertEquals(MineBlock.COBBLESTONE, MineBlock.fromSavedId(7)); + Map cells = new LinkedHashMap<>(); + for (int y = 20; y <= 80; y++) { + cells.put(new MinePoint(0, y, 0).packed(), MineBlock.AIR); + cells.put(new MinePoint(1, y, 0).packed(), MineBlock.LADDER_WEST); + cells.put(new MinePoint(2, y, 0).packed(), MineBlock.PLANKS); + } + + MineWorldData original = new MineWorldData("test"); + original.accept("old-shaft", new AbandonedMinePlan(123, + new MinePoint(0, 80, 0), new MinePoint(0, 20, 0), cells)); + NBTTagCompound saved = original.writeToNBT(new NBTTagCompound()); + MineWorldData loaded = new MineWorldData("test"); + loaded.readFromNBT(saved); + assertEquals(cells, loaded.plan("old-shaft").blocks()); + assertEquals(saved, loaded.writeToNBT(new NBTTagCompound())); + } + @Test void laterChunksUseTheExactSavedCellsAndPalette() { MineWorldData original = new MineWorldData("test"); diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/generation/PipeWorkingsRuntimeChecks.java b/src/test/java/zone/moddev/mc/realisticdeposits/generation/PipeWorkingsRuntimeChecks.java new file mode 100644 index 0000000..9998db6 --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/generation/PipeWorkingsRuntimeChecks.java @@ -0,0 +1,226 @@ +package zone.moddev.mc.realisticdeposits.generation; + +import static zone.moddev.mc.realisticdeposits.generation.MineWorkingsRuntimeProbe.*; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import com.google.gson.JsonObject; + +import net.minecraft.init.Blocks; +import net.minecraft.init.Items; +import net.minecraft.item.ItemStack; +import net.minecraft.tileentity.TileEntityChest; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.WorldServer; +import net.minecraft.world.gen.structure.MapGenStructureData; + +import zone.moddev.mc.orespawn.api.GeologySampler; +import zone.moddev.mc.orespawn.api.OreSpawnApi; +import zone.moddev.mc.realisticdeposits.config.DepositCatalogLoader; +import zone.moddev.mc.realisticdeposits.config.RealisticDepositsRuntime; +import zone.moddev.mc.realisticdeposits.geometry.AbandonedMinePlan; +import zone.moddev.mc.realisticdeposits.geometry.AbandonedMinePlanner; +import zone.moddev.mc.realisticdeposits.geometry.DiamondDistrict; +import zone.moddev.mc.realisticdeposits.geometry.DiamondPipeWorkingsBody; +import zone.moddev.mc.realisticdeposits.geometry.MinePoint; +import zone.moddev.mc.realisticdeposits.model.AbandonedMineSettings; +import zone.moddev.mc.realisticdeposits.model.DepositDefinition; + +/** Diamond access checks share the iron suite's exact block and reload assertions. */ +final class PipeWorkingsRuntimeChecks { + private PipeWorkingsRuntimeChecks() { } + + static void run(WorldServer world, String phase) throws Exception { + DepositDefinition captured = RealisticDepositsRuntime.state().definitions().stream() + .filter(DepositDefinition::isDiamondPipe).findFirst().get(); + JsonObject enabled = captured.toJson(); + enabled.addProperty("enabled", true); + DepositDefinition geometry = DepositCatalogLoader.loadDefinition(enabled, "pipe mine geometry"); + String scenario = System.getProperty("rd.mines.scenario", "flat"); + boolean obstructed = "blocked".equals(scenario); + boolean undergroundWater = "mineralogy".equals(scenario); + if (scenario.startsWith("mineralogy")) { + require(CoalGeology.active().physicalStrata, "The Mineralogy fixture did not activate physical strata"); + } + MineTerrainSampler terrain = new MineTerrainSampler(world, world.getChunkProvider().chunkGenerator); + Optional sampler = OreSpawnApi.createSampler(world); + Properties target = new Properties(); + DiamondPipeWorkingsBody body = null; + AbandonedMinePlan plan = null; + + if ("first".equals(phase)) { + int loaded = world.getChunkProvider().getLoadedChunkCount(); + int start = Integer.getInteger("rd.mines.startRegion", 32); + search: for (int rx = start; rx < start + 32; rx++) { + for (int rz = start; rz < start + 32; rz++) { + Optional district = district(world, geometry, terrain, sampler, rx, rz); + if (!district.isPresent()) continue; + + for (int index = 0; index < district.get().pipes().size(); index++) { + DiamondPipeWorkingsBody candidate = new DiamondPipeWorkingsBody(district.get(), index); + int surface = "wet".equals(scenario) ? 129 : terrain.surfaceY(AbandonedMinePlanner.landing(candidate)); + Optional proposed = "wet".equals(scenario) + ? AbandonedMinePlanner.plan(candidate, new AbandonedMineSettings(true, 1.0D), surface) + : AbandonedMinePlanner.plan(candidate, new AbandonedMineSettings(true, 1.0D), terrain::naturalSurfaceY); + if (!proposed.isPresent()) continue; + if (!"wet".equals(scenario) && (terrain.surfaceY(proposed.get().entrance()) != proposed.get().entrance().y() + || !terrain.dry(proposed.get()) || !terrain.covered(proposed.get()) + || !MineWorkingsGenerator.biomesAllowed(proposed.get(), geometry, world))) continue; + + body = candidate; + plan = proposed.get(); + target.setProperty("region_x", Integer.toString(rx)); + target.setProperty("region_z", Integer.toString(rz)); + target.setProperty("pipe_index", Integer.toString(index)); + break search; + } + } + } + + require(loaded == world.getChunkProvider().getLoadedChunkCount(), "Pipe mine search loaded a neighbouring chunk"); + require(body != null, "No safe pipe mine in the bounded search"); + target.setProperty("key", body.identity().toString()); + target.setProperty("entrance", plan.entrance().toString()); + target.setProperty("landing", plan.landing().toString()); + target.setProperty("plan", planHash(plan)); + store("pipe-target.properties", target); + } else { + try (FileInputStream input = new FileInputStream("pipe-target.properties")) { + target.load(input); + } + + DiamondDistrict district = district(world, geometry, terrain, sampler, + Integer.parseInt(target.getProperty("region_x")), Integer.parseInt(target.getProperty("region_z"))).get(); + body = new DiamondPipeWorkingsBody(district, Integer.parseInt(target.getProperty("pipe_index"))); + int surface = "wet".equals(scenario) ? 129 : terrain.surfaceY(AbandonedMinePlanner.landing(body)); + plan = "wet".equals(scenario) ? AbandonedMinePlanner.plan(body, new AbandonedMineSettings(true, 1.0D), surface).get() + : AbandonedMinePlanner.plan(body, new AbandonedMineSettings(true, 1.0D), terrain::naturalSurfaceY).get(); + require(target.getProperty("plan").equals(planHash(plan)), "Pipe geometry moved on reload"); + } + + String key = target.getProperty("key"); + boolean rejected = !captured.enabled() || !captured.abandonedMine().enabled() + || captured.abandonedMine().chance() == 0 || "wet".equals(scenario) || "structure".equals(scenario); + Properties report = new Properties(); + report.putAll(target); + report.setProperty("phase", phase); + MineWorldData data = MineWorldData.get(world); + + if ("first".equals(phase)) { + if (obstructed) { + BlockPos chestPosition = new BlockPos(plan.landing().x(), plan.landing().y() + 1, plan.landing().z()); + net.minecraft.world.chunk.Chunk chunk = world.getChunkProvider().provideChunk(chestPosition.getX() >> 4, chestPosition.getZ() >> 4); + require(!chunk.isTerrainPopulated(), "The obstruction must start in an unpopulated chunk"); + chunk.setBlockState(chestPosition, Blocks.CHEST.getDefaultState()); + TileEntityChest chest = (TileEntityChest) world.getTileEntity(chestPosition); + chest.setCustomName("Protected decline chest"); + chest.setInventorySlotContents(0, new ItemStack(Items.DIAMOND, 3)); + } + + if ("structure".equals(scenario)) { + MapGenStructureData structures = new MapGenStructureData("Mineshaft"); + NBTTagCompound existing = new NBTTagCompound(); + existing.setIntArray("BB", new int[] {plan.bounds().minimumX(), plan.bounds().minimumY(), + plan.bounds().minimumZ(), plan.bounds().maximumX(), plan.bounds().maximumY(), plan.bounds().maximumZ()}); + structures.writeInstance(existing, plan.landing().x() >> 4, plan.landing().z() >> 4); + world.getMapStorage().setData("Mineshaft", structures); + } + + if ("reverse".equals(scenario) || rejected || scenario.startsWith("normal") || "regular".equals(scenario) + || "mineralogy".equals(scenario) || obstructed) { + List chunks = allChunks(plan); + if ("reverse".equals(scenario)) Collections.reverse(chunks); + for (long chunk : chunks) populate(world, (int) (chunk >> 32), (int) chunk); + } else { + populate(world, plan.landing().x() >> 4, plan.landing().z() >> 4); + } + + if (!rejected) { + require(data.plan(key) != null, "Pipe mine rejected: " + data.rejectionReason(key)); + require(plan.blocks().equals(data.plan(key).blocks()), "Native pipe mine differs from its planner"); + if (!obstructed && !undergroundWater) { + verify(world, plan, populatedChunks(world, plan), false); + world.setBlockState(marker(plan), Blocks.DIAMOND_BLOCK.getDefaultState(), 2); + } + } + + List old = populatedChunks(world, plan); + target.setProperty("old_chunks", join(old)); + target.setProperty("terrain", terrainHash(world, old)); + store("pipe-target.properties", target); + if ("flat".equals(scenario)) require(old.size() < allChunks(plan).size(), "No pipe mine sections left for later exploration"); + } else { + List old = split(target.getProperty("old_chunks")); + require(target.getProperty("terrain").equals(terrainHash(world, old)), "Reload changed old pipe terrain or a player marker"); + if ("later".equals(phase)) { + for (long chunk : allChunks(plan)) populate(world, (int) (chunk >> 32), (int) chunk); + target.setProperty("completed_terrain", terrainHash(world, allChunks(plan))); + store("pipe-target.properties", target); + } else if ("stable".equals(phase)) { + require(target.getProperty("completed_terrain").equals(terrainHash(world, allChunks(plan))), + "Reload changed the later-generated pipe sections"); + } + require(target.getProperty("terrain").equals(terrainHash(world, old)), "Later workings excavated old chunks"); + } + + if (rejected) { + require(data.plan(key) == null, "Disabled or unsafe pipe acquired a mine"); + if ("structure".equals(scenario)) require("Another structure".equals(data.rejectionReason(key)), "Structure was not protected"); + } else { + require(data.plan(key) != null && planHash(data.plan(key)).equals(target.getProperty("plan")), "Saved pipe mine changed"); + if (obstructed) { + BlockPos chestPosition = new BlockPos(plan.landing().x(), plan.landing().y() + 1, plan.landing().z()); + require(world.getBlockState(chestPosition).getBlock() == Blocks.CHEST, "The obstructing chest was removed"); + TileEntityChest chest = (TileEntityChest) world.getTileEntity(chestPosition); + require(chest != null && "Protected decline chest".equals(chest.getName()) + && chest.getStackInSlot(0).stackSize == 3, "Obstructing chest data changed"); + require(world.getBlockState(chestPosition.down(2)).getBlock() != Blocks.PLANKS, + "The unsafe section was partly excavated"); + report.setProperty("obstructed_section_preserved", "true"); + } else if (undergroundWater) { + // This native-strata site has a spring beside the decline. The + // safety check must leave that whole section sealed, not drain it. + BlockPos spring = new BlockPos(49495, 36, 56632); + require(world.getBlockState(spring).getMaterial().isLiquid(), "The protected underground spring changed"); + for (AbandonedMinePlan.Cell cell : plan.cellsInChunk(spring.getX() >> 4, spring.getZ() >> 4)) { + if (cell.block() == zone.moddev.mc.realisticdeposits.geometry.MineBlock.AIR) continue; + MinePoint point = cell.point(); + require(!world.getBlockState(new BlockPos(point.x(), point.y(), point.z())).equals(expected(cell.block())), + "The flooded section was partly excavated at " + point); + } + report.setProperty("underground_water_preserved", "true"); + } else { + List built = populatedChunks(world, plan); + report.setProperty("checked_cells", Integer.toString(verify(world, plan, built, true))); + if (built.size() == allChunks(plan).size()) verifyAccess(world, plan, true); + } + } + + store("mine-" + phase + "-pass.properties", report); + } + + private static Optional district(WorldServer world, DepositDefinition definition, + MineTerrainSampler terrain, Optional sampler, int rx, int rz) { + return DiamondDistrict.plan(definition, world.getSeed(), "minecraft:overworld", rx, rz, new DiamondDistrict.Site() { + @Override public int surfaceY(int x, int z) { + return Boolean.getBoolean("rd.mines.wet") ? 129 : terrain.naturalSurfaceY(x, z); + } + + @Override public String geome(int x, int z, int y) { + return sampler.isPresent() ? sampler.get().sampleColumn(x, z, y).geome().toString() : null; + } + }); + } + + private static void store(String name, Properties properties) throws Exception { + try (FileOutputStream output = new FileOutputStream(name)) { + properties.store(output, "Disposable diamond pipe workings"); + } + } +} diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/generation/PolicyQualificationProbe.java b/src/test/java/zone/moddev/mc/realisticdeposits/generation/PolicyQualificationProbe.java index 20ecc2b..23c2635 100644 --- a/src/test/java/zone/moddev/mc/realisticdeposits/generation/PolicyQualificationProbe.java +++ b/src/test/java/zone/moddev/mc/realisticdeposits/generation/PolicyQualificationProbe.java @@ -38,6 +38,7 @@ import zone.moddev.mc.realisticdeposits.geometry.StratiformSeamBody; import zone.moddev.mc.realisticdeposits.geometry.CoalMeasuresBody; import zone.moddev.mc.realisticdeposits.geometry.MineableDepositBody; +import zone.moddev.mc.realisticdeposits.geometry.DiamondDistrict; import zone.moddev.mc.realisticdeposits.model.DepositDefinition; /** Packaged-server qualification fixture, excluded from the public mod jars. */ @@ -59,7 +60,9 @@ public void initialize(FMLInitializationEvent event) { @SubscribeEvent(priority = EventPriority.LOWEST, receiveCanceled = true) public void observeIron(OreGenEvent.GenerateMinable event) { - if (event.getType() != (coal() ? OreGenEvent.GenerateMinable.EventType.COAL : OreGenEvent.GenerateMinable.EventType.IRON)) { + OreGenEvent.GenerateMinable.EventType observed = diamond() ? OreGenEvent.GenerateMinable.EventType.DIAMOND + : coal() ? OreGenEvent.GenerateMinable.EventType.COAL : OreGenEvent.GenerateMinable.EventType.IRON; + if (event.getType() != observed) { return; } @@ -234,6 +237,7 @@ private JsonObject sample(WorldServer world, int base) throws Exception { private static Set plannedPositions(long worldSeed, int base) { DepositDefinition definition = RealisticDepositsRuntime.state().definitions().get(0); + if (definition.isDiamondPipe()) return diamondPositions(definition, worldSeed, base); int min = base * 16; int max = (base + SAMPLE_SIDE) * 16 - 1; int spacing = definition.regionSpacingChunks() * 16; @@ -272,7 +276,39 @@ private static Set plannedPositions(long worldSeed, int base) { private static boolean coal() { return "coal".equals(System.getProperty("rd.qualification.resource")); } - private static Block resource() { return coal() ? Blocks.COAL_ORE : Blocks.IRON_ORE; } + private static boolean diamond() { return "diamond".equals(System.getProperty("rd.qualification.resource")); } + + private static Block resource() { return diamond() ? Blocks.DIAMOND_ORE : coal() ? Blocks.COAL_ORE : Blocks.IRON_ORE; } + + private static Set diamondPositions(DepositDefinition definition, long seed, int base) { + Set positions = new HashSet<>(); + int min = base * 16; + int max = (base + SAMPLE_SIDE) * 16 - 1; + int spacing = definition.regionSpacingChunks() * 16; + int reach = definition.pipe().districtRadius() + definition.pipe().upperDiameter().maximum(); + DiamondDistrict.Site site = new DiamondDistrict.Site() { + @Override public int surfaceY(int x, int z) { return 241; } + @Override public String geome(int x, int z, int y) { return null; } + }; + + for (int rx = Math.floorDiv(min - reach, spacing); rx <= Math.floorDiv(max + reach, spacing); rx++) { + for (int rz = Math.floorDiv(min - reach, spacing); rz <= Math.floorDiv(max + reach, spacing); rz++) { + DiamondDistrict district = DiamondDistrict.plan(definition, seed, "minecraft:overworld", rx, rz, site).orElse(null); + if (district == null) continue; + for (DiamondDistrict.Pipe pipe : district.pipes()) { + BlockBounds bounds = pipe.bounds(); + for (int x = Math.max(min, bounds.minimumX()); x <= Math.min(max, bounds.maximumX()); x++) { + for (int z = Math.max(min, bounds.minimumZ()); z <= Math.min(max, bounds.maximumZ()); z++) { + for (int y = pipe.bottomY(); y <= pipe.topY(); y++) { + if (pipe.isOreAt(x, y, z)) positions.add(positionKey(x, y, z)); + } + } + } + } + } + } + return positions; + } private static long positionKey(int x, int y, int z) { return ((long) (x & 0x3ffffff) << 38) | ((long) (z & 0x3ffffff) << 12) | (y & 0xfff); diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/geometry/AbandonedMinePlannerTest.java b/src/test/java/zone/moddev/mc/realisticdeposits/geometry/AbandonedMinePlannerTest.java index 2083943..20731ba 100644 --- a/src/test/java/zone/moddev/mc/realisticdeposits/geometry/AbandonedMinePlannerTest.java +++ b/src/test/java/zone/moddev/mc/realisticdeposits/geometry/AbandonedMinePlannerTest.java @@ -57,7 +57,7 @@ void chanceIsStablePerDepositRatherThanRolledOnceForEveryChunk() { } @Test - void networkIsBoundedConnectedAndHasAUsableSurfaceLadder() { + void networkIsBoundedConnectedAndHasSteppedSurfaceAccess() { StratiformSeamBody body = body(-1, -1); AbandonedMinePlan plan = AbandonedMinePlanner.plan(body, ALWAYS, 100).get(); Set reachable = reachable(plan, plan.entrance()); @@ -77,12 +77,9 @@ void networkIsBoundedConnectedAndHasAUsableSurfaceLadder() { } } - for (int y = plan.landing().y(); y <= plan.entrance().y(); y++) { - assertEquals(MineBlock.LADDER_WEST, - plan.blocks().get(new MinePoint(plan.landing().x() + 1, y, plan.landing().z()).packed())); - assertEquals(MineBlock.PLANKS, - plan.blocks().get(new MinePoint(plan.landing().x() + 2, y, plan.landing().z()).packed())); - } + assertFalse(plan.blocks().containsValue(MineBlock.LADDER_WEST)); + assertTrue(plan.blocks().values().stream().anyMatch(block -> block.name().startsWith("STAIRS_"))); + assertEquals(plan.entrance(), MineDecline.plan(plan.landing(), 100, plan.identity()).entrance()); } @Test diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/geometry/DiamondDistrictTest.java b/src/test/java/zone/moddev/mc/realisticdeposits/geometry/DiamondDistrictTest.java new file mode 100644 index 0000000..c7e193f --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/geometry/DiamondDistrictTest.java @@ -0,0 +1,167 @@ +package zone.moddev.mc.realisticdeposits.geometry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import zone.moddev.mc.realisticdeposits.DiamondFixtures; +import zone.moddev.mc.realisticdeposits.config.DepositCatalogLoader; +import zone.moddev.mc.realisticdeposits.model.DepositDefinition; + +class DiamondDistrictTest { + private static final long SEED = 3346071746553592696L; + + private static DiamondDistrict.Site site(String geome, int surface) { + return new DiamondDistrict.Site() { + @Override public int surfaceY(int x, int z) { return surface; } + @Override public String geome(int x, int z, int height) { return geome; } + }; + } + + private static DiamondDistrict first() { + for (int x = -8; x < 8; x++) { + Optional body = DiamondDistrict.plan(DiamondFixtures.pipe(), SEED, + "minecraft:overworld", x, -3, site("orespawn:stable_craton", 80)); + if (body.isPresent()) return body.get(); + } + throw new AssertionError("No district selected"); + } + + @Test + void pipeRootsTopsAndMineralisationStayWithinTheirEnvelope() { + DiamondDistrict district = first(); + assertTrue(district.pipes().size() >= 1 && district.pipes().size() <= 3); + for (DiamondDistrict.Pipe pipe : district.pipes()) { + assertTrue(pipe.bottomY() >= 4 && pipe.bottomY() <= 12); + assertTrue(pipe.topY() >= 68 && pipe.topY() <= 76); + assertTrue(pipe.upperRadius() >= 24 && pipe.upperRadius() <= 48); + assertFalse(pipe.inside(pipe.centerX(), pipe.bottomY() - 1, pipe.centerZ())); + assertFalse(pipe.isOreAt(pipe.centerX(), pipe.topY() + 1, pipe.centerZ())); + } + Set ore = cells(district, false); + assertTrue(ore.size() > 0); + long envelope = 0; + for (DiamondDistrict.Pipe pipe : district.pipes()) { + BlockBounds bounds = pipe.bounds(); + for (int x = bounds.minimumX(); x <= bounds.maximumX(); x++) { + for (int z = bounds.minimumZ(); z <= bounds.maximumZ(); z++) { + for (int y = pipe.bottomY(); y <= pipe.topY(); y++) { + if (pipe.inside(x, y, z)) envelope++; + } + } + } + } + assertTrue(ore.size() < envelope / 2, "Most of a pipe must remain barren country rock"); + assertTrue(ore.stream().anyMatch(cell -> (cell & 255) > 40), "Shallow portions can contain diamonds"); + } + + @Test + void reversedChunkOrderAndReplanningGiveExactlyTheSameCells() { + DiamondDistrict district = first(); + assertEquals(cells(district, false), cells(district, true)); + assertEquals(cells(district, false), cells(first(), false)); + } + + @Test + void cratonsArePreferredWithoutForcingModernGeologyIntoLegacyWorlds() { + DepositDefinition definition = DiamondFixtures.pipe(); + int modern = 0; + int legacy = 0; + int unavailable = 0; + for (int x = -16; x < 16; x++) { + for (int z = -8; z < 8; z++) { + if (DiamondDistrict.plan(definition, SEED, "minecraft:overworld", x, z, + site("orespawn:stable_craton", 80)).isPresent()) modern++; + if (DiamondDistrict.plan(definition, SEED, "minecraft:overworld", x, z, + site("orespawn:cyano", 80)).isPresent()) legacy++; + if (DiamondDistrict.plan(definition, SEED, "minecraft:overworld", x, z, + site(null, 80)).isPresent()) unavailable++; + assertFalse(DiamondDistrict.plan(definition, SEED, "minecraft:overworld", x, z, + site("orespawn:volcanic_arc", 80)).isPresent()); + } + } + assertTrue(modern > legacy && legacy > 0); + assertEquals(legacy, unavailable); + assertFalse(DiamondDistrict.plan(definition, SEED, "minecraft:the_nether", 0, 0, + site("orespawn:stable_craton", 80)).isPresent()); + } + + @Test + void gradeChangesDoNotMoveTheSourcePlansOrTheirPlacers() { + DepositDefinition original = DiamondFixtures.pipe(); + com.google.gson.JsonObject json = original.toJson(); + json.getAsJsonObject("shape").addProperty("ore_density", 0.01); + DepositDefinition lowerGrade = DepositCatalogLoader.loadDefinition(json, "grade-only change"); + for (int x = -8; x < 8; x++) { + Optional a = DiamondDistrict.plan(original, SEED, "minecraft:overworld", x, -3, + site("orespawn:stable_craton", 80)); + Optional b = DiamondDistrict.plan(lowerGrade, SEED, "minecraft:overworld", x, -3, + site("orespawn:stable_craton", 80)); + assertEquals(a.isPresent(), b.isPresent()); + if (!a.isPresent()) continue; + assertEquals(a.get().identity().seed(), b.get().identity().seed()); + assertEquals(a.get().pipes().size(), b.get().pipes().size()); + for (int i = 0; i < a.get().pipes().size(); i++) { + DiamondDistrict.Pipe left = a.get().pipes().get(i); + DiamondDistrict.Pipe right = b.get().pipes().get(i); + assertEquals(left.centerX(), right.centerX()); + assertEquals(left.centerZ(), right.centerZ()); + assertEquals(left.bottomY(), right.bottomY()); + assertEquals(left.topY(), right.topY()); + assertEquals(left.upperRadius(), right.upperRadius()); + } + assertEquals(a.get().distanceSquared(0, 0), b.get().distanceSquared(0, 0)); + } + } + + @Test + void placerPocketsAndSourceDistancesDoNotDependOnExistingOre() { + DiamondDistrict district = first(); + assertEquals(0, district.distanceSquared(district.pipes().get(0).centerX(), district.pipes().get(0).centerZ())); + assertTrue(district.distanceSquared(district.centerX() + 10000, district.centerZ()) > 1024D * 1024); + int rich = 0; + long replannedSeed = first().identity().seed(); + for (int x = -32; x < 32; x++) { + for (int z = -32; z < 32; z++) { + boolean value = DiamondDistrict.placerPocket(district.identity().seed(), x, z, 0, 0.35); + assertEquals(value, DiamondDistrict.placerPocket(replannedSeed, x, z, 0, 0.35)); + if (value) rich++; + } + } + assertTrue(rich > 500 && rich < 2500); + } + + private static Set cells(DiamondDistrict district, boolean reverse) { + Set result = new HashSet<>(); + for (DiamondDistrict.Pipe pipe : district.pipes()) { + BlockBounds bounds = pipe.bounds(); + List chunks = new ArrayList<>(); + for (int x = Math.floorDiv(bounds.minimumX(), 16); x <= Math.floorDiv(bounds.maximumX(), 16); x++) { + for (int z = Math.floorDiv(bounds.minimumZ(), 16); z <= Math.floorDiv(bounds.maximumZ(), 16); z++) { + chunks.add(new int[] {x, z}); + } + } + if (reverse) Collections.reverse(chunks); + for (int[] chunk : chunks) { + for (int x = chunk[0] * 16; x < chunk[0] * 16 + 16; x++) { + for (int z = chunk[1] * 16; z < chunk[1] * 16 + 16; z++) { + for (int y = pipe.bottomY(); y <= pipe.topY(); y++) { + if (pipe.isOreAt(x, y, z)) result.add(((long) (x & 0xfffffff) << 36) + | ((long) (z & 0xfffffff) << 8) | y); + } + } + } + } + } + return result; + } +} diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/geometry/DiamondPipeWorkingsTest.java b/src/test/java/zone/moddev/mc/realisticdeposits/geometry/DiamondPipeWorkingsTest.java new file mode 100644 index 0000000..4743214 --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/geometry/DiamondPipeWorkingsTest.java @@ -0,0 +1,96 @@ +package zone.moddev.mc.realisticdeposits.geometry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import zone.moddev.mc.realisticdeposits.DiamondFixtures; +import zone.moddev.mc.realisticdeposits.model.AbandonedMineSettings; + +class DiamondPipeWorkingsTest { + private static final AbandonedMineSettings ALWAYS = new AbandonedMineSettings(true, 1.0D); + private static final DiamondDistrict.Site SITE = new DiamondDistrict.Site() { + @Override public int surfaceY(int x, int z) { return 80; } + @Override public String geome(int x, int z, int y) { return "orespawn:stable_craton"; } + }; + + @Test + void eachPipeHasItsOwnStableConnectedWorkings() { + int checked = 0; + boolean timberSupports = false; + boolean cobwebs = false; + + for (int region = -8; region < 8; region++) { + java.util.Optional selected = DiamondDistrict.plan(DiamondFixtures.pipe(), + 3346071746553592696L, "minecraft:overworld", region, -3, SITE); + if (!selected.isPresent()) continue; + + DiamondDistrict district = selected.get(); + Set keys = new HashSet<>(); + + for (int index = 0; index < district.pipes().size(); index++) { + DiamondPipeWorkingsBody body = new DiamondPipeWorkingsBody(district, index); + AbandonedMinePlan plan = AbandonedMinePlanner.plan(body, ALWAYS, 80).get(); + AbandonedMinePlan repeated = AbandonedMinePlanner.plan( + new DiamondPipeWorkingsBody(district, index), ALWAYS, 80).get(); + + assertTrue(keys.add(body.identity().toString()), "Two pipes shared a mine identity"); + assertEquals(plan.blocks(), repeated.blocks()); + assertTrue(district.pipes().get(index).inside(plan.landing().x(), + plan.landing().y(), plan.landing().z())); + assertTrue(plan.entrance().y() - plan.landing().y() >= 6); + assertFalse(plan.blocks().containsValue(MineBlock.LADDER_WEST)); + assertTrue(plan.blocks().values().stream().anyMatch(block -> block.name().startsWith("STAIRS_"))); + assertTrue(plan.blocks().containsValue(MineBlock.PLANKS)); + timberSupports |= plan.blocks().containsValue(MineBlock.FENCE); + cobwebs |= plan.blocks().containsValue(MineBlock.COBWEB); + + Set reachable = reachable(plan); + assertTrue(reachable.contains(plan.landing().packed())); + int minedOre = 0; + + for (Map.Entry cell : plan.blocks().entrySet()) { + if (cell.getValue().passable()) assertTrue(reachable.contains(cell.getKey()), + "Disconnected " + cell.getValue() + " at " + MinePoint.unpack(cell.getKey()) + " in " + body.identity()); + MinePoint point = MinePoint.unpack(cell.getKey()); + if (body.isOreAt(point.x(), point.y(), point.z())) minedOre++; + } + + assertTrue(minedOre > 0, "The workings did not reach the pipe's diamond-bearing rock"); + assertTrue(minedOre < 600, "The access should not excavate the whole pipe"); + assertFalse(AbandonedMinePlanner.plan(body, AbandonedMineSettings.DISABLED, 80).isPresent()); + checked++; + } + } + + assertTrue(checked >= 8); + assertTrue(timberSupports); + assertTrue(cobwebs); + } + + private static Set reachable(AbandonedMinePlan plan) { + Set visited = new HashSet<>(); + ArrayDeque queue = new ArrayDeque<>(); + queue.add(plan.entrance()); + int[][] offsets = {{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}}; + + while (!queue.isEmpty()) { + MinePoint point = queue.remove(); + MineBlock block = plan.blocks().get(point.packed()); + if (block == null || !block.passable() || !visited.add(point.packed())) continue; + + for (int[] offset : offsets) { + queue.add(new MinePoint(point.x() + offset[0], point.y() + offset[1], point.z() + offset[2])); + } + } + + return visited; + } +} diff --git a/src/test/java/zone/moddev/mc/realisticdeposits/geometry/GradualMineAccessTest.java b/src/test/java/zone/moddev/mc/realisticdeposits/geometry/GradualMineAccessTest.java new file mode 100644 index 0000000..3f4ad42 --- /dev/null +++ b/src/test/java/zone/moddev/mc/realisticdeposits/geometry/GradualMineAccessTest.java @@ -0,0 +1,60 @@ +package zone.moddev.mc.realisticdeposits.geometry; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import zone.moddev.mc.realisticdeposits.model.AbandonedMineSettings; + +class GradualMineAccessTest { + + @Test + void thePortalUsesItsOwnSurfaceHeightRatherThanTheRockAboveTheLanding() { + java.util.function.ToIntBiFunction surface = (x, z) -> 100 + Math.floorMod(x + z, 3); + AbandonedMinePlan first = AbandonedMinePlanner.plan(body(), new AbandonedMineSettings(true, 1), surface).get(); + AbandonedMinePlan second = AbandonedMinePlanner.plan(body(), new AbandonedMineSettings(true, 1), surface).get(); + org.junit.jupiter.api.Assertions.assertEquals(surface.applyAsInt(first.entrance().x(), first.entrance().z()), first.entrance().y()); + org.junit.jupiter.api.Assertions.assertEquals(first.blocks(), second.blocks()); + assertFalse(AbandonedMinePlanner.plan(body(), new AbandonedMineSettings(true, 1), (x, z) -> -1).isPresent()); + } + + @Test + void deepAccessUsesStepsRatherThanAVerticalLadderPit() { + MineableDepositBody body = body(); + AbandonedMinePlan plan = AbandonedMinePlanner.plan(body, new AbandonedMineSettings(true, 1), 116).get(); + + assertFalse(plan.blocks().containsValue(MineBlock.LADDER_WEST), "New access must not rely on a deep ladder"); + assertTrue(plan.blocks().values().stream().anyMatch(block -> block.name().startsWith("STAIRS_"))); + assertTrue(Math.abs(plan.entrance().x() - plan.landing().x()) + + Math.abs(plan.entrance().z() - plan.landing().z()) > 12); + + for (int x = plan.bounds().minimumX(); x <= plan.bounds().maximumX(); x++) { + for (int z = plan.bounds().minimumZ(); z <= plan.bounds().maximumZ(); z++) { + int drop = 0; + for (int y = plan.bounds().minimumY(); y <= plan.bounds().maximumY(); y++) { + MineBlock block = plan.blocks().get(new MinePoint(x, y, z).packed()); + drop = block == MineBlock.AIR ? drop + 1 : 0; + assertTrue(drop <= 8, "An unguarded vertical opening at " + x + "," + y + "," + z); + } + } + } + } + + static MineableDepositBody body() { + return new MineableDepositBody() { + @Override public DepositIdentity identity() { return new DepositIdentity(123, "minecraft:overworld", "test:deep", 0, 0, 1); } + @Override public int centerX() { return 0; } + @Override public int centerY() { return 20; } + @Override public int centerZ() { return 0; } + @Override public int strikeLength() { return 80; } + @Override public BlockBounds bounds() { return new BlockBounds(-40, 18, -30, 40, 22, 30); } + @Override public int minimumCandidateYAt(int x, int z) { return 18; } + @Override public int maximumCandidateYAt(int x, int z) { return 22; } + @Override public boolean isOreAt(int x, int y, int z) { return y == 20; } + @Override public MinePoint miningRoutePoint(double along, double lateral) { + return new MinePoint((int) Math.round(along), 20, (int) Math.round(lateral)); + } + }; + } +} diff --git a/tools/qualification/DIAMOND_TESTING.md b/tools/qualification/DIAMOND_TESTING.md new file mode 100644 index 0000000..5cfc74d --- /dev/null +++ b/tools/qualification/DIAMOND_TESTING.md @@ -0,0 +1,64 @@ +# Diamond visual checks + +These steps use a separate test profile. They do not prepare a release or open +your usual development saves. + +1. Run `prepareDiamondDepositsManual` in Gradle, then refresh the project in + Eclipse. +2. Start `runDiamondDepositsClient.launch`. Its saves and configuration live + under `run-diamond-deposits`. +3. Create a Creative world with seed `3346071746553592696`, normal terrain and + commands enabled. Keep OreSpawn's default geology and both diamond rules on. +4. Use `/realisticdeposits preview 32 45 realisticdeposits:overworld_diamond_pipes` + to preview that region without changing blocks. If using Mineralogy, use a + separate new world: its geology may select different sites. + The standalone district is centred at X 16611, Z 23204. Start with + `/tp 16611 90 23204` and use Spectator mode to inspect its depth. +5. Inspect the pipe in Spectator mode or expose a narrow vertical slice in the + disposable world. Look for an irregular, widening upper body, a narrower + root and scattered diamonds rather than a solid column. The surrounding + stone stays unchanged; no kimberlite or lamproite blocks have been added. +6. Look in nearby rivers for subtly flecked gravel below the water. Rivers do + not all contain placers, and patches need existing sand or gravel. Mining a + source pipe should not affect placers in chunks explored later. + A separate surveyed river patch is around X 131505, Z 132973, at Y 54-59: + `/tp 131505 65 132973`. It uses the same seed and standalone settings. +7. Mine several rich blocks normally, then try Fortune and Silk Touch. Normal + mining leaves ordinary gravel and usually no diamond. Silk Touch gives only + the rich block. Dropping that block onto a torch should return its item, + not extract diamonds. Place spent ordinary gravel again and confirm it + behaves like vanilla gravel. +8. Place rich gravel high above a solid floor, let it fall, save and reload. + It should remain rich gravel. Check its inventory icon and all block faces. +9. Try a resource pack which replaces vanilla gravel. The background should + change while the subtle flecks remain. Ore and river terrain should not + move when the world is reloaded. + +Both diamond definitions are in `diamonds.json`. Restart after changing their +geometry or source link. To test disabling generation, create another new +world after disabling the pipe rule; its linked placer must also be off. A +world which already captured those rules keeps its saved OreSpawn settings. + +## Surface access to a pipe + +Use `prepareUndergroundMinesManual`, refresh Eclipse and start +`runUndergroundMinesClient.launch`. This uses fresh settings under +`run-underground-mines` rather than changing an older profile's opt-outs. +Create another normal Creative world with the seed above. Workings are enabled +for every iron formation, coalfield and diamond pipe where safe; rivers are +left alone. + +The standalone pipe-access test is near `/tp 49521 75 56635`, with the entrance +at **49521, 67, 56635** and underground landing at **49497, 24, 56653**. +Walk down the stairs and turning landings, then explore the short crosscuts. +You should be able to walk back up without jumping or using a ladder. Save +and reload, then check the route again. If you change geology or terrain settings, +sites may differ. Some deposits have no usable entrance because water, trees, +structures or older chunks prevent safe excavation. + +The regular `runClient` profile also has workings enabled in its local +catalogues. Restart Minecraft and create a **new** world to try them there. +This does not change the settings or mine layouts in any existing save. + +Do not use original worlds for these checks. An older world must keep its +existing profile and background diamonds without receiving the new rules. diff --git a/tools/qualification/MINES_TESTING.md b/tools/qualification/MINES_TESTING.md index 23f7def..44c4ff1 100644 --- a/tools/qualification/MINES_TESTING.md +++ b/tools/qualification/MINES_TESTING.md @@ -1,6 +1,7 @@ # Trying the abandoned mines -Use this separate profile so your ordinary saves and settings stay untouched. +Use a new world for these checks. Existing worlds keep their captured settings +and any mines they already contain. 1. Refresh the Gradle project in Eclipse, then run **prepareAbandonedMinesManual** from Gradle Tasks. It prepares a separate configuration with mines enabled @@ -9,11 +10,13 @@ Use this separate profile so your ordinary saves and settings stay untouched. 3. Create a Creative world using seed **3346071746553592696**, normal world type, structures enabled and cheats allowed. Leave OreSpawn's terrain defaults unchanged for this comparison. -4. Run `/tp 15222 73 23842`. The tested entrance is around **15222, 68, 23842**, - with the landing around **15222, 49, 23842**. +4. For a deep diamond mine, use **runUndergroundMinesClient.launch** instead, + with the same world settings. Run `/tp 49521 75 56635`. The entrance is at + **49521, 67, 56635**, and the landing at **49497, 24, 56653**. -Look for the timber-framed opening and ladder. Walk down rather than using -Spectator mode first: check headroom, footing, ladder access and the junctions. +Look for the timber-framed opening and stepped tunnel. Walk down rather than +using Spectator mode first: check headroom, footing, turning landings and the junctions. +Walk back up without jumping. There should be no long open drop to bedrock. The workings should follow a short part of the seam, with ore still visible in the surrounding rock. Broken rails, timber supports, cobwebs and small rubble piles are intentional. An obstruction reported in the log should remain sealed. @@ -23,11 +26,23 @@ still there. Place a distinctive block in a finished passage before quitting; it must remain in place. The automated server checks separately cover mines whose remaining chunks are not generated until after a restart. -The profile uses `run-abandoned-mines`. It does not change `run`, `run-major-iron` -or their saved worlds. The prepared catalog uses a chance of `1.0` only to make -testing easier; normal definitions default to disabled, with a suggested chance -of `0.15` when enabled. To check opt-out, change `enabled` to `false`, restart -and create another new world. Existing workings are not removed. +The separate profiles use `run-abandoned-mines` and `run-underground-mines`. +Fresh catalogues attempt workings at every iron formation, coalfield and +diamond pipe where safe. River placers are unchanged. To check opt-out, change +`enabled` to `false`, restart and create another new world. Existing workings +are not removed. + +## Using the regular client + +The local `runClient` configuration now enables the same mine attempts. Its +older iron distribution settings have been kept; only the mine setting was +added. Coal and diamond catalogues were installed because they were absent. +Restart Minecraft and create a new world using the seed and settings above. +The diamond entrance coordinates above also work with this configuration. + +No existing saves have been changed. Older worlds keep their saved OreSpawn +profiles, and any old ladder shafts retain their original layout. The new +stepped access applies only to newly planned mines. ## Automated checks @@ -41,7 +56,11 @@ The probe is not included in public jars or the ordinary Eclipse profiles. The checks include normal and flat terrain, generation in reversed and shuffled chunk orders, reloads, unfinished sections, disabled settings, underwater sites, -known structure boxes and a chest obstructing a planned tunnel. The separate +known structure boxes and a chest obstructing a planned tunnel. Native movement +checks walk the new stairs in both directions. A Mineralogy spring fixture +checks that a flooded section remains sealed. The separate `mines_upgrade_110.py` helper creates worlds with the published 0.1.1 alpha, then upgrades disposable copies. It checks old terrain and chest contents on first load, reload and when a previously unopened saved chunk is populated. +`frozen_mines_110.py` separately compares completion of an older saved ladder +shaft with and without the new planner; the finished blocks must be identical. diff --git a/tools/qualification/README.md b/tools/qualification/README.md index e0852c1..4221a0a 100644 --- a/tools/qualification/README.md +++ b/tools/qualification/README.md @@ -52,7 +52,45 @@ whole-field reserves and native Fortune III rewards. Its reserve estimate is before cave, surface and host rejection, not a guarantee of recoverable coal. See the [coal walkthrough](COAL_TESTING.md) for the isolated visual-test launch. -## Run it +## Diamond checks + +`prepareDiamondQualification`, supplied by `probes.init.gradle`, builds the +separate server probe for `diamonds_110.py`. It checks native gravel harvesting, +Silk Touch, falling and save/reload, explosions, pipes, linked placers, disabled +rules and saved terrain. Normal launches and public jars do not include it. + +`qualify_110.py --resource diamond` runs the policy/import matrix with controlled +pipes above native diamonds. It disables unrelated shipped diamond definitions +so only the intended rule controls background generation. + +`prepareDiamondSurvey` builds the area-weighted survey. `diamond_survey_110.py` +uses four fixed calibration seeds and two separate holdouts. The default pass +measures complete pipe geometry against in-memory native terrain and samples +river sediment. `--native-sediment` also checks fully populated riverbeds; +`--control` measures ordinary diamonds with both new rules disabled. Keep these +results separate: pre-decoration sediment is not the complete river supply. +Multiply rich gravel by 1/16 before comparing rewards. Fortune applies only +after that roll and uses the same multiplier as vanilla diamond ore. + +See [the diamond walkthrough](DIAMOND_TESTING.md) for the separate manual launch. + +`diamond_balance_110.py` combines complete pipe surveys, fully decorated river +samples and matched vanilla controls. All inputs must identify the same candidate. +It checks the calibration, holdout and combined means against the 20% budget and +reports seed-level uncertainty and Fortune rewards rather than counting rich +gravel as if every block dropped a diamond. + +Mine checks cover iron, coal and individual diamond pipes, including the regular +development configuration. Clear routes are walked up and down using native +entity movement. Separate chest, water and structure fixtures check that unsafe +sections remain sealed. Changing an ore pass can change vanilla's later +decoration; that is not permission to remove an obstruction or rewrite old terrain. + +`frozen_mines_110.py` completes an older saved ladder shaft with both the old +planner and the new stepped-access build. It compares their blocks, player +markers, remaining sections and reloads. The source fixture is never modified. + +## Running the policy matrix For the imported-profile editor check, use the separate [visual-test walkthrough](EDITOR_TESTING.md). It opens four disposable @@ -119,3 +157,20 @@ CI prepares an official Forge server from a checksum-pinned installer and runs the complete 32-case fresh/reload/later-chunk matrix. Failures retain their logs and result reports as workflow artifacts. The optional local upgrade suite also checks disposable copies of worlds made before the fix. + +## Diamond-pipe workings + +`pipe_mines_110.py` uses the same exact public jars and isolated mine probe. +Supply `--java`, `--runtime`, `--mod`, `--orespawn`, `--probe` and a fresh +`--output` directory. The probe is built by `prepareMineQualification` with +`--init-script tools/qualification/probes.init.gradle`. + +The suite checks the shaft, galleries, player markers, saved layouts, reloads +and later-generated sections. It also checks reversed chunk order, opt-out, +zero chance, disabled source rules, water and native structures. The `normal` +case retains all factory defaults; `mineralogy` additionally requires that +target's compatible jar through `--mineralogy`. `mineralogy-flat` checks partial +workings with active strata and no cross-chunk vanilla decoration. Native +terrain cases populate the complete mine footprint before checking reloads; +vanilla decoration in adjacent new chunks can otherwise change unworked ground. +CI runs the controlled cases. diff --git a/tools/qualification/ci_runtime.py b/tools/qualification/ci_runtime.py index 93e7261..9dc990d 100644 --- a/tools/qualification/ci_runtime.py +++ b/tools/qualification/ci_runtime.py @@ -62,10 +62,31 @@ def main(): "--java", str(args.java), "--runtime", str(runtime), "--mod", str(mod), "--orespawn", str(orespawn), "--probe", str(probe), "--output", str(output / "matrix"), "--jobs", "2"] - subprocess.run(command, check=True) - result = json.loads((output / "matrix/results.json").read_text()) - if not result["passed"] or len(result["cases"]) != 32: - raise AssertionError("The complete packaged policy matrix did not pass") + for resource in ("iron", "diamond"): + matrix_output = output / ("matrix" if resource == "iron" else "diamond-matrix") + matrix_command = list(command) + matrix_command[matrix_command.index("--output") + 1] = str(matrix_output) + matrix_command.extend(["--resource", resource]) + subprocess.run(matrix_command, check=True) + result = json.loads((matrix_output / "results.json").read_text()) + if not result["passed"] or len(result["cases"]) != 32: + raise AssertionError(f"The complete {resource} policy matrix did not pass") + + diamond_probe = candidate / "diamond-probe.jar" + shutil.copy2(ROOT / "build/qualification/diamond-probe.jar", diamond_probe) + subprocess.run([sys.executable, str(ROOT / "tools/qualification/diamonds_110.py"), + "--java", str(args.java), "--runtime", str(runtime), "--mod", str(mod), + "--orespawn", str(orespawn), "--probe", str(diamond_probe), + "--output", str(output / "diamond-runtime"), + "--cases", "standalone", "reverse", "source-off", "placer-off", "source-enable"], check=True) + + mine_probe = candidate / "mine-probe.jar" + shutil.copy2(ROOT / "build/qualification/mine-probe.jar", mine_probe) + subprocess.run([sys.executable, str(ROOT / "tools/qualification/pipe_mines_110.py"), + "--java", str(args.java), "--runtime", str(runtime), "--mod", str(mod), + "--orespawn", str(orespawn), "--probe", str(mine_probe), + "--output", str(output / "pipe-workings"), + "--cases", "flat", "reverse", "off", "chance-zero", "source-off", "structure", "wet", "blocked"], check=True) if __name__ == "__main__": diff --git a/tools/qualification/coal_upgrade_110.py b/tools/qualification/coal_upgrade_110.py index 4d52c39..7ade023 100644 --- a/tools/qualification/coal_upgrade_110.py +++ b/tools/qualification/coal_upgrade_110.py @@ -13,6 +13,17 @@ OLD_MINE_BUILD = "58BAA8A5A69B904ED18387C48D8E4CE63B1C411A6DF06C3056B720C6228604A5" +def captured_settings(value): + """Ignore only OreSpawn's refreshed display version for this mod's source rows.""" + if isinstance(value, list): + return [captured_settings(item) for item in value] + if not isinstance(value, dict): + return value + own_source = value.get("owner") == "realisticdeposits" and str(value.get("source_id", "")).startswith("realisticdeposits:") + return {key: captured_settings(item) for key, item in value.items() + if not (own_source and key == "owner_version")} + + def run(java, root, phase): command = [str(java), "-Xms256m", "-Xmx2G", "-XX:ActiveProcessorCount=2", "-Drd.coal.upgrade=true", f"-Drd.coal.phase={phase}", "-jar", FORGE_JAR, "nogui"] @@ -40,12 +51,14 @@ def main(): for name in ("java", "runtime", "mod", "old-mod", "orespawn", "probe", "output"): parser.add_argument("--" + name, required=True, type=Path) parser.add_argument("--local-orespawn-sha256", help="Explicit checksum for an optional unpublished test candidate") + parser.add_argument("--old-mod-sha256", help="Exact checksum of a different, explicitly selected old release") + parser.add_argument("--new-rules", choices=("coal", "diamond"), default="coal") parser.add_argument("--mineralogy", type=Path) parser.add_argument("--legacy-profile", type=Path, help="Read-only geology reference for a separate legacy-coal-layer comparison") args = parser.parse_args() assert not args.output.exists(), "Keep old evidence and choose a new output" - assert digest(args.old_mod) == OLD_MINE_BUILD + assert digest(args.old_mod) == (args.old_mod_sha256 or OLD_MINE_BUILD).upper() assert digest(args.runtime / FORGE_JAR) == FORGE_HASH checked_orespawn_sha256(args.orespawn, args.local_orespawn_sha256) args.output.mkdir(parents=True) @@ -80,14 +93,14 @@ def main(): first = run(args.java, root, "first") source_hash = world_fingerprint(root / "coal-upgrade-world") profile_relative = "coal-upgrade-world/serverconfig/orespawn-worldgen.json" - original_profile = digest(root / profile_relative) + original_profile = json.loads((root / profile_relative).read_text(encoding="utf-8")) original_config = digest(root / "config/realisticdeposits/deposits/vanilla.json") completed = {} for name in ("control", "upgraded"): target = args.output / name shutil.copytree(root, target) if name == "upgraded": - # Both local candidates have the same unreleased product version. + # Replace the jar only in the disposable upgraded copy. shutil.copy2(args.mod, target / "mods" / args.old_mod.name) report = run(args.java, target, "complete") reload = run(args.java, target, "reload") @@ -95,10 +108,11 @@ def main(): assert first["chest"] == report["chest"] == reload["chest"] assert report["raw_marker"] and reload["raw_marker"] assert report["later_terrain"] == reload["later_terrain"] - assert digest(target / profile_relative) == original_profile, "World settings were changed" - assert digest(target / "config/realisticdeposits/deposits/vanilla.json") == original_config profile = json.loads((target / profile_relative).read_text(encoding="utf-8")) - assert not any("overworld_coal_measures" in key for key in profile["ores"]), "Coal was injected into an old world" + assert captured_settings(profile) == captured_settings(original_profile), "World settings were changed" + assert digest(target / "config/realisticdeposits/deposits/vanilla.json") == original_config + new_rule = "overworld_diamond_" if args.new_rules == "diamond" else "overworld_coal_measures" + assert not any(new_rule in key for key in profile["ores"]), "New rules were injected into an old world" completed[name] = report assert completed["control"] == completed["upgraded"], "The update changed later-generated terrain or coal supply" assert world_fingerprint(root / "coal-upgrade-world") == source_hash diff --git a/tools/qualification/diamond_balance_110.py b/tools/qualification/diamond_balance_110.py new file mode 100644 index 0000000..fa0f64b --- /dev/null +++ b/tools/qualification/diamond_balance_110.py @@ -0,0 +1,80 @@ +"""Compare expected diamond rewards with matched controls and show seed uncertainty.""" + +import argparse +import json +import random +import statistics +from pathlib import Path + +from diamond_survey_110 import SEEDS +from qualify_110 import digest, write_json + + +def interval(records): + random_source = random.Random(7029361) + estimates = [] + for _ in range(10000): + sampled = [random_source.choice(records) for _ in records] + estimates.append(sum(row["total"] for row in sampled) / sum(row["vanilla"] for row in sampled)) + estimates.sort() + return [estimates[249], estimates[9749]] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--pipes", required=True, type=Path) + parser.add_argument("--controls", required=True, type=Path) + parser.add_argument("--sediment", required=True, nargs="+", type=Path) + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args() + assert not args.output.exists(), "Keep previous reports and choose another output" + pipes = {row["seed"]: row for row in json.loads(args.pipes.read_text())} + controls = {row["seed"]: row for row in json.loads(args.controls.read_text())} + sediment = {} + for source in args.sediment: + for row in json.loads(source.read_text()): + assert row["seed"] not in sediment, "A sediment seed was counted twice" + assert row["native_sediment"], "Use fully decorated sediment samples" + sediment[row["seed"]] = row + assert set(pipes) == set(controls) == set(sediment) == set(SEEDS) + rows = [] + for seed in SEEDS: + pipe = pipes[seed]["pipe_ore"] / pipes[seed]["represented_chunks"] + placer = sediment[seed]["actual_rich_gravel"] / (16 * sediment[seed]["placer_sample_chunks"]) + vanilla = controls[seed]["diamond_ore"] / controls[seed]["chunks"] + background = vanilla * 0.15 + rows.append({"seed": seed, "set": pipes[seed]["set"], "pipes": pipe, "placers": placer, + "background": background, "total": pipe + placer + background, "vanilla": vanilla}) + sources = [args.pipes, args.controls, *args.sediment] + artifact_records = [json.loads((source.parent / "artifacts.json").read_text()) for source in sources] + candidates = {value for record in artifact_records for name, value in record.items() + if name.startswith("RealisticDeposits-") and name.endswith(".jar")} + assert len(candidates) == 1, "All balance samples must use the same immutable candidate" + report = {"candidate_sha256": candidates.pop(), "seeds": rows, "groups": {}, + "inputs": {str(source): digest(source) for source in sources}, "limitations": [ + "Pipes are an area-weighted native-terrain estimate before collisions with other ore placements and structures.", + "Background rewards use the verified 15% policy applied to matched native controls.", + "Whole-seed bootstrap intervals reflect rare-district clustering; no per-seed 20% promise is made.", + "Density-only tuning leaves district plans and placer links unchanged; native sediment provenance is recorded separately.", + ]} + for group in ("calibration", "holdout", "all"): + selected = [row for row in rows if group == "all" or row["set"] == group] + total = statistics.mean(row["total"] for row in selected) + vanilla = statistics.mean(row["vanilla"] for row in selected) + ratio = total / vanilla + assert 0.8 <= ratio <= 1.2, f"The {group} aggregate is outside the agreed reward budget: {ratio}" + report["groups"][group] = { + "seeds": len(selected), "expected_diamonds_per_chunk": total, "vanilla_diamonds_per_chunk": vanilla, + "ratio": ratio, "seed_bootstrap_95_percent_ratio": interval(selected), + "source_split": {key: statistics.mean(row[key] for row in selected) / total + for key in ("pipes", "placers", "background")}, + "fortune_expected_diamonds_per_chunk": {str(level): total * multiplier + for level, multiplier in enumerate((1, 4 / 3, 7 / 4, 11 / 5))}, + } + report["passed"] = True + write_json(args.output, report) + print(json.dumps(report["groups"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/tools/qualification/diamond_survey_110.py b/tools/qualification/diamond_survey_110.py new file mode 100644 index 0000000..6d9debb --- /dev/null +++ b/tools/qualification/diamond_survey_110.py @@ -0,0 +1,85 @@ +"""Measure diamond geometry and native river sediments without loading survey chunks.""" + +import argparse +import json +import shutil +import subprocess +from pathlib import Path + +from coal_110 import prepare +from qualify_110 import FORGE_JAR, checked_orespawn_sha256, digest, write_json +from ci_runtime import FORGE_HASH + + +SEEDS = (3346071746553592696, 741209317, -893146051, 608145372901, -2719046037, 49277810623) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("java", "runtime", "mod", "orespawn", "probe", "output"): + parser.add_argument("--" + name, required=True, type=Path) + parser.add_argument("--regions", type=int, default=16) + parser.add_argument("--chunks", type=int, default=4096) + parser.add_argument("--seed-index", type=int, nargs="+", default=list(range(len(SEEDS)))) + parser.add_argument("--control", action="store_true") + parser.add_argument("--native-sediment", action="store_true") + args = parser.parse_args() + assert not args.output.exists(), "Keep earlier evidence; choose a new output directory" + assert digest(args.runtime / FORGE_JAR) == FORGE_HASH + checked_orespawn_sha256(args.orespawn) + args.output.mkdir(parents=True) + inputs = args.output / "inputs" + inputs.mkdir() + for name in ("mod", "orespawn", "probe"): + path = getattr(args, name) + target = inputs / path.name + shutil.copy2(path, target) + setattr(args, name, target) + write_json(args.output / "artifacts.json", {p.name: digest(p) for p in inputs.iterdir()}) + results = [] + for index in args.seed_index: + seed = SEEDS[index] + args.port = 48900 + index + (32 if args.control else 64 if args.native_sediment else 0) + root = prepare(args, args.output / str(index), normal=True) + properties = (root / "server.properties").read_text().replace("coal-test-world", "diamond-test-world") + properties = properties.replace("3346071746553592696", str(seed)) + (root / "server.properties").write_text(properties, encoding="utf-8", newline="\n") + if args.control: + catalogue = json.loads((Path(__file__).resolve().parents[2] / + "src/main/resources/assets/realisticdeposits/defaults/diamonds.json").read_text()) + for definition in catalogue["deposits"]: + definition["enabled"] = False + write_json(root / "config/realisticdeposits/deposits/diamonds.json", catalogue) + command = [str(args.java), "-Xms256m", "-Xmx3G", "-XX:ActiveProcessorCount=2", + "-Drd.diamond.survey=true", f"-Drd.diamond.survey.regions={args.regions}", + f"-Drd.diamond.survey.control={str(args.control).lower()}", + f"-Drd.diamond.survey.native_sediment={str(args.native_sediment).lower()}", + f"-Drd.diamond.survey.chunks={args.chunks}", "-jar", FORGE_JAR, "nogui"] + with (root / "survey.log").open("w", encoding="utf-8") as output: + process = subprocess.Popen(command, cwd=root, stdout=output, stderr=subprocess.STDOUT, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)) + try: + code = process.wait(timeout=900) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + raise AssertionError(f"Survey timed out; keep {root}") + log = (root / "survey.log").read_text(encoding="utf-8", errors="replace") + assert code == 0 and "RD_DIAMOND_SURVEY_PASS" in log, f"Survey failed: {root}" + assert not list((root / "crash-reports").glob("*")), f"Survey crash: {root}" + report = json.loads((root / "diamond-survey.json").read_text()) + report["set"] = "calibration" if index < 4 else "holdout" + if args.control: + report["diamonds_per_chunk"] = report["diamond_ore"] / report["chunks"] + else: + report["pipe_diamonds_per_chunk"] = report["pipe_ore"] / report["represented_chunks"] + report["placer_diamonds_per_chunk"] = report["rich_gravel"] / (16 * report["placer_sample_chunks"]) + report["maximum_placer_diamonds_per_chunk"] = report["eligible_sediment"] / (16 * report["placer_sample_chunks"]) + report["actual_placer_diamonds_per_chunk"] = report["actual_rich_gravel"] / (16 * report["placer_sample_chunks"]) + results.append(report) + write_json(args.output / "results.json", results) + print(json.dumps(report), flush=True) + + +if __name__ == "__main__": + main() diff --git a/tools/qualification/diamonds_110.py b/tools/qualification/diamonds_110.py new file mode 100644 index 0000000..56d4d2a --- /dev/null +++ b/tools/qualification/diamonds_110.py @@ -0,0 +1,162 @@ +"""Check diamond pipes and river placers in disposable packaged Forge worlds.""" + +import argparse +import json +import shutil +import subprocess +from pathlib import Path + +from coal_110 import prepare +from qualify_110 import FORGE_JAR, checked_orespawn_sha256, digest, write_json +from ci_runtime import FORGE_HASH + + +ROOT = Path(__file__).resolve().parents[2] + + +def run(args, root, phase, case): + command = [str(args.java), "-Xms256m", "-Xmx2G", "-XX:ActiveProcessorCount=2", + "-Drd.diamond.enabled=true", f"-Drd.diamond.phase={phase}", + f"-Drd.diamond.fixture_river={str(case not in ('normal', 'mineralogy', 'inactive', 'legacy')).lower()}", + f"-Drd.diamond.reverse={str(case == 'reverse').lower()}", + "-jar", FORGE_JAR, "nogui"] + with (root / f"{phase}.log").open("w", encoding="utf-8") as output: + process = subprocess.Popen(command, cwd=root, stdout=output, stderr=subprocess.STDOUT, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)) + try: + code = process.wait(timeout=420) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + raise AssertionError(f"Timed out: {root}/{phase}; evidence retained") + log = (root / f"{phase}.log").read_text(encoding="utf-8", errors="replace") + assert code == 0 and f"RD_DIAMOND_PASS {phase}" in log, f"Failed: {root}/{phase}" + unexpected = [line for line in log.splitlines() if ("/ERROR]" in line or "/FATAL]" in line) + and "FML appears to be missing any signature data" not in line] + unexpected.extend(line for line in log.splitlines() if "Tried to assign a mutable BlockPos" in line) + assert not unexpected and not list((root / "crash-reports").glob("*")), unexpected + if (root / "logs/fml-server-latest.log").is_file(): + shutil.copy2(root / "logs/fml-server-latest.log", root / f"{phase}.fml.log") + print(f"PASS {case}/{phase}", flush=True) + return json.loads((root / f"diamond-{phase}.json").read_text()) + + +def same(left, right): + for key in ("terrain_sha256", "diamonds_sha256", "pipe_ore", "rich_gravel", "background_ore"): + assert left[key] == right[key], f"Saved sample changed: {key}" + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("java", "runtime", "mod", "orespawn", "probe", "output"): + parser.add_argument("--" + name, required=True, type=Path) + parser.add_argument("--mineralogy", type=Path) + parser.add_argument("--geology-profile", type=Path, help="Read-only active-strata fixture for inactive and legacy cases") + parser.add_argument("--base-port", type=int, default=48800) + parser.add_argument("--cases", nargs="+", default=("standalone", "normal", "source-off", "placer-off", "reverse"), + choices=("standalone", "normal", "source-off", "source-enable", "placer-off", "reverse", "mineralogy", "inactive", "legacy")) + args = parser.parse_args() + assert not args.output.exists(), "Use a new evidence directory" + assert digest(args.runtime / FORGE_JAR) == FORGE_HASH + checked_orespawn_sha256(args.orespawn) + args.output.mkdir(parents=True) + inputs = args.output / "inputs" + inputs.mkdir() + for name in ("mod", "orespawn", "probe", "mineralogy"): + source = getattr(args, name) + if source: + destination = inputs / source.name + shutil.copy2(source, destination) + setattr(args, name, destination) + write_json(args.output / "artifacts.json", {p.name: digest(p) for p in inputs.iterdir()}) + results = [] + for index, case in enumerate(args.cases): + args.port = args.base_port + index + natural = case in ("normal", "mineralogy", "inactive", "legacy") + root = prepare(args, args.output / case, mineralogy=case in ("mineralogy", "inactive", "legacy"), normal=natural) + properties = (root / "server.properties").read_text().replace("coal-test-world", "diamond-test-world") + if not natural: + # Fixed hosts make the chunk-order comparison independent of vanilla's + # cross-chunk lakes and decoration. Normal cases retain that decoration. + properties = properties.replace(";1;decoration", ";7;") + (root / "server.properties").write_text(properties, encoding="utf-8", newline="\n") + diamonds = json.loads((ROOT / "src/main/resources/assets/realisticdeposits/defaults/diamonds.json").read_text()) + if not natural: + diamonds["deposits"][0]["alignment"] = "massive_rare" + diamonds["deposits"][0].pop("geomes") + if case in ("source-off", "source-enable"): + diamonds["deposits"][0]["enabled"] = False + if case == "placer-off": + diamonds["deposits"][1]["enabled"] = False + write_json(root / "config/realisticdeposits/deposits/diamonds.json", diamonds) + if case in ("inactive", "legacy"): + assert args.geology_profile, "Supply a sealed active-strata reference" + profile = json.loads(args.geology_profile.read_text()) + profile["ores"] = {key: value for key, value in profile["ores"].items() + if not key.startswith("realisticdeposits:")} + profile.get("providers", {}).pop("realisticdeposits", None) + profile.get("ore_providers", {}).pop("realisticdeposits", None) + profile.pop("ore_sources", None) + if case == "inactive": + profile["terrain_dimensions"]["minecraft:overworld"]["enabled"] = False + else: + profile["geology_mode"] = "legacy" + profile["cyano"]["realistic_coal_layers"] = True + write_json(root / "config/orespawn-worldgen.json", profile) + first = run(args, root, "first", case) + sample = first["first"] + if case in ("source-off", "source-enable"): + assert sample["shallow_ore"] == 0 and sample["rich_gravel"] == 0 + else: + assert sample["pipe_ore"] > 0, "The pipe did not generate" + if case in ("standalone", "reverse"): + assert sample["rich_gravel"] > 0, "The river placer did not generate" + if case == "placer-off": + assert sample["rich_gravel"] == 0 + profile = root / "diamond-test-world/serverconfig/orespawn-worldgen.json" + explicitly_enabled = None + if case == "source-enable": + captured = json.loads(profile.read_text()) + for rule_id, rule in captured["ores"].items(): + if rule_id.startswith("realisticdeposits:") and "overworld_diamond_" in rule_id: + rule["enabled"] = True + write_json(profile, captured) + explicitly_enabled = captured + profile_hash = digest(profile) + for phase in ("reload", "later", "stable"): + current = run(args, root, phase, case) + same(first["first"], current["first"]) + if case == "source-enable" and phase == "reload": + refreshed = json.loads(profile.read_text()) + # Enabling a source refreshes OreSpawn's derived source directory. + for key in set(explicitly_enabled) | set(refreshed): + if key not in ("ore_sources", "ore_source_policies"): + assert explicitly_enabled.get(key) == refreshed.get(key), f"World setting changed: {key}" + before_policies = explicitly_enabled["ore_source_policies"] + after_policies = refreshed["ore_source_policies"] + for key, previous in before_policies.items(): + assert key in after_policies, "An existing source policy disappeared" + old_choices = {name: value for name, value in previous.items() if name != "candidates"} + new_choices = {name: value for name, value in after_policies[key].items() if name != "candidates"} + assert old_choices == new_choices, "Enabling diamonds changed an existing source choice" + added = set(after_policies) - set(before_policies) + assert added <= {"realisticdeposits:diamond_gravel|minecraft:overworld"} + profile_hash = digest(profile) + else: + assert digest(profile) == profile_hash, "Saved profile changed" + if phase == "later": + later = current["later"] + if case == "source-enable": + assert later["shallow_ore"] > 0 and later["rich_gravel"] > 0, "The explicit world override did not enable diamonds" + elif phase == "stable": + same(later, current["later"]) + results.append({"case": case, "passed": True, "sample": sample, "profile_sha256": profile_hash}) + if case == "reverse": + forward = next((item for item in results if item["case"] == "standalone"), None) + if forward: + same(forward["sample"], sample) + write_json(args.output / "results.json", {"passed": True, "cases": results}) + + +if __name__ == "__main__": + main() diff --git a/tools/qualification/frozen_mines_110.py b/tools/qualification/frozen_mines_110.py new file mode 100644 index 0000000..b59b591 --- /dev/null +++ b/tools/qualification/frozen_mines_110.py @@ -0,0 +1,94 @@ +"""Complete an older saved shaft on disposable control and upgraded servers.""" + +import argparse +import json +import shutil +import subprocess +from pathlib import Path + +from coal_110 import prepare +from ci_runtime import FORGE_HASH +from mines_110 import run +from qualify_110 import FORGE_JAR, checked_orespawn_sha256, digest, world_fingerprint, write_json + + +ROOT = Path(__file__).resolve().parents[2] + + +def frozen_boot(java, root, phase): + command = [str(java), "-Xms256m", "-Xmx2G", "-XX:ActiveProcessorCount=2", + "-Drd.frozen.enabled=true", f"-Drd.frozen.phase={phase}", "-jar", FORGE_JAR, "nogui"] + with (root / f"frozen-{phase}.log").open("w", encoding="utf-8") as output: + process = subprocess.Popen(command, cwd=root, stdout=output, stderr=subprocess.STDOUT, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)) + try: + code = process.wait(timeout=300) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + raise AssertionError("Frozen shaft check timed out; retain its evidence") + log = (root / f"frozen-{phase}.log").read_text(encoding="utf-8", errors="replace") + assert code == 0 and f"RD_FROZEN_MINE_PASS {phase}" in log, f"{root.name}/{phase} failed" + assert not [line for line in log.splitlines() if ("/ERROR]" in line or "/FATAL]" in line) + and "FML appears to be missing any signature data" not in line] + assert not list((root / "crash-reports").glob("*")) + shutil.copy2(root / "logs/fml-server-latest.log", root / f"frozen-{phase}.fml.log") + print(f"PASS {root.name}/{phase}", flush=True) + return json.loads((root / f"frozen-{phase}.json").read_text()) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("java", "runtime", "mod", "old-mod", "orespawn", "old-probe", "frozen-probe", "output"): + parser.add_argument("--" + name, required=True, type=Path) + parser.add_argument("--old-mod-sha256", required=True) + args = parser.parse_args() + assert not args.output.exists(), "Use a fresh evidence directory" + assert digest(args.old_mod) == args.old_mod_sha256.upper() + assert digest(args.runtime / FORGE_JAR) == FORGE_HASH + checked_orespawn_sha256(args.orespawn) + args.output.mkdir(parents=True) + inputs = args.output / "inputs" + inputs.mkdir() + for name in ("mod", "old_mod", "orespawn", "old_probe", "frozen_probe"): + source = getattr(args, name) + destination = inputs / (name + ".jar") + shutil.copy2(source, destination) + setattr(args, name, destination) + write_json(args.output / "artifacts.json", {p.name: digest(p) for p in inputs.iterdir()}) + current = args.mod + args.mod = args.old_mod + args.probe = args.old_probe + args.port = 48670 + source = prepare(args, args.output / "source") + shutil.copy2(args.frozen_probe, source / "mods/frozen_probe.jar") + settings = (source / "server.properties").read_text().replace("coal-test-world", "mine-test-world").replace(";1;decoration", ";1;") + (source / "server.properties").write_text(settings, encoding="utf-8", newline="\n") + for name in ("vanilla", "coal", "diamonds"): + catalogue = json.loads((ROOT / f"src/main/resources/assets/realisticdeposits/defaults/{name}.json").read_text()) + for definition in catalogue["deposits"]: + definition["enabled"] = name == "vanilla" + write_json(source / f"config/realisticdeposits/deposits/{name}.json", catalogue) + run(args.java, source, "first") + before = world_fingerprint(source / "mine-test-world") + reports = [] + for index, name in enumerate(("control", "upgraded")): + target = args.output / name + shutil.copytree(source, target) + if name == "upgraded": + shutil.copy2(current, target / "mods/old_mod.jar") + (target / "server.properties").write_text(settings.replace("48670", str(48671 + index)), encoding="utf-8", newline="\n") + profile = target / "mine-test-world/serverconfig/orespawn-worldgen.json" + profile_hash = digest(profile) + complete = frozen_boot(args.java, target, "complete") + assert frozen_boot(args.java, target, "reload") == complete + assert digest(profile) == profile_hash, "Saved profile changed" + reports.append(complete) + assert reports[0] == reports[1], "New access logic changed the old saved shaft or its later terrain" + assert before == world_fingerprint(source / "mine-test-world"), "The source fixture changed" + write_json(args.output / "results.json", {"passed": True, "boots": 5, "control_matches_upgrade": True, + "source_unchanged": True, "shaft": reports[0]}) + + +if __name__ == "__main__": + main() diff --git a/tools/qualification/mines_110.py b/tools/qualification/mines_110.py index 7772a21..523fdc0 100644 --- a/tools/qualification/mines_110.py +++ b/tools/qualification/mines_110.py @@ -44,7 +44,7 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) for name in ("java", "runtime", "mod", "orespawn", "probe", "output"): parser.add_argument(f"--{name}", required=True, type=Path) - cases = ["flat", "normal", "off", "chance-zero", "wet", "order-reverse", "order-shuffle", "structure", "blocked"] + cases = ["flat", "normal", "off", "chance-zero", "wet", "order-reverse", "order-shuffle", "structure", "blocked", "diamond-coexistence"] parser.add_argument("--cases", nargs="+", default=cases, choices=cases) parser.add_argument("--local-orespawn-sha256", help="Exact checksum of the deliberately selected local candidate") args = parser.parse_args() @@ -74,12 +74,13 @@ def main(): layers = "minecraft:bedrock,127*minecraft:stone,minecraft:grass" if case == "wet": layers = "minecraft:bedrock,127*minecraft:stone,20*minecraft:water" - level_type = "DEFAULT" if case == "normal" else "FLAT" - generator_settings = "" if case == "normal" else f"3;{layers};1;" + normal = case in ("normal", "diamond-coexistence") + level_type = "DEFAULT" if normal else "FLAT" + generator_settings = "" if normal else f"3;{layers};1;" settings = ( f"level-name=mine-test-world\nlevel-seed=3346071746553592696\nlevel-type={level_type}\n" f"generator-settings={generator_settings}\nserver-ip=127.0.0.1\nserver-port={48610 + index}\n" - f"online-mode=false\ngenerate-structures={'true' if case == 'normal' else 'false'}\nview-distance=3\nmax-tick-time=-1\n" + f"online-mode=false\ngenerate-structures={'true' if normal else 'false'}\nview-distance=3\nmax-tick-time=-1\n" "allow-nether=false\nspawn-animals=false\nspawn-monsters=false\n" ) (root / "server.properties").write_text(settings, encoding="utf-8", newline="\n") @@ -88,16 +89,29 @@ def main(): catalog["deposits"][0]["abandoned_mine"] = { "enabled": case != "off", "chance": 0.0 if case == "chance-zero" else 1.0} write_json(root / "config/realisticdeposits/deposits/vanilla.json", catalog) + # This suite checks iron workings. Coal and pipe access have separate + # fixtures, so their new defaults must not create unrelated mine data. + coal = json.loads((ROOT / "src/main/resources/assets/realisticdeposits/defaults/coal.json").read_text()) + coal["deposits"][0]["abandoned_mine"]["enabled"] = False + write_json(root / "config/realisticdeposits/deposits/coal.json", coal) + # Keep the accepted iron/mine baseline's vanilla decoration sequence. + # A separate case exercises the new diamond background policy as well. + diamonds = json.loads((ROOT / "src/main/resources/assets/realisticdeposits/defaults/diamonds.json").read_text()) + for definition in diamonds["deposits"]: + definition["enabled"] = case == "diamond-coexistence" + if "abandoned_mine" in definition: + definition["abandoned_mine"]["enabled"] = False + write_json(root / "config/realisticdeposits/deposits/diamonds.json", diamonds) if case == "flat": phases = ["first", "complete", "reload"] - elif case == "normal": + elif normal: phases = ["default-first", "default-reload"] elif case.startswith("order-") or case in ("structure", "blocked"): phases = [case] else: phases = ["wet" if case == "wet" else "off"] for phase in phases: - run(args.java, root, phase) + run(args.java, root, phase, {"rd.mines.diamondCoexistence": str(case == "diamond-coexistence").lower()}) results.append({"case": case, "phase": phase, "passed": True}) write_json(output / "results.json", {"passed": True, "boots": len(results), "results": results}) diff --git a/tools/qualification/mines_upgrade_110.py b/tools/qualification/mines_upgrade_110.py index 1598d1b..9814b37 100644 --- a/tools/qualification/mines_upgrade_110.py +++ b/tools/qualification/mines_upgrade_110.py @@ -4,6 +4,7 @@ import json import shutil import subprocess +import zipfile from pathlib import Path from qualify_110 import checked_orespawn_sha256, digest, world_fingerprint, write_json, FORGE_JAR @@ -35,15 +36,15 @@ def edit_definition(value): return count -def expected_upgrade_profile(value): +def expected_upgrade_profile(value, version): if isinstance(value, dict): if value.get("owner") == "realisticdeposits" and value.get("owner_version") == "0.1.1.110021": - value["owner_version"] = "0.2.0.110021" + value["owner_version"] = version for child in value.values(): - expected_upgrade_profile(child) + expected_upgrade_profile(child, version) elif isinstance(value, list): for child in value: - expected_upgrade_profile(child) + expected_upgrade_profile(child, version) def main(): @@ -68,6 +69,9 @@ def main(): target = inputs / f"{role}.jar" shutil.copy2(source, target) write_json(output / "artifacts.json", {path.name: digest(path) for path in inputs.iterdir()}) + with zipfile.ZipFile(inputs / "current.jar") as archive: + metadata = json.loads(archive.read("mcmod.info")) + version = next(mod["version"] for mod in metadata if mod["modid"] == "realisticdeposits") baseline = output / "old-alpha" baseline.mkdir() shutil.copytree(args.runtime / "libraries", baseline / "libraries") @@ -130,7 +134,7 @@ def main(): assert edit_definition(profile) == 1, "Expected one captured deposit definition" write_json(profile_path, profile) expected_profile = json.loads(profile_path.read_text(encoding="utf-8")) - expected_upgrade_profile(expected_profile) + expected_upgrade_profile(expected_profile, version) profile_after_upgrade = None for phase in ("upgrade-first", "upgrade-reload", "upgrade-later"): run(args.java, target, phase, {"rd.mines.upgrade.optIn": str(explicit).lower()}) diff --git a/tools/qualification/pipe_mines_110.py b/tools/qualification/pipe_mines_110.py new file mode 100644 index 0000000..12a0d9c --- /dev/null +++ b/tools/qualification/pipe_mines_110.py @@ -0,0 +1,100 @@ +"""Check diamond-pipe workings, saved plans and safe opt-outs on disposable servers.""" + +import argparse +import json +import shutil +from pathlib import Path + +from coal_110 import prepare +from ci_runtime import FORGE_HASH +from mines_110 import run +from qualify_110 import FORGE_JAR, checked_orespawn_sha256, digest, write_json + + +ROOT = Path(__file__).resolve().parents[2] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + for name in ("java", "runtime", "mod", "orespawn", "probe", "output"): + parser.add_argument("--" + name, required=True, type=Path) + parser.add_argument("--mineralogy", type=Path) + parser.add_argument("--snapshot-first", action="store_true", help="Keep a first-load world copy for terrain diagnostics") + parser.add_argument("--cases", nargs="+", default=("flat", "reverse", "off", "chance-zero", "source-off", "structure", "wet", "normal", "blocked"), + choices=("flat", "reverse", "off", "chance-zero", "source-off", "structure", "wet", "normal", "normal-structures", "blocked", "mineralogy", "mineralogy-flat", "regular")) + args = parser.parse_args() + assert not args.output.exists(), "Use a fresh evidence directory" + assert digest(args.runtime / FORGE_JAR) == FORGE_HASH + checked_orespawn_sha256(args.orespawn) + assert (args.runtime / "eula.txt").read_text().strip() == "eula=true" + args.output.mkdir(parents=True) + sealed = args.output / "inputs" + sealed.mkdir() + + for name in ("mod", "orespawn", "probe", "mineralogy"): + source = getattr(args, name) + if source: + target = sealed / source.name + shutil.copy2(source, target) + setattr(args, name, target) + write_json(args.output / "artifacts.json", {p.name: digest(p) for p in sealed.iterdir()}) + + results = [] + for index, case in enumerate(args.cases): + args.port = 48920 + index + natural = case in ("normal", "normal-structures", "mineralogy", "regular") + root = prepare(args, args.output / case, mineralogy=case.startswith("mineralogy"), normal=natural) + properties = (root / "server.properties").read_text().replace("coal-test-world", "mine-test-world") + if case in ("normal-structures", "regular"): + properties = properties.replace("generate-structures=false", "generate-structures=true") + if case == "wet": + properties = properties.replace("127*minecraft:stone,minecraft:grass;1;decoration", + "127*minecraft:stone,20*minecraft:water;1;") + elif not natural: + properties = properties.replace(";1;decoration", ";1;") + (root / "server.properties").write_text(properties, encoding="utf-8", newline="\n") + + for name in ("vanilla", "coal", "diamonds"): + location = ROOT / (f"run/config/realisticdeposits/deposits/{name}.json" if case == "regular" + else f"src/main/resources/assets/realisticdeposits/defaults/{name}.json") + catalogue = json.loads(location.read_text()) + if not natural: + for definition in catalogue["deposits"]: + definition["enabled"] = definition["shape"]["type"] == "diamond_pipe" and case != "source-off" + if definition["shape"]["type"] != "diamond_pipe": + continue + definition["alignment"] = "massive_rare" + definition.pop("geomes") + definition["shape"]["upper_diameter"] = [96, 96] + if case == "off": + definition["abandoned_mine"]["enabled"] = False + if case == "chance-zero": + definition["abandoned_mine"]["chance"] = 0.0 + write_json(root / f"config/realisticdeposits/deposits/{name}.json", catalogue) + + saved_profile = root / "mine-test-world/serverconfig/orespawn-worldgen.json" + for phase in ("first", "reload", "later", "stable"): + run(args.java, root, phase, {"rd.mines.pipe": "true", "rd.mines.scenario": case, + "rd.mines.startRegion": "96" if case.startswith("normal") or case in ("mineralogy", "regular") else "32", + "rd.mines.wet": str(case == "wet").lower()}) + if phase == "first": + profile_hash = digest(saved_profile) + if args.snapshot_first: + shutil.copytree(root / "mine-test-world", root / "first-world-snapshot") + else: + assert digest(saved_profile) == profile_hash, "Workings changed the captured OreSpawn settings" + + receipt = dict(line.split("=", 1) for line in (root / "pipe-target.properties").read_text().splitlines() + if "=" in line and not line.startswith("#")) + if case == "reverse": + forward = next((item for item in results if item["case"] == "flat"), None) + if forward: + assert receipt["plan"] == forward["plan"], "Chunk order changed the pipe workings layout" + assert receipt["completed_terrain"] == forward["terrain"], "Chunk order changed the completed workings" + results.append({"case": case, "passed": True, "profile_sha256": profile_hash, + "plan": receipt["plan"], "terrain": receipt["completed_terrain"]}) + write_json(args.output / "results.json", {"passed": True, "boots": len(results) * 4, "cases": results}) + + +if __name__ == "__main__": + main() diff --git a/tools/qualification/probes.init.gradle b/tools/qualification/probes.init.gradle index 21ac954..b071338 100644 --- a/tools/qualification/probes.init.gradle +++ b/tools/qualification/probes.init.gradle @@ -1,5 +1,38 @@ gradle.afterProject { project, state -> if (project == project.rootProject && state.failure == null) { + def surveyInput = project.tasks.register('diamondSurveyProbeInput', Jar) { + dependsOn project.tasks.named('testClasses') + from project.sourceSets.test.output.classesDirs + include 'zone/moddev/mc/realisticdeposits/generation/DiamondSurveyProbe*.class' + archiveFileName = 'diamond-survey-deobf.jar' + destinationDirectory = project.layout.buildDirectory.dir('qualification') + } + def surveyPackaged = project.renamer.classes(surveyInput) { + map.from project.minecraft.dependency.toSrgFile + output = project.layout.buildDirectory.file('qualification/diamond-survey.jar') + } + project.tasks.register('prepareDiamondSurvey') { + group = 'verification' + description = 'Builds the isolated area-weighted diamond reward survey.' + dependsOn surveyPackaged + } + def diamondInput = project.tasks.register('diamondQualificationProbeInput', Jar) { + dependsOn project.tasks.named('testClasses') + from project.sourceSets.test.output.classesDirs + include 'zone/moddev/mc/realisticdeposits/generation/DiamondRuntimeProbe*.class' + include 'zone/moddev/mc/realisticdeposits/generation/DiamondGravelRuntimeChecks*.class' + archiveFileName = 'diamond-probe-deobf.jar' + destinationDirectory = project.layout.buildDirectory.dir('qualification') + } + def diamondPackaged = project.renamer.classes(diamondInput) { + map.from project.minecraft.dependency.toSrgFile + output = project.layout.buildDirectory.file('qualification/diamond-probe.jar') + } + project.tasks.register('prepareDiamondQualification') { + group = 'verification' + description = 'Builds the isolated diamond deposit server probe.' + dependsOn diamondPackaged + } def input = project.tasks.register('policyQualificationProbeInput', Jar) { dependsOn project.tasks.named('testClasses') from project.sourceSets.test.output.classesDirs @@ -23,6 +56,8 @@ gradle.afterProject { project, state -> dependsOn project.tasks.named('testClasses') from project.sourceSets.test.output.classesDirs include 'zone/moddev/mc/realisticdeposits/generation/CoalRuntimeProbe*.class' + include 'zone/moddev/mc/realisticdeposits/generation/MineWorkingsRuntimeProbe*.class' + include 'zone/moddev/mc/realisticdeposits/generation/PipeWorkingsRuntimeChecks*.class' archiveFileName = 'coal-probe-deobf.jar' destinationDirectory = project.layout.buildDirectory.dir('qualification') } @@ -57,6 +92,7 @@ gradle.afterProject { project, state -> dependsOn project.tasks.named('testClasses') from project.sourceSets.test.output.classesDirs include 'zone/moddev/mc/realisticdeposits/generation/MineWorkingsRuntimeProbe*.class' + include 'zone/moddev/mc/realisticdeposits/generation/PipeWorkingsRuntimeChecks*.class' archiveFileName = 'mine-probe-deobf.jar' destinationDirectory = project.layout.buildDirectory.dir('qualification') } @@ -66,6 +102,18 @@ gradle.afterProject { project, state -> output = project.layout.buildDirectory.file('qualification/mine-probe.jar') } + def frozenInput = project.tasks.register('frozenMineProbeInput', Jar) { + dependsOn project.tasks.named('testClasses') + from project.sourceSets.test.output.classesDirs + include 'zone/moddev/mc/realisticdeposits/generation/FrozenMineRuntimeProbe*.class' + archiveFileName = 'frozen-mine-probe-deobf.jar' + destinationDirectory = project.layout.buildDirectory.dir('qualification') + } + def frozenPackaged = project.renamer.classes(frozenInput) { + map.from project.minecraft.dependency.toSrgFile + output = project.layout.buildDirectory.file('qualification/frozen-mine-probe.jar') + } + def oldTerrainInput = project.tasks.register('legacyMineProbeInput', Jar) { dependsOn project.tasks.named('testClasses') from project.sourceSets.test.output.classesDirs @@ -83,6 +131,7 @@ gradle.afterProject { project, state -> group = 'verification' description = 'Builds the isolated abandoned-mine server probe.' dependsOn minePackaged + dependsOn frozenPackaged dependsOn oldTerrainPackaged } } diff --git a/tools/qualification/qualify_110.py b/tools/qualification/qualify_110.py index 5fcd8db..5bc75e6 100644 --- a/tools/qualification/qualify_110.py +++ b/tools/qualification/qualify_110.py @@ -77,7 +77,7 @@ def prepare(args, name, origin, policy, managed, port, bootstrap=False): settings = ( "level-name=world\nlevel-seed=3346071746553592696\nlevel-type=FLAT\n" - f"generator-settings=3;minecraft:bedrock,{239 if RESOURCE_KEY == 'coal_ore' else 127}*minecraft:stone,minecraft:grass;1;decoration\n" + f"generator-settings=3;minecraft:bedrock,{239 if RESOURCE_KEY in ('coal_ore', 'diamond_ore') else 127}*minecraft:stone,minecraft:grass;1;decoration\n" f"server-ip=127.0.0.1\nserver-port={port}\nonline-mode=false\n" "generate-structures=false\nview-distance=3\nmax-tick-time=-1\nallow-nether=false\n" "spawn-animals=false\nspawn-monsters=false\n" @@ -103,6 +103,14 @@ def prepare(args, name, origin, policy, managed, port, bootstrap=False): "beds": [2, 2], "thickness": [1, 1], "rock_interval": [2, 2], "maximum_dip_degrees": 0, "fold_amplitude": 0, "maximum_fault_throw": 0, } + elif RESOURCE_KEY == "diamond_ore": + # Separate pipe diamonds from the native/imported low-level supply. + definition.update(min_y=180, max_y=210) + definition["shape"] = { + "type": "diamond_pipe", "pipes": [1, 2], "upper_diameter": [48, 64], + "root_y": [4, 12], "surface_cover": [4, 12], "district_radius": 64, + "ore_density": 0.05, "legacy_selection_scale": 0.25, + } definition["interaction"] = "hybrid" if policy.startswith("hybrid") else policy if policy == "hybrid-15": @@ -113,6 +121,11 @@ def prepare(args, name, origin, policy, managed, port, bootstrap=False): .read_text(encoding="utf-8")) coal["deposits"][0]["enabled"] = False write_json(root / "config/realisticdeposits/deposits/coal.json", coal) + diamonds = json.loads((Path(__file__).parents[2] / "src/main/resources/assets/realisticdeposits/defaults/diamonds.json") + .read_text(encoding="utf-8")) + for default in diamonds["deposits"]: + default["enabled"] = False + write_json(root / "config/realisticdeposits/deposits/diamonds.json", diamonds) sources = {} if origin != "native": @@ -230,6 +243,10 @@ def check_profile(report, origin, policy, managed): default_coal = owned.pop("realisticdeposits:deposit/realisticdeposits/overworld_coal_measures", None) if default_coal is not None: assert default_coal["enabled"] is False, "The unrelated default coalfield must remain disabled" + for name in ("overworld_diamond_pipes", "overworld_diamond_placers"): + default = owned.pop("realisticdeposits:deposit/realisticdeposits/" + name, None) + if default is not None: + assert default["enabled"] is False, "An unrelated default diamond rule became active" assert set(owned) == {DEPOSIT_ID}, "Duplicate or missing Realistic Deposits rule" rule = owned[DEPOSIT_ID] assert rule["retrogen"] is False @@ -405,7 +422,7 @@ def main(): parser.add_argument("--jobs", type=int, default=2) parser.add_argument("--base-port", type=int, default=25900, help="First loopback port; choose a separate range for concurrent matrices") - parser.add_argument("--resource", choices=("iron", "coal"), default="iron") + parser.add_argument("--resource", choices=("iron", "coal", "diamond"), default="iron") parser.add_argument("--only", default="", help="Run matching case names for focused diagnostics") parser.add_argument("--local-orespawn-sha256", help="Explicit checksum for testing an unpublished local OreSpawn fix") @@ -420,6 +437,13 @@ def main(): raise AssertionError("Keep previous evidence and choose a new output directory") args.output.mkdir(parents=True) + inputs = args.output / "inputs" + inputs.mkdir() + for name in ("mod", "orespawn", "probe"): + source = getattr(args, name) + target = inputs / source.name + shutil.copy2(source, target) + setattr(args, name, target) write_json(args.output / "artifacts.json", { "orespawn_kind": "local" if args.local_orespawn_sha256 else "published", "orespawn_sha256": orespawn_sha256, diff --git a/tools/qualification/render_diamonds.py b/tools/qualification/render_diamonds.py new file mode 100644 index 0000000..bf23267 --- /dev/null +++ b/tools/qualification/render_diamonds.py @@ -0,0 +1,53 @@ +"""Draw a vertical block section from the diamond server probe's JSON report.""" + +import argparse +import json +from pathlib import Path + +from PIL import Image, ImageDraw + + +COLOURS = { + "minecraft:air": "#202d3c", + "minecraft:water": "#3b6aa7", + "minecraft:flowing_water": "#3b6aa7", + "minecraft:stone": "#777b80", + "minecraft:bedrock": "#303134", + "minecraft:grass": "#779848", + "minecraft:dirt": "#806344", + "minecraft:sand": "#c7bc84", + "minecraft:gravel": "#9d9790", + "minecraft:diamond_ore": "#35e0dc", + "realisticdeposits:diamond_gravel": "#e6ae49", +} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("report", type=Path) + parser.add_argument("output", type=Path) + parser.add_argument("--sample", default="first") + args = parser.parse_args() + assert not args.output.exists(), "Keep the existing map and choose another output name" + sample = json.loads(args.report.read_text())[args.sample] + cells = sample["section"] + minimum = min(cell[0] for cell in cells) + maximum = max(cell[0] for cell in cells) + top = max(cell[1] for cell in cells) + scale = 5 + image = Image.new("RGB", ((maximum - minimum + 1) * scale + 80, (top + 1) * scale + 100), "#17212c") + draw = ImageDraw.Draw(image) + draw.text((18, 12), f"Diamond section: Z {sample['centre_z']} | X {minimum}..{maximum}", fill="white") + for x, y, block in cells: + px = 48 + (x - minimum) * scale + py = 42 + (top - y) * scale + draw.rectangle((px, py, px + scale - 1, py + scale - 1), fill=COLOURS.get(block, "#68635b")) + for y in range(0, top + 1, 20): + draw.text((12, 42 + (top - y) * scale), str(y), fill="white") + draw.text((18, image.height - 35), "Cyan: diamond ore | Gold: rich gravel | Other colours: existing terrain", fill="white") + args.output.parent.mkdir(parents=True, exist_ok=True) + image.save(args.output) + + +if __name__ == "__main__": + main() diff --git a/tools/qualification/test_upgrade_profile.py b/tools/qualification/test_upgrade_profile.py new file mode 100644 index 0000000..fc1ed29 --- /dev/null +++ b/tools/qualification/test_upgrade_profile.py @@ -0,0 +1,29 @@ +"""A version-label refresh must not hide changed world settings.""" + +import copy +import unittest + +from coal_upgrade_110 import captured_settings + + +class UpgradeProfileTest(unittest.TestCase): + def test_only_owned_source_display_versions_are_ignored(self): + old = {"owner": "realisticdeposits", "source_id": "realisticdeposits:deposit/iron", + "owner_version": "0.2.0.110021", "enabled": True} + current = dict(old, owner_version="0.3.0.110021") + self.assertEqual(captured_settings(old), captured_settings(current)) + current["enabled"] = False + self.assertNotEqual(captured_settings(old), captured_settings(current)) + + def test_other_mod_metadata_and_nested_settings_remain_protected(self): + old = {"ores": {"custom": {"owner": "someone_else", "owner_version": "1", "frequency": 0.15}}} + current = copy.deepcopy(old) + current["ores"]["custom"]["owner_version"] = "2" + self.assertNotEqual(captured_settings(old), captured_settings(current)) + current = copy.deepcopy(old) + current["ores"]["custom"]["frequency"] = 1.0 + self.assertNotEqual(captured_settings(old), captured_settings(current)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/release/test_release.py b/tools/release/test_release.py index 3ac769b..20b10cf 100644 --- a/tools/release/test_release.py +++ b/tools/release/test_release.py @@ -13,7 +13,7 @@ def test_public_dependency_and_beta_candidate_version(self): properties = dict(line.split("=", 1) for line in (ROOT / "gradle.properties").read_text().splitlines() if "=" in line and not line.startswith("#")) - self.assertEqual("0.2.0.110021", properties["mod_version"]) + self.assertEqual("0.3.0.110021", properties["mod_version"]) self.assertEqual("4.1.4.110021", properties["orespawn_version"]) self.assertEqual("9356289", properties["orespawn_curse_file_id"]) self.assertEqual("DB254E9E865685A74262DE9D469A006AB3C8053573CC6136FD4A8591AD806CA3",