Public Access
Advance to 1.0.1.54-beta.1 after player validation. Port material, vegetation, lake, terrain-volume, crystal, Dense Ores and fissure features with saved plans. Update beta documentation and release notes. Refs #98, #104
This commit is contained in:
1 parent
d75154fa9b
commit
c223f3fdca
37 files changed
+2559
-193
No files matched your search
@@ -7,9 +7,18 @@ of [Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to
|
|||||||
Minecraft 1.21.1 on NeoForge. Mystcraft veterans will recognize the writing
|
Minecraft 1.21.1 on NeoForge. Mystcraft veterans will recognize the writing
|
||||||
flow, D'ni-inspired glyphs, libraries, crystals, and instability. Rivencraft
|
flow, D'ni-inspired glyphs, libraries, crystals, and instability. Rivencraft
|
||||||
also lets you author modern terrain, biomes, structures, seasons, and celestial
|
also lets you author modern terrain, biomes, structures, seasons, and celestial
|
||||||
bodies. It is an active pre-public-release project, not a byte-for-byte copy of
|
bodies. It is an independently developed project, not a byte-for-byte copy of
|
||||||
the 1.12.2 mod.
|
the 1.12.2 mod.
|
||||||
|
|
||||||
|
The gameplay guide below describes the **Minecraft 1.21.1 release**. A separate,
|
||||||
|
incomplete **Minecraft 26.3 beta** is available: Java 25, NeoForge 26.3.0.99,
|
||||||
|
and a first-party Age backend (no Dynamic Dimensions or Bad Packets). It now
|
||||||
|
supports authored terrain/biomes and a growing set of generation features,
|
||||||
|
but does not yet provide full 1.21.1 gameplay or optional-integration parity.
|
||||||
|
See the [26.3 beta guide](ports/mc263/README.md) and
|
||||||
|
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.54-beta.1.md).
|
||||||
|
Keep the builds and their saves separate, and back up worlds before updating.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
1. Make an Ink Vial from two black dyes, a glass bottle, and a water bucket.
|
1. Make an Ink Vial from two black dyes, a glass bottle, and a water bucket.
|
||||||
|
|||||||
@@ -522,6 +522,165 @@ terrain, biome, and structure generation. Cave and Void are intentionally
|
|||||||
excluded until their feature placement rules are ported. This is not yet the
|
excluded until their feature placement rules are ported. This is not yet the
|
||||||
full 1.21.1 feature grammar or instability system.
|
full 1.21.1 feature grammar or instability system.
|
||||||
|
|
||||||
|
The local `1.0.1.42-beta.1` follow-up resolves unmodified Rocks to a
|
||||||
|
seed-shuffled, full-block material and accepts up to 32 ordered Rocks pages.
|
||||||
|
Each page adds an independent, deterministic surface placement attempt.
|
||||||
|
Explicit Block modifiers still bind only to the immediately following Rocks
|
||||||
|
page. The material sequence persists in the Age index; the codec also reads
|
||||||
|
the single-material format from the previous beta. The remaining feature
|
||||||
|
families and instability scoring are not yet ported.
|
||||||
|
|
||||||
|
The local `1.0.1.43-beta.1` slice uses 26.3's `minecraft:forest_rock`
|
||||||
|
placed-feature rules and `BlockBlobFeature` for authored Rocks, supplying the
|
||||||
|
Age's selected material and removing only the vanilla biome filter. This
|
||||||
|
replaces the port's temporary 3×3 pile algorithm and follows the 1.21.1
|
||||||
|
generator's authored-placement approach. It affects new chunks only.
|
||||||
|
|
||||||
|
The local `1.0.1.44-beta.1` slice extends this Age-scoped feature wrapper to
|
||||||
|
Flowers, Trees, and Huge Trees. Ordered authored pages retain independent
|
||||||
|
attempts and use vanilla 26.3 placed-feature rules after biome decoration.
|
||||||
|
The vegetation plan is persisted alongside terrain, biome, and Rocks plans;
|
||||||
|
the dedicated-server smoke compares generated flowers and logs against a
|
||||||
|
same-seed Flat control and checks persistence after restart.
|
||||||
|
|
||||||
|
The local `1.0.1.45-beta.1` slice adds authored Dungeons separately from
|
||||||
|
surface vegetation. Its 26.3 placed-feature JSON mirrors the 1.21.1 eight
|
||||||
|
monster-room attempts over the world height; ordered duplicate pages add
|
||||||
|
attempts. The count persists in the Age index and generator codec. The server
|
||||||
|
smoke creates and samples a dungeon Age, counts monster spawners across a
|
||||||
|
4×4-chunk area, and verifies the count after restart. Room appearance and
|
||||||
|
normal book authoring remain in-game checks.
|
||||||
|
|
||||||
|
The local `1.0.1.46-beta.1` slice ports Surface Lakes, Deep Lakes, and Small
|
||||||
|
Lakes, including ordered Fluid modifiers. The 26.3 LakeFeature retains its
|
||||||
|
vanilla shape and replacement predicates while using the Age's frozen fluid
|
||||||
|
provider. Surface placement keeps Rivencraft 1.21.1's one-in-twelve rarity;
|
||||||
|
underground lakes use vanilla placement. Repeated unmodified pages resolve
|
||||||
|
their fluids independently from a seeded, shuffled source-fluid pool. Lake
|
||||||
|
plans participate in restore, unload/reload, reseed, and retirement rollback.
|
||||||
|
The server smoke compares water generation against a same-seed control and
|
||||||
|
checks explicit/random plans and fluid blocks across restart.
|
||||||
|
|
||||||
|
The local `1.0.1.47-beta.1` slice adapts the 1.21.1 FloatingIslandFeature
|
||||||
|
algorithm to 26.3 chunk access and the biomes already assigned to each chunk.
|
||||||
|
It retains source range 3, one-in-128 candidate rarity, Y=168..215 center
|
||||||
|
selection, radii 24..41, coordinate-noise contours, air-only filling, and the
|
||||||
|
biome-dependent soil cap. It supports Flat/Void in addition to noise terrains;
|
||||||
|
Block modifiers and independent seeded random materials are frozen in an
|
||||||
|
ordered terrain-feature plan. Rocks and islands share material resolution.
|
||||||
|
|
||||||
|
26.3's final NoiseBasedChunkGenerator merges noise, surface, and carving and
|
||||||
|
does not expose a pre-surface hook. This port appends islands to the completed
|
||||||
|
terrain future before decoration; existing terrain is preserved, but base
|
||||||
|
carvers do not cut the added islands. The smoke verifies actual high-altitude
|
||||||
|
material placement on noise, Flat, and Void terrain, distinct random material
|
||||||
|
choices, authored biome preservation, and block fingerprints across restart.
|
||||||
|
|
||||||
|
The local `1.0.1.48-beta.1` slice ports TendrilFeature's Legacy-derived
|
||||||
|
branching solid volumes using the 26.3 chunk mutation and height APIs. Source
|
||||||
|
range 8, rate 15, size 18, nested node/height selection, branch splitting, and
|
||||||
|
bedrock preservation match the 1.21.1 implementation. Chunk-local writes and
|
||||||
|
neighbor-source seeds preserve continuous paths across chunk borders.
|
||||||
|
|
||||||
|
The existing terrain-feature plan now supports Tendrils on all terrain types,
|
||||||
|
including Cave and Void. Compilation retains mixed feature order and applies
|
||||||
|
the 32-occurrence limit per feature. Block modifiers and seeded random choices
|
||||||
|
use the shared solid-material resolver; existing save/lifecycle handling carries
|
||||||
|
both feature types. World validation is manual at the player's request; no
|
||||||
|
development server is started for this slice.
|
||||||
|
|
||||||
|
The local `1.0.1.49-beta.1` slice ports the Legacy-derived Skylands filter
|
||||||
|
from Rivencraft 1.21.1 with 26.3 chunk height/mutation APIs. The cutoff uses
|
||||||
|
the same salt, seven octaves, initial cell width 128, amplitude 12, and base
|
||||||
|
Y=76. Solid/liquid removal and world-height coverage match the reference.
|
||||||
|
It runs once after base terrain construction and before authored solid
|
||||||
|
volumes, retaining the feature ordering used by the 1.21.1 implementation.
|
||||||
|
|
||||||
|
Skylands is a material-free feature, accepted alongside every terrain type.
|
||||||
|
Repeated pages retain the same idempotent effect. The existing terrain-feature
|
||||||
|
plan stores an optional Skylands flag, defaulting to false for earlier beta
|
||||||
|
Ages; no separate backend/lifecycle path is introduced. A Skylands-only plan
|
||||||
|
can contain no material choices. Build and static checks are automated; world
|
||||||
|
generation and restart validation are left to the player.
|
||||||
|
|
||||||
|
The local `1.0.1.50-beta.1` slice adapts the 1.21.1 SpheresFeature and
|
||||||
|
SpikesFeature to 26.3. Spheres retain their source range 8, five-percent
|
||||||
|
source chance, nested Y selection starting at 32, node geometry, and bedrock
|
||||||
|
protection. They run after the base terrain/Skylands filter and work on every
|
||||||
|
terrain type. Spikes retain their one-in-18 chunk chance, 6..37 height roll,
|
||||||
|
width 1..4, solid-footprint checks, and chunk-local writes during decoration.
|
||||||
|
They remain unavailable on Cave and Void terrain, matching 1.21.1.
|
||||||
|
|
||||||
|
Both page types use ordered saved solid-feature choices, preceding Block
|
||||||
|
modifiers, and independent seeded materials/occurrences. No backend lifecycle
|
||||||
|
branch or generated artwork is added. Compilation and static review are the
|
||||||
|
automated checks; the player handles generation and restart validation.
|
||||||
|
|
||||||
|
The local `1.0.1.51-beta.1` slice ports ObelisksFeature using the 26.3 chunk
|
||||||
|
coordinates and world-height APIs. The site salt, seed mixing, one-in-128
|
||||||
|
chance, chunk-local site selection, 4×4 plinth, and 2×12×2 shaft match 1.21.1.
|
||||||
|
Every footing column must be sturdy, dry, and free of block entities; the
|
||||||
|
base surface can vary by at most two blocks and the full shaft must be open.
|
||||||
|
No partial structure is written when a placement check fails.
|
||||||
|
|
||||||
|
The page joins the existing material-feature plan and shared material resolver.
|
||||||
|
It runs during surface decoration, with its own occurrence counter, on all
|
||||||
|
surface terrain types including Flat; Cave and Void remain unsupported.
|
||||||
|
Spikes and Obelisks use shared stage/restriction classification. Build/static
|
||||||
|
checks are automated; generation and restart checks remain with the player.
|
||||||
|
|
||||||
|
The local `1.0.1.52-beta.1` slice ports CrystalFormationsFeature directly
|
||||||
|
from 1.21.1 with only registry, ChunkPos, and height API substitutions. It
|
||||||
|
retains the site salt, one-in-fifteen rarity, 3×3 ground/clearance checks,
|
||||||
|
three-block center height, randomized outer heights, budding hearts, and
|
||||||
|
initial small buds. Existing blue crystal growth, sprites, drops, and sounds
|
||||||
|
are reused without adding assets.
|
||||||
|
|
||||||
|
Crystal Formations is an ordered surface feature with fixed crystal material,
|
||||||
|
not a consumer of Block/Fluid modifiers. It shares the existing saved feature
|
||||||
|
plan and surface-terrain restrictions (including Flat, excluding Cave/Void),
|
||||||
|
with independent occurrence seeds and the current 32-page cap. No backend
|
||||||
|
lifecycle changes are needed. The Obelisks slice passed the player's in-game
|
||||||
|
check; Crystal Formations build/static checks pass, with generation, growth,
|
||||||
|
and restart validation handed to the player. No test server is started.
|
||||||
|
|
||||||
|
The local `1.0.1.53-beta.1` slice adapts 1.21.1 AgeDenseOres to modern
|
||||||
|
`AbstractOreFeature` targets and `FeaturePlacer.placeWithBiomeCheck`. It keeps
|
||||||
|
the common ore tag/name fallback, explicit `rivencraft:dense_ores` placed-
|
||||||
|
feature opt-in tag, nearby biome discovery, stable registry-ID ordering,
|
||||||
|
and independent per-occurrence seeds. Registered modded ore features remain
|
||||||
|
eligible without depending on an ore-providing mod. Biomes and ore placements
|
||||||
|
are collected once per chunk rather than once per repeated page.
|
||||||
|
|
||||||
|
The optional Dense Ores count is saved in the existing terrain-feature plan;
|
||||||
|
older plans default to zero. Grammar accepts 1–32 occurrences on noise terrain
|
||||||
|
(including Cave), rejects Flat/Void and material modifiers, and checks explicit
|
||||||
|
biome pools for at least one eligible ore feature. No extra lifecycle path or
|
||||||
|
asset is introduced. Random feature selection and instability remain later
|
||||||
|
port work. Crystal Formations passed the player's in-game check; this slice
|
||||||
|
passes build/static checks, with ore generation and restart validation manual.
|
||||||
|
|
||||||
|
The local `1.0.1.54-beta.1` slice ports Star Fissures. Its footprint matches
|
||||||
|
1.21.1 exactly (including Legacy attribution); carving uses the dimension's
|
||||||
|
minimum Y, clears fluids, and protects Cave terrain's roof. The authored flag
|
||||||
|
is optional in the existing feature-plan codec. Duplicate pages retain the
|
||||||
|
1.21.1 single-fissure result; all terrains are accepted, with no material
|
||||||
|
modifier. Random feature selection is not introduced in this slice.
|
||||||
|
|
||||||
|
First arrival generates the fissure and stores its spawn/completion in per-Age
|
||||||
|
SavedData. Re-entry/restart does not repeat carving; an obstructed saved arrival
|
||||||
|
is rechecked for safe footing. The return block ports modern entity-inside and
|
||||||
|
TeleportTransition APIs, has a registered Rivencraft block-entity type, and
|
||||||
|
uses Minecraft's starfield renderer with 1.21.1 face offsets. Shared Overworld
|
||||||
|
spawn safety search and fallback match 1.21.1, rather than respawning at a bed.
|
||||||
|
The existing fissure OGG/model are reused. Client renderer registration stays
|
||||||
|
client-only; the block has no item/drop and is inert in the Overworld.
|
||||||
|
|
||||||
|
Dense Ores passed the player's in-game check. Build, footprint comparison,
|
||||||
|
resource packaging, and static checks pass for this slice. Generation, visual
|
||||||
|
rendering, actual return travel, and restart checks are left to the player;
|
||||||
|
no development server is started.
|
||||||
|
|
||||||
## Verification of this first slice
|
## Verification of this first slice
|
||||||
|
|
||||||
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
|
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
|
||||||
@@ -530,3 +689,6 @@ extraction. The GameTest server ran 93 tests; 92 passed, while
|
|||||||
desks instead of one. That desk failure is outside the dimension-backend edit
|
desks instead of one. That desk failure is outside the dimension-backend edit
|
||||||
and must be resolved or rechecked before a release. The 1.21.1 `build` task
|
and must be resolved or rechecked before a release. The 1.21.1 `build` task
|
||||||
passed again after the 26.3 registry slice.
|
passed again after the 26.3 registry slice.
|
||||||
|
On 2026-11-01, the default 1.21.1 GameTest server was rerun and all 93
|
||||||
|
required tests passed, including the desk test. Issue #97 was then closed;
|
||||||
|
remaining 26.3 parity work stays under #98–#104.
|
||||||
@@ -211,6 +211,146 @@ first surface-feature path; unsupported combinations stay in the Binder.
|
|||||||
Random feature selection, unpaired materials, duplicate feature pages, and
|
Random feature selection, unpaired materials, duplicate feature pages, and
|
||||||
instability scoring remain future port steps.
|
instability scoring remain future port steps.
|
||||||
|
|
||||||
|
The local `1.0.1.42-beta.1` follow-up lets a Rocks page choose a deterministic
|
||||||
|
full-block material when no Block page precedes it. Repeated Rocks pages now
|
||||||
|
add independent placement attempts (up to 32) instead of being rejected. The
|
||||||
|
ordered material choices are frozen in the Age index and restored after a
|
||||||
|
restart; old single-material beta Ages still load. An explicit Block page
|
||||||
|
must still directly precede the Rocks page it modifies. Most other feature
|
||||||
|
pages and material-feature pairings remain unported.
|
||||||
|
|
||||||
|
The local `1.0.1.43-beta.1` slice replaces the temporary 3×3 Rocks piles
|
||||||
|
with Minecraft 26.3's forest-rock block-blob feature and its placement rules,
|
||||||
|
matching the 1.21.1 Age feature path more closely. The selected material and
|
||||||
|
repeat count still come from the frozen Age plan. Only newly generated chunks
|
||||||
|
use the new rock shape; previously generated chunks remain unchanged.
|
||||||
|
|
||||||
|
The local `1.0.1.44-beta.1` slice ports ordered Flowers, Trees, and Huge
|
||||||
|
Trees pages to surface Ages. Their 26.3 vanilla placed features run after
|
||||||
|
biome decoration, with the Age's own seed; repeated pages make independent
|
||||||
|
placement attempts. Cave/Void terrain and cave/Nether/End biome pools are
|
||||||
|
not treated as surface vegetation. The feature plan survives server restart;
|
||||||
|
already generated chunks do not gain new decoration.
|
||||||
|
|
||||||
|
The local `1.0.1.45-beta.1` slice ports the Dungeons page for Standard,
|
||||||
|
Amplified, and Island noise terrain. It uses the 1.21.1 placement definition
|
||||||
|
(eight monster-room attempts per page per chunk across the build height),
|
||||||
|
stores repeat count with the Age, and rejects Dungeons pages on Flat, Void,
|
||||||
|
or Cave terrain. New chunks only; existing chunks are not regenerated.
|
||||||
|
|
||||||
|
The local `1.0.1.46-beta.1` slice adds Surface Lakes, Deep Lakes, and Small
|
||||||
|
Lakes on surface noise terrain. A Fluid page immediately before a lake page
|
||||||
|
selects its fluid. Unmodified pages choose a seeded fluid from the same source-
|
||||||
|
fluid pool exposed in Creative, with repeated choices and their order saved
|
||||||
|
with the Age. Surface/Small Lakes retain the 1.21.1 one-in-twelve attempt rule;
|
||||||
|
Deep Lakes retain vanilla underground placement. These pages currently require
|
||||||
|
Standard, Amplified, or Island terrain. Existing chunks do not redecorate.
|
||||||
|
|
||||||
|
The local `1.0.1.47-beta.1` slice adds authored Floating Islands alongside
|
||||||
|
Standard, Amplified, Island, Flat, and Void terrain. A preceding Block page
|
||||||
|
sets the island core material; otherwise each occurrence resolves a seeded
|
||||||
|
material from the full-block pool. The 1.21.1 shape, high altitude, rarity,
|
||||||
|
and biome-dependent grass/dirt cap are retained. Repeated pages use independent
|
||||||
|
island seeds. Cave terrain remains unsupported for this feature.
|
||||||
|
|
||||||
|
26.3 combines base noise, surface, and cave carving in one terrain operation.
|
||||||
|
Floating Islands run after that operation and before decoration, so the added
|
||||||
|
islands are not cut by the base terrain's carvers. The saved feature plan is
|
||||||
|
used again after restart and for new chunks; existing chunks keep their blocks.
|
||||||
|
|
||||||
|
The local `1.0.1.48-beta.1` slice adds authored Tendrils on every terrain
|
||||||
|
type, including Cave and Void. A preceding Block page selects the material;
|
||||||
|
unmodified occurrences resolve seeded materials from the full-block pool.
|
||||||
|
Repeated pages use independent paths, with up to 32 occurrences per feature.
|
||||||
|
Mixed Floating Islands/Tendrils retain their authored order in the saved plan.
|
||||||
|
|
||||||
|
Tendrils retain the 1.21.1 branching path algorithm, source rates, and vertical
|
||||||
|
distribution concentrated below Y=128. They can replace existing terrain while
|
||||||
|
preserving bedrock. As with Floating Islands, they run after 26.3's combined
|
||||||
|
terrain operation and before decoration. Build checks are automated; in-game
|
||||||
|
generation, appearance, and restart validation are handed to the player.
|
||||||
|
|
||||||
|
The local `1.0.1.49-beta.1` slice adds the Skylands feature page. Its seeded
|
||||||
|
seven-octave filter removes base terrain at/below a varying cutoff near Y=76
|
||||||
|
and removes liquid above it, preserving the upper solid terrain. It accepts
|
||||||
|
all terrain types and no Block modifier. On low Flat terrain this removes
|
||||||
|
the base layers; on Void it leaves the empty base unchanged.
|
||||||
|
|
||||||
|
The filter runs once before authored Floating Islands and Tendrils, matching
|
||||||
|
1.21.1; duplicate Skylands pages do not compound the filter. Its flag is saved
|
||||||
|
with the terrain-feature plan. Existing chunks are unchanged. Try Amplified
|
||||||
|
terrain for a clear visual check; generation and restart validation are manual.
|
||||||
|
|
||||||
|
The local `1.0.1.50-beta.1` slice ports Spheres and Spikes using the 1.21.1
|
||||||
|
generation algorithms. Spheres add solid nodes on all terrain types, including
|
||||||
|
Cave and Void, while preserving bedrock. Spikes require solid surface terrain:
|
||||||
|
Standard, Amplified, Island, or Flat. Their circular footprint stays in one
|
||||||
|
chunk and must have solid ground beneath every column.
|
||||||
|
|
||||||
|
Each page accepts a preceding Block material or resolves a seeded random
|
||||||
|
material when unmodified. Repeated pages have independent placement seeds,
|
||||||
|
up to 32 per feature. Spheres run during terrain construction, after Skylands;
|
||||||
|
Spikes run after biome decoration. Both use the existing saved solid-feature
|
||||||
|
plan. New chunks only; in-game appearance and restart checks remain manual.
|
||||||
|
|
||||||
|
The local `1.0.1.51-beta.1` slice ports Obelisks. It preserves the 1.21.1
|
||||||
|
one-in-128 candidate chance per chunk/occurrence, 4×4 plinth, and 2×2 shaft
|
||||||
|
extending 12 blocks above it. Placement requires dry, sturdy ground across
|
||||||
|
the base, at most two blocks of surface variation, and clear/replaceable
|
||||||
|
space for the complete structure. The footprint stays within its chunk.
|
||||||
|
|
||||||
|
Obelisks support Standard, Amplified, Island, and Flat terrain. A preceding
|
||||||
|
Block page sets their material; otherwise a seeded full-block material is
|
||||||
|
selected. Repeated pages make independent attempts, up to 32 occurrences.
|
||||||
|
Spikes and Obelisks share surface-stage classification and preserve their
|
||||||
|
authored order within that stage. World and restart validation are manual.
|
||||||
|
|
||||||
|
The local `1.0.1.52-beta.1` slice ports Crystal Formations using the 1.21.1
|
||||||
|
surface-mound algorithm and the existing blue crystal blocks. Each occurrence
|
||||||
|
has a one-in-fifteen candidate chance per chunk; suitable dry, nearly level
|
||||||
|
ground produces a 3×3 mound with budding crystal and occasional small buds.
|
||||||
|
The existing budding blocks continue growing crystals through random ticks.
|
||||||
|
|
||||||
|
Crystal Formations accepts Standard, Amplified, Island, and Flat terrain,
|
||||||
|
but not Cave/Void terrain or Block/Fluid modifiers. Up to 32 repeated pages
|
||||||
|
make independent attempts. The ordered feature plan survives restart, and
|
||||||
|
only newly generated chunks receive formations. For a quick manual test,
|
||||||
|
bind a Flat/Plains Age with several Crystal Formations pages, inspect the
|
||||||
|
blue mounds and bud growth, then revisit after restarting the world.
|
||||||
|
|
||||||
|
The local `1.0.1.53-beta.1` slice ports Dense Ores. Each page replays eligible
|
||||||
|
ore placements from the Age's actual biomes, preserving their height ranges,
|
||||||
|
replacement rules, and biome filters. Vanilla and modded ore features are
|
||||||
|
recognized by their ore targets; custom placements can opt in through the
|
||||||
|
`rivencraft:dense_ores` placed-feature tag, just as in 1.21.1.
|
||||||
|
|
||||||
|
Up to 32 pages add independent passes on Standard, Amplified, Island, or Cave
|
||||||
|
terrain. Flat/Void and Block/Fluid modifiers are unsupported; an explicitly
|
||||||
|
authored biome pool must contain at least one ore-producing biome. The count
|
||||||
|
survives restart. Only new chunks gain extra ore, and this slice does not yet
|
||||||
|
port the later instability system or random-Age feature selection.
|
||||||
|
|
||||||
|
For manual validation, create a Standard/Plains Age with several Dense Ores
|
||||||
|
pages and inspect underground ore abundance, then restart and explore more
|
||||||
|
chunks. A Cave Age with a Nether biome can check Nether ore placements too.
|
||||||
|
|
||||||
|
The local `1.0.1.54-beta.1` slice ports the Star Fissures page. On first book/
|
||||||
|
operator arrival, it carves the 1.21.1 Legacy-shaped rift near the arrival chunk
|
||||||
|
and places a starfield portal at the dimension's minimum Y. Cave terrain keeps
|
||||||
|
its bedrock roof. All terrain types are supported; repeated pages still create
|
||||||
|
one fissure, and Block/Fluid modifiers are not accepted.
|
||||||
|
|
||||||
|
The first arrival and completed generation are saved per Age, so revisits and
|
||||||
|
restarts do not recarve the rift. Touching its bottom returns entities to a safe
|
||||||
|
location near the shared Overworld spawn, not a bed, with the original fissure
|
||||||
|
sound. The portal is unobtainable and inert in the Overworld. Random-Age feature
|
||||||
|
selection remains later work; author a Star Fissures page to test this slice.
|
||||||
|
|
||||||
|
For manual validation, create a Flat/Plains Age with Star Fissures. Look near
|
||||||
|
the arrival point (generally east/south), descend through the rift, and touch
|
||||||
|
the starfield to return. Re-enter after restarting; check that the same fissure
|
||||||
|
and its portal still work. A Cave Age checks that carving leaves the roof intact.
|
||||||
|
|
||||||
From the repository root:
|
From the repository root:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Rivencraft 26.3 beta 1.0.1.54-beta.1
|
||||||
|
|
||||||
|
This beta collects the player-validated Age feature port slices since
|
||||||
|
1.0.1.41-beta.1. It is not yet feature-complete with Minecraft 1.21.1.
|
||||||
|
|
||||||
|
## Age generation
|
||||||
|
|
||||||
|
- Rocks now use the established 1.21.1 forest-rock placement. Unmodified
|
||||||
|
material-bearing features choose seeded materials independently; a preceding
|
||||||
|
compatible Block or Fluid page selects the material explicitly.
|
||||||
|
- Added Flowers, Trees, Huge Trees, Dungeons, Surface Lakes, Deep Lakes, and
|
||||||
|
Small Lakes using the 1.21.1 placement rules adapted to 26.3.
|
||||||
|
- Added Floating Islands, Tendrils, Skylands, Spheres, Spikes, and Obelisks
|
||||||
|
using the existing Rivencraft generation algorithms.
|
||||||
|
- Added blue Crystal Formations with budding crystal and initially seeded buds.
|
||||||
|
- Added Dense Ores: repeated pages replay eligible biome ore placements,
|
||||||
|
including compatible modded ores and explicitly tagged custom placements.
|
||||||
|
- Added Star Fissures: one Legacy-shaped rift near the first arrival point,
|
||||||
|
a starfield at the dimension's minimum Y, and return travel to the shared
|
||||||
|
Overworld spawn with the original fissure sound. Cave roofs are preserved.
|
||||||
|
- Feature plans, materials, repeated occurrences, and fissure completion
|
||||||
|
persist across restart. Existing generated chunks are not redecorated.
|
||||||
|
|
||||||
|
## Current limits
|
||||||
|
|
||||||
|
- Authored features retain terrain restrictions. Most repeated features allow
|
||||||
|
up to 32 occurrences; exceeding the current authoring cap blocks assembly.
|
||||||
|
Skylands and Star Fissures retain their single-effect behavior when repeated.
|
||||||
|
- Put a material page immediately before a compatible feature. Dangling
|
||||||
|
material assignment, full Legacy grammar recovery, instability, and random
|
||||||
|
feature selection are not yet ported. Pageless books randomize terrain and
|
||||||
|
biome distribution, not the newly authored feature list.
|
||||||
|
- 26.3 combines base terrain, surface, and carving. Added solid volumes run
|
||||||
|
after that combined operation and before decoration, so base carvers do not
|
||||||
|
cut those added volumes.
|
||||||
|
- The full 1.21.1 writing/discovery loop, structures, environmental systems,
|
||||||
|
crystal portals, and optional integration parity remain in progress. The
|
||||||
|
presence of a Creative page is not a promise that its effect is ported.
|
||||||
|
|
||||||
|
Build and static checks passed. The player reported in-game passes for these
|
||||||
|
slices. Earlier server smoke coverage was expanded, but the later generation
|
||||||
|
slices were manually validated; this release does not claim a new full
|
||||||
|
dedicated-server regression pass.
|
||||||
|
|
||||||
|
Requires Minecraft **26.3**, NeoForge **26.3.0.99** or a compatible 26.3 build,
|
||||||
|
and **Java 25**. Dynamic Dimensions and Bad Packets are not required by this
|
||||||
|
26.3 build. Do not use Minecraft 1.21.1 dependencies or saves with it. Back up
|
||||||
|
the entire world before updating; beta-save compatibility is not guaranteed.
|
||||||
|
|
||||||
|
Report issues and help with documentation at
|
||||||
|
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||||
@@ -124,6 +124,9 @@ sourceSets.main.resources.srcDir generatePortLanguage
|
|||||||
tasks.named('processResources', ProcessResources).configure {
|
tasks.named('processResources', ProcessResources).configure {
|
||||||
// Reuse the existing sprite and base model without copying binary assets.
|
// Reuse the existing sprite and base model without copying binary assets.
|
||||||
from('../../src/main/resources') {
|
from('../../src/main/resources') {
|
||||||
|
include 'assets/rivencraft/models/block/star_fissure.json'
|
||||||
|
include 'assets/rivencraft/blockstates/star_fissure.json'
|
||||||
|
include 'assets/rivencraft/sounds/linking/link-fissure.ogg'
|
||||||
include 'assets/rivencraft/textures/item/crystal_shard.png'
|
include 'assets/rivencraft/textures/item/crystal_shard.png'
|
||||||
include 'assets/rivencraft/textures/item/page.png'
|
include 'assets/rivencraft/textures/item/page.png'
|
||||||
include 'assets/rivencraft/textures/item/page_link_panel.png'
|
include 'assets/rivencraft/textures/item/page_link_panel.png'
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ neo_version=26.3.0.99
|
|||||||
mod_id=rivencraft
|
mod_id=rivencraft
|
||||||
mod_name=Rivencraft
|
mod_name=Rivencraft
|
||||||
mod_license=LGPL-3.0-or-later
|
mod_license=LGPL-3.0-or-later
|
||||||
mod_version=1.0.1.41-beta.1
|
mod_version=1.0.1.54-beta.1
|
||||||
@@ -8,6 +8,8 @@ import net.dnorberg.rivencraft.age.AgeCatalogSync;
|
|||||||
import net.dnorberg.rivencraft.block.BuddingCrystalBlock;
|
import net.dnorberg.rivencraft.block.BuddingCrystalBlock;
|
||||||
import net.dnorberg.rivencraft.block.PortBookBinderBlock;
|
import net.dnorberg.rivencraft.block.PortBookBinderBlock;
|
||||||
import net.dnorberg.rivencraft.block.InkMixerBlock;
|
import net.dnorberg.rivencraft.block.InkMixerBlock;
|
||||||
|
import net.dnorberg.rivencraft.block.StarFissureBlock;
|
||||||
|
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||||
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
||||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
||||||
@@ -26,6 +28,7 @@ import net.dnorberg.rivencraft.writing.LinkingBookRecipe;
|
|||||||
import net.minecraft.core.component.DataComponentType;
|
import net.minecraft.core.component.DataComponentType;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.sounds.SoundEvent;
|
||||||
import net.minecraft.network.codec.ByteBufCodecs;
|
import net.minecraft.network.codec.ByteBufCodecs;
|
||||||
import net.minecraft.world.level.block.Block;
|
import net.minecraft.world.level.block.Block;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
@@ -54,6 +57,11 @@ public final class Rivencraft {
|
|||||||
public static final Logger LOGGER = LogUtils.getLogger();
|
public static final Logger LOGGER = LogUtils.getLogger();
|
||||||
private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MOD_ID);
|
private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MOD_ID);
|
||||||
private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MOD_ID);
|
private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MOD_ID);
|
||||||
|
private static final DeferredRegister<SoundEvent> SOUNDS =
|
||||||
|
DeferredRegister.create(Registries.SOUND_EVENT, MOD_ID);
|
||||||
|
public static final Supplier<SoundEvent> LINK_FISSURE = SOUNDS.register(
|
||||||
|
"linking.link-fissure", () -> SoundEvent.createVariableRangeEvent(
|
||||||
|
Identifier.fromNamespaceAndPath(MOD_ID, "linking.link-fissure")));
|
||||||
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
|
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
|
||||||
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID);
|
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID);
|
||||||
private static final DeferredRegister<MenuType<?>> MENUS =
|
private static final DeferredRegister<MenuType<?>> MENUS =
|
||||||
@@ -94,6 +102,12 @@ public final class Rivencraft {
|
|||||||
|
|
||||||
public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block",
|
public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block",
|
||||||
Block::new, () -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK));
|
Block::new, () -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK));
|
||||||
|
public static final DeferredBlock<StarFissureBlock> STAR_FISSURE =
|
||||||
|
BLOCKS.registerBlock("star_fissure", StarFissureBlock::new,
|
||||||
|
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.END_PORTAL).lightLevel(state -> 6));
|
||||||
|
public static final Supplier<BlockEntityType<StarFissureBlockEntity>>
|
||||||
|
STAR_FISSURE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("star_fissure", () -> new BlockEntityType<>(
|
||||||
|
StarFissureBlockEntity::new, STAR_FISSURE.get()));
|
||||||
public static final DeferredBlock<PortBookBinderBlock> BOOK_BINDER = BLOCKS.registerBlock(
|
public static final DeferredBlock<PortBookBinderBlock> BOOK_BINDER = BLOCKS.registerBlock(
|
||||||
"book_binder", PortBookBinderBlock::new,
|
"book_binder", PortBookBinderBlock::new,
|
||||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
||||||
@@ -146,6 +160,7 @@ public final class Rivencraft {
|
|||||||
|
|
||||||
public Rivencraft(IEventBus modEventBus) {
|
public Rivencraft(IEventBus modEventBus) {
|
||||||
BLOCKS.register(modEventBus);
|
BLOCKS.register(modEventBus);
|
||||||
|
SOUNDS.register(modEventBus);
|
||||||
ITEMS.register(modEventBus);
|
ITEMS.register(modEventBus);
|
||||||
BLOCK_ENTITY_TYPES.register(modEventBus);
|
BLOCK_ENTITY_TYPES.register(modEventBus);
|
||||||
MENUS.register(modEventBus);
|
MENUS.register(modEventBus);
|
||||||
|
|||||||
@@ -26,7 +26,15 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
Codec.STRING.xmap(PortAgeTerrain::valueOf, PortAgeTerrain::name))
|
Codec.STRING.xmap(PortAgeTerrain::valueOf, PortAgeTerrain::name))
|
||||||
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain),
|
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain),
|
||||||
Codec.unboundedMap(Identifier.CODEC, PortRockPlan.CODEC).optionalFieldOf("rocks", Map.of())
|
Codec.unboundedMap(Identifier.CODEC, PortRockPlan.CODEC).optionalFieldOf("rocks", Map.of())
|
||||||
.forGetter(index -> index.rocks)
|
.forGetter(index -> index.rocks),
|
||||||
|
Codec.unboundedMap(Identifier.CODEC, PortVegetationPlan.CODEC)
|
||||||
|
.optionalFieldOf("vegetation", Map.of()).forGetter(index -> index.vegetation),
|
||||||
|
Codec.unboundedMap(Identifier.CODEC, Codec.intRange(1, 32))
|
||||||
|
.optionalFieldOf("dungeons", Map.of()).forGetter(index -> index.dungeons),
|
||||||
|
Codec.unboundedMap(Identifier.CODEC, PortLakePlan.CODEC)
|
||||||
|
.optionalFieldOf("lakes", Map.of()).forGetter(index -> index.lakes),
|
||||||
|
Codec.unboundedMap(Identifier.CODEC, PortTerrainFeaturePlan.CODEC)
|
||||||
|
.optionalFieldOf("terrain_features", Map.of()).forGetter(index -> index.terrainFeatures)
|
||||||
).apply(instance, AgeLevelIndex::new));
|
).apply(instance, AgeLevelIndex::new));
|
||||||
|
|
||||||
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
||||||
@@ -41,6 +49,10 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
private final Map<Identifier, PortBiomePlan> biomePlans;
|
private final Map<Identifier, PortBiomePlan> biomePlans;
|
||||||
private final Map<Identifier, PortAgeTerrain> terrain;
|
private final Map<Identifier, PortAgeTerrain> terrain;
|
||||||
private final Map<Identifier, PortRockPlan> rocks;
|
private final Map<Identifier, PortRockPlan> rocks;
|
||||||
|
private final Map<Identifier, PortVegetationPlan> vegetation;
|
||||||
|
private final Map<Identifier, Integer> dungeons;
|
||||||
|
private final Map<Identifier, PortLakePlan> lakes;
|
||||||
|
private final Map<Identifier, PortTerrainFeaturePlan> terrainFeatures;
|
||||||
|
|
||||||
private AgeLevelIndex() {
|
private AgeLevelIndex() {
|
||||||
this.ages = new LinkedHashMap<>();
|
this.ages = new LinkedHashMap<>();
|
||||||
@@ -49,19 +61,31 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
this.biomePlans = new LinkedHashMap<>();
|
this.biomePlans = new LinkedHashMap<>();
|
||||||
this.terrain = new LinkedHashMap<>();
|
this.terrain = new LinkedHashMap<>();
|
||||||
this.rocks = new LinkedHashMap<>();
|
this.rocks = new LinkedHashMap<>();
|
||||||
|
this.vegetation = new LinkedHashMap<>();
|
||||||
|
this.dungeons = new LinkedHashMap<>();
|
||||||
|
this.lakes = new LinkedHashMap<>();
|
||||||
|
this.terrainFeatures = new LinkedHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
|
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
|
||||||
Map<Identifier, Identifier> biomeOverrides,
|
Map<Identifier, Identifier> biomeOverrides,
|
||||||
Map<Identifier, PortBiomePlan> biomePlans,
|
Map<Identifier, PortBiomePlan> biomePlans,
|
||||||
Map<Identifier, PortAgeTerrain> terrain,
|
Map<Identifier, PortAgeTerrain> terrain,
|
||||||
Map<Identifier, PortRockPlan> rocks) {
|
Map<Identifier, PortRockPlan> rocks,
|
||||||
|
Map<Identifier, PortVegetationPlan> vegetation,
|
||||||
|
Map<Identifier, Integer> dungeons,
|
||||||
|
Map<Identifier, PortLakePlan> lakes,
|
||||||
|
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures) {
|
||||||
this.ages = new LinkedHashMap<>(ages);
|
this.ages = new LinkedHashMap<>(ages);
|
||||||
this.retired = new LinkedHashSet<>(retired);
|
this.retired = new LinkedHashSet<>(retired);
|
||||||
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
||||||
this.biomePlans = new LinkedHashMap<>(biomePlans);
|
this.biomePlans = new LinkedHashMap<>(biomePlans);
|
||||||
this.terrain = new LinkedHashMap<>(terrain);
|
this.terrain = new LinkedHashMap<>(terrain);
|
||||||
this.rocks = new LinkedHashMap<>(rocks);
|
this.rocks = new LinkedHashMap<>(rocks);
|
||||||
|
this.vegetation = new LinkedHashMap<>(vegetation);
|
||||||
|
this.dungeons = new LinkedHashMap<>(dungeons);
|
||||||
|
this.lakes = new LinkedHashMap<>(lakes);
|
||||||
|
this.terrainFeatures = new LinkedHashMap<>(terrainFeatures);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<Identifier, Long> entries() {
|
Map<Identifier, Long> entries() {
|
||||||
@@ -88,9 +112,14 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PortRockPlan rocks(Identifier id) { return rocks.get(id); }
|
PortRockPlan rocks(Identifier id) { return rocks.get(id); }
|
||||||
|
PortVegetationPlan vegetation(Identifier id) { return vegetation.get(id); }
|
||||||
|
int dungeons(Identifier id) { return dungeons.getOrDefault(id, 0); }
|
||||||
|
PortLakePlan lakes(Identifier id) { return lakes.get(id); }
|
||||||
|
PortTerrainFeaturePlan terrainFeatures(Identifier id) { return terrainFeatures.get(id); }
|
||||||
|
|
||||||
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
|
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
|
||||||
PortRockPlan rockPlan) {
|
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||||
|
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
|
||||||
if (contains(id)) {
|
if (contains(id)) {
|
||||||
throw new IllegalArgumentException("Age already exists: " + id);
|
throw new IllegalArgumentException("Age already exists: " + id);
|
||||||
}
|
}
|
||||||
@@ -98,6 +127,10 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||||
if (rockPlan != null) rocks.put(id, rockPlan);
|
if (rockPlan != null) rocks.put(id, rockPlan);
|
||||||
|
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
|
||||||
|
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
|
||||||
|
if (lakePlan != null) lakes.put(id, lakePlan);
|
||||||
|
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
|
||||||
setDirty();
|
setDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,17 +140,27 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
biomePlans.remove(id);
|
biomePlans.remove(id);
|
||||||
terrain.remove(id);
|
terrain.remove(id);
|
||||||
rocks.remove(id);
|
rocks.remove(id);
|
||||||
|
vegetation.remove(id);
|
||||||
|
dungeons.remove(id);
|
||||||
|
lakes.remove(id);
|
||||||
|
terrainFeatures.remove(id);
|
||||||
retired.add(id);
|
retired.add(id);
|
||||||
setDirty();
|
setDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
|
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
|
||||||
PortAgeTerrain ageTerrain, PortRockPlan rockPlan) {
|
PortAgeTerrain ageTerrain, PortRockPlan rockPlan,
|
||||||
|
PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||||
|
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
|
||||||
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
|
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
|
||||||
ages.put(id, seed);
|
ages.put(id, seed);
|
||||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||||
if (rockPlan != null) rocks.put(id, rockPlan);
|
if (rockPlan != null) rocks.put(id, rockPlan);
|
||||||
|
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
|
||||||
|
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
|
||||||
|
if (lakePlan != null) lakes.put(id, lakePlan);
|
||||||
|
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
|
||||||
setDirty();
|
setDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import net.minecraft.server.level.ServerPlayer;
|
|||||||
import net.minecraft.util.Prediction;
|
import net.minecraft.util.Prediction;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.tags.BlockTags;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||||
import net.neoforged.fml.ModList;
|
import net.neoforged.fml.ModList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -130,6 +132,51 @@ public final class PortAgeCommands {
|
|||||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
.executes(context -> createAge(context, PortAgeTerrain.FLAT,
|
.executes(context -> createAge(context, PortAgeTerrain.FLAT,
|
||||||
null, new PortRockPlan(Identifier.withDefaultNamespace("gold_block")))))))
|
null, new PortRockPlan(Identifier.withDefaultNamespace("gold_block")))))))
|
||||||
|
.then(Commands.literal("create_rocks_multi")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
|
||||||
|
fourGoldRocks(LongArgumentType.getLong(context, "seed")))))))
|
||||||
|
.then(Commands.literal("create_rocks_random")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
|
||||||
|
net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(
|
||||||
|
java.util.List.of(Identifier.fromNamespaceAndPath(
|
||||||
|
Rivencraft.MOD_ID, "feature/rocks")),
|
||||||
|
LongArgumentType.getLong(context, "seed")))))))
|
||||||
|
.then(Commands.literal("create_vegetation")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
|
||||||
|
null, testVegetation())))))
|
||||||
|
.then(Commands.literal("create_dungeons")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
|
||||||
|
null, null, null, testDungeons())))))
|
||||||
|
.then(Commands.literal("create_lakes")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
|
||||||
|
null, null, null, 0, testLakes(
|
||||||
|
LongArgumentType.getLong(context, "seed"), false))))))
|
||||||
|
.then(Commands.literal("create_lakes_random")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
|
||||||
|
null, null, null, 0, testLakes(
|
||||||
|
LongArgumentType.getLong(context, "seed"), true))))))
|
||||||
|
.then(Commands.literal("create_islands")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||||
|
.executes(context -> createIslandAge(context, false))))))
|
||||||
|
.then(Commands.literal("create_islands_random")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||||
|
.executes(context -> createIslandAge(context, true))))))
|
||||||
.then(Commands.literal("list")
|
.then(Commands.literal("list")
|
||||||
.executes(context -> {
|
.executes(context -> {
|
||||||
var ages = RuntimeAgeBackend.savedAges(context.getSource().getServer());
|
var ages = RuntimeAgeBackend.savedAges(context.getSource().getServer());
|
||||||
@@ -236,6 +283,18 @@ public final class PortAgeCommands {
|
|||||||
.then(Commands.literal("rocks_probe")
|
.then(Commands.literal("rocks_probe")
|
||||||
.then(Commands.argument("name", StringArgumentType.word())
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
.executes(PortAgeCommands::probeRocks)))
|
.executes(PortAgeCommands::probeRocks)))
|
||||||
|
.then(Commands.literal("vegetation_probe")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.executes(PortAgeCommands::probeVegetation)))
|
||||||
|
.then(Commands.literal("dungeons_probe")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.executes(PortAgeCommands::probeDungeons)))
|
||||||
|
.then(Commands.literal("lakes_probe")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.executes(PortAgeCommands::probeLakes)))
|
||||||
|
.then(Commands.literal("islands_probe")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.executes(PortAgeCommands::probeIslands)))
|
||||||
.then(Commands.literal("biome")
|
.then(Commands.literal("biome")
|
||||||
.then(Commands.argument("name", StringArgumentType.word())
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
.then(Commands.argument("x", IntegerArgumentType.integer())
|
.then(Commands.argument("x", IntegerArgumentType.integer())
|
||||||
@@ -255,30 +314,229 @@ public final class PortAgeCommands {
|
|||||||
private static int probeRocks(CommandContext<CommandSourceStack> context) {
|
private static int probeRocks(CommandContext<CommandSourceStack> context) {
|
||||||
Identifier id = ageId(context);
|
Identifier id = ageId(context);
|
||||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortRockChunkGenerator rocks)) {
|
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)
|
||||||
|
|| features.rocks() == null) {
|
||||||
context.getSource().sendFailure(Component.literal("Age has no loaded Rocks generator: " + id));
|
context.getSource().sendFailure(Component.literal("Age has no loaded Rocks generator: " + id));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
var material = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getValue(rocks.rocks().material());
|
var materials = features.rocks().materials().stream()
|
||||||
|
.map(net.minecraft.core.registries.BuiltInRegistries.BLOCK::getValue).toList();
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
|
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
|
||||||
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1;
|
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1;
|
||||||
if (level.getBlockState(new net.minecraft.core.BlockPos(x, y, z)).is(material)) count++;
|
var state = level.getBlockState(new net.minecraft.core.BlockPos(x, y, z));
|
||||||
|
if (materials.stream().anyMatch(state::is)) count++;
|
||||||
}
|
}
|
||||||
int placed = count;
|
int placed = count;
|
||||||
context.getSource().sendSuccess(() -> Component.literal(
|
context.getSource().sendSuccess(() -> Component.literal(
|
||||||
"Rocks probe " + id + " material=" + rocks.rocks().material() + " count=" + placed), false);
|
"Rocks probe " + id + " materials=" + features.rocks().materials() + " count=" + placed), false);
|
||||||
return placed;
|
return placed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int probeVegetation(CommandContext<CommandSourceStack> context) {
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
if (level == null) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int flowers = 0;
|
||||||
|
int logs = 0;
|
||||||
|
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
|
||||||
|
for (int y = 4; y < 32; y++) {
|
||||||
|
var state = level.getBlockState(new net.minecraft.core.BlockPos(x, y, z));
|
||||||
|
if (state.is(BlockTags.FLOWERS)) flowers++;
|
||||||
|
if (state.is(BlockTags.LOGS)) logs++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int flowerCount = flowers;
|
||||||
|
int logCount = logs;
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(
|
||||||
|
"Vegetation probe " + id + " flowers=" + flowerCount + " logs=" + logCount), false);
|
||||||
|
return flowerCount + logCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PortVegetationPlan testVegetation() {
|
||||||
|
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/flowers"));
|
||||||
|
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/trees"));
|
||||||
|
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/huge_trees"));
|
||||||
|
}
|
||||||
|
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.vegetationPlan(pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int testDungeons() {
|
||||||
|
Identifier page = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dungeons");
|
||||||
|
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.dungeonCount(
|
||||||
|
java.util.Collections.nCopies(8, page));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int probeDungeons(CommandContext<CommandSourceStack> context) {
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age has no loaded feature generator: " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int count = features.dungeons();
|
||||||
|
int spawners = 0;
|
||||||
|
for (int chunkX = 0; chunkX < 4; chunkX++) for (int chunkZ = 0; chunkZ < 4; chunkZ++) {
|
||||||
|
var chunk = level.getChunk(chunkX, chunkZ);
|
||||||
|
for (var pos : chunk.getBlockEntitiesPos()) {
|
||||||
|
if (chunk.getBlockState(pos).is(Blocks.SPAWNER)) spawners++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int placed = spawners;
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(
|
||||||
|
"Dungeons probe " + id + " pages=" + count + " spawners=" + placed), false);
|
||||||
|
return placed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PortLakePlan testLakes(long seed, boolean randomFluids) {
|
||||||
|
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
for (String kind : java.util.List.of("surface_lakes", "deep_lakes", "lava_lakes")) {
|
||||||
|
if (!randomFluids) pages.add(net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
|
||||||
|
Identifier.withDefaultNamespace("water")));
|
||||||
|
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + kind));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.lakePlan(pages, seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int probeLakes(CommandContext<CommandSourceStack> context) {
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
if (level == null) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
PortLakePlan lakes = level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
|
||||||
|
? features.lakes() : null;
|
||||||
|
// Finish neighboring features before counting, since lake bowls can
|
||||||
|
// cross a chunk boundary. Include underground lakes in the probe.
|
||||||
|
for (int x = -1; x <= 4; x++) for (int z = -1; z <= 4; z++) level.getChunk(x, z);
|
||||||
|
int[] fluids = new int[2];
|
||||||
|
for (int x = 0; x < 4; x++) for (int z = 0; z < 4; z++) {
|
||||||
|
level.getChunk(x, z).findBlocks(state -> state.is(Blocks.WATER) || state.is(Blocks.LAVA),
|
||||||
|
(pos, state) -> fluids[state.is(Blocks.WATER) ? 0 : 1]++);
|
||||||
|
}
|
||||||
|
int waterCount = fluids[0];
|
||||||
|
int lavaCount = fluids[1];
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(
|
||||||
|
"Lakes probe " + id + " pages=" + (lakes == null ? 0 : lakes.choices().size())
|
||||||
|
+ " fluids=" + (lakes == null ? java.util.List.of() : lakes.choices().stream()
|
||||||
|
.map(PortLakePlan.Choice::fluid).distinct().toList())
|
||||||
|
+ " plan=" + (lakes == null ? 0 : lakes.choices().hashCode())
|
||||||
|
+ " water=" + waterCount + " lava=" + lavaCount), false);
|
||||||
|
return waterCount + lavaCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createIslandAge(CommandContext<CommandSourceStack> context, boolean randomMaterials) {
|
||||||
|
PortAgeTerrain terrain;
|
||||||
|
try {
|
||||||
|
terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Unknown island test terrain"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||||
|
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
|
||||||
|
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome/forest"));
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if (!randomMaterials) pages.add(net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
|
||||||
|
Identifier.withDefaultNamespace("gold_block")));
|
||||||
|
pages.add(PortTerrainFeaturePlan.FLOATING_ISLANDS);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
long seed = LongArgumentType.getLong(context, "seed");
|
||||||
|
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||||
|
pages, context.getSource().getServer().registryAccess());
|
||||||
|
var features = net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed);
|
||||||
|
return createAge(context, terrain, biomes, null, null, 0, null, features);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int probeIslands(CommandContext<CommandSourceStack> context) {
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)
|
||||||
|
|| features.terrainFeatures() == null) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age has no loaded island generator: " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var choices = features.terrainFeatures().choices();
|
||||||
|
var materials = choices.stream().map(PortTerrainFeaturePlan.Choice::material).distinct().toList();
|
||||||
|
var blocks = materials.stream().map(net.minecraft.core.registries.BuiltInRegistries.BLOCK::getValue).toList();
|
||||||
|
for (int x = -1; x <= 4; x++) for (int z = -1; z <= 4; z++) level.getChunk(x, z);
|
||||||
|
int[] count = {0};
|
||||||
|
long[] fingerprint = {0};
|
||||||
|
for (int x = 0; x < 4; x++) for (int z = 0; z < 4; z++) {
|
||||||
|
level.getChunk(x, z).findBlocks(state -> !state.isAir(), (pos, state) -> {
|
||||||
|
if (pos.getY() < 128 || pos.getY() >= 240) return;
|
||||||
|
if (blocks.stream().anyMatch(state::is)) count[0]++;
|
||||||
|
fingerprint[0] += pos.asLong() * 0x9E3779B97F4A7C15L ^ state.toString().hashCode();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(
|
||||||
|
"Islands probe " + id + " pages=" + choices.size() + " materials=" + materials
|
||||||
|
+ " count=" + count[0] + " fingerprint=" + fingerprint[0]), false);
|
||||||
|
return count[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Exercise the actual ordered writing parser, not only the generator codec. */
|
||||||
|
private static PortRockPlan fourGoldRocks(long seed) {
|
||||||
|
Identifier block = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
|
||||||
|
Identifier.withDefaultNamespace("gold_block"));
|
||||||
|
Identifier feature = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/rocks");
|
||||||
|
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
pages.add(block);
|
||||||
|
pages.add(feature);
|
||||||
|
}
|
||||||
|
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(pages, seed);
|
||||||
|
}
|
||||||
|
|
||||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||||
PortAgeTerrain terrain, PortBiomePlan biomePlan, PortRockPlan rockPlan) {
|
PortAgeTerrain terrain, PortBiomePlan biomePlan, PortRockPlan rockPlan) {
|
||||||
|
return createAge(context, terrain, biomePlan, rockPlan, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||||
|
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||||
|
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan) {
|
||||||
|
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||||
|
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||||
|
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||||
|
int dungeons) {
|
||||||
|
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||||
|
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||||
|
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||||
|
int dungeons, PortLakePlan lakes) {
|
||||||
|
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||||
|
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||||
|
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||||
|
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures) {
|
||||||
String name = StringArgumentType.getString(context, "name");
|
String name = StringArgumentType.getString(context, "name");
|
||||||
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||||
long seed = LongArgumentType.getLong(context, "seed");
|
long seed = LongArgumentType.getLong(context, "seed");
|
||||||
ServerLevel level;
|
ServerLevel level;
|
||||||
try {
|
try {
|
||||||
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan, terrain, rockPlan);
|
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
|
||||||
|
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures);
|
||||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -25,8 +25,13 @@ public final class PortAgeTravel {
|
|||||||
if (!AgeCatalogSync.ready(player, id)) {
|
if (!AgeCatalogSync.ready(player, id)) {
|
||||||
return "Age catalog not acknowledged by client; remaining in source dimension";
|
return "Age catalog not acknowledged by client; remaining in source dimension";
|
||||||
}
|
}
|
||||||
BlockPos landing = findLanding(target);
|
boolean fissure = target.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
|
||||||
|
&& features.terrainFeatures() != null && features.terrainFeatures().starFissures();
|
||||||
|
BlockPos landing = fissure ? PortStarFissureFeature.savedSpawn(target) : null;
|
||||||
|
if (landing != null) landing = findFootedDestination(target, landing).orElse(null);
|
||||||
|
if (landing == null) landing = findLanding(target);
|
||||||
if (landing == null) return "No safe test landing available; remaining in source dimension";
|
if (landing == null) return "No safe test landing available; remaining in source dimension";
|
||||||
|
if (fissure) PortStarFissureFeature.generateOnce(target, landing, isCaveTerrain(target));
|
||||||
boolean moved = player.teleportTo(target, landing.getX() + 0.5, landing.getY(), landing.getZ() + 0.5,
|
boolean moved = player.teleportTo(target, landing.getX() + 0.5, landing.getY(), landing.getZ() + 0.5,
|
||||||
Set.of(), player.getYRot(), player.getXRot(), true);
|
Set.of(), player.getYRot(), player.getXRot(), true);
|
||||||
return moved ? null : "Teleportation failed; remaining in source dimension";
|
return moved ? null : "Teleportation failed; remaining in source dimension";
|
||||||
@@ -34,12 +39,8 @@ public final class PortAgeTravel {
|
|||||||
|
|
||||||
private static BlockPos findLanding(ServerLevel target) {
|
private static BlockPos findLanding(ServerLevel target) {
|
||||||
net.minecraft.world.level.chunk.ChunkGenerator generator = target.getChunkSource().getGenerator();
|
net.minecraft.world.level.chunk.ChunkGenerator generator = target.getChunkSource().getGenerator();
|
||||||
if (generator instanceof PortRockChunkGenerator rocks) generator = rocks.base();
|
if (generator instanceof PortFeatureChunkGenerator features) generator = features.base();
|
||||||
if (generator instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator noise
|
if (isCaveTerrain(target)) {
|
||||||
&& noise.generatorSettings().value().noiseSettings().equals(target.registryAccess()
|
|
||||||
.lookupOrThrow(Registries.NOISE_SETTINGS)
|
|
||||||
.getOrThrow(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES)
|
|
||||||
.value().noiseSettings())) {
|
|
||||||
// The cave roof is not a valid arrival surface. Search down from
|
// The cave roof is not a valid arrival surface. Search down from
|
||||||
// Legacy's Y=64 start, then construct the normal safety platform
|
// Legacy's Y=64 start, then construct the normal safety platform
|
||||||
// at that height only when no footed opening can be found.
|
// at that height only when no footed opening can be found.
|
||||||
@@ -88,6 +89,16 @@ public final class PortAgeTravel {
|
|||||||
return makePlatform(target, new BlockPos(0, y, 0));
|
return makePlatform(target, new BlockPos(0, y, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isCaveTerrain(ServerLevel target) {
|
||||||
|
var generator = target.getChunkSource().getGenerator();
|
||||||
|
if (generator instanceof PortFeatureChunkGenerator features) generator = features.base();
|
||||||
|
return generator instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator noise
|
||||||
|
&& noise.generatorSettings().value().noiseSettings().equals(target.registryAccess()
|
||||||
|
.lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||||
|
.getOrThrow(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES)
|
||||||
|
.value().noiseSettings());
|
||||||
|
}
|
||||||
|
|
||||||
private static BlockPos makePlatform(ServerLevel target, BlockPos foot) {
|
private static BlockPos makePlatform(ServerLevel target, BlockPos foot) {
|
||||||
if (foot.getY() <= target.getMinY() || foot.getY() + 2 >= target.getMaxY()) return null;
|
if (foot.getY() <= target.getMinY() || foot.getY() + 2 >= target.getMaxY()) return null;
|
||||||
// A carved pocket inside a flooded cave immediately refills. Refuse
|
// A carved pocket inside a flooded cave immediately refills. Refuse
|
||||||
@@ -112,7 +123,36 @@ public final class PortAgeTravel {
|
|||||||
return target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP)
|
return target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP)
|
||||||
&& target.getFluidState(foot).isEmpty()
|
&& target.getFluidState(foot).isEmpty()
|
||||||
&& target.getFluidState(foot.above()).isEmpty()
|
&& target.getFluidState(foot.above()).isEmpty()
|
||||||
&& target.getBlockState(foot).getCollisionShape(target, foot).isEmpty()
|
&& passable(target, foot) && passable(target, foot.above());
|
||||||
&& target.getBlockState(foot.above()).getCollisionShape(target, foot.above()).isEmpty();
|
}
|
||||||
|
|
||||||
|
private static boolean passable(ServerLevel level, BlockPos pos) {
|
||||||
|
var state = level.getBlockState(pos);
|
||||||
|
return state.getCollisionShape(level, pos).isEmpty() || state.is(net.minecraft.tags.BlockTags.LEAVES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Same local-then-surface safe-return search used by 1.21.1 fissures. */
|
||||||
|
public static java.util.Optional<BlockPos> findFootedDestination(ServerLevel level, BlockPos origin) {
|
||||||
|
var local = BlockPos.betweenClosedStream(origin.offset(-4, -2, -4), origin.offset(4, 2, 4))
|
||||||
|
.map(BlockPos::immutable).sorted(java.util.Comparator.comparingDouble(pos -> pos.distSqr(origin)))
|
||||||
|
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
|
||||||
|
.filter(pos -> hasFooting(level, pos)).findFirst();
|
||||||
|
if (local.isPresent()) return local;
|
||||||
|
for (int radius = 1; radius <= 64; radius++) {
|
||||||
|
final int ring = radius;
|
||||||
|
var found = BlockPos.betweenClosedStream(origin.offset(-ring, 0, -ring), origin.offset(ring, 0, ring))
|
||||||
|
.filter(pos -> Math.max(Math.abs(pos.getX() - origin.getX()), Math.abs(pos.getZ() - origin.getZ())) == ring)
|
||||||
|
.map(BlockPos::immutable)
|
||||||
|
.flatMap(pos -> {
|
||||||
|
BlockPos surface = new BlockPos(pos.getX(),
|
||||||
|
level.getHeight(Heightmap.Types.MOTION_BLOCKING, pos.getX(), pos.getZ()), pos.getZ());
|
||||||
|
return java.util.stream.Stream.of(surface, surface.above(), surface.below(),
|
||||||
|
surface.above(2), surface.below(2));
|
||||||
|
})
|
||||||
|
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
|
||||||
|
.filter(pos -> hasFooting(level, pos)).findFirst();
|
||||||
|
if (found.isPresent()) return found;
|
||||||
|
}
|
||||||
|
return java.util.Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.AmethystClusterBlock;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
/** 26.3 port of the 1.21.1 surface crystal mound with budding hearts and seeded buds. */
|
||||||
|
final class PortCrystalFormationsFeature {
|
||||||
|
private static final long SALT = 0x4352595354414C53L;
|
||||||
|
private static final int CHANCE = 15;
|
||||||
|
|
||||||
|
private PortCrystalFormationsFeature() {}
|
||||||
|
|
||||||
|
record Site(int x, int z, long seed) {}
|
||||||
|
|
||||||
|
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
|
||||||
|
long seed = mix(ageSeed ^ SALT ^ (long) chunk.x() * 341873128712L
|
||||||
|
^ (long) chunk.z() * 132897987541L ^ (long) occurrence * 0x9E3779B97F4A7C15L);
|
||||||
|
RandomSource random = RandomSource.create(seed);
|
||||||
|
if (random.nextInt(CHANCE) != 0) return null;
|
||||||
|
return new Site(chunk.getMinBlockX() + 5 + random.nextInt(6),
|
||||||
|
chunk.getMinBlockZ() + 5 + random.nextInt(6), seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, int occurrence) {
|
||||||
|
Site site = site(ageSeed, chunk.getPos(), occurrence);
|
||||||
|
if (site == null) return;
|
||||||
|
RandomSource random = RandomSource.create(site.seed() ^ 0x42554444494E474CL);
|
||||||
|
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, site.x(), site.z());
|
||||||
|
if (y <= level.getMinY() + 1 || y + 4 >= level.getMaxY()) return;
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
for (int dx = -1; dx <= 1; dx++) {
|
||||||
|
for (int dz = -1; dz <= 1; dz++) {
|
||||||
|
int x = site.x() + dx;
|
||||||
|
int z = site.z() + dz;
|
||||||
|
if (Math.abs(level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - y) > 1) return;
|
||||||
|
BlockState floor = level.getBlockState(pos.set(x, y - 1, z));
|
||||||
|
if (!floor.isFaceSturdy(level, pos, Direction.UP) || !floor.getFluidState().isEmpty()
|
||||||
|
|| floor.hasBlockEntity()) return;
|
||||||
|
for (int h = 0; h < 4; h++) {
|
||||||
|
BlockState existing = level.getBlockState(pos.set(x, y + h, z));
|
||||||
|
if (!existing.isAir() && !existing.canBeReplaced()) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int dx = -1; dx <= 1; dx++) {
|
||||||
|
for (int dz = -1; dz <= 1; dz++) {
|
||||||
|
boolean center = dx == 0 && dz == 0;
|
||||||
|
int height = center ? 3 : random.nextInt(3) == 0 ? 2 : 1;
|
||||||
|
for (int h = 0; h < height; h++) {
|
||||||
|
boolean budding = h == height - 1 && (center || random.nextBoolean());
|
||||||
|
level.setBlock(pos.set(site.x() + dx, y + h, site.z() + dz),
|
||||||
|
budding ? Rivencraft.BUDDING_CRYSTAL.get().defaultBlockState()
|
||||||
|
: Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 2);
|
||||||
|
}
|
||||||
|
if (level.getBlockState(pos.set(site.x() + dx, y + height - 1, site.z() + dz))
|
||||||
|
.is(Rivencraft.BUDDING_CRYSTAL) && random.nextBoolean()) {
|
||||||
|
level.setBlock(pos.set(site.x() + dx, y + height, site.z() + dz),
|
||||||
|
Rivencraft.SMALL_CRYSTAL_BUD.get().defaultBlockState()
|
||||||
|
.setValue(AmethystClusterBlock.FACING, Direction.UP), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long value) {
|
||||||
|
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||||
|
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||||
|
return value ^ value >>> 31;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.tags.TagKey;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.AbstractOreFeature;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.FeaturePlacer;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||||
|
import net.neoforged.neoforge.common.Tags;
|
||||||
|
|
||||||
|
/** Replays each eligible biome ore placement once per page, matching Rivencraft 1.21.1. */
|
||||||
|
public final class PortDenseOres {
|
||||||
|
public static final TagKey<PlacedFeature> EXTRA_ORES = TagKey.create(Registries.PLACED_FEATURE,
|
||||||
|
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "dense_ores"));
|
||||||
|
|
||||||
|
private PortDenseOres() {}
|
||||||
|
|
||||||
|
public static boolean hasOres(Holder<Biome> biome) {
|
||||||
|
return oreFeatures(biome).anyMatch(PortDenseOres::isOre);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modern ore features carry their targets directly rather than an OreConfiguration. */
|
||||||
|
public static boolean isOre(Holder<PlacedFeature> holder) {
|
||||||
|
if (holder.is(EXTRA_ORES)) return true;
|
||||||
|
if (!(holder.value().feature().value() instanceof AbstractOreFeature ore)) return false;
|
||||||
|
return ore.targetStates().stream().anyMatch(target -> isOreBlock(target.state()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isOreBlock(BlockState state) {
|
||||||
|
if (state.is(Tags.BlockItems.ORES.block())) return true;
|
||||||
|
String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
|
||||||
|
return path.endsWith("_ore") || path.contains("_ore_");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static java.util.stream.Stream<Holder<PlacedFeature>> oreFeatures(Holder<Biome> biome) {
|
||||||
|
return biome.value().getGenerationSettings().features().stream().flatMap(HolderSet::stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void place(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator generator,
|
||||||
|
long ageSeed, int count) {
|
||||||
|
if (count == 0) return;
|
||||||
|
Set<Holder<Biome>> biomes = new HashSet<>();
|
||||||
|
ChunkPos.rangeClosed(chunk.getPos(), 1).forEach(pos -> {
|
||||||
|
for (var section : level.getChunk(pos.x(), pos.z()).getSections())
|
||||||
|
section.getBiomes().getAll(biomes::add);
|
||||||
|
});
|
||||||
|
biomes.retainAll(generator.getBiomeSource().possibleBiomes());
|
||||||
|
// Collect once per chunk, not once per repeated page. Stable registry-ID
|
||||||
|
// order and per-occurrence seeds retain the 1.21.1 placement sequence.
|
||||||
|
Map<Identifier, PlacedFeature> ores = new TreeMap<>();
|
||||||
|
for (Holder<Biome> biome : biomes) {
|
||||||
|
oreFeatures(biome).filter(PortDenseOres::isOre).forEach(feature ->
|
||||||
|
feature.unwrapKey().ifPresent(key -> ores.putIfAbsent(key.identifier(), feature.value())));
|
||||||
|
}
|
||||||
|
BlockPos origin = new BlockPos(chunk.getPos().getMinBlockX(), level.getMinY(),
|
||||||
|
chunk.getPos().getMinBlockZ());
|
||||||
|
FeaturePlacer placer = new FeaturePlacer(level, generator);
|
||||||
|
for (int occurrence = 0; occurrence < count; occurrence++) {
|
||||||
|
for (var entry : ores.entrySet()) {
|
||||||
|
long seed = mix(ageSeed ^ (long) origin.getX() * 341873128712L
|
||||||
|
^ (long) origin.getZ() * 132897987541L
|
||||||
|
^ (long) entry.getKey().hashCode() * 0x9E3779B97F4A7C15L
|
||||||
|
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||||
|
placer.placeWithBiomeCheck(entry.getValue(), RandomSource.create(seed), origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long value) {
|
||||||
|
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||||
|
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||||
|
return value ^ value >>> 31;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
|
import net.minecraft.tags.TagKey;
|
||||||
|
import net.minecraft.tags.BiomeTags;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.NoiseColumn;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeManager;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||||
|
import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicate;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.BlockBlobFeature;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.LakeFeature;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.BiomeFilter;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.RarityFilter;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
|
||||||
|
/** Delegates terrain generation and applies frozen Age-scoped features. */
|
||||||
|
public final class PortFeatureChunkGenerator extends ChunkGenerator {
|
||||||
|
private static final ResourceKey<PlacedFeature> FOREST_ROCK = ResourceKey.create(
|
||||||
|
Registries.PLACED_FEATURE, Identifier.withDefaultNamespace("forest_rock"));
|
||||||
|
private static final ResourceKey<PlacedFeature> DUNGEONS = ResourceKey.create(
|
||||||
|
Registries.PLACED_FEATURE, Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "dungeons"));
|
||||||
|
private static final TagKey<Block> ROCK_GROUND = TagKey.create(
|
||||||
|
Registries.BLOCK, Identifier.withDefaultNamespace("forest_rock_can_place_on"));
|
||||||
|
public static final MapCodec<PortFeatureChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||||
|
ChunkGenerator.CODEC.fieldOf("base").forGetter(generator -> generator.base),
|
||||||
|
PortRockPlan.CODEC.optionalFieldOf("rocks").forGetter(generator -> Optional.ofNullable(generator.rocks)),
|
||||||
|
PortVegetationPlan.CODEC.optionalFieldOf("vegetation")
|
||||||
|
.forGetter(generator -> Optional.ofNullable(generator.vegetation)),
|
||||||
|
com.mojang.serialization.Codec.intRange(0, 32).optionalFieldOf("dungeons", 0)
|
||||||
|
.forGetter(generator -> generator.dungeons),
|
||||||
|
PortLakePlan.CODEC.optionalFieldOf("lakes")
|
||||||
|
.forGetter(generator -> Optional.ofNullable(generator.lakes)),
|
||||||
|
PortTerrainFeaturePlan.CODEC.optionalFieldOf("terrain_features")
|
||||||
|
.forGetter(generator -> Optional.ofNullable(generator.terrainFeatures)),
|
||||||
|
com.mojang.serialization.Codec.LONG.fieldOf("age_seed").forGetter(generator -> generator.ageSeed)
|
||||||
|
).apply(instance, (base, rocks, vegetation, dungeons, lakes, terrainFeatures, seed) ->
|
||||||
|
new PortFeatureChunkGenerator(base, rocks.orElse(null), vegetation.orElse(null),
|
||||||
|
dungeons, lakes.orElse(null), terrainFeatures.orElse(null), seed)));
|
||||||
|
|
||||||
|
private final ChunkGenerator base;
|
||||||
|
private final PortRockPlan rocks;
|
||||||
|
private final PortVegetationPlan vegetation;
|
||||||
|
private final int dungeons;
|
||||||
|
private final PortLakePlan lakes;
|
||||||
|
private final PortTerrainFeaturePlan terrainFeatures;
|
||||||
|
private final long ageSeed;
|
||||||
|
|
||||||
|
public PortFeatureChunkGenerator(ChunkGenerator base, PortRockPlan rocks,
|
||||||
|
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||||
|
PortTerrainFeaturePlan terrainFeatures, long ageSeed) {
|
||||||
|
super(base.getBiomeSource(), base::getBiomeGenerationSettings);
|
||||||
|
this.base = base;
|
||||||
|
this.rocks = rocks;
|
||||||
|
this.vegetation = vegetation;
|
||||||
|
this.dungeons = dungeons;
|
||||||
|
this.lakes = lakes;
|
||||||
|
this.terrainFeatures = terrainFeatures;
|
||||||
|
this.ageSeed = ageSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChunkGenerator base() { return base; }
|
||||||
|
public PortRockPlan rocks() { return rocks; }
|
||||||
|
public PortVegetationPlan vegetation() { return vegetation; }
|
||||||
|
public int dungeons() { return dungeons; }
|
||||||
|
public PortLakePlan lakes() { return lakes; }
|
||||||
|
public PortTerrainFeaturePlan terrainFeatures() { return terrainFeatures; }
|
||||||
|
|
||||||
|
@Override protected MapCodec<? extends ChunkGenerator> codec() { return CODEC; }
|
||||||
|
@Override public void validate() { base.validate(); }
|
||||||
|
@Override public ChunkPos getOrigin(RandomState random) { return base.getOrigin(random); }
|
||||||
|
@Override public java.util.Optional<Identifier> getTypeNameForDataFixer() { return base.getTypeNameForDataFixer(); }
|
||||||
|
@Override public CompletableFuture<ChunkAccess> createBiomes(RandomState random, Blender blender,
|
||||||
|
StructureManager structures, ChunkAccess chunk) {
|
||||||
|
return base.createBiomes(random, blender, structures, chunk);
|
||||||
|
}
|
||||||
|
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
|
||||||
|
RandomState random, long seed) {
|
||||||
|
return base.createState(sets, random, seed);
|
||||||
|
}
|
||||||
|
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
|
||||||
|
StructureManager structures, ChunkAccess chunk,
|
||||||
|
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||||
|
base.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||||
|
}
|
||||||
|
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) {
|
||||||
|
base.createReferences(level, structures, chunk);
|
||||||
|
}
|
||||||
|
@Override public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level,
|
||||||
|
HolderSet<Structure> targets, BlockPos origin, int radius, boolean skipKnown) {
|
||||||
|
return base.findNearestMapStructure(level, targets, origin, radius, skipKnown);
|
||||||
|
}
|
||||||
|
@Override public void spawnOriginalMobs(WorldGenRegion level) { base.spawnOriginalMobs(level); }
|
||||||
|
@Override public int getSpawnHeight(LevelHeightAccessor height) { return base.getSpawnHeight(height); }
|
||||||
|
@Override public int getGenDepth() { return base.getGenDepth(); }
|
||||||
|
@Override public int getSeaLevel() { return base.getSeaLevel(); }
|
||||||
|
@Override public int getMinY() { return base.getMinY(); }
|
||||||
|
@Override public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender,
|
||||||
|
RandomState random, StructureManager structures, BiomeManager biomes,
|
||||||
|
WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
|
||||||
|
CompletableFuture<ChunkAccess> built = base.buildTerrain(
|
||||||
|
chunk, blender, random, structures, biomes, carverBiomeRegion, possibleBiomes);
|
||||||
|
if (terrainFeatures == null || !terrainFeatures.skylands() && terrainFeatures.choices().isEmpty()) return built;
|
||||||
|
return built.thenApply(result -> {
|
||||||
|
// Match 1.21.1: filter the base terrain once, then add the authored
|
||||||
|
// solid volumes, irrespective of the Skylands page's written position.
|
||||||
|
if (terrainFeatures.skylands()) PortSkylandsFeature.filter(result, ageSeed);
|
||||||
|
java.util.Map<Identifier, Integer> occurrences = new java.util.HashMap<>();
|
||||||
|
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
|
||||||
|
if (PortTerrainFeaturePlan.isSurface(choice.page())) continue;
|
||||||
|
int occurrence = occurrences.merge(choice.page(), 1, Integer::sum) - 1;
|
||||||
|
long featureSeed = ageSeed ^ (long) occurrence * 0x9E3779B97F4A7C15L;
|
||||||
|
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
|
||||||
|
if (choice.page().equals(PortTerrainFeaturePlan.FLOATING_ISLANDS))
|
||||||
|
PortFloatingIslandFeature.generate(result, featureSeed, material);
|
||||||
|
else if (choice.page().equals(PortTerrainFeaturePlan.TENDRILS))
|
||||||
|
PortTendrilFeature.generate(result, featureSeed, material);
|
||||||
|
else if (choice.page().equals(PortTerrainFeaturePlan.SPHERES))
|
||||||
|
PortSpheresFeature.generate(result, featureSeed, material);
|
||||||
|
else throw new IllegalStateException("Unsupported terrain feature: " + choice.page());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@Override public int getBaseHeight(int x, int z, Heightmap.Types type,
|
||||||
|
LevelHeightAccessor height, RandomState random) {
|
||||||
|
return base.getBaseHeight(x, z, type, height, random);
|
||||||
|
}
|
||||||
|
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState random) {
|
||||||
|
return base.getBaseColumn(x, z, height, random);
|
||||||
|
}
|
||||||
|
@Override public void addDebugScreenInfo(List<String> info, RandomState random,
|
||||||
|
BlockPos pos, SamplerContext context) {
|
||||||
|
base.addDebugScreenInfo(info, random, pos, context);
|
||||||
|
}
|
||||||
|
@Override public void refreshFeaturesPerStep() {
|
||||||
|
base.refreshFeaturesPerStep();
|
||||||
|
super.refreshFeaturesPerStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk,
|
||||||
|
StructureManager structures) {
|
||||||
|
base.applyBiomeDecoration(level, chunk, structures);
|
||||||
|
if (terrainFeatures != null) {
|
||||||
|
PortDenseOres.place(level, chunk, this, ageSeed, terrainFeatures.denseOres());
|
||||||
|
int spikeOccurrence = 0;
|
||||||
|
int obeliskOccurrence = 0;
|
||||||
|
int crystalOccurrence = 0;
|
||||||
|
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
|
||||||
|
if (!PortTerrainFeaturePlan.isSurface(choice.page())) continue;
|
||||||
|
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
|
||||||
|
if (choice.page().equals(PortTerrainFeaturePlan.SPIKES))
|
||||||
|
PortSpikesFeature.generate(level, chunk, ageSeed, material, spikeOccurrence++);
|
||||||
|
else if (choice.page().equals(PortTerrainFeaturePlan.OBELISKS))
|
||||||
|
PortObelisksFeature.generate(level, chunk, ageSeed, material, obeliskOccurrence++);
|
||||||
|
else if (choice.page().equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS))
|
||||||
|
PortCrystalFormationsFeature.generate(level, chunk, ageSeed, crystalOccurrence++);
|
||||||
|
else throw new IllegalStateException("Unsupported surface feature: " + choice.page());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChunkPos chunkPos = chunk.getPos();
|
||||||
|
long chunkSeed = ageSeed ^ (long) chunkPos.x() * 341873128712L
|
||||||
|
^ (long) chunkPos.z() * 132897987541L;
|
||||||
|
BlockPos origin = new BlockPos(chunkPos.getMinBlockX(), level.getMinY(), chunkPos.getMinBlockZ());
|
||||||
|
if (rocks != null) {
|
||||||
|
PlacedFeature vanilla = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||||
|
.getOrThrow(FOREST_ROCK).value();
|
||||||
|
var placement = vanilla.placement().stream()
|
||||||
|
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList();
|
||||||
|
for (int occurrence = 0; occurrence < rocks.materials().size(); occurrence++) {
|
||||||
|
RandomSource random = RandomSource.create(chunkSeed
|
||||||
|
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||||
|
BlockState material = BuiltInRegistries.BLOCK.getValue(
|
||||||
|
rocks.materials().get(occurrence)).defaultBlockState();
|
||||||
|
PlacedFeature authored = new PlacedFeature(Holder.direct(new BlockBlobFeature(
|
||||||
|
material, BlockPredicate.matchesTag(ROCK_GROUND))), placement);
|
||||||
|
authored.place(level, this, random, origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dungeons > 0) {
|
||||||
|
PlacedFeature authored = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||||
|
.getOrThrow(DUNGEONS).value();
|
||||||
|
for (int occurrence = 0; occurrence < dungeons; occurrence++) {
|
||||||
|
RandomSource random = RandomSource.create(chunkSeed
|
||||||
|
^ 0x44554E47454F4E53L ^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||||
|
authored.place(level, this, random, origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lakes != null) {
|
||||||
|
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||||
|
java.util.Map<PortLakePlan.Choice, Integer> occurrences = new java.util.HashMap<>();
|
||||||
|
for (PortLakePlan.Choice choice : lakes.choices()) {
|
||||||
|
PlacedFeature vanilla = registry.getOrThrow(ResourceKey.create(
|
||||||
|
Registries.PLACED_FEATURE, PortLakePlan.placedFeature(choice.page()))).value();
|
||||||
|
if (!(vanilla.feature().value() instanceof LakeFeature lake))
|
||||||
|
throw new IllegalStateException("Lake placement no longer references a LakeFeature: " + choice.page());
|
||||||
|
BlockState fluid = BuiltInRegistries.BLOCK.getValue(choice.fluid()).defaultBlockState();
|
||||||
|
LakeFeature feature = new LakeFeature(BlockStateProvider.holderOf(fluid), lake.barrier(),
|
||||||
|
lake.canPlaceFeature(), lake.canReplaceWithAirOrFluid(), lake.canReplaceWithBarrier());
|
||||||
|
PlacedFeature authored = new PlacedFeature(Holder.direct(feature), vanilla.placement().stream()
|
||||||
|
.filter(modifier -> !(modifier instanceof BiomeFilter))
|
||||||
|
.map(modifier -> !PortLakePlan.isDeep(choice.page()) && modifier instanceof RarityFilter
|
||||||
|
? RarityFilter.onAverageOnceEvery(12) : modifier)
|
||||||
|
.toList());
|
||||||
|
int occurrence = occurrences.merge(choice, 1, Integer::sum) - 1;
|
||||||
|
RandomSource random = RandomSource.create(chunkSeed
|
||||||
|
^ (long) choice.hashCode() * 0x9E3779B97F4A7C15L
|
||||||
|
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||||
|
authored.place(level, this, random, origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (vegetation != null) {
|
||||||
|
int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 8);
|
||||||
|
Holder<Biome> biome = level.getBiome(new BlockPos(
|
||||||
|
chunkPos.getMinBlockX() + 8, surface, chunkPos.getMinBlockZ() + 8));
|
||||||
|
if (biome.is(BiomeTags.IS_END) || PortRegistryPages.isCaveBiome(biome)) return;
|
||||||
|
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||||
|
for (int occurrence = 0; occurrence < vegetation.pages().size(); occurrence++) {
|
||||||
|
Identifier page = vegetation.pages().get(occurrence);
|
||||||
|
Identifier placedId = PortVegetationPlan.placedFeature(page);
|
||||||
|
PlacedFeature vanilla = registry.getOrThrow(
|
||||||
|
ResourceKey.create(Registries.PLACED_FEATURE, placedId)).value();
|
||||||
|
PlacedFeature authored = new PlacedFeature(vanilla.feature(), vanilla.placement().stream()
|
||||||
|
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList());
|
||||||
|
RandomSource random = RandomSource.create(chunkSeed
|
||||||
|
^ (long) page.hashCode() * 0x9E3779B97F4A7C15L
|
||||||
|
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||||
|
authored.place(level, this, random, origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.QuartPos;
|
||||||
|
import net.minecraft.tags.BiomeTags;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||||
|
|
||||||
|
/** 1.21.1 floating-island shape adapted to the 26.3 chunk terrain stage. */
|
||||||
|
final class PortFloatingIslandFeature {
|
||||||
|
private static final long SALT = 0x464C4F4154494E47L;
|
||||||
|
private static final int SOURCE_RANGE = 3;
|
||||||
|
private static final int CHANCE = 128;
|
||||||
|
|
||||||
|
private PortFloatingIslandFeature() {}
|
||||||
|
|
||||||
|
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||||
|
var target = chunk.getPos();
|
||||||
|
for (int sourceX = target.x() - SOURCE_RANGE; sourceX <= target.x() + SOURCE_RANGE; sourceX++) {
|
||||||
|
for (int sourceZ = target.z() - SOURCE_RANGE; sourceZ <= target.z() + SOURCE_RANGE; sourceZ++) {
|
||||||
|
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
|
||||||
|
^ (long) sourceX * 341873128712L ^ (long) sourceZ * 132897987541L));
|
||||||
|
if (random.nextInt(CHANCE) != 0) continue;
|
||||||
|
int centerX = sourceX * 16 + random.nextInt(16);
|
||||||
|
int centerZ = sourceZ * 16 + random.nextInt(16);
|
||||||
|
int centerY = Math.min(chunk.getMaxY() - 30, 168 + random.nextInt(48));
|
||||||
|
if (centerY < chunk.getMinY() + 24) continue;
|
||||||
|
int radiusX = 24 + random.nextInt(18);
|
||||||
|
int radiusZ = 24 + random.nextInt(18);
|
||||||
|
fill(chunk, ageSeed, material, centerX, centerY, centerZ, radiusX, radiusZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fill(ChunkAccess chunk, long ageSeed, BlockState material,
|
||||||
|
int centerX, int centerY, int centerZ, int radiusX, int radiusZ) {
|
||||||
|
int originX = chunk.getPos().getMinBlockX();
|
||||||
|
int originZ = chunk.getPos().getMinBlockZ();
|
||||||
|
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||||
|
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
for (int localX = 0; localX < 16; localX++) {
|
||||||
|
int worldX = originX + localX;
|
||||||
|
double dx = (worldX - centerX) / (double) radiusX;
|
||||||
|
for (int localZ = 0; localZ < 16; localZ++) {
|
||||||
|
int worldZ = originZ + localZ;
|
||||||
|
double dz = (worldZ - centerZ) / (double) radiusZ;
|
||||||
|
double distance = dx * dx + dz * dz;
|
||||||
|
if (distance >= 1.0) continue;
|
||||||
|
long detail = mix(ageSeed ^ (long) centerX * 341873128712L
|
||||||
|
^ (long) centerZ * 132897987541L ^ (long) worldX * 42317861L
|
||||||
|
^ (long) worldZ * 374761393L);
|
||||||
|
int top = centerY + (int) Math.round(3 * (1 - distance)) + Math.floorMod(detail, 3) - 1;
|
||||||
|
int depth = Math.max(1, (int) Math.round(3 + 17 * Math.pow(1 - distance, 1.5)));
|
||||||
|
// Biomes are already populated by createBiomes in 26.3, so
|
||||||
|
// sampling this chunk preserves its authored biome distribution.
|
||||||
|
var biome = chunk.getNoiseBiome(QuartPos.fromBlock(worldX), QuartPos.fromBlock(top),
|
||||||
|
QuartPos.fromBlock(worldZ));
|
||||||
|
BlockState soil = biome.is(BiomeTags.IS_END) || PortRegistryPages.isCaveBiome(biome)
|
||||||
|
? material : Blocks.DIRT.defaultBlockState();
|
||||||
|
BlockState cap = soil == material ? material : Blocks.GRASS_BLOCK.defaultBlockState();
|
||||||
|
for (int y = Math.max(top - depth, chunk.getMinY()); y <= top; y++) {
|
||||||
|
pos.set(worldX, y, worldZ);
|
||||||
|
if (!chunk.getBlockState(pos).isAir()) continue;
|
||||||
|
BlockState state = y == top ? cap : y >= top - 3 ? soil : material;
|
||||||
|
chunk.setBlockState(pos, state);
|
||||||
|
surface.update(localX, y, localZ, state);
|
||||||
|
floor.update(localX, y, localZ, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long value) {
|
||||||
|
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||||
|
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||||
|
return value ^ value >>> 31;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
import java.util.List;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
|
/** Ordered lake pages with fluid choices frozen when the Age is bound. */
|
||||||
|
public record PortLakePlan(List<Choice> choices) {
|
||||||
|
public static final Codec<PortLakePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
|
Choice.CODEC.listOf().fieldOf("choices").forGetter(PortLakePlan::choices)
|
||||||
|
).apply(instance, PortLakePlan::new));
|
||||||
|
|
||||||
|
public record Choice(Identifier page, Identifier fluid) {
|
||||||
|
public static final Codec<Choice> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
|
Identifier.CODEC.fieldOf("page").forGetter(Choice::page),
|
||||||
|
Identifier.CODEC.fieldOf("fluid").forGetter(Choice::fluid)
|
||||||
|
).apply(instance, Choice::new));
|
||||||
|
|
||||||
|
public Choice {
|
||||||
|
if (!isLake(page) || fluid == null) throw new IllegalArgumentException("Invalid lake choice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortLakePlan {
|
||||||
|
choices = List.copyOf(choices);
|
||||||
|
if (choices.isEmpty() || choices.size() > 32)
|
||||||
|
throw new IllegalArgumentException("Lake plan must contain 1–32 choices");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isLake(Identifier page) {
|
||||||
|
if (page == null || !page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
|
||||||
|
return switch (page.getPath()) {
|
||||||
|
case "feature/surface_lakes", "feature/deep_lakes", "feature/lava_lakes" -> true;
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isDeep(Identifier page) { return page.getPath().equals("feature/deep_lakes"); }
|
||||||
|
|
||||||
|
public static Identifier placedFeature(Identifier page) {
|
||||||
|
if (!isLake(page)) throw new IllegalArgumentException("Unsupported lake page " + page);
|
||||||
|
return Identifier.withDefaultNamespace(isDeep(page) ? "lake_lava_underground" : "lake_lava_surface");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
/** 26.3 port of Rivencraft 1.21.1's Legacy-style 4×4 plinth and 2×12×2 shaft. */
|
||||||
|
final class PortObelisksFeature {
|
||||||
|
private static final long SALT = 0x4F42454C49534B53L;
|
||||||
|
private static final int CHANCE = 128;
|
||||||
|
private static final int BASE = 4;
|
||||||
|
private static final int HEIGHT = 12;
|
||||||
|
|
||||||
|
private PortObelisksFeature() {}
|
||||||
|
|
||||||
|
record Site(int x, int z) {}
|
||||||
|
|
||||||
|
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
|
||||||
|
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
|
||||||
|
^ (long) chunk.x() * 341873128712L ^ (long) chunk.z() * 132897987541L
|
||||||
|
^ (long) occurrence * 0x9E3779B97F4A7C15L));
|
||||||
|
if (random.nextInt(CHANCE) != 0) return null;
|
||||||
|
return new Site(chunk.getMinBlockX() + 4 + random.nextInt(8),
|
||||||
|
chunk.getMinBlockZ() + 4 + random.nextInt(8));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
|
||||||
|
Site site = site(ageSeed, chunk.getPos(), occurrence);
|
||||||
|
if (site == null) return;
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
int minSurface = Integer.MAX_VALUE;
|
||||||
|
int maxSurface = Integer.MIN_VALUE;
|
||||||
|
int[][] surface = new int[BASE][BASE];
|
||||||
|
for (int dx = 0; dx < BASE; dx++) {
|
||||||
|
for (int dz = 0; dz < BASE; dz++) {
|
||||||
|
int x = site.x() + dx;
|
||||||
|
int z = site.z() + dz;
|
||||||
|
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||||
|
if (y <= level.getMinY() + 1) return;
|
||||||
|
BlockState ground = level.getBlockState(pos.set(x, y - 1, z));
|
||||||
|
if (!ground.isFaceSturdy(level, pos, Direction.UP)
|
||||||
|
|| !ground.getFluidState().isEmpty() || ground.hasBlockEntity()) return;
|
||||||
|
surface[dx][dz] = y;
|
||||||
|
minSurface = Math.min(minSurface, y);
|
||||||
|
maxSurface = Math.max(maxSurface, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxSurface - minSurface > 2 || maxSurface + HEIGHT >= level.getMaxY()) return;
|
||||||
|
for (int dx = 0; dx < BASE; dx++) {
|
||||||
|
for (int dz = 0; dz < BASE; dz++) {
|
||||||
|
for (int y = surface[dx][dz]; y <= maxSurface; y++) {
|
||||||
|
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int dx = 1; dx <= 2; dx++) {
|
||||||
|
for (int dz = 1; dz <= 2; dz++) {
|
||||||
|
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++) {
|
||||||
|
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int dx = 0; dx < BASE; dx++) {
|
||||||
|
for (int dz = 0; dz < BASE; dz++) {
|
||||||
|
for (int y = surface[dx][dz]; y <= maxSurface; y++)
|
||||||
|
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int dx = 1; dx <= 2; dx++) {
|
||||||
|
for (int dz = 1; dz <= 2; dz++) {
|
||||||
|
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++)
|
||||||
|
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean open(BlockState state) {
|
||||||
|
return state.getFluidState().isEmpty() && (state.isAir() || state.canBeReplaced());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long value) {
|
||||||
|
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||||
|
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||||
|
return value ^ value >>> 31;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package net.dnorberg.rivencraft.age;
|
|
||||||
|
|
||||||
import com.mojang.serialization.MapCodec;
|
|
||||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import net.minecraft.core.BlockPos;
|
|
||||||
import net.minecraft.core.Holder;
|
|
||||||
import net.minecraft.core.HolderLookup;
|
|
||||||
import net.minecraft.core.RegistryAccess;
|
|
||||||
import net.minecraft.core.registries.BuiltInRegistries;
|
|
||||||
import net.minecraft.resources.Identifier;
|
|
||||||
import net.minecraft.resources.ResourceKey;
|
|
||||||
import net.minecraft.server.level.ServerLevel;
|
|
||||||
import net.minecraft.server.level.WorldGenRegion;
|
|
||||||
import net.minecraft.util.RandomSource;
|
|
||||||
import net.minecraft.world.level.ChunkPos;
|
|
||||||
import net.minecraft.world.level.LevelHeightAccessor;
|
|
||||||
import net.minecraft.world.level.NoiseColumn;
|
|
||||||
import net.minecraft.world.level.StructureManager;
|
|
||||||
import net.minecraft.world.level.WorldGenLevel;
|
|
||||||
import net.minecraft.world.level.biome.Biome;
|
|
||||||
import net.minecraft.world.level.biome.BiomeManager;
|
|
||||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
|
||||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
|
||||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
|
||||||
import net.minecraft.world.level.levelgen.RandomState;
|
|
||||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
|
||||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
|
||||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
|
||||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
|
||||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
|
||||||
import com.mojang.datafixers.util.Pair;
|
|
||||||
import net.minecraft.core.HolderSet;
|
|
||||||
import net.minecraft.world.level.Level;
|
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
|
||||||
|
|
||||||
/** Delegates 26.3 terrain generation and adds one Age-scoped Rocks decoration. */
|
|
||||||
public final class PortRockChunkGenerator extends ChunkGenerator {
|
|
||||||
public static final MapCodec<PortRockChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
|
||||||
ChunkGenerator.CODEC.fieldOf("base").forGetter(generator -> generator.base),
|
|
||||||
PortRockPlan.CODEC.fieldOf("rocks").forGetter(generator -> generator.rocks),
|
|
||||||
com.mojang.serialization.Codec.LONG.fieldOf("age_seed").forGetter(generator -> generator.ageSeed)
|
|
||||||
).apply(instance, PortRockChunkGenerator::new));
|
|
||||||
|
|
||||||
private final ChunkGenerator base;
|
|
||||||
private final PortRockPlan rocks;
|
|
||||||
private final long ageSeed;
|
|
||||||
|
|
||||||
public PortRockChunkGenerator(ChunkGenerator base, PortRockPlan rocks, long ageSeed) {
|
|
||||||
super(base.getBiomeSource(), base::getBiomeGenerationSettings);
|
|
||||||
this.base = base;
|
|
||||||
this.rocks = rocks;
|
|
||||||
this.ageSeed = ageSeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChunkGenerator base() { return base; }
|
|
||||||
public PortRockPlan rocks() { return rocks; }
|
|
||||||
|
|
||||||
@Override protected MapCodec<? extends ChunkGenerator> codec() { return CODEC; }
|
|
||||||
@Override public void validate() { base.validate(); }
|
|
||||||
@Override public ChunkPos getOrigin(RandomState random) { return base.getOrigin(random); }
|
|
||||||
@Override public java.util.Optional<Identifier> getTypeNameForDataFixer() { return base.getTypeNameForDataFixer(); }
|
|
||||||
@Override public CompletableFuture<ChunkAccess> createBiomes(RandomState random, Blender blender,
|
|
||||||
StructureManager structures, ChunkAccess chunk) {
|
|
||||||
return base.createBiomes(random, blender, structures, chunk);
|
|
||||||
}
|
|
||||||
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
|
|
||||||
RandomState random, long seed) {
|
|
||||||
return base.createState(sets, random, seed);
|
|
||||||
}
|
|
||||||
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
|
|
||||||
StructureManager structures, ChunkAccess chunk,
|
|
||||||
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
|
||||||
base.createStructures(registries, state, structures, chunk, templates, dimension);
|
|
||||||
}
|
|
||||||
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) {
|
|
||||||
base.createReferences(level, structures, chunk);
|
|
||||||
}
|
|
||||||
@Override public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level,
|
|
||||||
HolderSet<Structure> targets, BlockPos origin, int radius, boolean skipKnown) {
|
|
||||||
return base.findNearestMapStructure(level, targets, origin, radius, skipKnown);
|
|
||||||
}
|
|
||||||
@Override public void spawnOriginalMobs(WorldGenRegion level) { base.spawnOriginalMobs(level); }
|
|
||||||
@Override public int getSpawnHeight(LevelHeightAccessor height) { return base.getSpawnHeight(height); }
|
|
||||||
@Override public int getGenDepth() { return base.getGenDepth(); }
|
|
||||||
@Override public int getSeaLevel() { return base.getSeaLevel(); }
|
|
||||||
@Override public int getMinY() { return base.getMinY(); }
|
|
||||||
@Override public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender,
|
|
||||||
RandomState random, StructureManager structures, BiomeManager biomes,
|
|
||||||
WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
|
|
||||||
return base.buildTerrain(chunk, blender, random, structures, biomes, carverBiomeRegion, possibleBiomes);
|
|
||||||
}
|
|
||||||
@Override public int getBaseHeight(int x, int z, Heightmap.Types type,
|
|
||||||
LevelHeightAccessor height, RandomState random) {
|
|
||||||
return base.getBaseHeight(x, z, type, height, random);
|
|
||||||
}
|
|
||||||
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState random) {
|
|
||||||
return base.getBaseColumn(x, z, height, random);
|
|
||||||
}
|
|
||||||
@Override public void addDebugScreenInfo(List<String> info, RandomState random,
|
|
||||||
BlockPos pos, SamplerContext context) {
|
|
||||||
base.addDebugScreenInfo(info, random, pos, context);
|
|
||||||
}
|
|
||||||
@Override public void refreshFeaturesPerStep() {
|
|
||||||
base.refreshFeaturesPerStep();
|
|
||||||
super.refreshFeaturesPerStep();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk,
|
|
||||||
StructureManager structures) {
|
|
||||||
base.applyBiomeDecoration(level, chunk, structures);
|
|
||||||
ChunkPos chunkPos = chunk.getPos();
|
|
||||||
long seed = ageSeed ^ (long) chunkPos.x() * 341873128712L ^ (long) chunkPos.z() * 132897987541L;
|
|
||||||
RandomSource random = RandomSource.create(seed);
|
|
||||||
if (random.nextInt(5) != 0) return;
|
|
||||||
BlockState material = BuiltInRegistries.BLOCK.getValue(rocks.material()).defaultBlockState();
|
|
||||||
int centerX = chunkPos.getMinBlockX() + 2 + random.nextInt(12);
|
|
||||||
int centerZ = chunkPos.getMinBlockZ() + 2 + random.nextInt(12);
|
|
||||||
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) {
|
|
||||||
if (random.nextInt(4) == 0) continue;
|
|
||||||
int x = centerX + dx;
|
|
||||||
int z = centerZ + dz;
|
|
||||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
|
||||||
BlockPos place = new BlockPos(x, y, z);
|
|
||||||
if (level.getBlockState(place).isAir()
|
|
||||||
&& level.getFluidState(place).isEmpty()
|
|
||||||
&& level.getBlockState(place.below()).isFaceSturdy(level, place.below(), net.minecraft.core.Direction.UP))
|
|
||||||
level.setBlock(place, material, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,23 @@ package net.dnorberg.rivencraft.age;
|
|||||||
|
|
||||||
import com.mojang.serialization.Codec;
|
import com.mojang.serialization.Codec;
|
||||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
/** First frozen authored feature: one Rocks page with an explicit block material. */
|
/** Frozen, ordered Rocks materials. Old single-material beta Ages remain readable. */
|
||||||
public record PortRockPlan(Identifier material) {
|
public record PortRockPlan(List<Identifier> materials) {
|
||||||
public static final Codec<PortRockPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
public static final Codec<PortRockPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
Identifier.CODEC.fieldOf("material").forGetter(PortRockPlan::material)
|
Identifier.CODEC.optionalFieldOf("material").forGetter(plan -> Optional.empty()),
|
||||||
).apply(instance, PortRockPlan::new));
|
Identifier.CODEC.listOf().optionalFieldOf("materials", List.of()).forGetter(PortRockPlan::materials)
|
||||||
|
).apply(instance, (old, current) -> new PortRockPlan(current.isEmpty()
|
||||||
|
? old.map(List::of).orElse(List.of()) : current)));
|
||||||
|
|
||||||
|
public PortRockPlan {
|
||||||
|
materials = List.copyOf(materials);
|
||||||
|
if (materials.isEmpty() || materials.size() > 32)
|
||||||
|
throw new IllegalArgumentException("Rocks plan must contain 1–32 materials");
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortRockPlan(Identifier material) { this(List.of(material)); }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 26.3 port of Rivencraft's subtractive Skylands filter, adapted from
|
||||||
|
* Mystcraft Legacy's SymbolSkylands (LGPL-3.0-or-later). Keeps upper solid
|
||||||
|
* terrain above a seven-octave cutoff near Y=76 and removes liquids.
|
||||||
|
*/
|
||||||
|
final class PortSkylandsFeature {
|
||||||
|
private static final long SALT = 0x534B594C414E4453L;
|
||||||
|
private static final int BASE_CUTOFF = 76;
|
||||||
|
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
|
||||||
|
|
||||||
|
private PortSkylandsFeature() {}
|
||||||
|
|
||||||
|
static void filter(ChunkAccess chunk, long ageSeed) {
|
||||||
|
int originX = chunk.getPos().getMinBlockX();
|
||||||
|
int originZ = chunk.getPos().getMinBlockZ();
|
||||||
|
int minY = chunk.getMinY();
|
||||||
|
int maxY = chunk.getMaxY();
|
||||||
|
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||||
|
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
for (int localX = 0; localX < 16; localX++) {
|
||||||
|
for (int localZ = 0; localZ < 16; localZ++) {
|
||||||
|
int cutoff = cutoff(ageSeed, originX + localX, originZ + localZ);
|
||||||
|
for (int y = minY; y < maxY; y++) {
|
||||||
|
pos.set(originX + localX, y, originZ + localZ);
|
||||||
|
BlockState state = chunk.getBlockState(pos);
|
||||||
|
if (!removes(state, y, cutoff)) continue;
|
||||||
|
chunk.setBlockState(pos, AIR);
|
||||||
|
surface.update(localX, y, localZ, AIR);
|
||||||
|
floor.update(localX, y, localZ, AIR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean removes(BlockState state, int y, int cutoff) {
|
||||||
|
return !state.isAir() && (y <= cutoff || state.liquid());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cutoff(long ageSeed, int x, int z) {
|
||||||
|
double variation = 0.0D;
|
||||||
|
int cellSize = 128;
|
||||||
|
double amplitude = 12.0D;
|
||||||
|
for (int octave = 0; octave < 7; octave++) {
|
||||||
|
variation += valueNoise(ageSeed ^ SALT ^ (long) octave * 0x9E3779B97F4A7C15L,
|
||||||
|
x, z, cellSize) * amplitude;
|
||||||
|
cellSize /= 2;
|
||||||
|
amplitude /= 2.0D;
|
||||||
|
}
|
||||||
|
return BASE_CUTOFF + (int) Math.round(variation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double valueNoise(long seed, int x, int z, int cellSize) {
|
||||||
|
int cellX = Math.floorDiv(x, cellSize);
|
||||||
|
int cellZ = Math.floorDiv(z, cellSize);
|
||||||
|
double u = smooth((x - cellX * cellSize) / (double) cellSize);
|
||||||
|
double v = smooth((z - cellZ * cellSize) / (double) cellSize);
|
||||||
|
double north = lerp(value(seed, cellX, cellZ), value(seed, cellX + 1, cellZ), u);
|
||||||
|
double south = lerp(value(seed, cellX, cellZ + 1), value(seed, cellX + 1, cellZ + 1), u);
|
||||||
|
return lerp(north, south, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double value(long seed, int x, int z) {
|
||||||
|
long hash = mix(seed ^ (long) x * 0xD1B54A32D192ED03L ^ (long) z * 0xABC98388FB8FAC03L);
|
||||||
|
return (hash >>> 11) * 0x1.0p-52 - 1.0D;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double smooth(double value) { return value * value * (3.0D - 2.0D * value); }
|
||||||
|
|
||||||
|
private static double lerp(double first, double second, double amount) {
|
||||||
|
return first + (second - first) * amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long value) {
|
||||||
|
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||||
|
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||||
|
return value ^ value >>> 31;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 26.3 adaptation of Rivencraft 1.21.1's solid sphere nodes, derived from
|
||||||
|
* Mystcraft Legacy's MapGenSpheresMyst (Mystcraft contributors,
|
||||||
|
* LGPL-3.0-or-later, commit 9bc8ddc). Writes stay within the target chunk.
|
||||||
|
*/
|
||||||
|
final class PortSpheresFeature {
|
||||||
|
private static final int SOURCE_RANGE = 8;
|
||||||
|
private static final long SALT = 0x53504845524553L;
|
||||||
|
|
||||||
|
private PortSpheresFeature() {}
|
||||||
|
|
||||||
|
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||||
|
var target = chunk.getPos();
|
||||||
|
Random master = new Random(ageSeed ^ SALT);
|
||||||
|
long xSeed = master.nextLong();
|
||||||
|
long zSeed = master.nextLong();
|
||||||
|
for (int sx = target.x() - SOURCE_RANGE; sx <= target.x() + SOURCE_RANGE; sx++) {
|
||||||
|
for (int sz = target.z() - SOURCE_RANGE; sz <= target.z() + SOURCE_RANGE; sz++) {
|
||||||
|
Random source = new Random((long) sx * xSeed ^ (long) sz * zSeed ^ ageSeed ^ SALT);
|
||||||
|
if (source.nextFloat() > 0.05F) continue;
|
||||||
|
double x = sx * 16 + source.nextInt(16);
|
||||||
|
double y = source.nextInt(source.nextInt(192) + 1) + 32;
|
||||||
|
double z = sz * 16 + source.nextInt(16);
|
||||||
|
trace(chunk, material, source.nextLong(), x, y, z,
|
||||||
|
1.0F + source.nextFloat() * 4.0F, 0, 0, -1, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void trace(ChunkAccess chunk, BlockState material, long seed,
|
||||||
|
double x, double y, double z, float radius, float yaw, float pitch,
|
||||||
|
int step, int length) {
|
||||||
|
Random random = new Random(seed);
|
||||||
|
if (length <= 0) {
|
||||||
|
int maximum = SOURCE_RANGE * 16 - 16;
|
||||||
|
length = maximum - random.nextInt(maximum / 4);
|
||||||
|
}
|
||||||
|
boolean singleNode = step == -1;
|
||||||
|
if (singleNode) step = length / 2;
|
||||||
|
int splitAt = random.nextInt(length / 2) + length / 4;
|
||||||
|
boolean slowPitch = random.nextInt(6) == 0;
|
||||||
|
float yawDrift = 0;
|
||||||
|
float pitchDrift = 0;
|
||||||
|
double middleX = chunk.getPos().getMinBlockX() + 8;
|
||||||
|
double middleZ = chunk.getPos().getMinBlockZ() + 8;
|
||||||
|
for (; step < length; step++) {
|
||||||
|
double nodeRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
|
||||||
|
float horizontal = Mth.cos(pitch);
|
||||||
|
x += Mth.cos(yaw) * horizontal;
|
||||||
|
y += Mth.sin(pitch);
|
||||||
|
z += Mth.sin(yaw) * horizontal;
|
||||||
|
pitch *= slowPitch ? 0.92F : 0.7F;
|
||||||
|
pitch += pitchDrift * 0.1F;
|
||||||
|
yaw += yawDrift * 0.1F;
|
||||||
|
pitchDrift = pitchDrift * 0.9F
|
||||||
|
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
|
||||||
|
yawDrift = yawDrift * 0.75F
|
||||||
|
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
|
||||||
|
if (!singleNode && step == splitAt && radius > 1.0F) {
|
||||||
|
trace(chunk, material, random.nextLong(), x, y, z,
|
||||||
|
random.nextFloat() * 0.5F + 0.5F, yaw - Mth.HALF_PI, pitch / 3, step, length);
|
||||||
|
trace(chunk, material, random.nextLong(), x, y, z,
|
||||||
|
random.nextFloat() * 0.5F + 0.5F, yaw + Mth.HALF_PI, pitch / 3, step, length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!singleNode && random.nextInt(4) == 0) continue;
|
||||||
|
double dx = x - middleX;
|
||||||
|
double dz = z - middleZ;
|
||||||
|
double remaining = length - step;
|
||||||
|
double reach = radius + 18;
|
||||||
|
if (dx * dx + dz * dz - remaining * remaining > reach * reach) return;
|
||||||
|
if (Math.abs(dx) <= 16 + nodeRadius * 2 && Math.abs(dz) <= 16 + nodeRadius * 2)
|
||||||
|
fillNode(chunk, material, x, y, z, nodeRadius);
|
||||||
|
if (singleNode) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fillNode(ChunkAccess chunk, BlockState material,
|
||||||
|
double centerX, double centerY, double centerZ, double radius) {
|
||||||
|
int originX = chunk.getPos().getMinBlockX();
|
||||||
|
int originZ = chunk.getPos().getMinBlockZ();
|
||||||
|
int minX = Math.max(0, Mth.floor(centerX - radius) - originX - 1);
|
||||||
|
int maxX = Math.min(16, Mth.floor(centerX + radius) - originX + 2);
|
||||||
|
int minZ = Math.max(0, Mth.floor(centerZ - radius) - originZ - 1);
|
||||||
|
int maxZ = Math.min(16, Mth.floor(centerZ + radius) - originZ + 2);
|
||||||
|
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - radius) - 1);
|
||||||
|
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + radius) + 2);
|
||||||
|
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||||
|
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
for (int worldY = minY; worldY < maxY; worldY++) {
|
||||||
|
double dy = (worldY + 0.5D - centerY) / radius;
|
||||||
|
for (int localZ = minZ; localZ < maxZ; localZ++) {
|
||||||
|
double dz = (originZ + localZ + 0.5D - centerZ) / radius;
|
||||||
|
for (int localX = minX; localX < maxX; localX++) {
|
||||||
|
double dx = (originX + localX + 0.5D - centerX) / radius;
|
||||||
|
if (!insideSphere(dx, dy, dz)) continue;
|
||||||
|
pos.set(originX + localX, worldY, originZ + localZ);
|
||||||
|
BlockState previous = chunk.getBlockState(pos);
|
||||||
|
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
|
||||||
|
chunk.setBlockState(pos, material);
|
||||||
|
surface.update(localX, worldY, localZ, material);
|
||||||
|
floor.update(localX, worldY, localZ, material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean insideSphere(double dx, double dy, double dz) {
|
||||||
|
return dx * dx + dy * dy + dz * dz < 1.0D;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 26.3 port of Rivencraft 1.21.1's rare surface spikes, adapted from
|
||||||
|
* Mystcraft Legacy's WorldGenSpikesAdv (Mystcraft contributors,
|
||||||
|
* LGPL-3.0-or-later, commit 9bc8ddc). The footprint stays within one chunk.
|
||||||
|
*/
|
||||||
|
final class PortSpikesFeature {
|
||||||
|
private static final long SALT = 0x5350494B4553L;
|
||||||
|
|
||||||
|
private PortSpikesFeature() {}
|
||||||
|
|
||||||
|
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
|
||||||
|
int originX = chunk.getPos().getMinBlockX();
|
||||||
|
int originZ = chunk.getPos().getMinBlockZ();
|
||||||
|
RandomSource random = RandomSource.create(ageSeed ^ SALT
|
||||||
|
^ (long) originX * 341873128712L ^ (long) originZ * 132897987541L
|
||||||
|
^ (long) occurrence * 0x9E3779B97F4A7C15L);
|
||||||
|
if (random.nextInt(18) != 0) return;
|
||||||
|
int x = originX + 4 + random.nextInt(8);
|
||||||
|
int z = originZ + 4 + random.nextInt(8);
|
||||||
|
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||||
|
int height = 6 + random.nextInt(32);
|
||||||
|
int width = 1 + random.nextInt(4);
|
||||||
|
if (y < level.getMinY() + 1 || y + height >= level.getMaxY()) return;
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
if (!level.isEmptyBlock(pos.set(x, y, z))) return;
|
||||||
|
for (int px = x - width; px <= x + width; px++) {
|
||||||
|
for (int pz = z - width; pz <= z + width; pz++) {
|
||||||
|
if (!insideFootprint(px - x, pz - z, width)) continue;
|
||||||
|
if (!level.getBlockState(pos.set(px, y - 1, pz)).isSolid()) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int px = x - width; px <= x + width; px++) {
|
||||||
|
for (int pz = z - width; pz <= z + width; pz++) {
|
||||||
|
if (!insideFootprint(px - x, pz - z, width)) continue;
|
||||||
|
int top = y + 1 + random.nextInt(random.nextInt(height) + 1);
|
||||||
|
for (int py = y; py < top; py++) {
|
||||||
|
pos.set(px, py, pz);
|
||||||
|
if (level.isEmptyBlock(pos)) level.setBlock(pos, material, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean insideFootprint(int dx, int dz, int width) {
|
||||||
|
return dx * dx + dz * dz <= width * width + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.saveddata.SavedData;
|
||||||
|
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ports Rivencraft 1.21.1's Legacy fissure footprint/carving.
|
||||||
|
* Derived from Mystcraft's WorldGenMystStarFissure, LGPL-3.0,
|
||||||
|
* copyright XCompWiz and Mystcraft contributors.
|
||||||
|
*/
|
||||||
|
public final class PortStarFissureFeature {
|
||||||
|
private static final long SALT = 0x5354415246495353L;
|
||||||
|
|
||||||
|
private PortStarFissureFeature() {}
|
||||||
|
|
||||||
|
/** Per-Age data prevents revisits/restarts from carving the same fissure again. */
|
||||||
|
private static final class Arrival extends SavedData {
|
||||||
|
private static final Codec<Arrival> CODEC = BlockPos.CODEC.optionalFieldOf("spawn").codec()
|
||||||
|
.xmap(Arrival::new, data -> Optional.ofNullable(data.spawn));
|
||||||
|
private static final SavedDataType<Arrival> TYPE = new SavedDataType<>(
|
||||||
|
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "star_fissure_arrival"),
|
||||||
|
() -> new Arrival(Optional.empty()), CODEC);
|
||||||
|
private BlockPos spawn;
|
||||||
|
|
||||||
|
private Arrival(Optional<BlockPos> spawn) { this.spawn = spawn.orElse(null); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BlockPos savedSpawn(ServerLevel level) {
|
||||||
|
return level.getDataStorage().computeIfAbsent(Arrival.TYPE).spawn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void generateOnce(ServerLevel level, BlockPos spawn, boolean caveTerrain) {
|
||||||
|
Arrival data = level.getDataStorage().computeIfAbsent(Arrival.TYPE);
|
||||||
|
if (data.spawn != null) return;
|
||||||
|
generate(level, level.getSeed(), spawn, caveTerrain);
|
||||||
|
data.spawn = spawn.immutable();
|
||||||
|
data.setDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Legacy chooses 8–23 blocks after the spawn chunk, then grows along +Z. */
|
||||||
|
public static List<BlockPos> footprint(long ageSeed, BlockPos spawn) {
|
||||||
|
long chunkX = spawn.getX() >> 4;
|
||||||
|
long chunkZ = spawn.getZ() >> 4;
|
||||||
|
Random random = new Random(ageSeed ^ SALT ^ chunkX ^ (chunkZ << 32));
|
||||||
|
int originX = ((int) chunkX << 4) + random.nextInt(16) + 8;
|
||||||
|
int originZ = ((int) chunkZ << 4) + random.nextInt(16) + 8;
|
||||||
|
int length = random.nextInt(8) + 10;
|
||||||
|
int[][] intervals = new int[length][2];
|
||||||
|
List<BlockPos> columns = new ArrayList<>();
|
||||||
|
addRow(columns, originX, originZ, 0, 0, 0);
|
||||||
|
for (int row = 1; row < length; row++) {
|
||||||
|
int range = length + Math.min(row, length - row) + 1;
|
||||||
|
int right = (random.nextInt(range) - (range >> 1) + intervals[row - 1][1]) / 2;
|
||||||
|
int left = (random.nextInt(range) - (range >> 1) + intervals[row - 1][0]) / 2;
|
||||||
|
if (left > right) {
|
||||||
|
int swap = left;
|
||||||
|
left = right;
|
||||||
|
right = swap;
|
||||||
|
}
|
||||||
|
left = Math.min(left, intervals[row - 1][1]);
|
||||||
|
right = Math.max(right, intervals[row - 1][0]);
|
||||||
|
intervals[row][0] = left;
|
||||||
|
intervals[row][1] = right;
|
||||||
|
addRow(columns, originX, originZ, row, left, right);
|
||||||
|
}
|
||||||
|
return List.copyOf(columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addRow(List<BlockPos> columns, int originX, int originZ, int row, int left, int right) {
|
||||||
|
for (int x = left; x <= right; x++) {
|
||||||
|
// Only X/Z are used; the real floor is the dimension's minimum Y.
|
||||||
|
columns.add(new BlockPos(originX + x, 0, originZ + row * 2));
|
||||||
|
columns.add(new BlockPos(originX + x, 0, originZ + row * 2 + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void generate(ServerLevel level, long ageSeed, BlockPos spawn, boolean caveTerrain) {
|
||||||
|
int floorY = level.getMinY();
|
||||||
|
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
|
||||||
|
for (BlockPos column : footprint(ageSeed, spawn)) {
|
||||||
|
int top = level.getHeight(Heightmap.Types.WORLD_SURFACE, column.getX(), column.getZ());
|
||||||
|
if (caveTerrain) {
|
||||||
|
for (int y = Math.max(spawn.getY(), floorY + 1); y < top; y++) {
|
||||||
|
if (level.getBlockState(mutable.set(column.getX(), y, column.getZ())).is(Blocks.BEDROCK)) {
|
||||||
|
top = y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int y = floorY + 1; y < top; y++)
|
||||||
|
level.setBlock(mutable.set(column.getX(), y, column.getZ()), Blocks.AIR.defaultBlockState(), 2);
|
||||||
|
level.setBlock(mutable.set(column.getX(), floorY, column.getZ()),
|
||||||
|
Rivencraft.STAR_FISSURE.get().defaultBlockState(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 26.3 adaptation of Rivencraft 1.21.1's winding solid volumes, originally
|
||||||
|
* adapted from Mystcraft Legacy's SymbolTendrils and MapGenCavesMyst (LGPL-3.0-or-later).
|
||||||
|
* Each chunk reconstructs the neighboring source paths to preserve continuous borders.
|
||||||
|
*/
|
||||||
|
final class PortTendrilFeature {
|
||||||
|
private static final long SALT = 0x54454E4452494C53L;
|
||||||
|
private static final int SOURCE_RANGE = 8;
|
||||||
|
private static final int SOURCE_RATE = 15;
|
||||||
|
private static final int SOURCE_SIZE = 18;
|
||||||
|
|
||||||
|
private PortTendrilFeature() {}
|
||||||
|
|
||||||
|
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||||
|
var target = chunk.getPos();
|
||||||
|
Random master = new Random(ageSeed ^ SALT);
|
||||||
|
long xSeed = master.nextLong();
|
||||||
|
long zSeed = master.nextLong();
|
||||||
|
for (int sourceX = target.x() - SOURCE_RANGE; sourceX <= target.x() + SOURCE_RANGE; sourceX++) {
|
||||||
|
for (int sourceZ = target.z() - SOURCE_RANGE; sourceZ <= target.z() + SOURCE_RANGE; sourceZ++) {
|
||||||
|
Random source = new Random((long) sourceX * xSeed ^ (long) sourceZ * zSeed ^ ageSeed ^ SALT);
|
||||||
|
int nodes = source.nextInt(source.nextInt(source.nextInt(SOURCE_SIZE) + 1) + 1);
|
||||||
|
if (source.nextInt(SOURCE_RATE) != 0) continue;
|
||||||
|
for (int node = 0; node < nodes; node++) {
|
||||||
|
double x = sourceX * 16 + source.nextInt(16);
|
||||||
|
double y = source.nextInt(source.nextInt(120) + 8);
|
||||||
|
double z = sourceZ * 16 + source.nextInt(16);
|
||||||
|
int branches = 1;
|
||||||
|
if (source.nextInt(4) == 0) {
|
||||||
|
trace(chunk, material, source.nextLong(), x, y, z,
|
||||||
|
1.0F + source.nextFloat() * 6.0F, 0.0F, 0.0F, -1, -1, 0.5D);
|
||||||
|
branches += source.nextInt(4);
|
||||||
|
}
|
||||||
|
for (int branch = 0; branch < branches; branch++) {
|
||||||
|
float yaw = source.nextFloat() * Mth.TWO_PI;
|
||||||
|
float pitch = (source.nextFloat() - 0.5F) / 4.0F;
|
||||||
|
float radius = source.nextFloat() * 2.0F + source.nextFloat();
|
||||||
|
if (source.nextInt(10) == 0)
|
||||||
|
radius *= source.nextFloat() * source.nextFloat() * 3.0F + 1.0F;
|
||||||
|
trace(chunk, material, source.nextLong(), x, y, z,
|
||||||
|
radius, yaw, pitch, 0, 0, 1.0D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void trace(ChunkAccess chunk, BlockState material, long seed,
|
||||||
|
double x, double y, double z, float radius, float yaw, float pitch,
|
||||||
|
int step, int length, double verticalScale) {
|
||||||
|
Random random = new Random(seed);
|
||||||
|
if (length <= 0) {
|
||||||
|
int maximum = SOURCE_RANGE * 16 - 16;
|
||||||
|
length = maximum - random.nextInt(maximum / 4);
|
||||||
|
}
|
||||||
|
boolean singleNode = step == -1;
|
||||||
|
if (singleNode) step = length / 2;
|
||||||
|
int splitAt = random.nextInt(length / 2) + length / 4;
|
||||||
|
boolean slowPitch = random.nextInt(6) == 0;
|
||||||
|
float yawDrift = 0.0F;
|
||||||
|
float pitchDrift = 0.0F;
|
||||||
|
double middleX = chunk.getPos().getMinBlockX() + 8;
|
||||||
|
double middleZ = chunk.getPos().getMinBlockZ() + 8;
|
||||||
|
for (; step < length; step++) {
|
||||||
|
double horizontalRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
|
||||||
|
double verticalRadius = horizontalRadius * verticalScale;
|
||||||
|
float horizontalMotion = Mth.cos(pitch);
|
||||||
|
x += Mth.cos(yaw) * horizontalMotion;
|
||||||
|
y += Mth.sin(pitch);
|
||||||
|
z += Mth.sin(yaw) * horizontalMotion;
|
||||||
|
pitch *= slowPitch ? 0.92F : 0.7F;
|
||||||
|
pitch += pitchDrift * 0.1F;
|
||||||
|
yaw += yawDrift * 0.1F;
|
||||||
|
pitchDrift = pitchDrift * 0.9F
|
||||||
|
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
|
||||||
|
yawDrift = yawDrift * 0.75F
|
||||||
|
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
|
||||||
|
if (!singleNode && step == splitAt && radius > 1.0F) {
|
||||||
|
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
|
||||||
|
yaw - Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
|
||||||
|
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
|
||||||
|
yaw + Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!singleNode && random.nextInt(4) == 0) continue;
|
||||||
|
double distanceX = x - middleX;
|
||||||
|
double distanceZ = z - middleZ;
|
||||||
|
double remaining = length - step;
|
||||||
|
double reach = radius + 18.0D;
|
||||||
|
if (distanceX * distanceX + distanceZ * distanceZ - remaining * remaining > reach * reach) return;
|
||||||
|
if (x >= middleX - 16.0D - horizontalRadius * 2.0D
|
||||||
|
&& x <= middleX + 16.0D + horizontalRadius * 2.0D
|
||||||
|
&& z >= middleZ - 16.0D - horizontalRadius * 2.0D
|
||||||
|
&& z <= middleZ + 16.0D + horizontalRadius * 2.0D) {
|
||||||
|
fillNode(chunk, material, x, y, z, horizontalRadius, verticalRadius);
|
||||||
|
}
|
||||||
|
if (singleNode) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fillNode(ChunkAccess chunk, BlockState material, double centerX, double centerY,
|
||||||
|
double centerZ, double horizontalRadius, double verticalRadius) {
|
||||||
|
int originX = chunk.getPos().getMinBlockX();
|
||||||
|
int originZ = chunk.getPos().getMinBlockZ();
|
||||||
|
int minX = Math.max(0, Mth.floor(centerX - horizontalRadius) - originX - 1);
|
||||||
|
int maxX = Math.min(16, Mth.floor(centerX + horizontalRadius) - originX + 2);
|
||||||
|
int minZ = Math.max(0, Mth.floor(centerZ - horizontalRadius) - originZ - 1);
|
||||||
|
int maxZ = Math.min(16, Mth.floor(centerZ + horizontalRadius) - originZ + 2);
|
||||||
|
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - verticalRadius) - 1);
|
||||||
|
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + verticalRadius) + 2);
|
||||||
|
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||||
|
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
for (int worldY = minY; worldY < maxY; worldY++) {
|
||||||
|
double dy = (worldY + 0.5D - centerY) / verticalRadius;
|
||||||
|
if (dy <= -0.7D || dy * dy >= 1.0D) continue;
|
||||||
|
for (int localZ = minZ; localZ < maxZ; localZ++) {
|
||||||
|
double dz = (originZ + localZ + 0.5D - centerZ) / horizontalRadius;
|
||||||
|
for (int localX = minX; localX < maxX; localX++) {
|
||||||
|
double dx = (originX + localX + 0.5D - centerX) / horizontalRadius;
|
||||||
|
if (dx * dx + dy * dy + dz * dz >= 1.0D) continue;
|
||||||
|
pos.set(originX + localX, worldY, originZ + localZ);
|
||||||
|
BlockState previous = chunk.getBlockState(pos);
|
||||||
|
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
|
||||||
|
chunk.setBlockState(pos, material);
|
||||||
|
surface.update(localX, worldY, localZ, material);
|
||||||
|
floor.update(localX, worldY, localZ, material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
|
/** Frozen terrain filters and ordered solid/surface features for an Age. */
|
||||||
|
public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres, boolean starFissures) {
|
||||||
|
public static final Identifier FLOATING_ISLANDS = Identifier.fromNamespaceAndPath(
|
||||||
|
Rivencraft.MOD_ID, "feature/floating_islands");
|
||||||
|
public static final Identifier TENDRILS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/tendrils");
|
||||||
|
public static final Identifier SKYLANDS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/skylands");
|
||||||
|
public static final Identifier SPHERES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spheres");
|
||||||
|
public static final Identifier SPIKES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spikes");
|
||||||
|
public static final Identifier OBELISKS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/obelisks");
|
||||||
|
public static final Identifier CRYSTAL_FORMATIONS = Identifier.fromNamespaceAndPath(
|
||||||
|
Rivencraft.MOD_ID, "feature/crystal_formations");
|
||||||
|
public static final Identifier CRYSTAL_MATERIAL = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "crystal_block");
|
||||||
|
public static final Identifier DENSE_ORES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dense_ores");
|
||||||
|
public static final Identifier STAR_FISSURES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/star_fissures");
|
||||||
|
public static final int MAX_PER_FEATURE = 32;
|
||||||
|
public static final Codec<PortTerrainFeaturePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
|
Choice.CODEC.listOf().fieldOf("choices").forGetter(PortTerrainFeaturePlan::choices),
|
||||||
|
Codec.BOOL.optionalFieldOf("skylands", false).forGetter(PortTerrainFeaturePlan::skylands),
|
||||||
|
Codec.intRange(0, MAX_PER_FEATURE).optionalFieldOf("dense_ores", 0)
|
||||||
|
.forGetter(PortTerrainFeaturePlan::denseOres),
|
||||||
|
Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures)
|
||||||
|
).apply(instance, PortTerrainFeaturePlan::new));
|
||||||
|
|
||||||
|
public record Choice(Identifier page, Identifier material) {
|
||||||
|
public static final Codec<Choice> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
|
Identifier.CODEC.fieldOf("page").forGetter(Choice::page),
|
||||||
|
Identifier.CODEC.fieldOf("material").forGetter(Choice::material)
|
||||||
|
).apply(instance, Choice::new));
|
||||||
|
|
||||||
|
public Choice {
|
||||||
|
if (!isSupported(page) || material == null)
|
||||||
|
throw new IllegalArgumentException("Invalid terrain feature choice");
|
||||||
|
if (CRYSTAL_FORMATIONS.equals(page) && !CRYSTAL_MATERIAL.equals(material))
|
||||||
|
throw new IllegalArgumentException("Crystal formations use Rivencraft crystal, not a Block modifier");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortTerrainFeaturePlan {
|
||||||
|
choices = List.copyOf(choices);
|
||||||
|
if (denseOres < 0 || denseOres > MAX_PER_FEATURE)
|
||||||
|
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences");
|
||||||
|
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures)
|
||||||
|
throw new IllegalArgumentException("Terrain feature plan must not be empty");
|
||||||
|
var counts = new HashMap<Identifier, Integer>();
|
||||||
|
for (Choice choice : choices) {
|
||||||
|
if (counts.merge(choice.page(), 1, Integer::sum) > MAX_PER_FEATURE)
|
||||||
|
throw new IllegalArgumentException("Terrain feature plan permits at most 32 occurrences per feature");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres) {
|
||||||
|
this(choices, skylands, denseOres, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands) { this(choices, skylands, 0, false); }
|
||||||
|
|
||||||
|
public PortTerrainFeaturePlan(List<Choice> choices) { this(choices, false, 0, false); }
|
||||||
|
|
||||||
|
public static boolean isSupported(Identifier page) {
|
||||||
|
return FLOATING_ISLANDS.equals(page) || TENDRILS.equals(page) || SPHERES.equals(page) || isSurface(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean acceptsMaterial(Identifier page) {
|
||||||
|
return isSupported(page) && !CRYSTAL_FORMATIONS.equals(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSurface(Identifier page) {
|
||||||
|
return SPIKES.equals(page) || OBELISKS.equals(page) || CRYSTAL_FORMATIONS.equals(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
import java.util.List;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
|
/** Ordered, frozen authored vegetation pages for surface Ages. */
|
||||||
|
public record PortVegetationPlan(List<Identifier> pages) {
|
||||||
|
public static final Identifier FLOWERS = page("flowers");
|
||||||
|
public static final Identifier TREES = page("trees");
|
||||||
|
public static final Identifier HUGE_TREES = page("huge_trees");
|
||||||
|
public static final Codec<PortVegetationPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
|
Identifier.CODEC.listOf().fieldOf("pages").forGetter(PortVegetationPlan::pages)
|
||||||
|
).apply(instance, PortVegetationPlan::new));
|
||||||
|
|
||||||
|
public PortVegetationPlan {
|
||||||
|
pages = List.copyOf(pages);
|
||||||
|
if (pages.isEmpty() || pages.size() > 32 || pages.stream().anyMatch(page -> !isVegetation(page)))
|
||||||
|
throw new IllegalArgumentException("Vegetation plan requires 1–32 supported pages");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isVegetation(Identifier page) {
|
||||||
|
return page.equals(FLOWERS) || page.equals(TREES) || page.equals(HUGE_TREES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Identifier placedFeature(Identifier page) {
|
||||||
|
if (page.equals(FLOWERS)) return Identifier.withDefaultNamespace("flower_default");
|
||||||
|
if (page.equals(TREES)) return Identifier.withDefaultNamespace("trees_plains");
|
||||||
|
if (page.equals(HUGE_TREES)) return Identifier.withDefaultNamespace("trees_old_growth_spruce_taiga");
|
||||||
|
throw new IllegalArgumentException("Unsupported vegetation page " + page);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Identifier page(String name) {
|
||||||
|
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,7 +57,8 @@ public final class RuntimeAgeBackend {
|
|||||||
for (Map.Entry<Identifier, Long> age : index.entries().entrySet()) {
|
for (Map.Entry<Identifier, Long> age : index.entries().entrySet()) {
|
||||||
try {
|
try {
|
||||||
load(server, age.getKey(), age.getValue(), index.biomePlan(age.getKey()),
|
load(server, age.getKey(), age.getValue(), index.biomePlan(age.getKey()),
|
||||||
index.terrain(age.getKey()), index.rocks(age.getKey()));
|
index.terrain(age.getKey()), index.rocks(age.getKey()), index.vegetation(age.getKey()),
|
||||||
|
index.dungeons(age.getKey()), index.lakes(age.getKey()), index.terrainFeatures(age.getKey()));
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
Rivencraft.LOGGER.error("Could not restore Age {}. Its saved index remains intact.", age.getKey(), exception);
|
Rivencraft.LOGGER.error("Could not restore Age {}. Its saved index remains intact.", age.getKey(), exception);
|
||||||
}
|
}
|
||||||
@@ -80,12 +81,37 @@ public final class RuntimeAgeBackend {
|
|||||||
|
|
||||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
|
||||||
|
return create(server, id, seed, biomePlan, terrain, rocks, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||||
|
PortVegetationPlan vegetation) {
|
||||||
|
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||||
|
PortVegetationPlan vegetation, int dungeons) {
|
||||||
|
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||||
|
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes) {
|
||||||
|
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||||
|
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||||
|
PortTerrainFeaturePlan terrainFeatures) {
|
||||||
checkServerThread(server);
|
checkServerThread(server);
|
||||||
ageName(id);
|
ageName(id);
|
||||||
AgeLevelIndex index = index(server);
|
AgeLevelIndex index = index(server);
|
||||||
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
|
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
|
||||||
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks);
|
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||||
index.add(id, seed, biomePlan, terrain, rocks);
|
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||||
// A newly bound book may be used immediately. Persist its Age identity
|
// A newly bound book may be used immediately. Persist its Age identity
|
||||||
// before publishing the catalog so a later crash cannot leave that
|
// before publishing the catalog so a later crash cannot leave that
|
||||||
// book pointing at a world omitted from the on-disk index.
|
// book pointing at a world omitted from the on-disk index.
|
||||||
@@ -106,7 +132,8 @@ public final class RuntimeAgeBackend {
|
|||||||
checkServerThread(server);
|
checkServerThread(server);
|
||||||
long seed = requireSeed(server, id);
|
long seed = requireSeed(server, id);
|
||||||
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id),
|
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id),
|
||||||
index(server).rocks(id));
|
index(server).rocks(id), index(server).vegetation(id), index(server).dungeons(id), index(server).lakes(id),
|
||||||
|
index(server).terrainFeatures(id));
|
||||||
AgeCatalogSync.onCatalogChanged(server);
|
AgeCatalogSync.onCatalogChanged(server);
|
||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
@@ -119,18 +146,22 @@ public final class RuntimeAgeBackend {
|
|||||||
PortBiomePlan biomePlan = index.biomePlan(id);
|
PortBiomePlan biomePlan = index.biomePlan(id);
|
||||||
PortAgeTerrain terrain = index.terrain(id);
|
PortAgeTerrain terrain = index.terrain(id);
|
||||||
PortRockPlan rocks = index.rocks(id);
|
PortRockPlan rocks = index.rocks(id);
|
||||||
|
PortVegetationPlan vegetation = index.vegetation(id);
|
||||||
|
int dungeons = index.dungeons(id);
|
||||||
|
PortLakePlan lakes = index.lakes(id);
|
||||||
|
PortTerrainFeaturePlan terrainFeatures = index.terrainFeatures(id);
|
||||||
if (previousSeed == seed) return;
|
if (previousSeed == seed) return;
|
||||||
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
||||||
if (loaded) unloadLoaded(server, id);
|
if (loaded) unloadLoaded(server, id);
|
||||||
try {
|
try {
|
||||||
if (loaded) load(server, id, seed, biomePlan, terrain, rocks);
|
if (loaded) load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||||
index.reseed(id, seed);
|
index.reseed(id, seed);
|
||||||
server.overworld().getDataStorage().saveAndJoin();
|
server.overworld().getDataStorage().saveAndJoin();
|
||||||
if (loaded) AgeCatalogSync.onCatalogChanged(server);
|
if (loaded) AgeCatalogSync.onCatalogChanged(server);
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
try {
|
try {
|
||||||
load(server, id, previousSeed, biomePlan, terrain, rocks);
|
load(server, id, previousSeed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||||
AgeCatalogSync.onCatalogChanged(server);
|
AgeCatalogSync.onCatalogChanged(server);
|
||||||
} catch (RuntimeException rollbackFailure) {
|
} catch (RuntimeException rollbackFailure) {
|
||||||
exception.addSuppressed(rollbackFailure);
|
exception.addSuppressed(rollbackFailure);
|
||||||
@@ -149,6 +180,10 @@ public final class RuntimeAgeBackend {
|
|||||||
PortBiomePlan biomePlan = index.biomePlan(id);
|
PortBiomePlan biomePlan = index.biomePlan(id);
|
||||||
PortAgeTerrain terrain = index.terrain(id);
|
PortAgeTerrain terrain = index.terrain(id);
|
||||||
PortRockPlan rocks = index.rocks(id);
|
PortRockPlan rocks = index.rocks(id);
|
||||||
|
PortVegetationPlan vegetation = index.vegetation(id);
|
||||||
|
int dungeons = index.dungeons(id);
|
||||||
|
PortLakePlan lakes = index.lakes(id);
|
||||||
|
PortTerrainFeaturePlan terrainFeatures = index.terrainFeatures(id);
|
||||||
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
||||||
ArchivePaths paths = archivePaths(server, name);
|
ArchivePaths paths = archivePaths(server, name);
|
||||||
if (loaded) unloadLoaded(server, id);
|
if (loaded) unloadLoaded(server, id);
|
||||||
@@ -167,14 +202,15 @@ public final class RuntimeAgeBackend {
|
|||||||
}
|
}
|
||||||
} catch (IOException | RuntimeException exception) {
|
} catch (IOException | RuntimeException exception) {
|
||||||
try {
|
try {
|
||||||
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks);
|
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||||
|
terrainFeatures);
|
||||||
server.overworld().getDataStorage().saveAndJoin();
|
server.overworld().getDataStorage().saveAndJoin();
|
||||||
} catch (RuntimeException rollbackFailure) {
|
} catch (RuntimeException rollbackFailure) {
|
||||||
exception.addSuppressed(rollbackFailure);
|
exception.addSuppressed(rollbackFailure);
|
||||||
}
|
}
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
try {
|
try {
|
||||||
load(server, id, seed, biomePlan, terrain, rocks);
|
load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||||
} catch (RuntimeException rollbackFailure) {
|
} catch (RuntimeException rollbackFailure) {
|
||||||
exception.addSuppressed(rollbackFailure);
|
exception.addSuppressed(rollbackFailure);
|
||||||
}
|
}
|
||||||
@@ -290,7 +326,9 @@ public final class RuntimeAgeBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
||||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||||
|
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||||
|
PortTerrainFeaturePlan terrainFeatures) {
|
||||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
||||||
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
||||||
|
|
||||||
@@ -361,7 +399,8 @@ public final class RuntimeAgeBackend {
|
|||||||
};
|
};
|
||||||
generator = new NoiseBasedChunkGenerator(source, settings);
|
generator = new NoiseBasedChunkGenerator(source, settings);
|
||||||
}
|
}
|
||||||
if (rocks != null) generator = new PortRockChunkGenerator(generator, rocks, seed);
|
if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
|
||||||
|
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
|
||||||
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
|
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
|
||||||
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
|
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
|
||||||
ServerLevel level = new ServerLevel(server, server.executor, server.storageSource,
|
ServerLevel level = new ServerLevel(server, server.executor, server.storageSource,
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package net.dnorberg.rivencraft.block;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.sounds.SoundSource;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import net.minecraft.world.entity.InsideBlockEffectApplier;
|
||||||
|
import net.minecraft.world.level.BlockGetter;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.EndPortalBlock;
|
||||||
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||||
|
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.portal.TeleportTransition;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||||
|
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||||
|
|
||||||
|
/** Non-obtainable starfield portal returning entities to the shared Overworld spawn. */
|
||||||
|
public final class StarFissureBlock extends EndPortalBlock {
|
||||||
|
private static final VoxelShape SHAPE = Block.box(0, 0, 0, 16, 1.6, 16);
|
||||||
|
|
||||||
|
public StarFissureBlock(BlockBehaviour.Properties properties) { super(properties); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||||
|
return new StarFissureBlockEntity(pos, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void animateTick(BlockState state, Level level, BlockPos pos, net.minecraft.util.RandomSource random) {
|
||||||
|
// Legacy fissures have a starfield, not vanilla End portal smoke.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
|
||||||
|
return SHAPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity,
|
||||||
|
InsideBlockEffectApplier effects, boolean precise) {
|
||||||
|
// Do not inherit End credits or End-dimension travel from the vanilla block.
|
||||||
|
if (entity.canUsePortal(false)) entity.setAsInsidePortal(this, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos) {
|
||||||
|
if (level.dimension() == Level.OVERWORLD) return null;
|
||||||
|
ServerLevel overworld = level.getServer().overworld();
|
||||||
|
if (overworld == null) return null;
|
||||||
|
BlockPos spawn = overworld.getRespawnData().pos();
|
||||||
|
BlockPos landing = net.dnorberg.rivencraft.age.PortAgeTravel.findFootedDestination(overworld, spawn).orElse(spawn);
|
||||||
|
level.playSound(null, pos, Rivencraft.LINK_FISSURE.get(), SoundSource.BLOCKS, 1.0F, 1.0F);
|
||||||
|
return new TeleportTransition(overworld, Vec3.atBottomCenterOf(landing), entity.getDeltaMovement(),
|
||||||
|
entity.getYRot(), entity.getXRot(), TeleportTransition.PLACE_PORTAL_TICKET);
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package net.dnorberg.rivencraft.block.entity;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.block.entity.TheEndPortalBlockEntity;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
|
||||||
|
public final class StarFissureBlockEntity extends TheEndPortalBlockEntity {
|
||||||
|
public StarFissureBlockEntity(BlockPos pos, BlockState state) {
|
||||||
|
super(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), pos, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -14,6 +14,12 @@ import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
|
|||||||
public final class PortBookBinderClientEvents {
|
public final class PortBookBinderClientEvents {
|
||||||
private PortBookBinderClientEvents() {}
|
private PortBookBinderClientEvents() {}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void registerBlockEntityRenderers(
|
||||||
|
net.neoforged.neoforge.client.event.EntityRenderersEvent.RegisterRenderers event) {
|
||||||
|
event.registerBlockEntityRenderer(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), context -> new StarFissureRenderer());
|
||||||
|
}
|
||||||
|
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
public static void registerScreens(RegisterMenuScreensEvent event) {
|
public static void registerScreens(RegisterMenuScreensEvent event) {
|
||||||
event.register(Rivencraft.BOOK_BINDER_MENU.get(), PortBookBinderScreen::new);
|
event.register(Rivencraft.BOOK_BINDER_MENU.get(), PortBookBinderScreen::new);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package net.dnorberg.rivencraft.client;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
|
import java.util.List;
|
||||||
|
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||||
|
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||||
|
import net.minecraft.client.renderer.blockentity.AbstractEndPortalRenderer;
|
||||||
|
import net.minecraft.client.renderer.blockentity.state.EndPortalRenderState;
|
||||||
|
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||||
|
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
|
||||||
|
/** Retains 1.21.1's UP-at-zero/DOWN-at-one planes with the modern starfield renderer. */
|
||||||
|
public final class StarFissureRenderer extends AbstractEndPortalRenderer<StarFissureBlockEntity, EndPortalRenderState> {
|
||||||
|
@Override
|
||||||
|
public EndPortalRenderState createRenderState() { return new EndPortalRenderState(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void submit(EndPortalRenderState state, PoseStack pose, SubmitNodeCollector collector,
|
||||||
|
CameraRenderState camera) {
|
||||||
|
if (state.facesToShow.contains(Direction.UP)) {
|
||||||
|
pose.pushPose();
|
||||||
|
pose.translate(0, -1, 0);
|
||||||
|
submitCube(List.of(Direction.UP), RenderTypes.endPortal(), pose, collector);
|
||||||
|
pose.popPose();
|
||||||
|
}
|
||||||
|
if (state.facesToShow.contains(Direction.DOWN)) {
|
||||||
|
pose.pushPose();
|
||||||
|
pose.translate(0, 1, 0);
|
||||||
|
submitCube(List.of(Direction.DOWN), RenderTypes.endPortal(), pose, collector);
|
||||||
|
pose.popPose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -135,7 +135,11 @@ public final class PortBookNetworking {
|
|||||||
biomePlan = random.biomes();
|
biomePlan = random.biomes();
|
||||||
}
|
}
|
||||||
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
||||||
PortAuthoredWriting.rockPlan(pages));
|
PortAuthoredWriting.rockPlan(pages, seed),
|
||||||
|
PortAuthoredWriting.vegetationPlan(pages),
|
||||||
|
PortAuthoredWriting.dungeonCount(pages),
|
||||||
|
PortAuthoredWriting.lakePlan(pages, seed),
|
||||||
|
PortAuthoredWriting.terrainFeaturePlan(pages, seed));
|
||||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||||
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package net.dnorberg.rivencraft.registry;
|
|||||||
import com.mojang.serialization.MapCodec;
|
import com.mojang.serialization.MapCodec;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.dnorberg.rivencraft.age.PortFlatChunkGenerator;
|
import net.dnorberg.rivencraft.age.PortFlatChunkGenerator;
|
||||||
import net.dnorberg.rivencraft.age.PortRockChunkGenerator;
|
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
@@ -16,7 +16,8 @@ public final class PortChunkGenerators {
|
|||||||
|
|
||||||
static {
|
static {
|
||||||
GENERATORS.register("age_flat", () -> PortFlatChunkGenerator.CODEC);
|
GENERATORS.register("age_flat", () -> PortFlatChunkGenerator.CODEC);
|
||||||
GENERATORS.register("age_rocks", () -> PortRockChunkGenerator.CODEC);
|
// Keep the beta codec ID so previously saved Rocks Ages remain readable.
|
||||||
|
GENERATORS.register("age_rocks", () -> PortFeatureChunkGenerator.CODEC);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PortChunkGenerators() {}
|
private PortChunkGenerators() {}
|
||||||
|
|||||||
+164
-13
@@ -2,18 +2,30 @@ package net.dnorberg.rivencraft.writing;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import net.dnorberg.rivencraft.age.PortBiomePlan;
|
import net.dnorberg.rivencraft.age.PortBiomePlan;
|
||||||
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
||||||
import net.dnorberg.rivencraft.age.PortRockPlan;
|
import net.dnorberg.rivencraft.age.PortRockPlan;
|
||||||
|
import net.dnorberg.rivencraft.age.PortVegetationPlan;
|
||||||
|
import net.dnorberg.rivencraft.age.PortLakePlan;
|
||||||
|
import net.dnorberg.rivencraft.age.PortTerrainFeaturePlan;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.minecraft.core.HolderLookup;
|
import net.minecraft.core.HolderLookup;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
import net.minecraft.resources.ResourceKey;
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.tags.BiomeTags;
|
||||||
|
|
||||||
/** Authored noise terrain and biome distribution supported by the 26.3 backend. */
|
/** Authored noise terrain and biome distribution supported by the 26.3 backend. */
|
||||||
public final class PortAuthoredWriting {
|
public final class PortAuthoredWriting {
|
||||||
private static final int MAX_AUTHORED_BIOMES = 252;
|
private static final int MAX_AUTHORED_BIOMES = 252;
|
||||||
|
private static final int MAX_ROCKS = 32;
|
||||||
|
private static final int MAX_VEGETATION = 32;
|
||||||
|
private static final int MAX_DUNGEONS = 32;
|
||||||
|
private static final int MAX_LAKES = 32;
|
||||||
private static final Identifier STANDARD_TERRAIN = Identifier.fromNamespaceAndPath(
|
private static final Identifier STANDARD_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||||
Rivencraft.MOD_ID, "terrain/standard");
|
Rivencraft.MOD_ID, "terrain/standard");
|
||||||
private static final Identifier AMPLIFIED_TERRAIN = Identifier.fromNamespaceAndPath(
|
private static final Identifier AMPLIFIED_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||||
@@ -28,6 +40,8 @@ public final class PortAuthoredWriting {
|
|||||||
Rivencraft.MOD_ID, "terrain/cave");
|
Rivencraft.MOD_ID, "terrain/cave");
|
||||||
private static final Identifier ROCKS = Identifier.fromNamespaceAndPath(
|
private static final Identifier ROCKS = Identifier.fromNamespaceAndPath(
|
||||||
Rivencraft.MOD_ID, "feature/rocks");
|
Rivencraft.MOD_ID, "feature/rocks");
|
||||||
|
private static final Identifier DUNGEONS = Identifier.fromNamespaceAndPath(
|
||||||
|
Rivencraft.MOD_ID, "feature/dungeons");
|
||||||
|
|
||||||
private PortAuthoredWriting() {}
|
private PortAuthoredWriting() {}
|
||||||
|
|
||||||
@@ -36,19 +50,51 @@ public final class PortAuthoredWriting {
|
|||||||
boolean terrain = false;
|
boolean terrain = false;
|
||||||
int biomes = 0;
|
int biomes = 0;
|
||||||
int controllers = 0;
|
int controllers = 0;
|
||||||
boolean material = false;
|
String pendingMaterial = null;
|
||||||
boolean rocks = false;
|
int rocks = 0;
|
||||||
|
int vegetation = 0;
|
||||||
|
int dungeons = 0;
|
||||||
|
int lakes = 0;
|
||||||
|
int skylands = 0;
|
||||||
|
int denseOres = 0;
|
||||||
|
int starFissures = 0;
|
||||||
|
Map<Identifier, Integer> terrainFeatures = new HashMap<>();
|
||||||
for (Identifier page : pages) {
|
for (Identifier page : pages) {
|
||||||
|
if (pendingMaterial != null && !(pendingMaterial.equals("rivencraft:block")
|
||||||
|
&& (page.equals(ROCKS) || PortTerrainFeaturePlan.acceptsMaterial(page))
|
||||||
|
|| pendingMaterial.equals("rivencraft:fluid") && PortLakePlan.isLake(page))) return false;
|
||||||
if (isTerrain(page) && !terrain) {
|
if (isTerrain(page) && !terrain) {
|
||||||
terrain = true;
|
terrain = true;
|
||||||
} else if (controller(page) != null && controllers++ == 0) {
|
} else if (controller(page) != null && controllers++ == 0) {
|
||||||
// An explicit controller needs an authored pool.
|
// An explicit controller needs an authored pool.
|
||||||
} else if (isBlockPage(page) && !material && !rocks
|
} else if (isBlockPage(page) && pendingMaterial == null
|
||||||
&& PortSymbolPages.find(page) != null
|
&& PortSymbolPages.find(page) != null
|
||||||
&& PortSymbolPages.find(page).category().equals("rivencraft:block")) {
|
&& (PortSymbolPages.find(page).category().equals("rivencraft:block")
|
||||||
material = true;
|
|| PortSymbolPages.find(page).category().equals("rivencraft:fluid"))) {
|
||||||
} else if (page.equals(ROCKS) && material && !rocks) {
|
pendingMaterial = PortSymbolPages.find(page).category();
|
||||||
rocks = true;
|
} else if (page.equals(ROCKS) && rocks < MAX_ROCKS) {
|
||||||
|
pendingMaterial = null;
|
||||||
|
rocks++;
|
||||||
|
} else if (PortLakePlan.isLake(page) && lakes < MAX_LAKES) {
|
||||||
|
pendingMaterial = null;
|
||||||
|
lakes++;
|
||||||
|
} else if (PortTerrainFeaturePlan.isSupported(page)
|
||||||
|
&& terrainFeatures.getOrDefault(page, 0) < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
|
||||||
|
pendingMaterial = null;
|
||||||
|
terrainFeatures.merge(page, 1, Integer::sum);
|
||||||
|
} else if (page.equals(PortTerrainFeaturePlan.SKYLANDS)
|
||||||
|
&& skylands < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
|
||||||
|
skylands++;
|
||||||
|
} else if (page.equals(PortTerrainFeaturePlan.DENSE_ORES)
|
||||||
|
&& denseOres < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
|
||||||
|
denseOres++;
|
||||||
|
} else if (page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
|
||||||
|
&& starFissures < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
|
||||||
|
starFissures++;
|
||||||
|
} else if (PortVegetationPlan.isVegetation(page) && vegetation < MAX_VEGETATION) {
|
||||||
|
vegetation++;
|
||||||
|
} else if (page.equals(DUNGEONS) && dungeons < MAX_DUNGEONS) {
|
||||||
|
dungeons++;
|
||||||
} else if (page.getNamespace().equals(Rivencraft.MOD_ID)
|
} else if (page.getNamespace().equals(Rivencraft.MOD_ID)
|
||||||
&& page.getPath().startsWith("biome/") && biomes < MAX_AUTHORED_BIOMES
|
&& page.getPath().startsWith("biome/") && biomes < MAX_AUTHORED_BIOMES
|
||||||
&& PortSymbolPages.find(page) != null
|
&& PortSymbolPages.find(page) != null
|
||||||
@@ -57,7 +103,14 @@ public final class PortAuthoredWriting {
|
|||||||
biomes++;
|
biomes++;
|
||||||
} else return false;
|
} else return false;
|
||||||
}
|
}
|
||||||
if (material != rocks || rocks && (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
if (pendingMaterial != null || (rocks > 0 || vegetation > 0)
|
||||||
|
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||||
|
if ((dungeons > 0 || lakes > 0) && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN)
|
||||||
|
|| pages.contains(CAVE_TERRAIN))) return false;
|
||||||
|
if (denseOres > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||||
|
if (pages.contains(PortTerrainFeaturePlan.FLOATING_ISLANDS) && pages.contains(CAVE_TERRAIN)) return false;
|
||||||
|
if (pages.stream().anyMatch(PortTerrainFeaturePlan::isSurface)
|
||||||
|
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||||
if (controllers != 0) {
|
if (controllers != 0) {
|
||||||
PortBiomePlan.Distribution distribution = pages.stream().map(PortAuthoredWriting::controller)
|
PortBiomePlan.Distribution distribution = pages.stream().map(PortAuthoredWriting::controller)
|
||||||
.filter(value -> value != null).findFirst().orElseThrow();
|
.filter(value -> value != null).findFirst().orElseThrow();
|
||||||
@@ -107,11 +160,94 @@ public final class PortAuthoredWriting {
|
|||||||
return PortAgeTerrain.STANDARD;
|
return PortAgeTerrain.STANDARD;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PortRockPlan rockPlan(List<Identifier> pages) {
|
public static PortRockPlan rockPlan(List<Identifier> pages, long seed) {
|
||||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||||
return pages.contains(ROCKS) ? new PortRockPlan(pages.stream()
|
List<Identifier> materials = solidMaterials(pages, seed, ROCKS);
|
||||||
.filter(PortAuthoredWriting::isBlockPage).map(PortRegistryPages::blockId)
|
return materials.isEmpty() ? null : new PortRockPlan(materials);
|
||||||
.findFirst().orElseThrow()) : null;
|
}
|
||||||
|
|
||||||
|
public static PortTerrainFeaturePlan terrainFeaturePlan(List<Identifier> pages, long seed) {
|
||||||
|
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||||
|
Map<Identifier, List<Identifier>> materials = new HashMap<>();
|
||||||
|
Map<Identifier, Integer> occurrences = new HashMap<>();
|
||||||
|
List<PortTerrainFeaturePlan.Choice> choices = new ArrayList<>();
|
||||||
|
for (Identifier page : pages) {
|
||||||
|
if (!PortTerrainFeaturePlan.isSupported(page)) continue;
|
||||||
|
if (page.equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS)) {
|
||||||
|
choices.add(new PortTerrainFeaturePlan.Choice(page, PortTerrainFeaturePlan.CRYSTAL_MATERIAL));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<Identifier> selected = materials.computeIfAbsent(page, feature -> solidMaterials(pages, seed, feature));
|
||||||
|
int occurrence = occurrences.merge(page, 1, Integer::sum) - 1;
|
||||||
|
choices.add(new PortTerrainFeaturePlan.Choice(page, selected.get(occurrence)));
|
||||||
|
}
|
||||||
|
boolean skylands = pages.contains(PortTerrainFeaturePlan.SKYLANDS);
|
||||||
|
int denseOres = (int) pages.stream().filter(PortTerrainFeaturePlan.DENSE_ORES::equals).count();
|
||||||
|
boolean starFissures = pages.contains(PortTerrainFeaturePlan.STAR_FISSURES);
|
||||||
|
return choices.isEmpty() && !skylands && denseOres == 0 && !starFissures ? null
|
||||||
|
: new PortTerrainFeaturePlan(choices, skylands, denseOres, starFissures);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Identifier> solidMaterials(List<Identifier> pages, long seed, Identifier feature) {
|
||||||
|
if (!pages.contains(feature)) return List.of();
|
||||||
|
List<Identifier> randomPool = new ArrayList<>(PortRegistryPages.solidMaterialIds());
|
||||||
|
if (randomPool.isEmpty()) throw new IllegalStateException("No full-block materials are registered");
|
||||||
|
Collections.shuffle(randomPool, new Random(mix(seed ^ (long) feature.hashCode() * 0x9E3779B97F4A7C15L)));
|
||||||
|
List<Identifier> materials = new ArrayList<>();
|
||||||
|
Identifier pending = null;
|
||||||
|
int randomIndex = 0;
|
||||||
|
for (Identifier page : pages) {
|
||||||
|
if (isBlockPage(page)) pending = PortRegistryPages.blockId(page);
|
||||||
|
else if (page.equals(feature)) {
|
||||||
|
materials.add(pending != null ? pending : randomPool.get(randomIndex++ % randomPool.size()));
|
||||||
|
pending = null;
|
||||||
|
} else pending = null;
|
||||||
|
}
|
||||||
|
return List.copyOf(materials);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PortLakePlan lakePlan(List<Identifier> pages, long seed) {
|
||||||
|
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||||
|
if (pages.stream().noneMatch(PortLakePlan::isLake)) return null;
|
||||||
|
List<Identifier> fluidPool = PortRegistryPages.fluidMaterialIds();
|
||||||
|
if (fluidPool.isEmpty()) throw new IllegalStateException("No source-fluid materials are registered");
|
||||||
|
Map<Identifier, List<Identifier>> orders = new HashMap<>();
|
||||||
|
Map<Identifier, Integer> occurrences = new HashMap<>();
|
||||||
|
List<PortLakePlan.Choice> choices = new ArrayList<>();
|
||||||
|
Identifier pending = null;
|
||||||
|
for (Identifier page : pages) {
|
||||||
|
if (isBlockPage(page)) pending = PortRegistryPages.blockId(page);
|
||||||
|
else if (PortLakePlan.isLake(page)) {
|
||||||
|
int occurrence = occurrences.merge(page, 1, Integer::sum) - 1;
|
||||||
|
List<Identifier> fluids = orders.computeIfAbsent(page, id -> {
|
||||||
|
List<Identifier> shuffled = new ArrayList<>(fluidPool);
|
||||||
|
Collections.shuffle(shuffled, new Random(mix(seed ^ (long) id.hashCode()
|
||||||
|
* 0x9E3779B97F4A7C15L)));
|
||||||
|
return shuffled;
|
||||||
|
});
|
||||||
|
choices.add(new PortLakePlan.Choice(page,
|
||||||
|
pending != null ? pending : fluids.get(occurrence % fluids.size())));
|
||||||
|
pending = null;
|
||||||
|
} else pending = null;
|
||||||
|
}
|
||||||
|
return new PortLakePlan(choices);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PortVegetationPlan vegetationPlan(List<Identifier> pages) {
|
||||||
|
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||||
|
List<Identifier> vegetation = pages.stream().filter(PortVegetationPlan::isVegetation).toList();
|
||||||
|
return vegetation.isEmpty() ? null : new PortVegetationPlan(vegetation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int dungeonCount(List<Identifier> pages) {
|
||||||
|
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||||
|
return (int) pages.stream().filter(DUNGEONS::equals).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long value) {
|
||||||
|
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||||
|
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||||
|
return value ^ value >>> 31;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolves all authored biomes from the active registry before creating a level. */
|
/** Resolves all authored biomes from the active registry before creating a level. */
|
||||||
@@ -121,8 +257,15 @@ public final class PortAuthoredWriting {
|
|||||||
List<Identifier> biomes = new ArrayList<>();
|
List<Identifier> biomes = new ArrayList<>();
|
||||||
PortBiomePlan.Distribution distribution = null;
|
PortBiomePlan.Distribution distribution = null;
|
||||||
boolean caveTerrain = pages.contains(CAVE_TERRAIN);
|
boolean caveTerrain = pages.contains(CAVE_TERRAIN);
|
||||||
|
boolean hasSurfaceBiome = false;
|
||||||
|
boolean hasOreBiome = false;
|
||||||
for (Identifier page : pages) {
|
for (Identifier page : pages) {
|
||||||
if (isTerrain(page) || page.equals(ROCKS) || isBlockPage(page)) continue;
|
if (isTerrain(page) || page.equals(ROCKS) || page.equals(DUNGEONS)
|
||||||
|
|| page.equals(PortTerrainFeaturePlan.SKYLANDS)
|
||||||
|
|| page.equals(PortTerrainFeaturePlan.DENSE_ORES)
|
||||||
|
|| page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
|
||||||
|
|| PortLakePlan.isLake(page) || PortTerrainFeaturePlan.isSupported(page) || isBlockPage(page)
|
||||||
|
|| PortVegetationPlan.isVegetation(page)) continue;
|
||||||
PortBiomePlan.Distribution selected = controller(page);
|
PortBiomePlan.Distribution selected = controller(page);
|
||||||
if (selected != null) { distribution = selected; continue; }
|
if (selected != null) { distribution = selected; continue; }
|
||||||
Identifier biome = PortRegistryPages.biomeId(page);
|
Identifier biome = PortRegistryPages.biomeId(page);
|
||||||
@@ -131,8 +274,16 @@ public final class PortAuthoredWriting {
|
|||||||
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biome));
|
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biome));
|
||||||
if (caveTerrain && !PortRegistryPages.isCaveBiome(holder))
|
if (caveTerrain && !PortRegistryPages.isCaveBiome(holder))
|
||||||
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biome);
|
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biome);
|
||||||
|
if (!PortRegistryPages.isCaveBiome(holder) && !holder.is(BiomeTags.IS_END))
|
||||||
|
hasSurfaceBiome = true;
|
||||||
|
if (net.dnorberg.rivencraft.age.PortDenseOres.hasOres(holder)) hasOreBiome = true;
|
||||||
biomes.add(biome);
|
biomes.add(biome);
|
||||||
}
|
}
|
||||||
|
if (pages.stream().anyMatch(PortVegetationPlan::isVegetation)
|
||||||
|
&& !biomes.isEmpty() && !hasSurfaceBiome)
|
||||||
|
throw new IllegalArgumentException("Vegetation pages need at least one surface Overworld biome");
|
||||||
|
if (pages.contains(PortTerrainFeaturePlan.DENSE_ORES) && !biomes.isEmpty() && !hasOreBiome)
|
||||||
|
throw new IllegalArgumentException("Dense Ores needs at least one biome with registered ore placements");
|
||||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||||
return new PortBiomePlan(distribution, List.of());
|
return new PortBiomePlan(distribution, List.of());
|
||||||
if (biomes.isEmpty()) return null;
|
if (biomes.isEmpty()) return null;
|
||||||
|
|||||||
@@ -76,13 +76,27 @@ public final class PortRegistryPages {
|
|||||||
|
|
||||||
public static List<ItemStack> materialPages(boolean fluids) {
|
public static List<ItemStack> materialPages(boolean fluids) {
|
||||||
String category = fluids ? "rivencraft:fluid" : "rivencraft:block";
|
String category = fluids ? "rivencraft:fluid" : "rivencraft:block";
|
||||||
return BuiltInRegistries.BLOCK.keySet().stream().sorted()
|
return materialIds(category).stream()
|
||||||
.filter(id -> category.equals(materialCategory(BuiltInRegistries.BLOCK.getValue(id))))
|
|
||||||
.map(id -> PageItem.createSymbolPage(blockPageId(id))).toList();
|
.map(id -> PageItem.createSymbolPage(blockPageId(id))).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isCaveBiome(net.minecraft.core.Holder.Reference<net.minecraft.world.level.biome.Biome> holder) {
|
/** The same full-block pool used by Creative pages, in stable registry-ID order. */
|
||||||
String path = holder.key().identifier().getPath();
|
public static List<Identifier> solidMaterialIds() {
|
||||||
|
return materialIds("rivencraft:block");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Identifier> fluidMaterialIds() {
|
||||||
|
return materialIds("rivencraft:fluid");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Identifier> materialIds(String category) {
|
||||||
|
return BuiltInRegistries.BLOCK.keySet().stream().sorted()
|
||||||
|
.filter(id -> category.equals(materialCategory(BuiltInRegistries.BLOCK.getValue(id))))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCaveBiome(net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome> holder) {
|
||||||
|
String path = holder.unwrapKey().map(key -> key.identifier().getPath()).orElse("");
|
||||||
return holder.is(CAVE_BIOMES) || holder.is(BiomeTags.IS_NETHER)
|
return holder.is(CAVE_BIOMES) || holder.is(BiomeTags.IS_NETHER)
|
||||||
|| path.equals("deep_dark") || path.endsWith("_caves");
|
|| path.equals("deep_dark") || path.endsWith("_caves");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"linking.link-fissure": {
|
||||||
|
"sounds": [{"name": "rivencraft:linking/link-fissure"}]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"feature": "minecraft:monster_room",
|
||||||
|
"placement": [
|
||||||
|
{ "type": "minecraft:count", "count": 8 },
|
||||||
|
{ "type": "minecraft:in_square" },
|
||||||
|
{
|
||||||
|
"type": "minecraft:height_range",
|
||||||
|
"height": {
|
||||||
|
"type": "minecraft:uniform",
|
||||||
|
"min_inclusive": { "above_bottom": 1 },
|
||||||
|
"max_inclusive": { "below_top": 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+142
-5
@@ -77,6 +77,18 @@ send_and_wait 'rivencraft_port age create_multi smoke_void_multi 13580 void' 'Cr
|
|||||||
send_and_wait 'rivencraft_port age create_multi smoke_cave_multi 112234 cave' 'Created rivencraft:age/smoke_cave_multi with seed 112234 terrain=CAVE'
|
send_and_wait 'rivencraft_port age create_multi smoke_cave_multi 112234 cave' 'Created rivencraft:age/smoke_cave_multi with seed 112234 terrain=CAVE'
|
||||||
send_and_wait 'rivencraft_port age create_random smoke_random 889900' 'Random Age plan: terrain='
|
send_and_wait 'rivencraft_port age create_random smoke_random 889900' 'Random Age plan: terrain='
|
||||||
send_and_wait 'rivencraft_port age create_rocks smoke_rocks 445566' 'Created rivencraft:age/smoke_rocks with seed 445566 terrain=FLAT'
|
send_and_wait 'rivencraft_port age create_rocks smoke_rocks 445566' 'Created rivencraft:age/smoke_rocks with seed 445566 terrain=FLAT'
|
||||||
|
send_and_wait 'rivencraft_port age create_rocks_multi smoke_rocks_multi 445566' 'Created rivencraft:age/smoke_rocks_multi with seed 445566 terrain=FLAT'
|
||||||
|
send_and_wait 'rivencraft_port age create_rocks_random smoke_rocks_random 445566' 'Created rivencraft:age/smoke_rocks_random with seed 445566 terrain=FLAT'
|
||||||
|
send_and_wait 'rivencraft_port age create_vegetation smoke_vegetation 24680' 'Created rivencraft:age/smoke_vegetation with seed 24680 terrain=FLAT'
|
||||||
|
send_and_wait 'rivencraft_port age create_dungeons smoke_dungeons 135791' 'Created rivencraft:age/smoke_dungeons with seed 135791 terrain=STANDARD'
|
||||||
|
send_and_wait 'rivencraft_port age create smoke_lake_control 667788' 'Created rivencraft:age/smoke_lake_control with seed 667788 terrain=STANDARD'
|
||||||
|
send_and_wait 'rivencraft_port age create_lakes smoke_lakes 667788' 'Created rivencraft:age/smoke_lakes with seed 667788 terrain=STANDARD'
|
||||||
|
send_and_wait 'rivencraft_port age create_lakes_random smoke_lakes_random 667788' 'Created rivencraft:age/smoke_lakes_random with seed 667788 terrain=STANDARD'
|
||||||
|
# This fixture includes a rare island candidate near the origin at normal rarity.
|
||||||
|
send_and_wait 'rivencraft_port age create_islands smoke_islands 887767 void' 'Created rivencraft:age/smoke_islands with seed 887767 terrain=VOID'
|
||||||
|
send_and_wait 'rivencraft_port age create_islands smoke_islands_flat 887767 flat' 'Created rivencraft:age/smoke_islands_flat with seed 887767 terrain=FLAT'
|
||||||
|
send_and_wait 'rivencraft_port age create_islands smoke_islands_noise 887767 standard' 'Created rivencraft:age/smoke_islands_noise with seed 887767 terrain=STANDARD'
|
||||||
|
send_and_wait 'rivencraft_port age create_islands_random smoke_islands_random 887767 void' 'Created rivencraft:age/smoke_islands_random with seed 887767 terrain=VOID'
|
||||||
# The index must reach disk while the server is still running. A graceful
|
# The index must reach disk while the server is still running. A graceful
|
||||||
# shutdown would hide the interrupted-save regression this checks.
|
# shutdown would hide the interrupted-save regression this checks.
|
||||||
index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/runtime_ages.dat"
|
index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/runtime_ages.dat"
|
||||||
@@ -94,6 +106,15 @@ gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_void_multi'
|
|||||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_cave_multi'
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_cave_multi'
|
||||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_random'
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_random'
|
||||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks'
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks_multi'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks_random'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_vegetation'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_dungeons'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_lakes'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_lakes_random'
|
||||||
|
for island_age in smoke_islands smoke_islands_flat smoke_islands_noise smoke_islands_random; do
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq "rivencraft:age/$island_age"
|
||||||
|
done
|
||||||
send_and_wait 'rivencraft_port age sample smoke_alpha' 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_alpha' 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_beta' 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_beta' 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_flat' 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_flat' 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain='
|
||||||
@@ -104,6 +125,52 @@ send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smok
|
|||||||
send_and_wait 'rivencraft_port age sample smoke_cave_multi' 'rivencraft:age/smoke_cave_multi seed=112234 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_cave_multi' 'rivencraft:age/smoke_cave_multi seed=112234 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_random' 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_random' 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_rocks' 'rivencraft:age/smoke_rocks seed=445566 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_rocks' 'rivencraft:age/smoke_rocks seed=445566 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_rocks_multi' 'rivencraft:age/smoke_rocks_multi seed=445566 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_rocks_random' 'rivencraft:age/smoke_rocks_random seed=445566 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_vegetation' 'rivencraft:age/smoke_vegetation seed=24680 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_dungeons' 'rivencraft:age/smoke_dungeons seed=135791 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age dungeons_probe smoke_dungeons' 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners='
|
||||||
|
dungeons_before=$(grep -F 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners=' "$log_file" | tail -1 | sed 's/.*spawners=//')
|
||||||
|
[[ "$dungeons_before" =~ ^[1-9][0-9]*$ ]] || {
|
||||||
|
printf 'Authored Dungeons placed no monster spawners: %s\n' "$dungeons_before" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
send_and_wait 'rivencraft_port age lakes_probe smoke_lake_control' 'Lakes probe rivencraft:age/smoke_lake_control pages=0'
|
||||||
|
lake_control=$(grep -F 'Lakes probe rivencraft:age/smoke_lake_control pages=0' "$log_file" | tail -1 | sed 's/.* water=//; s/ lava=.*//')
|
||||||
|
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes' 'Lakes probe rivencraft:age/smoke_lakes pages=24 fluids=[minecraft:water]'
|
||||||
|
lakes_before=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
|
||||||
|
lake_water=$(printf '%s' "$lakes_before" | sed 's/.* water=//; s/ lava=.*//')
|
||||||
|
[[ "$lake_water" -gt "$lake_control" ]] || {
|
||||||
|
printf 'Water lake pages did not add water: %s -> %s\n%s\n' "$lake_control" "$lake_water" "$lakes_before" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes_random' 'Lakes probe rivencraft:age/smoke_lakes_random pages=24'
|
||||||
|
lakes_random_before=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes_random pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
|
||||||
|
[[ "$lakes_random_before" == *minecraft:water* && "$lakes_random_before" == *minecraft:lava* ]] || {
|
||||||
|
printf 'Unmodified lake pages did not resolve both vanilla fluids: %s\n' "$lakes_random_before" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
declare -A islands_before
|
||||||
|
for island_age in smoke_islands smoke_islands_flat smoke_islands_noise smoke_islands_random; do
|
||||||
|
send_and_wait "rivencraft_port age islands_probe $island_age" "Islands probe rivencraft:age/$island_age pages=4"
|
||||||
|
island_result=$(grep -F "Islands probe rivencraft:age/$island_age pages=4" "$log_file" | tail -1 | sed 's/.*Islands probe /Islands probe /')
|
||||||
|
island_blocks=$(printf '%s' "$island_result" | sed 's/.* count=//; s/ fingerprint=.*//')
|
||||||
|
[[ "$island_blocks" =~ ^[1-9][0-9]*$ ]] || {
|
||||||
|
printf 'Floating Islands placed no selected material: %s\n' "$island_result" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
islands_before[$island_age]=$island_result
|
||||||
|
done
|
||||||
|
island_materials=${islands_before[smoke_islands_random]#*materials=[}
|
||||||
|
island_materials=${island_materials%%]*}
|
||||||
|
IFS=',' read -r -a island_material_list <<< "$island_materials"
|
||||||
|
[[ ${#island_material_list[@]} -eq 4 ]] || {
|
||||||
|
printf 'Repeated unmodified islands did not choose independent materials: %s\n' "$island_materials" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_islands if block 0 0 0 minecraft:air run say Island Void remains empty below' 'Island Void remains empty below'
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_islands_flat if block 0 0 0 minecraft:bedrock run say Island Flat retains its floor' 'Island Flat retains its floor'
|
||||||
|
send_and_wait 'rivencraft_port age biome smoke_islands 128 128' 'rivencraft:age/smoke_islands biome at 128,128=minecraft:forest'
|
||||||
send_and_wait 'execute in rivencraft:age/smoke_flat run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_flat to be force loaded'
|
send_and_wait 'execute in rivencraft:age/smoke_flat run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_flat to be force loaded'
|
||||||
send_and_wait 'execute in rivencraft:age/smoke_void run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_void to be force loaded'
|
send_and_wait 'execute in rivencraft:age/smoke_void run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_void to be force loaded'
|
||||||
send_and_wait 'execute in rivencraft:age/smoke_flat_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_flat_multi to be force loaded'
|
send_and_wait 'execute in rivencraft:age/smoke_flat_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_flat_multi to be force loaded'
|
||||||
@@ -111,12 +178,40 @@ send_and_wait 'execute in rivencraft:age/smoke_void_multi run forceload add 0 0'
|
|||||||
send_and_wait 'execute in rivencraft:age/smoke_cave run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_cave to be force loaded'
|
send_and_wait 'execute in rivencraft:age/smoke_cave run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_cave to be force loaded'
|
||||||
send_and_wait 'execute in rivencraft:age/smoke_cave_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_cave_multi to be force loaded'
|
send_and_wait 'execute in rivencraft:age/smoke_cave_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_cave_multi to be force loaded'
|
||||||
send_and_wait 'execute in rivencraft:age/smoke_rocks run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks to be force loaded'
|
send_and_wait 'execute in rivencraft:age/smoke_rocks run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks to be force loaded'
|
||||||
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count='
|
send_and_wait 'execute in rivencraft:age/smoke_rocks_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks_multi to be force loaded'
|
||||||
rocks_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count=' "$log_file" | tail -1 | sed 's/.*count=//')
|
send_and_wait 'execute in rivencraft:age/smoke_rocks_random run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks_random to be force loaded'
|
||||||
|
send_and_wait 'execute in rivencraft:age/smoke_vegetation run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_vegetation to be force loaded'
|
||||||
|
send_and_wait 'rivencraft_port age vegetation_probe smoke_flat' 'Vegetation probe rivencraft:age/smoke_flat flowers='
|
||||||
|
flat_vegetation=$(grep -F 'Vegetation probe rivencraft:age/smoke_flat flowers=' "$log_file" | tail -1 | sed 's/.*flowers=//')
|
||||||
|
send_and_wait 'rivencraft_port age vegetation_probe smoke_vegetation' 'Vegetation probe rivencraft:age/smoke_vegetation flowers='
|
||||||
|
authored_vegetation=$(grep -F 'Vegetation probe rivencraft:age/smoke_vegetation flowers=' "$log_file" | tail -1 | sed 's/.*flowers=//')
|
||||||
|
flat_flowers=${flat_vegetation%% logs=*}
|
||||||
|
flat_logs=${flat_vegetation##* logs=}
|
||||||
|
authored_flowers=${authored_vegetation%% logs=*}
|
||||||
|
authored_logs=${authored_vegetation##* logs=}
|
||||||
|
[[ "$authored_flowers" -gt "$flat_flowers" && "$authored_logs" -gt "$flat_logs" ]] || {
|
||||||
|
printf 'Authored vegetation did not increase flowers and logs: %s -> %s\n' \
|
||||||
|
"$flat_vegetation" "$authored_vegetation" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count='
|
||||||
|
rocks_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count=' "$log_file" | tail -1 | sed 's/.*count=//')
|
||||||
[[ "$rocks_before" =~ ^[1-9][0-9]*$ ]] || {
|
[[ "$rocks_before" =~ ^[1-9][0-9]*$ ]] || {
|
||||||
printf 'Authored Rocks placed no gold blocks: %s\n' "$rocks_before" >&2
|
printf 'Authored Rocks placed no gold blocks: %s\n' "$rocks_before" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_multi' 'Rocks probe rivencraft:age/smoke_rocks_multi materials=[minecraft:gold_block, minecraft:gold_block, minecraft:gold_block, minecraft:gold_block] count='
|
||||||
|
rocks_multi_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_multi materials=' "$log_file" | tail -1 | sed 's/.*count=//')
|
||||||
|
[[ "$rocks_multi_before" =~ ^[1-9][0-9]*$ && "$rocks_multi_before" -gt "$rocks_before" ]] || {
|
||||||
|
printf 'Repeated Rocks pages did not increase placement: %s -> %s\n' "$rocks_before" "$rocks_multi_before" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_random' 'Rocks probe rivencraft:age/smoke_rocks_random materials=['
|
||||||
|
rocks_random_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_random materials=' "$log_file" | tail -1 | sed 's/.*Rocks probe /Rocks probe /')
|
||||||
|
[[ "$rocks_random_before" =~ count=[1-9][0-9]*$ ]] || {
|
||||||
|
printf 'Unmodified Rocks page placed no material: %s\n' "$rocks_random_before" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
send_and_wait 'execute in rivencraft:age/smoke_flat if block 0 0 0 minecraft:bedrock run say Flat bedrock starts at Y0' 'Flat bedrock starts at Y0'
|
send_and_wait 'execute in rivencraft:age/smoke_flat if block 0 0 0 minecraft:bedrock run say Flat bedrock starts at Y0' 'Flat bedrock starts at Y0'
|
||||||
send_and_wait 'execute in rivencraft:age/smoke_flat if block 0 3 0 minecraft:grass_block run say Flat surface is Y3' 'Flat surface is Y3'
|
send_and_wait 'execute in rivencraft:age/smoke_flat if block 0 3 0 minecraft:grass_block run say Flat surface is Y3' 'Flat surface is Y3'
|
||||||
send_and_wait 'execute in rivencraft:age/smoke_void if block 0 0 0 minecraft:air run say Void remains empty' 'Void remains empty'
|
send_and_wait 'execute in rivencraft:age/smoke_void if block 0 0 0 minecraft:air run say Void remains empty' 'Void remains empty'
|
||||||
@@ -155,8 +250,32 @@ send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smok
|
|||||||
send_and_wait 'rivencraft_port age sample smoke_cave_multi' 'rivencraft:age/smoke_cave_multi seed=112234 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_cave_multi' 'rivencraft:age/smoke_cave_multi seed=112234 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_random' 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_random' 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_rocks' 'rivencraft:age/smoke_rocks seed=445566 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_rocks' 'rivencraft:age/smoke_rocks seed=445566 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count='
|
send_and_wait 'rivencraft_port age sample smoke_rocks_multi' 'rivencraft:age/smoke_rocks_multi seed=445566 chunkOffset=0 terrain='
|
||||||
rocks_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count=' "$log_file" | tail -1 | sed 's/.*count=//')
|
send_and_wait 'rivencraft_port age sample smoke_rocks_random' 'rivencraft:age/smoke_rocks_random seed=445566 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_vegetation' 'rivencraft:age/smoke_vegetation seed=24680 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_dungeons' 'rivencraft:age/smoke_dungeons seed=135791 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age dungeons_probe smoke_dungeons' 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners='
|
||||||
|
dungeons_after=$(grep -F 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners=' "$log_file" | tail -1 | sed 's/.*spawners=//')
|
||||||
|
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes' 'Lakes probe rivencraft:age/smoke_lakes pages=24 fluids=[minecraft:water]'
|
||||||
|
lakes_after=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
|
||||||
|
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes_random' 'Lakes probe rivencraft:age/smoke_lakes_random pages=24'
|
||||||
|
lakes_random_after=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes_random pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
|
||||||
|
for island_age in smoke_islands smoke_islands_flat smoke_islands_noise smoke_islands_random; do
|
||||||
|
send_and_wait "rivencraft_port age islands_probe $island_age" "Islands probe rivencraft:age/$island_age pages=4"
|
||||||
|
island_result=$(grep -F "Islands probe rivencraft:age/$island_age pages=4" "$log_file" | tail -1 | sed 's/.*Islands probe /Islands probe /')
|
||||||
|
[[ "${islands_before[$island_age]}" == "$island_result" ]] || {
|
||||||
|
printf 'Floating Islands changed across restart: %s\n%s\n' "${islands_before[$island_age]}" "$island_result" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count='
|
||||||
|
rocks_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count=' "$log_file" | tail -1 | sed 's/.*count=//')
|
||||||
|
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_multi' 'Rocks probe rivencraft:age/smoke_rocks_multi materials=['
|
||||||
|
rocks_multi_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_multi materials=' "$log_file" | tail -1 | sed 's/.*count=//')
|
||||||
|
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_random' 'Rocks probe rivencraft:age/smoke_rocks_random materials=['
|
||||||
|
rocks_random_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_random materials=' "$log_file" | tail -1 | sed 's/.*Rocks probe /Rocks probe /')
|
||||||
|
send_and_wait 'rivencraft_port age vegetation_probe smoke_vegetation' 'Vegetation probe rivencraft:age/smoke_vegetation flowers='
|
||||||
|
authored_vegetation_after=$(grep -F 'Vegetation probe rivencraft:age/smoke_vegetation flowers=' "$log_file" | tail -1 | sed 's/.*flowers=//')
|
||||||
send_and_wait 'rivencraft_port age biome smoke_flat_multi 384 128' 'rivencraft:age/smoke_flat_multi biome at 384,128=minecraft:forest'
|
send_and_wait 'rivencraft_port age biome smoke_flat_multi 384 128' 'rivencraft:age/smoke_flat_multi biome at 384,128=minecraft:forest'
|
||||||
send_and_wait 'rivencraft_port age biome smoke_void_multi 384 128' 'rivencraft:age/smoke_void_multi biome at 384,128=minecraft:forest'
|
send_and_wait 'rivencraft_port age biome smoke_void_multi 384 128' 'rivencraft:age/smoke_void_multi biome at 384,128=minecraft:forest'
|
||||||
send_and_wait 'rivencraft_port age biome smoke_cave_multi 384 128' 'rivencraft:age/smoke_cave_multi biome at 384,128=minecraft:dripstone_caves'
|
send_and_wait 'rivencraft_port age biome smoke_cave_multi 384 128' 'rivencraft:age/smoke_cave_multi biome at 384,128=minecraft:dripstone_caves'
|
||||||
@@ -185,5 +304,23 @@ random_after=$(grep -F 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 te
|
|||||||
"$rocks_before" "$rocks_after" >&2
|
"$rocks_before" "$rocks_after" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
[[ "$rocks_multi_before" == "$rocks_multi_after" && "$rocks_random_before" == "$rocks_random_after" ]] || {
|
||||||
|
printf 'Repeated or unmodified Rocks changed across restart\n' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$authored_vegetation" == "$authored_vegetation_after" ]] || {
|
||||||
|
printf 'Authored vegetation changed across restart: %s -> %s\n' \
|
||||||
|
"$authored_vegetation" "$authored_vegetation_after" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$dungeons_before" == "$dungeons_after" ]] || {
|
||||||
|
printf 'Authored Dungeons changed across restart: %s -> %s\n' \
|
||||||
|
"$dungeons_before" "$dungeons_after" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ "$lakes_before" == "$lakes_after" && "$lakes_random_before" == "$lakes_random_after" ]] || {
|
||||||
|
printf 'Explicit or random lake plans/blocks changed across restart\n' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
stop_server
|
stop_server
|
||||||
printf '26.3 dedicated-server smoke passed: Standard, Flat, Void, Cave, random and authored Rocks Ages persisted across restart.\n'
|
printf '26.3 dedicated-server smoke passed: terrain, biomes, Rocks, vegetation, Dungeons, lakes, and Floating Islands persisted across restart.\n'
|
||||||
Reference in new issue
Block a user