Compare commits

..
Author SHA1 Message Date
dnorberg a7a73d3c17 Port Age lighting and animated sky styles to 26.3 beta
Build / build (push) Canceled after 0s
2026-11-02 15:49:58 -05:00
dnorberg 48fbae9be9 Port structures, weather, vegetation and safe surface arrivals to 26.3 beta
Build / build (push) Canceled after 0s
2026-11-02 09:30:22 -05:00
dnorberg c223f3fdca Port authored Age features and Star Fissures to 26.3 beta
Build / build (push) Canceled after 0s
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
2026-11-02 01:51:20 -05:00
dnorberg d75154fa9b Port seeded random Ages and authored Rocks to 26.3 beta
Build / build (push) Canceled after 0s
2026-11-01 13:25:42 -05:00
68 changed files with 5181 additions and 89 deletions

No files matched your search

+12 -1
View File
@@ -7,9 +7,20 @@ of [Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to
Minecraft 1.21.1 on NeoForge. Mystcraft veterans will recognize the writing
flow, D'ni-inspired glyphs, libraries, crystals, and instability. Rivencraft
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 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, structure and vegetation pages, independent
Age weather/lighting, Overworld/Nether/End and animated Static skies,
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.71-beta.1.md).
Keep the builds and their saves separate, and back up worlds before updating.
## Getting started
1. Make an Ink Vial from two black dyes, a glass bottle, and a water bucket.
+560
View File
@@ -508,6 +508,563 @@ versions.
The local `1.0.1.39-beta.1` follow-up makes the 26.3 landing search reject
fluid at feet and head height and prevents a submerged fallback platform.
The local `1.0.1.40-beta.1` slice gives pageless Descriptive Books a
seed-selected terrain and biome plan. It follows the 1.21.1 rank weights for
terrain and biome distribution, selects registered Cave/Nether biomes for Cave
Ages and overworld-source biomes for other non-Void terrains, and persists the
resolved choices. Other random Age traits remain a later port task.
The local `1.0.1.41-beta.1` slice adds the first Age-scoped feature wrapper to
the 26.3 generator. A Block page followed by Rocks now places clusters of the
chosen block in new Standard, Amplified, Island, or Flat terrain chunks. The
Age index persists that selection, and the wrapper delegates all vanilla
terrain, biome, and structure generation. Cave and Void are intentionally
excluded until their feature placement rules are ported. This is not yet the
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.
The local `1.0.1.55-beta.1` slice begins the authored structure-set port with
Villages, Mineshafts, and Strongholds. Saved page occurrences use the existing
feature plan's new optional structure list. Terrain gates, random-spread
frequency/spacing scaling, and bounded concentric-ring count/spread follow
1.21.1. Modern `AbstractSpreadingStructurePlacement` accessors replace the old
placement-class accessors. Scaled holder copies never mutate global registries.
Structure-state planning includes authored eligibility biomes while continuing
to sample the actual terrain source. A scoped Structure mixin carries over
1.21.1's authored biome override. The wrapper handles structure starts itself
when a structure plan exists, allowing the hook to recognize the authored Age;
unrelated/unplanned generators remain delegated. Only these three controlled
families are gated in this slice; remaining structure families, random picks,
and biome-mismatch instability remain unported. Existing plans without the
optional list retain their previous structure path. Mod metadata now correctly
calls this target Beta rather than Alpha.
The player passed Star Fissures. Build/static checks pass for this new slice.
A disposable dedicated-server startup reached `Done` without mixin-loading
errors. Its console `/stop` command hit a null-level CommandSourceStack error;
the exact test JVM was then stopped through SIGTERM and saved/shut down. This
was a startup check, not authored generation or density validation. The
disposable world is removed; client authoring, actual structures, density,
biome overrides, and restart checks are handed to the player.
The local `1.0.1.56-beta.1` follow-up corrects the structure biome override.
Client logs confirmed the build-55 mixin was applied, but 26.3 JigsawPlacement
checks `GenerationContext.couldStructureExistInColumn` inside generation,
before the final check bypassed by the 1.21.1 hook. The replacement ModifyArg
changes only the biome predicate passed to the GenerationContext constructor
in Structure.generate for explicitly authored Age structures. Both early
and final checks now see the override; all other generation constraints and
unrelated generators retain their existing behavior.
Build/static checks pass. An isolated `--initSettings` bootstrap initialized
settings without a mixin application error, then FML exited nonzero because
that settings-only mode creates no server thread. This is not a full server
or generation pass. No world was created, and the temporary settings directory
is removed. In-game validation requires a new Age or ungenerated chunks;
previously rejected chunks are not regenerated. No commit/release is made.
The local `1.0.1.57-beta.1` slice completes the existing 1.21.1 structure-page
mapping (Dungeons remains its separate placed feature) and enables 26.3's
Abandoned Camps page. The shared planner now recognizes 15 structure sets.
Page/terrain restrictions match AgeStructureSelection for the 1.21.1 families;
Abandoned Camps follows surface noise terrain. No material modifiers are added.
The separate Fortress/Bastion set resources are reused directly from 1.21.1.
The controlled-set gate includes the vanilla combined Nether set, preventing
unrequested cross-family placement. All structure pages share the previously
validated saved list, density logic, and corrected context-level biome override.
Unknown non-spreading placements are left unchanged rather than cast unsafely.
Unmodified/no-authored-structure Ages retain their prior native path; previously
saved authored plans now gate all supported families in new chunks. Instability
and random feature selection remain pending.
The player passed the build-56 biome fix. Build, diff checks, all 15 registry
and Creative-page mappings, and target structure resource existence checks pass.
Packaged Fortress/Bastion definitions match 1.21.1 byte-for-byte. No server is
started; actual structure placement, terrain-specific limits, repeated density,
and restart validation are handed to the player. Changes remain local.
The local `1.0.1.58-beta.1` slice ports Caves/Ravines feature pages separately
from Cave terrain. A new optional saved carver-page list participates in the
existing feature-plan lifecycle. Grammar accepts noise terrain including Cave,
rejects Flat/Void and material modifiers, and checks the modern CARVER registry.
26.3 combines carving with terrain construction and exposes carvers that emit
geometry rather than mutate blocks. A scoped NoiseBasedChunkGenerator mixin
adds authored geometry to its native CarvingMask immediately before that mask
is tested/applied. It only acts for the matching runtime Age wrapper, using
its frozen seed and actual biome resolver. Vanilla applies the combined mask
with its own aquifer/surface rules; no closed NoiseChunk is accessed afterward.
The source range, salts, Nether cave selection, and Nether ravine skip match
1.21.1. Duplicate Caves/Ravines repeat identical seeds in the baseline, so the
port avoids redundant identical mask passes while retaining authored pages.
Added terrain volumes still follow the combined terrain operation, as noted
for the earlier Floating Islands port.
The player passed structure slice 57. Build/static checks and packaging checks
for the hook, Creative entries, and three required vanilla carvers pass. A
settings-only bootstrap completed initialization without a mixin application
error; FML then returned nonzero because no server thread exists in that mode.
This is not world-generation validation. No world was created; the temporary
settings directory is removed. Actual carving and restart checks are manual,
and no commit/push/publication is performed.
The local `1.0.1.59-beta.1` slice replaces stale per-page port information
and adds the requested red hue for unsupported pages. A shared
PortAuthoredWriting.isPageImplemented classifier uses the same terrain,
controller, material/biome, and feature recognizers as the grammar, without
misclassifying context-dependent modifiers/controllers by testing them alone.
Page tooltips, the extracted item-render color, and the Binder background use
this single availability check. Tinting does not generate extra textures or
change stored page data. Corrupted-page names/descriptions remain obfuscated.
Port language overrides remove the old Standard/one-Biome claim, distinguish
implemented/unimplemented pages and invalid combinations, and correct all
15 structure-family descriptions, material adjacency, and selected terrain/
feature restrictions. The 1.21.1 language assets are unchanged. This is a UI
and information slice, not new generation functionality.
The player passed Caves/Ravines. Build, diff checks, unique language-key checks,
and packaged-language override checks pass. No server/world is started.
Inventory tint, Binder tint/layering, and tooltip readability await the
player's visual check. Changes remain local and unpublished.
The local `1.0.1.60-beta.1` slice implements the four requested 26.3-only
vegetation pages: Poplar Trees, Fallen Poplar Trees, Red Shrubs, and Shelf
Mushrooms. They extend PortVegetationPlan recognition, so grammar, saved plans,
Creative status, item tint, and tooltip availability agree without another
page list. The existing ordered vegetation cap and surface-terrain/biome
restrictions are retained. The 1.21.1 code/assets are unchanged.
Poplar Trees selects only the three vanilla poplar variants, retaining their
relative native weights and six placement attempts. Fallen poplars reuse the
vanilla feature but add authored in-square/surface placement before its sapling
check; the vanilla placed feature alone has no surface-height placement. Red
Shrubs directly reuses its registered patch. No tree/shrub algorithm or artwork
is duplicated.
Shelf Mushrooms is a tree decorator in 26.3, not a placed feature. The authored
page therefore applies ShelfMushroomDecorator to existing connected log hosts
at its position in the vegetation sequence. Section palettes skip log-free
sections, repeated pages reuse the host scan, and tree placements invalidate
that cache. Host ordering and page seeds are deterministic; callback writes
are chunk-local, fluid-free, supported, and height-bounded. The decorator keeps
its native standing/fallen logic and side/age/adjacency checks. This is an
authored extension using the native decorator, not a new vanilla feature ID.
Build/static and resource-reference checks pass. The four pages' descriptions
explain their actual effects and requirements. No server/world is started;
the player validates vegetation appearance, support/tint, ordering, and restart.
Changes remain local and unpublished.
The local `1.0.1.61-beta.1` follow-up separates log-hosted Shelf Mushrooms
from surface plants. A shared requiresSurface classification now governs
terrain/explicit-biome validation; decoration handles Shelf Mushrooms before
the surface-biome skip. Trees, shrubs, and flowers retain their restrictions.
Void/Cave/End-biome log hosts are no longer excluded by the vegetation path.
The host scan is a stable log-position list rather than connected tree groups.
Native tree decorators assume a meaningful base and only decorate a limited
height band, which is unsuitable for log tendrils. The authored extension
therefore checks exposed log faces at all heights, reusing the native decorator's
replaceability/water predicate and block facing/age/survival rules, with spacing
and chunk bounds. Cached scans still avoid repeated full-volume work. No tree
page or log-producing page is required, but actual log hosts must exist during
chunk generation. This supersedes slice 60's whole-tree height-band behavior.
Build/static checks pass. Manual validation: new Void + Oak Log + Tendrils +
Shelf Mushrooms Age, without any tree page; inspect exposed log faces and
restart/new-chunk generation. No server is started and nothing is published.
The player passed the local `1.0.1.62-beta.1` sound slice: the complete 1.21.1
sound manifest/OGGs are reused, book travel emits modifier-selected audio and
cloud bursts, and Star Fissures play audio after arrival as well as departure.
Portal/meteor events are registered but their gameplay ports remain separate.
The local `1.0.1.63-beta.1` slice adds authored Normal/Fast/Slow/Off/Always/Storm
weather policies. One policy is accepted per book, with availability/tint and
tooltips following the shared classifier. Cave terrain always suppresses it.
An omitted policy defaults to Normal for now; random-trait selection is pending.
Unlike 1.21.1, 26.3 ServerLevel.getWeatherData delegates to the server's shared
weather object. A scoped Runtime Age ServerLevel subclass instead loads native
WeatherData from that Age's data storage, alongside its saved policy. Native
weather ticking and client packets remain in charge; only the data owner and
Legacy-style duration policy change. The constructor replaces inherited global
rain/thunder visual levels before publishing the level. Retirement, reseeding,
reload and restart retain the policy through existing per-Age storage lifecycle.
Vanilla /weather remains a global server command; the Age policy is not a
global command override. Native biome precipitation rules still apply.
Build/diff checks pass. A disposable dedicated server created all six weather
policies: every Age reported independent native data and the expected flags/
duration ranges. Setting global thunder left the Off Age clear. Unload/reload
retained Storm, and a full stop/restart restored all six policies and their
live timers. Both servers shut down cleanly; the temporary test world is
deleted after completion. Client appearance, cross-Age travel, authoring/tint,
and Cave suppression remain manual checks.
No commit/push/publication is authorized by the player's testing pass alone.
The local `1.0.1.64-beta.1` follow-up fixes weather-only authoring. Build 63's
server weather checks bypassed the Binder's biome compiler, whose exclusion
list did not yet include weather. As a result weather/storm was incorrectly
resolved as a biome ID despite grammar acceptance. The biome compiler now
selects actual biome-category entries after full-book grammar validation;
controllers retain their separate handling. This avoids repeating the same
missed-exclusion bug as more non-biome page families are ported. The operator
weather fixture now calls that compiler before backend creation. All six
weather-only symbol lists compiled and created their expected weather states
on a fresh dedicated server. Build/diff checks pass. The server shut down
cleanly and its disposable world was removed; player GUI validation remains.
The local `1.0.1.65-beta.1` spawn correction traces non-Cave arrival columns
from maximum build height, inspecting actual fully generated chunk blocks.
The configured Flat-layer shortcut could select ground below later authored
terrain, and the old heightmap-only path did not explicitly ensure an exposed
surface. The new path stops at the first fluid/leaves/collision, requires dry
footing and headroom, and otherwise tries the next column rather than searching
below an obstruction. Chunk lookup is cached per column and a mutable position
avoids per-air-block allocations. A fallback platform goes above the actual
highest obstruction; empty Void retains floor Y=63. Surface footing accepts
leaves, rejects fluid-bearing floors, and checks build/border bounds.
Stored non-Cave Star Fissure arrivals also use the surface search rather than
accepting old underground pockets. Cave terrain keeps its Y=64-down search.
Exact bound Linking Books and the general fissure Overworld-return anchor
search are not redirected to the sky. A read-only surface probe command helps
validate the chosen column/nearby surface (chunk generation is possible).
Build/diff checks pass. A disposable dedicated-server probe selected Y=4 on
plain Flat ground, Y=141 above leaves placed at Y=140, and a neighboring dry
column instead of descending below water added above those leaves. A larger
roof-fixture fill was rejected by vanilla because some target chunks were not
loaded; that fixture is not claimed as tested. The server shut down cleanly,
and its temporary world was deleted. Actual player arrival in the affected
Age and the unchanged Cave/Linking Book paths remain manual checks.
The local `1.0.1.66-beta.1` slice ports the three 1.21.1 lighting controls.
Normal surface types keep the host Overworld holder; Dark disables skylight,
Bright sets ambient light to 0.25. Cave Bright is explicitly supported with
0.25 ambient and no skylight; Cave Normal/Dark have 0.0 ambient and no skylight.
The native vanilla registry-sync path supplies types to clients before travel.
Generated data derives three variants from the exact 26.3 vanilla Overworld
resource, preserving every key except has_skylight and ambient_light. The
generator has declared inputs/outputs and depends on Minecraft artifacts.
The Age index has an optional lighting map, defaulting old entries to Normal.
Creation supplies the choice before constructing the level and saving the
index. Restore/reload/reseed read it from the index; retirement removes it and
failed retirement restores it before reloading. Pages share grammar/tint and
the positive biome-page filter, avoiding the weather-only compiler regression.
Missing lighting stays Normal until the random-trait port; Bright instability
is pending with the instability system. No sky/celestial renderer is added.
The static variants derive vanilla data, not arbitrary data-pack overrides
to the host's Overworld type; broader custom type inheritance is not claimed.
Build/diff checks and generated-data equality checks pass (only the two
lighting fields differ from vanilla). A disposable server with a normal
Overworld compiled all six surface/Cave lighting-page combinations and
reported the expected type, skylight and ambient values. Cave Bright survived
unload/reload and reseeding, and all six choices survived a full restart.
Overworld skylight/ambient stayed unchanged. An earlier Superflat-host fixture
could test surface variants but could not create Cave terrain because the
existing backend requires a noise-based host generator; it was discarded and
the Cave checks ran on a fresh normal-host fixture. Test servers stopped
cleanly and both temporary worlds were deleted. Client-side appearance,
authoring, travel, placed lights and multiplayer registry behavior are manual
checks, not inferred from the server probes. No commit/push/publication.
The local `1.0.1.67-beta.1` corrects the visual failure found in slice 66:
LightmapRenderStateExtractor and lightmap.fsh use AMBIENT_LIGHT_COLOR, while
dimension ambient_light only affects other brightness queries. Merely porting
1.21.1's 0.25 field was insufficient. Bright types now provide white native
ambient color and ambient_light 1.0; Cave Bright retains no skylight. A scoped
NeoForge spawn-placement handler denies natural/chunk-generation MONSTER
spawns in Bright runtime Ages, leaving passive/existing mobs, spawners,
commands/eggs and event spawns untouched. It does not manufacture propagated
block light or alter crop light checks. This supersedes slice 66's Bright
appearance and natural-spawn limitation; existing Bright types update on boot.
Requested follow-up for the color-page port: add ambient-light color as a
separate authorable target using minecraft:visual/ambient_light_color. Keep
the existing number/color mixing semantics and white Bright default; do not
confuse ambient tint with sky/celestial color or its server spawn policy.
Color pages themselves remain unported in this slice.
Build/diff and generated attribute checks pass. A disposable dedicated server
verified the registered event handler denies a successful-default natural
zombie placement in surface/Cave Bright, leaves spawner zombies and natural
cows unchanged, and leaves Normal unaffected. Those are isolated event-policy
probes, not a long-running natural-spawn census. The server shut down and its
temporary world was deleted. Actual client appearance awaits player validation.
The local `1.0.1.68-beta.1` removes the requested-against MobSpawnEvent veto.
An Age-scoped LevelLightEngine.getRawBrightness return modifier contributes
Bright's full ambient minimum of 15 to combined brightness on both sides.
Vanilla Monster.isDarkEnoughToSpawn now sees that light, rather than being
overridden or filtered by mob category/spawn reason. Light-ignoring mobs and
special spawns retain vanilla rules; no spawner exemptions are introduced.
Block/sky storage and propagation remain physical, while combined-light
consumers (including plant checks) see ambient light. Cave Bright retains
no skylight, and the native white render attribute remains. This supersedes
slice 67's separate spawn policy and unchanged-crop-light behavior.
The lighting probe now calls vanilla's darkness predicate and combined
brightness instead of constructing synthetic spawn events. Ambient-color
authoring remains a planned part of the color-page port.
Build/diff checks pass. A disposable dedicated server loaded the required
mixin and reported effective brightness 15 / vanilla darkness false for
surface/Cave Bright, versus 0 / true for Dark. These are direct brightness/
vanilla-predicate probes, not a natural-spawn census or a fully generated
underground-room test; an unloaded Normal column returned native sky light 15.
The server stopped cleanly and its temporary world was deleted. Client visual
validation remains with the player; no publication is performed.
The local `1.0.1.69-beta.1` slice completes missing weather/lighting choices
at book binding. PortEnvironmentSelection resolves explicit supported pages
first and uses independent seed-salted Legacy-rank rolls for omissions.
Normal is rank 1, remaining supported modes rank 2: 3:1:1 lighting and
6:1:1:1:1:1 weather. These weights derive from the active enum pool and reuse
the existing weighted ticket routine, rather than duplicating random-choice
logic or enabling unported Cloudy/Rain/Snow modes. It is rank-policy parity,
not exact seed-stream parity with 1.21.1's broader trait pool.
The selected weather/lighting are passed through the existing save-before-
catalog creation path. Restoring Ages never calls the resolver, so old Ages
retain their saved values. Cave weather is always Off; Cave lighting remains
independent and supports Bright. Explicit developer fixtures remain controlled,
but create_random now uses the same resolver as pageless books. A read-only
environment probe checks seed repeatability and explicit overrides. Superseded
Normal-only fromPages helpers are removed. Random feature, sky/celestial,
blocklist policy and instability work remain pending.
Build/diff checks pass. Disposable dedicated-server probes repeated seeds
0/4/7, verified explicit overrides and independent one-field completion, and
confirmed Cave weather Off with Bright lighting. Three random Ages saved
Always/Normal, Normal/Dark and Fast/Bright respectively; weather/lighting
choices and live weather timers persisted through a full server restart.
Overworld weather/type stayed unchanged. The server stopped cleanly and its
temporary world was deleted. Player book-authoring/visual checks remain
manual. No commit, push or publication is performed.
The local `1.0.1.70-beta.1` slice ports native Overworld/Nether/End sky styles.
The shared grammar accepts one style and the biome compiler ignores it by
category. Sky is saved in an optional Age-index map with Overworld fallback;
restore/reseed/retirement rollback use the saved choice. It does not alter
the missing-weather/lighting seed-completion rules, add random sky selection,
or enable Static Sky/color/individual-celestial pages prematurely.
The dimension-data generator now produces 11 sky/lighting variants from the
vanilla Overworld baseline. It changes visual skybox/fog/cloud settings only
in addition to lighting. Geometry, scale, ceiling, monster settings, default
clock and gameplay attributes are retained; no Nether gameplay or End dragon
fight is inherited. A derived native day timeline omits only sky/fog/cloud
color tracks for non-Overworld styles, preserving schedules and illumination.
Native End/no-sky rendering needs no optional renderer or new artwork. The
Legacy animated Static Sky renderer cannot be replaced by native flat gray
without losing its behavior, so its page remains red/unimplemented.
Bright detection includes all registered sky variants, preserving both white
ambient rendering and the effective brightness floor for surface/Cave Bright.
Data equality checks confirm all 11 variants preserve geometry/gameplay and
the day timeline keeps native tracks except the three excluded visual colors.
Its global time markers remain defined only by minecraft:day, not duplicated
in the derived timeline. The grouped timeline list is registered as a tag.
Client appearance and multiplayer travel validation remain manual.
Registry validation initially caught an invalid inline tag reference and
duplicate global clock markers in the derived day timeline; both were fixed
before handoff. The sky diagnostic also now supplies a position when sampling
the spatial fog attribute. A disposable dedicated server then restored all
sampled types after restart, including Nether normal/Bright and End normal/
Cave Bright, with the expected reddish/purple fog at the sample position,
unchanged geometry/gameplay flags, and effective light 15 on Bright variants.
Cave Bright End retained its type after unload/reload; a reseeded Nether Age
retained its style after restart. Servers stopped cleanly and temporary test
directories/worlds were deleted. These are server/data checks, not a rendered
client pass. No commit/push/publication is performed.
## Static Sky renderer slice
`1.0.1.71-beta.1` ports the 1.21.1 Legacy-style animated Static Sky, superseding
the previous unsupported/red status. The six independent 48×48 noise faces
use the existing default gray tint (146 per channel), noise range 65–224,
and a two-world-tick refresh. A cached 24-vertex cube and a single 288×48
dynamic texture use the native textured sky pipeline instead of rebuilding
thousands of colored cells every frame. No new asset or dependency is needed.
Client-only mixins carry the animation frame in extracted sky state, reset
it with the state, and own/close GPU resources with SkyRenderer. Static types
have native skybox NONE; the extracted state alone requests a sky pass and
the custom renderer replaces it. End flashes and End gameplay are not enabled.
Normal/Dark/Bright/Cave Bright variants preserve the existing lighting policy,
geometry and gameplay. Existing persistence handles STATIC as a sky enum value.
Random sky selection, color pages, and individual celestial bodies remain
separate unfinished work; this slice does not claim complete sky parity.
Verification: port build passed (no automated unit tests configured). A
disposable dedicated server created Static Standard Normal and Static Cave
Bright Ages, confirmed their gray fog, NONE native skybox and correct skylight/
ambient values, then restored both choices after restart. It stopped cleanly;
the temporary server/world was deleted. Client animation, travel between sky
styles, and restart appearance still require manual validation. No user world
was modified, and nothing was committed, pushed or published.
## Verification of this first slice
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
@@ -516,3 +1073,6 @@ extraction. The GameTest server ran 93 tests; 92 passed, while
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
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.
+507 -3
View File
@@ -4,9 +4,9 @@ This is an **experimental beta port** for issues #97–#104, not gameplay parity
with the Minecraft 1.21.1 release. The 26.3 backend persists separate Ages and
their seeds. Books, the Book Binder, and the Ink Mixer work, and the Binder can
author Standard, Amplified, Island, Flat, Void, or Cave terrain with supported
biome-distribution pages. Other terrain families, feature/material effects,
portals, and much of
the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not
biome-distribution pages, selected material/vegetation/structure features, and
independent Age weather and lighting. Celestial systems, instability, crystal
portals, and much of the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not
substitute this JAR for the 1.21.1 mod or import 1.21.1 saves.
The entries below record how the port was built, including earlier alpha-only
@@ -194,6 +194,510 @@ The local `1.0.1.39-beta.1` follow-up rejects fluid at both feet and head
height during safe-landing searches. It also refuses to carve the fallback
platform into submerged space if no dry footing can be found.
The `1.0.1.40-beta.1` port slice seeds terrain and biome selection for
Descriptive Books with no authored symbol pages. It uses the same terrain and
biome-controller rank weights as the 1.21.1 line, freezes the resulting plan
in the Age index, and accepts the result after restart. The operator-only
`age create_random <name> <seed>` probe exercises this path. Random feature,
sky, weather, and instability choices are not yet ported; this slice covers
terrain and biomes only.
The `1.0.1.41-beta.1` slice establishes an Age-scoped decoration wrapper
for 26.3 and accepts one Block material page immediately before a Rocks feature
page. Newly generated surface chunks place small rock clusters made from that
block without modifying the shared biome registry. The selected block and
generator survive a restart. Cave and Void terrain remain unsupported for this
first surface-feature path; unsupported combinations stay in the Binder.
Random feature selection, unpaired materials, duplicate feature pages, and
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.
The local `1.0.1.55-beta.1` slice adds authored Villages, Mineshafts, and
Strongholds. Repeated pages (up to 32 per type) scale the registered placement
using the 1.21.1 frequency/spacing and ring-count rules. Copies are Age-local:
the Overworld's placements and biome source are not modified. Authored
structures can bypass their usual biome eligibility without changing the
terrain biome; their instability cost remains pending with the instability port.
Villages and Strongholds allow surface noise terrain and Flat, but not Cave
or Void. Mineshafts allow noise terrain including Cave, but not Flat or Void.
These pages do not accept Block/Fluid modifiers. Their ordered counts persist
in the feature plan. In books using these structure pages, unselected sets
from these three families are disabled; other structure families and random
feature selection remain later port work. Earlier books without this new
structure plan retain their existing native structure behavior.
For manual checks, bind a new Standard/Plains Age with Villages, Mineshafts,
and Strongholds, then use `/locate structure minecraft:village_plains`,
`/locate structure minecraft:mineshaft`, and
`/locate structure minecraft:stronghold` to inspect generated structures.
Compare one page with several copies in a different Age, and restart/revisit.
Flat/Plains with Villages and an otherwise incompatible surface biome with
Villages provide separate terrain and biome-override checks.
The local `1.0.1.56-beta.1` follow-up fixes authored structures outside their
normal biomes. Build 55 only bypassed the final biome check; 26.3's jigsaw
placement can reject a whole biome column earlier. The override now supplies
the generation context's biome predicate before either check, only for the
explicitly authored structures in that Age. Terrain/height/geometry rules and
unrelated structures keep their normal checks.
Test with a new Standard or Flat Age using a normally unsupported village
biome, such as The End, plus a Villages page. Multiple copies increase density.
Use `/locate structure #minecraft:village` rather than selecting only the
Plains variant: an override Age can choose other village styles. Old chunks
that already rejected their structure attempt are not regenerated; existing
build-55 Ages can also test the fix in previously unexplored chunks.
The local `1.0.1.57-beta.1` slice adds the remaining 1.21.1 authored structure
families: Nether Fortresses, Bastions, Trial Chambers, Buried Treasure,
Shipwrecks, Ocean Ruins, Pillager Outposts, Ruined Portals, Nether Fossils,
Ancient Cities, and Trail Ruins. It also enables the existing 26.3-only
Abandoned Camps page using Minecraft's registered camp set and variants.
All 15 supported structure-set pages share saved counts, biome overrides,
and repeated-page density scaling (up to 32 per type). Fortress and Bastion
sets reuse the separate 1.21.1 placement definitions; the combined vanilla
Nether set is disabled for authored structure plans so the pages stay separate.
Unselected supported families are gated in these plans. Already generated
chunks and Ages without an authored structure plan keep their existing path.
Only Villages/Strongholds support Flat; none support Void. Fortress, Bastion,
and Nether Fossils require Cave terrain. Villages/Strongholds, Buried Treasure,
Shipwrecks, Ocean Ruins, Outposts, Trail Ruins, and Abandoned Camps require
surface terrain. Mineshafts, Trial Chambers, Ancient Cities, and Ruined Portals
allow surface noise or Cave terrain. Physical terrain/height checks still apply
even when the authored page overrides biome eligibility. Instability penalties
and random selection of these features are not part of this slice.
For a quick in-game check, author a new Standard Age with Trial Chambers,
Trail Ruins, and Abandoned Camps. Use `/locate structure minecraft:trial_chambers`,
`/locate structure minecraft:trail_ruins`, and
`/locate structure minecraft:abandoned_camp_forest` to inspect actual generation.
Separately, test a Cave/Nether Wastes Age with Fortress and Bastion pages
(`/locate structure minecraft:fortress` and
`/locate structure minecraft:bastion_remnant`). Repeated pages and restart/
new-chunk generation remain useful checks. Structures can still fail when
their geometry or required terrain is unavailable.
The local `1.0.1.58-beta.1` slice enables Caves and Ravines feature pages.
They add carving to the chosen noise terrain (Standard, Amplified, Island,
or Cave); they are not aliases for the Cave terrain page. Flat/Void and
Block/Fluid modifiers remain unsupported, matching the 1.21.1 feature rules.
Authored paths retain 1.21.1's source range and seeded salts, using 26.3's
registered cave/Nether-cave/canyon carvers. Nether-biome sources select the
Nether cave carver and skip Ravines. The paths merge into the native carving
mask before Minecraft applies aquifer, replacement, and surface rules; no
second approximation carves raw air after terrain generation. Biomes are not
replaced. The page list is saved, and old plans default to no extra carving.
Duplicate pages are accepted up to 32 per type, but repeat the same seeded
pattern in 1.21.1; this port skips those redundant passes rather than multiplying
carving density. Random feature selection and instability remain pending.
New chunks only. For manual testing, author a Standard/Plains Age with one
Caves page and one Ravines page and inspect underground in Spectator mode.
Restart and explore more chunks. A separate Cave/Nether Wastes Age with Caves
checks the Nether variant without altering its biome distribution.
The local `1.0.1.59-beta.1` slice audits page presentation for the current port.
Each Symbol Page now reports whether its effect is implemented. Unported pages
have red-tinted item artwork and a red Binder page background, plus a red status
line. Supported pages retain their normal artwork. This indicates port
availability only: supported pages in an invalid order, terrain combination,
or excessive quantity are not individually tinted red. Link Panels are unchanged.
The stale Standard/one-Biome message is removed. Structure descriptions now
describe authored biome overrides rather than falsely requiring vanilla biome
eligibility. Material pages explain the immediate-before-feature requirement;
Flat/Caves/Ravines/Floating Islands descriptions reflect the current rules.
These overrides are specific to the 26.3 language file and do not change 1.21.1.
No new generation effect is introduced in this UI slice.
For manual validation, compare a working Villages or Caves page with an
unported celestial or lighting page in Creative inventory
and the Binder. Only the latter should be red; supported Block/Fluid modifiers
and biome controllers should stay normal even though they need other pages.
The local `1.0.1.60-beta.1` slice implements Poplar Trees, Fallen Poplar Trees,
Red Shrubs, and Shelf Mushrooms. They use the existing saved vegetation plan,
ordered attempts, surface-biome rules, and combined 32-vegetation-page limit.
They accept Standard, Amplified, Island, and Flat terrain, but not Cave/Void
or Block/Fluid modifiers. The four pages now show as supported, without a red tint.
Poplar Trees uses the vanilla red/orange/yellow leaf-litter tree variants with
their relative Dappled Forest weights and six attempts per page per chunk;
it does not also select spruce or fallen trees. Fallen Poplar Trees makes one
surface attempt per page, using the vanilla fallen-tree feature, decorators,
and sapling survival rules. Red Shrubs uses the vanilla patch placement.
All still require suitable ground and space; authoring does not bypass those
requirements, and existing chunks are not redecorated.
Shelf Mushrooms applies Minecraft's standing/fallen-log decorator to existing
log groups, not a new tree feature. It can decorate natural trees or logs from
earlier authored tree pages; no suitable logs means no mushrooms. The vanilla
side/age/spacing and water-avoidance logic is retained. Writes stay within the
current chunk and require valid support. Consecutive mushroom pages reuse the
host scan; adding trees invalidates the cached host list for subsequent pages.
For manual testing, bind a new Flat/Plains Age with Poplar Trees, Fallen Poplar
Trees, Red Shrubs, then Shelf Mushrooms. Check leaf colours, fallen trunks,
shrubs, and mushrooms on logs. The four pages should no longer be red. Restart,
revisit existing terrain, and explore new chunks to check persistence.
The local `1.0.1.61-beta.1` follow-up removes the inappropriate surface-terrain
and surface-biome gates from Shelf Mushrooms only. No tree page is required:
natural logs or generated log Tendrils/Spheres/other features are hosts on
any terrain, including Void and Cave, and in any biome. Other plants keep their
existing terrain/biome restrictions. For example, Void + Oak Log + Tendrils +
Shelf Mushrooms is valid authoring; there must still be exposed log faces.
The mushroom scan now considers log faces throughout the chunk height instead
of treating a connected tendril as a standing tree with a small base-height
decoration band. Placement retains native replaceability/water checks, valid
support, random age, a per-side chance, and spacing checks. It does not grow
hosts or continuously decorate player-built logs after chunk generation.
Test a new Void Age with the example pages and inspect tendril surfaces.
The local `1.0.1.62-beta.1` sound slice reuses the complete 1.21.1 sound manifest
and all six OGG files directly in the build. Successful Linking/Descriptive Book
travel now plays the normal, Intra-Linking, or Disarm sound at departure and
arrival, with the same modifier precedence and cloud bursts as 1.21.1. Failed
travel stays silent. Star Fissures also play their return sound after arrival,
instead of relying only on audio sent immediately before a dimension switch.
Portal and meteor sound events are bundled for their later gameplay ports;
this does not implement those systems. Binding still uses vanilla page audio.
Manual checks: normal book travel, Intra-Linking, Disarm, rejected travel, and
a Star Fissure return. Existing books/Ages do not need to be recreated.
The local `1.0.1.63-beta.1` slice ports the six active weather pages: Normal,
Fast, Slow, Off, Always, and Storm. One weather page is accepted per book and
its policy is saved in the Age's own data directory. Missing weather pages
currently use Normal; random weather selection is still part of the pending
random-trait port. Cave terrain forces Off, matching 1.21.1. Precipitation
still follows biome temperature and precipitation rules; a desert does not
start showing rain merely because Always or Storm is selected.
26.3 normally shares server WeatherData. Runtime Ages now use an isolated
native WeatherData instance with its own persisted rain/thunder timers. The
weather controller runs before the native cycle, allowing vanilla rendering,
lightning, snow and client dimension-specific packets to handle the result.
Fixed weather is refreshed before its timers expire; Fast and Slow use the
1.21.1 duration ranges and preserve saved timers on reload. Overworld weather
and other Ages are not changed. The vanilla `/weather` command still targets
the server's global weather, not the independent Age policy.
Operator diagnostics: `/rivencraft_port age create_weather <name> <seed> <mode>`
creates a Flat test Age; `/rivencraft_port age weather_probe <name>` reports
its policy, rain/thunder state and timers, data independence, and global state.
For manual testing, bind separate Storm/Always/Off books with Plains, compare
the Overworld, revisit and restart, and check that Cave + Storm remains dry.
The six weather pages should no longer be tinted red. Existing Ages get a
Normal independent cycle; author a new book to select another policy.
The local `1.0.1.64-beta.1` fix corrects the Binder's biome compiler: it now
positively selects biome-category pages instead of treating every page not
in an exclusion list as a biome. Weather pages no longer trigger an unknown
biome lookup. The weather fixture command now exercises the same page compiler
before creating an Age, covering the weather-only symbol list used by a Link
Panel + Storm book. Existing Binder contents can be reused after updating.
The local `1.0.1.65-beta.1` spawn correction removes the Flat-layer shortcut
and heightmap-only choice for non-Cave Descriptive Book arrivals. The search
starts at the actual maximum build height and traces each nearby column down
to its first colliding block, leaves, or fluid. A column without safe dry
footing/headroom is skipped instead of descending through its roof or water
into an underground pocket. Leaves are valid footing; fluids are not.
This also revalidates a stored Star Fissure arrival from above on non-Cave
terrain. Cave terrain retains its Y=64-downward search, and bound Linking
Books retain their exact coordinates. If no natural surface is safe, the
safety platform is placed above the actual highest obstruction (or floor
Y=63 in an empty Void), never buried below it. Plain Flat terrain still lands
on its ground, not an artificially elevated platform. No new Age is required.
Operator-only `/rivencraft_port age surface_probe <name> <x> <z>` reports the
actual highest obstruction and a safe nearby surface without creating a
platform or teleporting. It can generate chunks while inspecting the region.
The local `1.0.1.66-beta.1` slice ports Normal, Dark, and Bright Lighting.
One lighting page is accepted per book and its choice is saved in the Age
index before catalog publication. Missing lighting currently defaults to
Normal; random lighting selection and Bright's instability cost await those
systems' later ports. Lighting pages should no longer be tinted red.
Normal surface Ages retain the Overworld type. Dark removes skylight with
ambient light 0.0; Bright uses ambient light 0.25 and retains surface skylight,
matching 1.21.1. **Cave + Bright is supported:** it has ambient light 0.25
without skylight. Cave Normal/Dark use no skylight and ambient light 0.0.
Placed light sources continue working; ambient light is not a placed light
level or a guarantee that monsters cannot spawn.
The build derives three registered dimension-type variants from the current
vanilla Overworld JSON, changing only has_skylight and ambient_light. This
preserves native attributes/timelines and synchronizes types through the
normal login registry exchange; no dynamic client registry mutation is used.
These variants currently use the vanilla type, not custom runtime changes to
the host Overworld dimension type made by another data pack.
Saved lighting survives reload, reseed, restart and retirement rollback.
Existing non-Cave Ages without a choice remain Normal; old Cave Ages now use
the no-skylight baseline. Use new Ages for clear visual comparisons and avoid
confusing previously lit chunks with the current dimension settings.
Manual check: create Normal/Dark/Bright surface books and Cave + Bright;
compare an unlit enclosed room, check placed torches, revisit/restart and
confirm the Overworld stays unchanged. Operator diagnostics:
`/rivencraft_port age create_lighting <name> <seed> <mode> <terrain>` and
`/rivencraft_port age lighting_probe <name>` report the registered type,
skylight and ambient light. Test JAR: `1.0.1.66-beta.1`.
The local `1.0.1.67-beta.1` correction supersedes slice 66's Bright rendering:
26.3's lightmap uses `minecraft:visual/ambient_light_color`, not the old
dimension `ambient_light` field. Bright surface/Cave types now set that native
attribute to white and ambient_light to 1.0. Cave still has no skylight.
Bright Ages reject natural/chunk-generation hostile spawns independently of
stored light levels. Existing mobs, passive mobs, spawners, eggs, commands
and special-event spawns are not deleted or globally disabled. Actual block
light propagation and plant light requirements are unchanged. Existing Bright
Ages receive the corrected registered types after restart; no new Age needed.
Planned color-page target: **ambient light color**, alongside sky/celestial
colors. Reuse the native 26.3 attribute for authored tints; Bright defaults to
neutral white until the color-page port. This does not enable color pages yet.
The local `1.0.1.68-beta.1` replaces slice 67's special spawn veto with actual
effective ambient brightness. The spawn event handler is removed. Bright's
combined light-engine reading now has a minimum of 15 on server and client,
including after sky darkening. Its white rendering attribute is retained.
Vanilla darkness checks therefore reject light-sensitive monsters naturally;
light-ignoring mobs, spawners and other spawn sources use their native rules,
not Rivencraft exemptions or bans. This does not remove existing mobs.
Ambient light is not stored as emitted block light or fabricated skylight.
Consumers of combined brightness, including plant light checks, now see the
ambient contribution. Normal/Dark and non-Rivencraft dimensions are unchanged.
Cave Bright remains without skylight. Existing Bright Ages update on restart.
This supersedes slice 67's statement that crop light checks are unchanged.
The local `1.0.1.69-beta.1` slice seed-completes missing weather and lighting
pages for newly bound books, including Link-Panel-only random Ages. Explicit
pages take precedence. Cave weather remains Off, while Cave lighting can
still choose Bright. Old Ages retain their saved choices; they are not rerolled.
The supported pool uses the 1.21.1 Legacy-rank rule: Normal is rank 1, other
choices rank 2. Lighting weights are 3:1:1 for Normal/Dark/Bright; weather
weights are 6:1:1:1:1:1 for Normal/Fast/Slow/Off/Always/Storm. The common weight
is derived from the supported pool size. Unported Cloudy/Rain/Snow modes are
not selected. Independent seed salts keep a missing lighting roll unchanged
when weather is explicitly written, and vice versa. This matches the rank
policy, not the exact 1.21.1 seed stream/pool. No random feature list, sky or
celestial choices are added by this slice.
`/rivencraft_port age create_random <name> <seed>` uses the same environment
resolver and logs its frozen plan. `/rivencraft_port age environment_probe
<seed> <terrain>` inspects deterministic defaults and explicit Storm/Bright
precedence without creating an Age. Other explicit operator fixtures keep
their Normal defaults to isolate the feature under test.
For manual checking, create several Link-Panel-only books and verify weather/
lighting varies across Ages and persists on revisit/restart. Then author an
explicit Normal Weather + Normal Lighting book, and a Cave + Bright book.
Include explicit Normal pages when comparing terrain without random lighting
or weather changes. Random blocklist policy and instability remain pending.
The local `1.0.1.70-beta.1` slice ports the Overworld, Nether, and End Sky
pages. One sky style is accepted per book and persisted independently of
terrain, weather and lighting. Missing sky pages still use Overworld; random
sky selection is not part of this slice. Static Sky remains unsupported/red
until its Legacy animated renderer is ported, rather than being replaced by
a flat gray backdrop. Color and individual celestial pages are also pending.
Nether Sky uses the native no-sky backdrop and reddish fog; End Sky uses the
native End backdrop. Neither changes dimension height, coordinate scale,
terrain, Nether water/bed rules, or enables an End dragon fight. All lighting
choices work with these styles, including Cave + Bright. Bright's render
attribute and effective ambient level 15 are retained on each variant.
The registered variants derive the vanilla Overworld type and preserve its
gameplay attributes. Non-Overworld skies use a derived day timeline without
the three sky/fog/cloud tint tracks, retaining native gameplay schedules and
illumination while avoiding Overworld day tint on those backdrops. Their
cloud color is transparent. Biome/weather fog modifiers still apply. Types
sync through normal registry login; no new rendering dependency is required.
Sky choices survive reload/reseed/restart and retirement rollback. Old Ages
without a sky choice retain Overworld style.
For visual checks, author Plains + Normal Lighting + No Weather with each
sky page; compare the sky/fog and verify the same terrain and normal bed/water
behavior. Then try Cave + Bright + End Sky and revisit after restarting.
Static Sky should remain red. Operator fixtures:
`/rivencraft_port age create_sky <name> <seed> <sky> <lighting> <terrain>` and
`/rivencraft_port age sky_probe <name>`. Use new books to author a sky choice;
this does not rewrite the sky choice of already-bound books.
The local `1.0.1.71-beta.1` slice adds the previously pending Static Sky.
Its client renderer adapts the 1.21.1 Legacy-style background: six independent
48×48 noise faces, default gray tint `#929292`, and noise intensity 65–224.
The noise updates every two world ticks. A single small dynamic texture and
cached cube replace thousands of per-cell draw vertices; resources are closed
with the sky renderer. No artwork or rendering dependency is added.
Static Sky is now supported rather than red, and persists through the same
Age index as other sky styles. Four registered lighting variants retain the
existing terrain/gameplay rules. Their native skybox is NONE (no End flashes);
only the extracted client state schedules the custom background sky pass.
Animation state resets between frames and does not consult a different live
world while rendering. Color pages, celestial pages, and random sky selection
remain pending.
Build and disposable dedicated-server creation/restart checks passed for
Static + Normal + Standard and Static + Bright + Cave. These do not validate
client rendering. For manual testing, bind a new Static Sky + Normal Lighting
+ No Weather book, look around/above/below for animated gray static, then
visit an Overworld/End Sky Age and return to check there is no sky-state leak.
Revisit after restarting. Also try Static Sky + Cave + Bright (look outside
terrain or in spectator mode). The fixture accepts `static` as its sky value.
The dedicated test server was stopped and its temporary world deleted.
Changes remain local; no commit/push/publication was requested.
From the repository root:
```sh
@@ -0,0 +1,13 @@
# Rivencraft 26.3 beta 1.0.1.41-beta.1
This beta expands Age generation on Minecraft 26.3.
- Descriptive Books with no authored symbol pages now choose a seeded random terrain and biome distribution. The chosen plan is saved with the Age and restored after restart.
- A Block material page followed by a Rocks feature page now generates small clusters of that block in newly generated surface chunks. This first feature path supports Standard, Amplified, Island, and Flat terrain; Cave and Void combinations remain unsupported.
- Expanded dedicated-server checks cover random Age plans and authored Rocks generation across restart.
The player verified the Rocks feature in game. Random feature selection, duplicate feature pages, unpaired materials, and instability scoring are not yet ported. Existing chunks are not regenerated.
This 26.3 port is still experimental and does not yet include all features of the separate Minecraft 1.21.1 version. Use a test world and keep a backup. Minecraft 1.21.1 saves are not supported by this beta.
Requires Minecraft 26.3, NeoForge 26.3.0.99 or a compatible 26.3 build, and Java 25.
@@ -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).
@@ -0,0 +1,72 @@
# Rivencraft 26.3 beta 1.0.1.65-beta.1
This beta collects the player-tested port slices since 1.0.1.54-beta.1.
It is not yet feature-complete with the Minecraft 1.21.1 release.
## Age generation
- Added authored structure pages: Villages, Mineshafts, Strongholds, Trial
Chambers, Buried Treasure, Shipwrecks, Ocean Ruins, Pillager Outposts,
Ruined Portals, Nether Fossils, Ancient Cities, Trail Ruins, Nether
Fortresses, Bastions, and 26.3's Abandoned Camps. Dungeons remain their
separate feature.
- Fixed the early structure-biome check so authored structures can generate
outside their native biome restrictions, within the supported terrain rules.
Repeated structure pages increase density up to the current authoring cap.
- Added Caves and Ravines feature pages through 26.3's native carving stage.
These are distinct from Cave terrain; Flat/Void support remains pending.
- Added Poplar Trees, Fallen Poplar Trees, Red Shrubs, and Shelf Mushrooms.
Shelf Mushrooms find exposed logs at any height, including log Tendrils in
Void terrain. No Trees page is required.
- Added Normal, Fast, Slow, Off, Always, and Storm weather pages. Each Age
has independent saved weather data and timers; its policy does not change
the Overworld or other Ages. Cave terrain keeps weather disabled.
## Fixes and feedback
- Non-Cave Descriptive Book arrivals trace actual blocks down from maximum
build height instead of using a Flat-layer shortcut or only a heightmap.
The search stops at roofs/fluids rather than selecting underground pockets.
Leaves count as footing; submerged footing is rejected. Fallback platforms
sit above obstructions, with floor Y=63 in an empty Void.
- Stored surface-Age Star Fissure arrivals are rechecked from above. Cave
terrain still searches down from Y=64, and Linking Books retain exact
bound destinations.
- Restored Legacy linking sounds and successful-link cloud bursts. Disarm
and Intra-Linking use their respective sounds. Star Fissure return audio
also plays after arrival so a dimension switch does not discard it.
- Fixed weather-only authoring, including Link Panel + Storm books: non-biome
pages no longer get interpreted as biome IDs.
- Unsupported pages now have a red tint and availability tooltip. Supported
pages and their descriptions reflect the implemented 26.3 behavior.
- Added operator weather and surface diagnostics for testing.
## Current limits
- Only the supported page/terrain combinations can be bound. Most repeated
features are capped at 32; vegetation uses a combined 32-page limit. The
full 1.21.1 grammar recovery and instability systems remain unported.
- Pageless books currently randomize terrain and biome distribution, not the
complete feature/environment list. Omitted weather currently means Normal.
- Base carvers do not carve authored solid volumes added after 26.3's combined
terrain operation. Previously generated chunks are not regenerated.
- The full writing/discovery loop, celestial/lighting systems, crystal portals,
instability, and optional-integration parity remain in progress. Creative
availability alone does not mean a page is supported; check its tooltip.
- Vanilla `/weather` targets global server weather, not the independent Age
policy. Biomes retain their normal precipitation rules.
The player reported in-game passes for these slices. Build/static checks passed.
Targeted disposable dedicated-server checks covered all six weather policies,
Overworld isolation, unload/reload and restart persistence, weather-only page
compilation, and dry surface/leaf spawn probes. This is not a claim of full
dedicated-server or optional-mod regression coverage. Test servers were stopped
and their temporary worlds removed.
Requires Minecraft **26.3**, NeoForge **26.3.0.99** or a compatible 26.3 build,
and **Java 25**. This build does not require Dynamic Dimensions or Bad Packets.
Keep it separate from the 1.21.1 build and saves. Back up entire worlds before
updating; beta-save compatibility is not guaranteed.
Report issues and help improve the wiki at
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
@@ -0,0 +1,59 @@
# Rivencraft 26.3 beta 1.0.1.71-beta.1
This beta collects the player-tested lighting and sky port slices since
1.0.1.65-beta.1. It is not yet feature-complete with Minecraft 1.21.1.
## Lighting and seeded environments
- Added Normal, Dark, and Bright lighting pages with saved per-Age settings.
- Bright works in Cave terrain too, without adding skylight or weather.
- Bright supplies effective ambient light level 15 to native combined-light
checks and the white ambient rendering attribute. Vanilla darkness-based
monster checks respond to that lighting; there is no separate mob-spawn
veto. Mobs that ignore light retain their normal rules.
- Missing weather and lighting controllers now resolve deterministically from
the Age seed, using independent choices and Legacy-style Normal weighting.
Explicit pages win; Cave terrain still forces weather Off. Existing Ages
keep their saved choices rather than rerolling on load.
## Sky styles
- Added Overworld, Nether, End, and animated Static Sky pages, independently
of terrain, weather, and lighting. Choices persist on reload and restart.
- Nether Sky uses a reddish fog backdrop; End Sky uses the native End sky.
These are visual styles, not Nether/End terrain or gameplay conversions:
dimension geometry, coordinate scale, bed/water rules, and dragon behavior
are not changed.
- Static Sky adapts the existing Legacy-style renderer: six independent
48×48 animated gray noise faces. A cached cube and small dynamic texture
avoid rebuilding thousands of per-cell vertices each frame. Client-only
rendering resources are released with the sky renderer.
- Non-Overworld backgrounds have transparent clouds and a derived day
timeline that avoids Overworld sky/fog/cloud color tracks while retaining
gameplay and illumination schedules.
- Updated supported-page feedback and added operator sky/environment probes.
## Current limits and testing
- Missing Sky pages still default to Overworld; random sky selection, color
pages and individual celestial bodies remain pending. Number-controlled
Static Sky resolution is tracked separately in issue #109 for both versions.
- The writing/discovery loop, instability, portals, and optional-integration
parity are still incomplete. Creative availability is not a support claim;
check page tooltips and the beta guide for supported combinations.
- Previously generated chunks are not regenerated. Save compatibility during
the beta is not guaranteed; keep backups and keep both version tracks apart.
The player reported in-game passes for the included slices, including Static
Sky. Build checks passed for both version tracks. Targeted disposable-server
checks covered environment defaults, explicit choices, registered sky/lighting
variants, persistence and restart, including Cave + Bright and Static Sky.
This is not a claim of complete dedicated-server or optional-mod coverage.
Test servers were stopped and temporary worlds removed.
Requires Minecraft **26.3**, NeoForge **26.3.0.99** and **Java 25**. Dynamic
Dimensions and Bad Packets are not required by this build. The 1.21.1 release
and its dependencies are unchanged.
Report issues and help improve the wiki at
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
+71
View File
@@ -121,9 +121,80 @@ def generatePortLanguage = tasks.register('generatePortLanguage', GeneratePortLa
}
sourceSets.main.resources.srcDir generatePortLanguage
// Derive registered presentation variants without duplicating vanilla gameplay settings.
abstract class GeneratePortDimensionTypes extends DefaultTask {
@InputFile abstract RegularFileProperty getMinecraftJar()
@OutputDirectory abstract DirectoryProperty getOutputDir()
@TaskAction void generate() {
new java.util.zip.ZipFile(minecraftJar.get().asFile).withCloseable { archive ->
def entry = archive.getEntry('data/minecraft/dimension_type/overworld.json')
if (entry == null) throw new GradleException('Missing vanilla Overworld dimension type')
String template = archive.getInputStream(entry).withCloseable { it.getText('UTF-8') }
def dayEntry = archive.getEntry('data/minecraft/timeline/day.json')
if (dayEntry == null) throw new GradleException('Missing vanilla day timeline')
def day = archive.getInputStream(dayEntry).withCloseable {
new groovy.json.JsonSlurper().parseText(it.getText('UTF-8'))
}
// Keep gameplay schedules and illumination, but do not tint these
// native non-Overworld sky backgrounds blue/black with the day cycle.
['fog_color', 'sky_color', 'cloud_color'].each { day.tracks.remove("minecraft:visual/${it}".toString()) }
// Markers are global per clock and already owned by minecraft:day.
day.remove('time_markers')
File dayFile = new File(outputDir.get().asFile, 'data/rivencraft/timeline/age_sky_day.json')
dayFile.parentFile.mkdirs()
dayFile.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(day)) + '\n'
File timelineTag = new File(outputDir.get().asFile, 'data/rivencraft/tags/timeline/age_sky.json')
timelineTag.parentFile.mkdirs()
timelineTag.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson([
values: ['#minecraft:universal', 'rivencraft:age_sky_day',
'minecraft:moon', 'minecraft:early_game']])) + '\n'
['overworld', 'nether', 'end', 'static'].each { sky ->
['age_normal', 'age_dark', 'age_bright', 'age_cave_bright'].each { lighting ->
if (sky == 'overworld' && lighting == 'age_normal') return
String name = lighting + (sky == 'overworld' ? '' : "_sky_${sky}")
def definition = new groovy.json.JsonSlurper().parseText(template)
definition.has_skylight = lighting in ['age_normal', 'age_bright']
definition.ambient_light = lighting in ['age_bright', 'age_cave_bright'] ? 1.0 : 0.0
// 26.3's lightmap reads this attribute, not ambient_light.
if (definition.ambient_light == 1.0)
definition.attributes['minecraft:visual/ambient_light_color'] = '#ffffff'
if (sky != 'overworld') {
definition.skybox = sky == 'end' ? 'end' : 'none'
definition.attributes['minecraft:visual/fog_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#181318' : '#6b2725'
definition.attributes['minecraft:visual/sky_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#000000' : '#6b2725'
definition.attributes['minecraft:visual/cloud_color'] = '#00000000'
if (sky == 'nether') {
definition.attributes['minecraft:visual/fog_start_distance'] = 10.0
definition.attributes['minecraft:visual/fog_end_distance'] = 96.0
}
definition.timelines = '#rivencraft:age_sky'
}
File result = new File(outputDir.get().asFile, "data/rivencraft/dimension_type/${name}.json")
result.parentFile.mkdirs()
result.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(definition)) + '\n'
}
}
}
}
}
def generatePortDimensionTypes = tasks.register('generatePortDimensionTypes', GeneratePortDimensionTypes) {
dependsOn 'createMinecraftArtifacts'
minecraftJar.set(layout.buildDirectory.file("moddev/artifacts/minecraft-patched-${neo_version}.jar"))
outputDir.set(layout.buildDirectory.dir('generated/resources/dimensionTypes'))
}
sourceSets.main.resources.srcDir generatePortDimensionTypes
tasks.named('processResources', ProcessResources).configure {
// Reuse the existing sprite and base model without copying binary assets.
from('../../src/main/resources') {
include 'data/rivencraft/worldgen/structure_set/fortresses.json'
include 'data/rivencraft/worldgen/structure_set/bastions.json'
include 'assets/rivencraft/models/block/star_fissure.json'
include 'assets/rivencraft/blockstates/star_fissure.json'
include 'assets/rivencraft/sounds.json'
include 'assets/rivencraft/sounds/**/*.ogg'
include 'assets/rivencraft/textures/item/crystal_shard.png'
include 'assets/rivencraft/textures/item/page.png'
include 'assets/rivencraft/textures/item/page_link_panel.png'
+1 -1
View File
@@ -7,4 +7,4 @@ neo_version=26.3.0.99
mod_id=rivencraft
mod_name=Rivencraft
mod_license=LGPL-3.0-or-later
mod_version=1.0.1.39-beta.1
mod_version=1.0.1.71-beta.1
+42 -8
View File
@@ -6,19 +6,33 @@
"item.rivencraft.page.blank": "Blank Page",
"item.rivencraft.page.link_panel": "Link Panel Page",
"item.rivencraft.page.unresolved": "Unknown Symbol Page",
"item.rivencraft.page.symbol.port_status": "Binder currently supports Standard terrain and one Biome page; other effects are still being ported.",
"item.rivencraft.page.symbol.implemented": "Implemented in 26.3; book and terrain requirements still apply.",
"symbol.rivencraft.weather.normal.description": "Use an independent normal weather cycle. Cave terrain keeps weather off.",
"symbol.rivencraft.sky.overworld.description": "Use the native Overworld sky. Terrain and lighting remain independently authored.",
"symbol.rivencraft.sky.nether.description": "Use a Nether-style sky and reddish fog, without Nether terrain or gameplay rules.",
"symbol.rivencraft.sky.end.description": "Use the native End sky backdrop, without End terrain or a dragon fight.",
"symbol.rivencraft.sky.static.description": "An animated gray static sky surrounds the Age.",
"symbol.rivencraft.lighting.normal.description": "Use normal skylight and ambient brightness. Cave terrain remains without skylight.",
"symbol.rivencraft.lighting.dark.description": "Remove skylight from this Age; placed light sources still work.",
"symbol.rivencraft.lighting.bright.description": "Provide full ambient brightness and effective light level 15. Vanilla light-based spawn rules apply, including in Cave terrain without skylight.",
"symbol.rivencraft.weather.fast.description": "Use the faster Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
"symbol.rivencraft.weather.slow.description": "Use the slower Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
"symbol.rivencraft.weather.off.description": "Keep this Age clear without changing Overworld weather.",
"symbol.rivencraft.weather.always.description": "Keep this Age rainy, or snowy in cold biomes, without thunder. Cave terrain keeps weather off.",
"symbol.rivencraft.weather.storm.description": "Keep this Age rainy and thundering. Precipitation still depends on the biome; Cave terrain keeps weather off.",
"item.rivencraft.page.symbol.unimplemented": "Not yet implemented in 26.3; cannot be bound into an Age.",
"item.rivencraft.link_property.disarm": "Disarm",
"item.rivencraft.link_property.maintain_momentum": "Maintain Momentum",
"item.rivencraft.link_property.intra_linking": "Intra-Linking",
"item.rivencraft.ink_vial": "Ink Vial",
"item.rivencraft.ink_vial.remaining": "Ink: %s/%s",
"item.rivencraft.ink_vial.usage": "Use with paper in the other hand, or place in an Ink Mixer.",
"item.rivencraft.page.blank.port_status": "Base for future symbol writing; not yet usable in this port.",
"item.rivencraft.page.blank.port_status": "Internal symbol-page base; not an authoring ingredient.",
"item.rivencraft.page.link_panel.port_status": "Use with leather in a Book Binder to make a Descriptive Book.",
"block.rivencraft.book_binder": "Book Binder",
"container.rivencraft.book_binder": "Book Binder",
"screen.rivencraft.book_binder.missing_panel": "Link Panel required",
"screen.rivencraft.book_binder.authored_pending": "Writing not yet ported",
"screen.rivencraft.book_binder.authored_pending": "Unsupported page or page combination",
"block.rivencraft.ink_mixer": "Ink Mixer",
"container.rivencraft.ink_mixer": "Ink Mixer",
"screen.rivencraft.ink_mixer.ink": "Ink: %s/%s",
@@ -33,13 +47,33 @@
"itemGroup.rivencraft": "Rivencraft",
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
"symbol.rivencraft.feature.poplar_trees": "Poplar Trees",
"symbol.rivencraft.feature.poplar_trees.description": "Poplar tree generation symbol.",
"symbol.rivencraft.feature.poplar_trees.description": "Add red, orange, and yellow poplar trees to surface terrain with suitable soil.",
"symbol.rivencraft.feature.fallen_poplar_trees": "Fallen Poplar Trees",
"symbol.rivencraft.feature.fallen_poplar_trees.description": "Fallen poplar tree generation symbol.",
"symbol.rivencraft.feature.fallen_poplar_trees.description": "Add fallen poplar logs and stumps to surface terrain with suitable soil.",
"symbol.rivencraft.feature.shelf_mushrooms": "Shelf Mushrooms",
"symbol.rivencraft.feature.shelf_mushrooms.description": "Shelf mushroom generation symbol.",
"symbol.rivencraft.feature.shelf_mushrooms.description": "Add shelf mushrooms to exposed log faces on any terrain, including Void. No tree page is required.",
"symbol.rivencraft.feature.red_shrubs": "Red Shrubs",
"symbol.rivencraft.feature.red_shrubs.description": "Red shrub generation symbol.",
"symbol.rivencraft.feature.red_shrubs.description": "Add red shrub patches to surface terrain with suitable ground.",
"symbol.rivencraft.feature.abandoned_camps": "Abandoned Camps",
"symbol.rivencraft.feature.abandoned_camps.description": "Abandoned camp structure generation symbol."
"symbol.rivencraft.feature.abandoned_camps.description": "Allow abandoned camps on surface noise terrain, including outside their usual biomes.",
"symbol.rivencraft.block.generated.description": "Place immediately before a compatible feature; unpaired materials are not yet supported.",
"symbol.rivencraft.biome.native.description": "Use the terrain's native biome source; do not include Biome pages.",
"symbol.rivencraft.terrain.flat.description": "A level, biome-decorated landscape with its foundation at Y=0.",
"symbol.rivencraft.feature.floating_islands.description": "Add high floating islands alongside noise, Flat, or Void terrain; not Cave terrain.",
"symbol.rivencraft.feature.caves.description": "Add extra cave carving to noise terrain, including Cave; not Flat or Void.",
"symbol.rivencraft.feature.ravines.description": "Add canyon carving to noise terrain; not Flat or Void. Skips Nether biomes.",
"symbol.rivencraft.feature.mineshafts.description": "Allow mineshafts in noise terrain, including Cave, regardless of normal biome eligibility.",
"symbol.rivencraft.feature.nether_fortresses.description": "Allow Nether fortresses in Cave terrain; the authored page overrides biome eligibility.",
"symbol.rivencraft.feature.strongholds.description": "Allow strongholds in surface noise or Flat terrain, including outside their usual biomes.",
"symbol.rivencraft.feature.villages.description": "Allow villages in surface noise or Flat terrain, including outside their usual biomes.",
"symbol.rivencraft.feature.trial_chambers.description": "Allow underground Trial Chambers in noise terrain, including Cave, regardless of biome eligibility.",
"symbol.rivencraft.feature.bastions.description": "Allow Bastion Remnants in Cave terrain; the authored page overrides biome eligibility.",
"symbol.rivencraft.feature.buried_treasure.description": "Allow buried treasure in surface noise terrain; biome eligibility is overridden, but suitable terrain is required.",
"symbol.rivencraft.feature.shipwrecks.description": "Allow shipwrecks in surface noise terrain, including outside their usual ocean and beach biomes.",
"symbol.rivencraft.feature.ocean_ruins.description": "Allow ocean ruins in surface noise terrain, including outside their usual biomes.",
"symbol.rivencraft.feature.pillager_outposts.description": "Allow pillager outposts in surface noise terrain, including outside their usual biomes.",
"symbol.rivencraft.feature.ruined_portals.description": "Allow ruined portals in noise terrain, including Cave, regardless of normal biome eligibility.",
"symbol.rivencraft.feature.nether_fossils.description": "Allow Nether fossils in Cave terrain; the authored page overrides biome eligibility.",
"symbol.rivencraft.feature.ancient_cities.description": "Allow underground Ancient Cities in noise terrain, including Cave, regardless of biome eligibility.",
"symbol.rivencraft.feature.trail_ruins.description": "Allow Trail Ruins in surface noise terrain, including outside their usual biomes."
}
@@ -8,6 +8,8 @@ import net.dnorberg.rivencraft.age.AgeCatalogSync;
import net.dnorberg.rivencraft.block.BuddingCrystalBlock;
import net.dnorberg.rivencraft.block.PortBookBinderBlock;
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.InkMixerBlockEntity;
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.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
@@ -54,6 +57,19 @@ public final class Rivencraft {
public static final Logger LOGGER = LogUtils.getLogger();
private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(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 = sound("linking.link");
public static final Supplier<SoundEvent> LINK_DISARM = sound("linking.link-disarm");
public static final Supplier<SoundEvent> LINK_INTRA = sound("linking.link-intra");
public static final Supplier<SoundEvent> LINK_FISSURE = sound("linking.link-fissure");
public static final Supplier<SoundEvent> LINK_PORTAL = sound("linking.link-portal");
public static final Supplier<SoundEvent> METEOR_ROAR = sound("entity.meteor.roar");
private static Supplier<SoundEvent> sound(String name) {
return SOUNDS.register(name, () -> SoundEvent.createVariableRangeEvent(
Identifier.fromNamespaceAndPath(MOD_ID, name)));
}
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID);
private static final DeferredRegister<MenuType<?>> MENUS =
@@ -94,6 +110,12 @@ public final class Rivencraft {
public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_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(
"book_binder", PortBookBinderBlock::new,
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
@@ -146,6 +168,7 @@ public final class Rivencraft {
public Rivencraft(IEventBus modEventBus) {
BLOCKS.register(modEventBus);
SOUNDS.register(modEventBus);
ITEMS.register(modEventBus);
BLOCK_ENTITY_TYPES.register(modEventBus);
MENUS.register(modEventBus);
@@ -155,6 +178,7 @@ public final class Rivencraft {
PortBiomeSources.register(modEventBus);
PortChunkGenerators.register(modEventBus);
NeoForge.EVENT_BUS.addListener(RuntimeAgeBackend::onLevelLoad);
NeoForge.EVENT_BUS.addListener(net.dnorberg.rivencraft.age.PortAgeWeather::onLevelTickPre);
NeoForge.EVENT_BUS.addListener(PortAgeCommands::register);
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogout);
@@ -24,7 +24,21 @@ final class AgeLevelIndex extends SavedData {
.forGetter(index -> index.biomePlans),
Codec.unboundedMap(Identifier.CODEC,
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())
.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),
Codec.unboundedMap(Identifier.CODEC, PortLighting.CODEC)
.optionalFieldOf("lighting", Map.of()).forGetter(index -> index.lighting),
Codec.unboundedMap(Identifier.CODEC, PortSkyStyle.CODEC)
.optionalFieldOf("sky", Map.of()).forGetter(index -> index.sky)
).apply(instance, AgeLevelIndex::new));
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
@@ -38,6 +52,13 @@ final class AgeLevelIndex extends SavedData {
private final Map<Identifier, Identifier> biomeOverrides;
private final Map<Identifier, PortBiomePlan> biomePlans;
private final Map<Identifier, PortAgeTerrain> terrain;
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 final Map<Identifier, PortLighting> lighting;
private final Map<Identifier, PortSkyStyle> sky;
private AgeLevelIndex() {
this.ages = new LinkedHashMap<>();
@@ -45,17 +66,38 @@ final class AgeLevelIndex extends SavedData {
this.biomeOverrides = new LinkedHashMap<>();
this.biomePlans = new LinkedHashMap<>();
this.terrain = new LinkedHashMap<>();
this.rocks = new LinkedHashMap<>();
this.vegetation = new LinkedHashMap<>();
this.dungeons = new LinkedHashMap<>();
this.lakes = new LinkedHashMap<>();
this.terrainFeatures = new LinkedHashMap<>();
this.lighting = new LinkedHashMap<>();
this.sky = new LinkedHashMap<>();
}
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
Map<Identifier, Identifier> biomeOverrides,
Map<Identifier, PortBiomePlan> biomePlans,
Map<Identifier, PortAgeTerrain> terrain) {
Map<Identifier, PortAgeTerrain> terrain,
Map<Identifier, PortRockPlan> rocks,
Map<Identifier, PortVegetationPlan> vegetation,
Map<Identifier, Integer> dungeons,
Map<Identifier, PortLakePlan> lakes,
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures,
Map<Identifier, PortLighting> lighting,
Map<Identifier, PortSkyStyle> sky) {
this.ages = new LinkedHashMap<>(ages);
this.retired = new LinkedHashSet<>(retired);
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
this.biomePlans = new LinkedHashMap<>(biomePlans);
this.terrain = new LinkedHashMap<>(terrain);
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);
this.lighting = new LinkedHashMap<>(lighting);
this.sky = new LinkedHashMap<>(sky);
}
Map<Identifier, Long> entries() {
@@ -81,13 +123,30 @@ final class AgeLevelIndex extends SavedData {
return terrain.getOrDefault(id, PortAgeTerrain.STANDARD);
}
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain) {
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); }
PortLighting lighting(Identifier id) { return lighting.getOrDefault(id, PortLighting.NORMAL); }
PortSkyStyle sky(Identifier id) { return sky.getOrDefault(id, PortSkyStyle.OVERWORLD); }
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, int dungeonCount,
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light, PortSkyStyle skyStyle) {
if (contains(id)) {
throw new IllegalArgumentException("Age already exists: " + id);
}
ages.put(id, seed);
if (biomePlan != null) biomePlans.put(id, biomePlan);
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
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);
if (light != PortLighting.NORMAL) lighting.put(id, light);
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
setDirty();
}
@@ -96,16 +155,33 @@ final class AgeLevelIndex extends SavedData {
biomeOverrides.remove(id);
biomePlans.remove(id);
terrain.remove(id);
rocks.remove(id);
vegetation.remove(id);
dungeons.remove(id);
lakes.remove(id);
terrainFeatures.remove(id);
lighting.remove(id);
sky.remove(id);
retired.add(id);
setDirty();
}
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
PortAgeTerrain ageTerrain) {
PortAgeTerrain ageTerrain, PortRockPlan rockPlan,
PortVegetationPlan vegetationPlan, int dungeonCount,
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light,
PortSkyStyle skyStyle) {
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
ages.put(id, seed);
if (biomePlan != null) biomePlans.put(id, biomePlan);
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
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);
if (light != PortLighting.NORMAL) lighting.put(id, light);
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
setDirty();
}
@@ -18,6 +18,8 @@ import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.Prediction;
import net.minecraft.world.item.ItemStack;
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.fml.ModList;
import java.util.Map;
@@ -66,6 +68,48 @@ public final class PortAgeCommands {
return 1;
})))
.then(Commands.literal("age")
.then(Commands.literal("create_sky")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("sky", StringArgumentType.word())
.then(Commands.argument("lighting", StringArgumentType.word())
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::createSkyAge)))))))
.then(Commands.literal("sky_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeSky)))
.then(Commands.literal("environment_probe")
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::probeEnvironment))))
.then(Commands.literal("create_lighting")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("mode", StringArgumentType.word())
.suggests((context, builder) -> net.minecraft.commands.SharedSuggestionProvider.suggest(
java.util.Arrays.stream(PortLighting.values())
.map(mode -> mode.name().toLowerCase(Locale.ROOT)), builder))
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::createLightingAge))))))
.then(Commands.literal("lighting_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeLighting)))
.then(Commands.literal("surface_probe")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("x", IntegerArgumentType.integer())
.then(Commands.argument("z", IntegerArgumentType.integer())
.executes(PortAgeCommands::probeSurface)))))
.then(Commands.literal("create_weather")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("mode", StringArgumentType.word())
.suggests((context, builder) -> net.minecraft.commands.SharedSuggestionProvider.suggest(
java.util.Arrays.stream(PortWeatherMode.values())
.map(mode -> mode.name().toLowerCase(Locale.ROOT)), builder))
.executes(PortAgeCommands::createWeatherAge)))))
.then(Commands.literal("weather_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeWeather)))
.then(Commands.literal("create")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
@@ -112,6 +156,72 @@ public final class PortAgeCommands {
: java.util.List.of(Identifier.withDefaultNamespace("plains"),
Identifier.withDefaultNamespace("forest"))));
})))))
.then(Commands.literal("create_random")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> {
long seed = LongArgumentType.getLong(context, "seed");
PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select(
context.getSource().getServer(), seed);
var environment = PortEnvironmentSelection.resolve(java.util.List.of(), seed, selected.terrain());
int created = createAge(context, selected.terrain(), selected.biomes(),
null, null, 0, null, null, environment);
if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
"Random Age plan: terrain=" + selected.terrain()
+ " biomes=" + selected.biomes()
+ " environment=" + environment), false);
return created;
}))))
.then(Commands.literal("create_rocks")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.FLAT,
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")
.executes(context -> {
var ages = RuntimeAgeBackend.savedAges(context.getSource().getServer());
@@ -215,6 +325,21 @@ public final class PortAgeCommands {
.then(Commands.argument("chunkOffset", IntegerArgumentType.integer(0))
.executes(context -> sampleAge(context,
IntegerArgumentType.getInteger(context, "chunkOffset"))))))
.then(Commands.literal("rocks_probe")
.then(Commands.argument("name", StringArgumentType.word())
.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.argument("name", StringArgumentType.word())
.then(Commands.argument("x", IntegerArgumentType.integer())
@@ -222,18 +347,416 @@ public final class PortAgeCommands {
.executes(PortAgeCommands::sampleBiome)))))));
}
private static int createSkyAge(CommandContext<CommandSourceStack> context) {
try {
PortSkyStyle sky = PortSkyStyle.valueOf(StringArgumentType.getString(context, "sky").toUpperCase(Locale.ROOT));
PortLighting lighting = PortLighting.valueOf(StringArgumentType.getString(context, "lighting").toUpperCase(Locale.ROOT));
PortAgeTerrain terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var pages = java.util.List.of(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "sky/" + sky.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/" + lighting.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "weather/off"));
var server = context.getSource().getServer();
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
RuntimeAgeBackend.create(server, ageId(context), LongArgumentType.getLong(context, "seed"),
biomes, terrain, null, null, 0, null, null, PortWeatherMode.OFF, lighting, sky);
return probeSky(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeSky(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
var server = context.getSource().getServer();
var level = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
return 0;
}
var type = level.dimensionType();
context.getSource().sendSuccess(() -> Component.literal("Sky probe " + id
+ " type=" + level.dimensionTypeRegistration().unwrapKey().orElseThrow().identifier()
+ " skybox=" + type.skybox() + " skylight=" + type.hasSkyLight() + " ambient=" + type.ambientLight()
+ " fog=" + level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.FOG_COLOR,
new net.minecraft.core.BlockPos(0, 80, 0))
+ " ceiling=" + type.hasCeiling() + " scale=" + type.coordinateScale()
+ " minY=" + type.minY() + " height=" + type.height() + " dragon=" + type.hasEnderDragonFight()), false);
return 1;
}
private static int probeEnvironment(CommandContext<CommandSourceStack> context) {
try {
long seed = LongArgumentType.getLong(context, "seed");
PortAgeTerrain terrain = PortAgeTerrain.valueOf(
StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var selected = PortEnvironmentSelection.resolve(java.util.List.of(), seed, terrain);
var explicitPages = java.util.List.of(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "weather/storm"),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/bright"));
var explicit = PortEnvironmentSelection.resolve(explicitPages, seed, terrain);
var weatherOnly = PortEnvironmentSelection.resolve(java.util.List.of(explicitPages.get(0)), seed, terrain);
var lightingOnly = PortEnvironmentSelection.resolve(java.util.List.of(explicitPages.get(1)), seed, terrain);
if (!selected.equals(PortEnvironmentSelection.resolve(java.util.List.of(), seed, terrain))
|| explicit.lighting() != PortLighting.BRIGHT
|| explicit.weather() != (terrain == PortAgeTerrain.CAVE ? PortWeatherMode.OFF : PortWeatherMode.STORM)
|| weatherOnly.lighting() != selected.lighting() || lightingOnly.weather() != selected.weather())
throw new IllegalStateException("Environment selection invariant failed");
context.getSource().sendSuccess(() -> Component.literal("Environment probe seed=" + seed
+ " terrain=" + terrain + " selected=" + selected + " explicit=" + explicit
+ " deterministic=true"), false);
return 1;
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int createLightingAge(CommandContext<CommandSourceStack> context) {
try {
PortLighting mode = PortLighting.valueOf(
StringArgumentType.getString(context, "mode").toUpperCase(Locale.ROOT));
PortAgeTerrain terrain = PortAgeTerrain.valueOf(
StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var pages = java.util.List.of(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/" + mode.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
var server = context.getSource().getServer();
var biomePlan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
RuntimeAgeBackend.create(server, ageId(context), LongArgumentType.getLong(context, "seed"),
biomePlan, terrain, null, null, 0, null, null, PortWeatherMode.NORMAL, mode);
return probeLighting(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeLighting(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
var server = context.getSource().getServer();
ServerLevel level = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
return 0;
}
context.getSource().sendSuccess(() -> Component.literal("Lighting probe " + id
+ " type=" + level.dimensionTypeRegistration().unwrapKey().orElseThrow().identifier()
+ " skylight=" + level.dimensionType().hasSkyLight()
+ " ambient=" + level.dimensionType().ambientLight()
+ " undergroundBrightness=" + level.getMaxLocalRawBrightness(
new net.minecraft.core.BlockPos(0, level.getMinY() + 2, 0))
+ " vanillaDarkEnough=" + net.minecraft.world.entity.monster.Monster.isDarkEnoughToSpawn(
level, new net.minecraft.core.BlockPos(0, level.getMinY() + 2, 0),
net.minecraft.util.RandomSource.create(0))
+ " overworldSkylight=" + server.overworld().dimensionType().hasSkyLight()
+ " overworldAmbient=" + server.overworld().dimensionType().ambientLight()), false);
return 1;
}
private static int probeSurface(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 x = IntegerArgumentType.getInteger(context, "x");
int z = IntegerArgumentType.getInteger(context, "z");
var obstruction = PortAgeTravel.highestObstruction(level, x, z);
var landing = PortAgeTravel.findSurfaceLanding(level, x, z);
context.getSource().sendSuccess(() -> Component.literal("Surface probe " + id
+ " highest=" + obstruction + " landing=" + landing), false);
return landing == null ? 0 : 1;
}
private static int createWeatherAge(CommandContext<CommandSourceStack> context) {
try {
PortWeatherMode mode = PortWeatherMode.valueOf(
StringArgumentType.getString(context, "mode").toUpperCase(Locale.ROOT));
Identifier id = ageId(context);
var server = context.getSource().getServer();
// Exercise the Binder's page compiler too, not just direct backend creation.
var pages = java.util.List.of(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
"weather/" + mode.name().toLowerCase(Locale.ROOT)));
var biomePlan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
pages, server.registryAccess());
RuntimeAgeBackend.create(server, id,
LongArgumentType.getLong(context, "seed"), biomePlan, PortAgeTerrain.FLAT,
null, null, 0, null, null, mode);
return probeWeather(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeWeather(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
var server = context.getSource().getServer();
ServerLevel loaded = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (!(loaded instanceof PortAgeLevel level)) {
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
return 0;
}
var weather = level.getWeatherData();
context.getSource().sendSuccess(() -> Component.literal("Weather probe " + id
+ " mode=" + level.weatherMode() + " raining=" + weather.isRaining()
+ " thundering=" + weather.isThundering() + " rainTicks=" + weather.getRainTime()
+ " thunderTicks=" + weather.getThunderTime()
+ " independent=" + (weather != server.getWeatherData())
+ " overworldRaining=" + server.getWeatherData().isRaining()
+ " overworldThundering=" + server.getWeatherData().isThundering()), false);
return 1;
}
private static int createAge(CommandContext<CommandSourceStack> context, PortAgeTerrain terrain) {
return createAge(context, terrain, null);
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan) {
return createAge(context, terrain, biomePlan, null);
}
private static int probeRocks(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.rocks() == null) {
context.getSource().sendFailure(Component.literal("Age has no loaded Rocks generator: " + id));
return 0;
}
var materials = features.rocks().materials().stream()
.map(net.minecraft.core.registries.BuiltInRegistries.BLOCK::getValue).toList();
int count = 0;
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;
var state = level.getBlockState(new net.minecraft.core.BlockPos(x, y, z));
if (materials.stream().anyMatch(state::is)) count++;
}
int placed = count;
context.getSource().sendSuccess(() -> Component.literal(
"Rocks probe " + id + " materials=" + features.rocks().materials() + " count=" + placed), false);
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,
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) {
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
new PortEnvironmentSelection(PortWeatherMode.NORMAL, PortLighting.NORMAL));
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures,
PortEnvironmentSelection environment) {
String name = StringArgumentType.getString(context, "name");
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
long seed = LongArgumentType.getLong(context, "seed");
ServerLevel level;
try {
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan, terrain);
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
environment.weather(), environment.lighting());
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
@@ -0,0 +1,48 @@
package net.dnorberg.rivencraft.age;
import java.util.List;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.dimension.LevelStem;
import net.minecraft.world.level.saveddata.WeatherData;
import net.minecraft.world.level.storage.DerivedLevelData;
/** Runtime Age with independent native weather, rather than 26.3's shared server cycle. */
final class PortAgeLevel extends ServerLevel {
private final WeatherData ageWeather;
private final PortWeatherSettings weatherSettings;
private final boolean cave;
PortAgeLevel(MinecraftServer server, ResourceKey<Level> key, LevelStem stem,
long biomeZoomSeed, PortAgeTerrain terrain) {
super(server, server.executor, server.storageSource,
new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()),
key, stem, server.getWorldData().isDebugWorld(), biomeZoomSeed, List.of(), false);
ageWeather = getDataStorage().computeIfAbsent(WeatherData.TYPE);
weatherSettings = getDataStorage().computeIfAbsent(PortWeatherSettings.TYPE);
cave = terrain == PortAgeTerrain.CAVE;
PortAgeWeather.apply(this);
// The base constructor initializes visual levels from server weather.
// Replace them before publishing this Age or sending its join packets.
setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F);
setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F);
}
@Override
public WeatherData getWeatherData() {
// The base constructor may query weather before our fields are initialized.
return ageWeather == null ? super.getWeatherData() : ageWeather;
}
PortWeatherMode weatherMode() { return cave ? PortWeatherMode.OFF : weatherSettings.mode(); }
void configureWeather(PortWeatherMode mode) {
weatherSettings.setMode(mode);
PortAgeWeather.apply(this);
setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F);
setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F);
getDataStorage().saveAndJoin();
}
}
@@ -11,8 +11,6 @@ import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.FlatLevelSource;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
/** Shared, server-authoritative landing for the 26.3 book and operator probe. */
public final class PortAgeTravel {
@@ -25,19 +23,22 @@ public final class PortAgeTravel {
if (!AgeCatalogSync.ready(player, id)) {
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 = isCaveTerrain(target)
? findFootedDestination(target, landing).orElse(null)
: findSurfaceLanding(target, landing.getX(), landing.getZ());
if (landing == null) landing = findLanding(target);
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,
Set.of(), player.getYRot(), player.getXRot(), true);
return moved ? null : "Teleportation failed; remaining in source dimension";
}
private static BlockPos findLanding(ServerLevel target) {
if (target.getChunkSource().getGenerator() 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())) {
if (isCaveTerrain(target)) {
// The cave roof is not a valid arrival surface. Search down from
// Legacy's Y=64 start, then construct the normal safety platform
// at that height only when no footed opening can be found.
@@ -54,36 +55,50 @@ public final class PortAgeTravel {
}
return makePlatform(target, new BlockPos(0, 64, 0));
}
// A Flat Age has a known surface. Use it directly before a heightmap
// fallback can place a platform in the air above a low superflat world.
FlatLevelGeneratorSettings flatSettings = switch (target.getChunkSource().getGenerator()) {
case FlatLevelSource flat -> flat.settings();
case PortFlatChunkGenerator flat -> flat.settings();
default -> null;
};
if (flatSettings != null && !flatSettings.getLayersInfo().isEmpty()) {
int surfaceY = target.getMinY() + flatSettings.getLayersInfo().stream()
.mapToInt(layer -> layer.getHeight()).sum();
BlockPos surface = new BlockPos(0, surfaceY, 0);
if (hasFooting(target, surface)) return surface;
}
BlockPos surface = findSurfaceLanding(target, 0, 0);
if (surface != null) return surface;
// Only build above the actual highest obstruction, never in a pocket
// beneath it. Void Ages retain the usual platform floor at Y=63.
BlockPos obstruction = highestObstruction(target, 0, 0);
int y = obstruction == null ? 64 : Math.max(64, obstruction.getY() + 3);
return makePlatform(target, new BlockPos(0, y, 0));
}
static BlockPos findSurfaceLanding(ServerLevel target, int originX, int originZ) {
for (int radius = 0; radius <= 16; radius++) {
for (int x = -radius; x <= radius; x++) {
for (int z = -radius; z <= radius; z++) {
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
int y = target.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z);
BlockPos foot = new BlockPos(x, y, z);
if (hasFooting(target, foot)) {
return foot;
}
BlockPos floor = highestObstruction(target, originX + x, originZ + z);
if (floor != null && hasFooting(target, floor.above())) return floor.above();
}
}
}
// The operator/book probe needs a deterministic landing even when
// origin is ocean or foliage. Final book landing rules are ported later.
int y = Math.max(64, target.getHeight(Heightmap.Types.MOTION_BLOCKING, 0, 0) + 2);
if (y + 2 >= target.getMaxY()) return null;
return makePlatform(target, new BlockPos(0, y, 0));
return null;
}
/** First obstructing surface seen from the sky; never continue through a roof or liquid. */
static BlockPos highestObstruction(ServerLevel level, int x, int z) {
var chunk = level.getChunk(x >> 4, z >> 4);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(x, level.getMaxY() - 1, z);
for (int y = level.getMaxY() - 1; y >= level.getMinY(); y--) {
pos.setY(y);
var state = chunk.getBlockState(pos);
if (!state.isAir() && (!state.getFluidState().isEmpty()
|| state.is(net.minecraft.tags.BlockTags.LEAVES)
|| !state.getCollisionShape(level, pos).isEmpty())) return pos.immutable();
}
return null;
}
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) {
@@ -106,11 +121,45 @@ public final class PortAgeTravel {
}
private static boolean hasFooting(ServerLevel target, BlockPos foot) {
if (foot.getY() <= target.getMinY() || foot.getY() + 1 >= target.getMaxY()
|| !target.getWorldBorder().isWithinBounds(foot)) return false;
BlockPos floor = foot.below();
return target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP)
var floorState = target.getBlockState(floor);
return (floorState.isFaceSturdy(target, floor, Direction.UP)
|| floorState.is(net.minecraft.tags.BlockTags.LEAVES))
&& floorState.getFluidState().isEmpty()
&& target.getFluidState(foot).isEmpty()
&& target.getFluidState(foot.above()).isEmpty()
&& target.getBlockState(foot).getCollisionShape(target, foot).isEmpty()
&& target.getBlockState(foot.above()).getCollisionShape(target, foot.above()).isEmpty();
&& passable(target, foot) && passable(target, foot.above());
}
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();
}
}
@@ -0,0 +1,52 @@
package net.dnorberg.rivencraft.age;
import java.util.Map;
import java.util.WeakHashMap;
import net.minecraft.world.level.saveddata.WeatherData;
import net.neoforged.neoforge.event.tick.LevelTickEvent;
/** 1.21.1 weather policies, applied before the independent native Age cycle advances. */
public final class PortAgeWeather {
private static final Map<PortAgeLevel, Integer> STATES = new WeakHashMap<>();
private PortAgeWeather() {}
public static void onLevelTickPre(LevelTickEvent.Pre event) {
if (event.getLevel() instanceof PortAgeLevel level) apply(level);
}
static void apply(PortAgeLevel level) {
PortWeatherMode mode = level.weatherMode();
if (mode == PortWeatherMode.NORMAL) {
STATES.remove(level);
return;
}
WeatherData weather = level.getWeatherData();
if (mode == PortWeatherMode.FAST || mode == PortWeatherMode.SLOW) {
int state = (weather.isRaining() ? 1 : 0) | (weather.isThundering() ? 2 : 0);
Integer previous = STATES.put(level, state);
if (previous != null && previous == state) return;
// Keep persisted timers on reload; rescale only when a cycle changes.
if (previous == null && weather.getRainTime() > 0) return;
int scale = mode == PortWeatherMode.FAST ? 1 : 4;
int duration = 6000 * scale + level.getRandom().nextInt(
(weather.isRaining() ? 6000 : 84000) * scale);
set(weather, duration, weather.isRaining(), weather.isThundering());
} else {
STATES.remove(level);
boolean rain = mode != PortWeatherMode.OFF;
boolean thunder = mode == PortWeatherMode.STORM;
if (weather.isRaining() != rain || weather.isThundering() != thunder
|| weather.getRainTime() < 200 || weather.getThunderTime() < 200
|| weather.getClearWeatherTime() != 0) set(weather, 6000, rain, thunder);
}
}
private static void set(WeatherData data, int duration, boolean rain, boolean thunder) {
data.setClearWeatherTime(0);
data.setRainTime(duration);
data.setThunderTime(duration);
data.setRaining(rain);
data.setThundering(thunder);
}
}
@@ -0,0 +1,78 @@
package net.dnorberg.rivencraft.age;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.QuartPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.tags.BiomeTags;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.CarvingMask;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.LegacyRandomSource;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.WorldGenerationContext;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import net.minecraft.world.level.levelgen.carver.WorldCarver;
/** Authored Caves/Ravines, independent of the terrain page, with 1.21.1 source seeds. */
public final class PortCarverPages {
public static final Identifier CAVES = Identifier.fromNamespaceAndPath("rivencraft", "feature/caves");
public static final Identifier RAVINES = Identifier.fromNamespaceAndPath("rivencraft", "feature/ravines");
private static final long CAVE_SALT = 0x4341564553464541L;
private static final long RAVINE_SALT = 0x524156494E45534CL;
private PortCarverPages() {}
public static boolean supports(Identifier page) { return CAVES.equals(page) || RAVINES.equals(page); }
public static void validateCounts(List<Identifier> pages) {
Map<Identifier, Integer> counts = new HashMap<>();
for (Identifier page : pages) if (!supports(page) || counts.merge(page, 1, Integer::sum) > 32)
throw new IllegalArgumentException("Carver plan requires Caves/Ravines pages, at most 32 per type");
}
public static void validateRegistry(List<Identifier> pages, HolderLookup.Provider registries) {
if (pages.stream().noneMatch(PortCarverPages::supports)) return;
var carvers = registries.lookupOrThrow(Registries.CARVER);
for (String name : pages.contains(CAVES) ? List.of("cave", "nether_cave", "canyon") : List.of("canyon")) {
if (name.equals("canyon") && !pages.contains(RAVINES)) continue;
carvers.get(ResourceKey.create(Registries.CARVER, Identifier.withDefaultNamespace(name)))
.orElseThrow(() -> new IllegalArgumentException("Missing authored carver minecraft:" + name));
}
}
public static void carve(ChunkGenerator generator, ChunkAccess chunk, RandomState randomState,
HolderLookup.Provider registries, List<Identifier> pages, long seed, CarvingMask mask) {
if (pages.isEmpty()) return;
var registry = registries.lookupOrThrow(Registries.CARVER);
var biomes = generator.getBiomeSource().createUncachedResolver(randomState);
var context = new WorldGenerationContext(generator, chunk.getHeightAccessorForGeneration());
var random = new WorldgenRandom(new LegacyRandomSource(0L));
ChunkPos target = chunk.getPos();
// 1.21.1 repeats identical seeds for duplicate Caves/Ravines pages.
// Their union is identical; avoid replaying the same mask up to 32 times.
for (Identifier page : pages.stream().distinct().toList()) {
boolean ravine = page.equals(RAVINES);
WorldCarver overworld = registry.getOrThrow(ResourceKey.create(Registries.CARVER,
Identifier.withDefaultNamespace(ravine ? "canyon" : "cave"))).value();
WorldCarver nether = ravine ? null : registry.getOrThrow(ResourceKey.create(Registries.CARVER,
Identifier.withDefaultNamespace("nether_cave"))).value();
for (int dx = -8; dx <= 8; dx++) {
for (int dz = -8; dz <= 8; dz++) {
ChunkPos source = new ChunkPos(target.x() + dx, target.z() + dz);
boolean isNether = biomes.getNoiseBiome(QuartPos.fromBlock(source.getMinBlockX()), 0,
QuartPos.fromBlock(source.getMinBlockZ())).is(BiomeTags.IS_NETHER);
if (ravine && isNether) continue;
WorldCarver carver = isNether ? nether : overworld;
random.setLargeFeatureSeed(seed ^ (ravine ? RAVINE_SALT : CAVE_SALT), source.x(), source.z());
if (carver.isStartChunk(random)) carver.carve(context, random, target, source, mask);
}
}
}
}
}
@@ -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,28 @@
package net.dnorberg.rivencraft.age;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import net.minecraft.resources.Identifier;
/** Seed-completes missing environment controllers once, before an Age is saved. */
public record PortEnvironmentSelection(PortWeatherMode weather, PortLighting lighting) {
public static PortEnvironmentSelection resolve(List<Identifier> pages, long seed, PortAgeTerrain terrain) {
PortLighting lighting = pages.stream().map(PortLighting::forPage).filter(java.util.Objects::nonNull)
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x4C49474854494E47L), PortLighting.values()));
PortWeatherMode weather = terrain == PortAgeTerrain.CAVE ? PortWeatherMode.OFF
: pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x574541544845525FL), PortWeatherMode.values()));
return new PortEnvironmentSelection(weather, lighting);
}
private static <T> T rankedDefault(Random random, T[] choices) {
// LegacyRankChoice: the first (Normal) option is rank 1, the rest rank 2.
// Its rank-1 weight is the complete rare group plus one. Derive this
// from the supported pool, rather than selecting unported weather.
int[] weights = new int[choices.length];
Arrays.fill(weights, 1);
weights[0] = choices.length;
return PortRandomAgeSelection.weighted(random, List.of(choices), weights);
}
}
@@ -0,0 +1,290 @@
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) {
if (hasStructurePages()) {
var lookup = PortStructurePages.filtered(sets, terrainFeatures.structures(), getBiomeSource());
var planning = PortStructurePages.planningBiomes(getBiomeSource(), sets, terrainFeatures.structures());
if (base instanceof PortFlatChunkGenerator || base instanceof net.minecraft.world.level.levelgen.FlatLevelSource)
return ChunkGeneratorStructureState.createForFlat(random, seed, getOrigin(random), planning,
lookup.listElements().map(holder -> (Holder<StructureSet>) holder));
return ChunkGeneratorStructureState.createForNormal(random, seed, getOrigin(random), planning, lookup);
}
return base.createState(sets, random, seed);
}
private boolean hasStructurePages() {
return terrainFeatures != null && !terrainFeatures.structures().isEmpty();
}
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
StructureManager structures, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> dimension) {
if (hasStructurePages()) super.createStructures(registries, state, structures, chunk, templates, dimension);
else 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));
boolean surfaceBiome = !biome.is(BiomeTags.IS_END) && !PortRegistryPages.isCaveBiome(biome);
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
List<BlockPos> mushroomHosts = null;
for (int occurrence = 0; occurrence < vegetation.pages().size(); occurrence++) {
Identifier page = vegetation.pages().get(occurrence);
RandomSource random = RandomSource.create(chunkSeed
^ (long) page.hashCode() * 0x9E3779B97F4A7C15L
^ (long) occurrence * 0xD1B54A32D192ED03L);
if (page.equals(PortVegetationPlan.SHELF_MUSHROOMS)) {
if (mushroomHosts == null) mushroomHosts = PortShelfMushrooms.findHosts(chunk);
PortShelfMushrooms.decorate(level, chunk, mushroomHosts, random);
continue;
}
if (!surfaceBiome) continue;
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());
authored.place(level, this, random, origin);
if (PortVegetationPlan.addsLogs(page)) mushroomHosts = null;
}
}
}
}
@@ -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,52 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import java.util.Locale;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.Level;
/** Authored lighting choices using statically registered, client-synchronized types. */
public enum PortLighting {
NORMAL, DARK, BRIGHT;
public static final Codec<PortLighting> CODEC = Codec.STRING.xmap(
name -> valueOf(name.toUpperCase(Locale.ROOT)), mode -> mode.name().toLowerCase(Locale.ROOT));
public static PortLighting forPage(Identifier page) {
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
return switch (page.getPath()) {
case "lighting/normal" -> NORMAL;
case "lighting/dark" -> DARK;
case "lighting/bright" -> BRIGHT;
default -> null;
};
}
public static boolean isBright(Level level) {
return level.dimensionTypeRegistration().unwrapKey().map(key -> {
Identifier id = key.identifier();
return id.getNamespace().equals(Rivencraft.MOD_ID)
&& (id.getPath().equals("age_bright") || id.getPath().equals("age_cave_bright")
|| id.getPath().startsWith("age_bright_sky_")
|| id.getPath().startsWith("age_cave_bright_sky_"));
}).orElse(false);
}
Holder<DimensionType> dimensionType(MinecraftServer server, PortAgeTerrain terrain,
Holder<DimensionType> overworldType, PortSkyStyle sky) {
boolean cave = terrain == PortAgeTerrain.CAVE;
if (this == NORMAL && !cave && sky == PortSkyStyle.OVERWORLD) return overworldType;
String variant = this == BRIGHT ? (cave ? "age_cave_bright" : "age_bright")
: this == DARK || cave ? "age_dark" : "age_normal";
if (sky != PortSkyStyle.OVERWORLD) variant += "_sky_" + sky.name().toLowerCase(Locale.ROOT);
return server.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(
ResourceKey.create(Registries.DIMENSION_TYPE,
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, variant)));
}
}
@@ -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;
}
}
@@ -0,0 +1,81 @@
package net.dnorberg.rivencraft.age;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import net.dnorberg.rivencraft.writing.PortRegistryPages;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
/** Seed-frozen terrain and biome choices for a book with no authored symbols. */
public final class PortRandomAgeSelection {
private PortRandomAgeSelection() {}
public record Selection(PortAgeTerrain terrain, PortBiomePlan biomes) {}
public static Selection select(MinecraftServer server, long seed) {
// Match the 1.21.1 Legacy-rank choices: Standard and Flat are common;
// the other terrain families are rarer. The sorted order is deliberate.
PortAgeTerrain terrain = weighted(new Random(seed ^ 0x5445525241494E4CL),
List.of(PortAgeTerrain.AMPLIFIED, PortAgeTerrain.CAVE, PortAgeTerrain.FLAT,
PortAgeTerrain.ISLAND, PortAgeTerrain.STANDARD, PortAgeTerrain.VOID),
new int[]{1, 1, 5, 1, 6, 1});
if (terrain == PortAgeTerrain.VOID)
return new Selection(terrain, new PortBiomePlan(PortBiomePlan.Distribution.SINGLE,
List.of(Identifier.withDefaultNamespace("the_void"))));
PortBiomePlan.Distribution distribution = weighted(new Random(seed ^ 0x524956454E435241L),
List.of(PortBiomePlan.Distribution.NATIVE, PortBiomePlan.Distribution.SINGLE,
PortBiomePlan.Distribution.MEDIUM, PortBiomePlan.Distribution.TILED,
PortBiomePlan.Distribution.GRID, PortBiomePlan.Distribution.TINY,
PortBiomePlan.Distribution.SMALL, PortBiomePlan.Distribution.LARGE,
PortBiomePlan.Distribution.HUGE),
new int[]{3, 3, 3, 1, 1, 1, 1, 1, 1});
List<Identifier> eligible = eligibleBiomes(server, terrain);
if (eligible.size() < 2 && distribution != PortBiomePlan.Distribution.NATIVE)
distribution = PortBiomePlan.Distribution.SINGLE;
// 26.3 has no native multi-cave biome source; choose a seeded Cave
// biome rather than silently using Lush Caves for every random Age.
if (terrain == PortAgeTerrain.CAVE && distribution == PortBiomePlan.Distribution.NATIVE)
distribution = PortBiomePlan.Distribution.SINGLE;
if (distribution == PortBiomePlan.Distribution.NATIVE)
return new Selection(terrain, new PortBiomePlan(distribution, List.of()));
int count = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
if (count > 1) {
Random countRandom = new Random(seed ^ 0x42494F4D45434F55L);
while (count < eligible.size() && countRandom.nextInt(3) != 0) count++;
}
List<Identifier> shuffled = new ArrayList<>(eligible);
Collections.shuffle(shuffled, new Random(seed ^ 0x42494F4D45504F4FL));
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
}
private static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
if (terrain == PortAgeTerrain.CAVE) {
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
.filter(PortRegistryPages::isCaveBiome)
.map(holder -> holder.key().identifier())
.sorted().toList();
return caves.isEmpty() ? List.of(Identifier.withDefaultNamespace("lush_caves")) : caves;
}
List<Identifier> surface = server.overworld().getChunkSource().getGenerator().getBiomeSource()
.possibleBiomes().stream().flatMap(holder -> holder.unwrapKey().stream())
.map(key -> key.identifier()).distinct().sorted(Comparator.naturalOrder()).toList();
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
}
static <T> T weighted(Random random, List<T> choices, int[] weights) {
int total = 0;
for (int weight : weights) total += weight;
int ticket = random.nextInt(total);
for (int index = 0; index < choices.size(); index++) {
ticket -= weights[index];
if (ticket < 0) return choices.get(index);
}
throw new IllegalStateException("Random Age ticket was not consumed");
}
}
@@ -0,0 +1,24 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import java.util.Optional;
import net.minecraft.resources.Identifier;
/** Frozen, ordered Rocks materials. Old single-material beta Ages remain readable. */
public record PortRockPlan(List<Identifier> materials) {
public static final Codec<PortRockPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Identifier.CODEC.optionalFieldOf("material").forGetter(plan -> Optional.empty()),
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,70 @@
package net.dnorberg.rivencraft.age;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.tags.BlockTags;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.ShelfMushroomBlock;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.feature.treedecorators.ShelfMushroomDecorator;
import net.minecraft.world.level.levelgen.feature.treedecorators.TreeDecorator;
/** Shelf mushrooms on exposed log faces, regardless of how the logs were generated. */
final class PortShelfMushrooms {
private PortShelfMushrooms() {}
static List<BlockPos> findHosts(ChunkAccess chunk) {
List<BlockPos> logs = new ArrayList<>();
var sections = chunk.getSections();
for (int index = 0; index < sections.length; index++) {
var section = sections[index];
if (!section.maybeHas(state -> state.is(BlockTags.LOGS))) continue;
int baseY = chunk.getSectionYFromSectionIndex(index) << 4;
for (int y = 0; y < 16; y++) for (int z = 0; z < 16; z++) for (int x = 0; x < 16; x++) {
if (section.getBlockState(x, y, z).is(BlockTags.LOGS))
logs.add(new BlockPos(chunk.getPos().getMinBlockX() + x, baseY + y,
chunk.getPos().getMinBlockZ() + z));
}
}
return logs;
}
static void decorate(WorldGenLevel level, ChunkAccess chunk, List<BlockPos> logs, RandomSource random) {
// Reuse vanilla replaceability/water checks, but not its tree-base height
// band: a log tendril or sphere has no meaningful tree base.
TreeDecorator.Context context = new TreeDecorator.Context(level,
(pos, state) -> level.setBlock(pos, state, 2), random, Set.of(), Set.of(), Set.of());
for (BlockPos log : logs) {
if (!level.getBlockState(log).is(BlockTags.LOGS)) continue;
Direction first = Direction.Plane.HORIZONTAL.getRandomDirection(random);
for (Direction facing : List.of(first, first.getClockWise())) {
if (random.nextFloat() > 0.25F) continue;
BlockPos pos = log.relative(facing);
if ((pos.getX() >> 4) != chunk.getPos().x() || (pos.getZ() >> 4) != chunk.getPos().z()) continue;
if (pos.getY() < level.getMinY() || pos.getY() >= level.getMaxY()) continue;
if (!level.getFluidState(pos).isEmpty()
|| !ShelfMushroomDecorator.isBlockReplaceableWithShelfMushroom(context, pos)
|| level.getBlockState(pos.below()).is(Blocks.SHELF_MUSHROOM)) continue;
boolean adjacent = false;
for (Direction side : Direction.Plane.HORIZONTAL) {
if (level.getBlockState(pos.relative(side)).is(Blocks.SHELF_MUSHROOM)) {
adjacent = true;
break;
}
}
if (adjacent) continue;
var mushroom = Blocks.SHELF_MUSHROOM.defaultBlockState()
.setValue(ShelfMushroomBlock.FACING, facing)
.setValue(ShelfMushroomBlock.AGE, random.nextInt(2));
if (!mushroom.canSurvive(level, pos)) continue;
context.setBlock(pos, mushroom);
break;
}
}
}
}
@@ -0,0 +1,31 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import java.util.List;
import java.util.Locale;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
/** Persisted sky styles, including the client-rendered Legacy static background. */
public enum PortSkyStyle {
OVERWORLD, NETHER, END, STATIC;
public static final Codec<PortSkyStyle> CODEC = Codec.STRING.xmap(
name -> valueOf(name.toUpperCase(Locale.ROOT)), sky -> sky.name().toLowerCase(Locale.ROOT));
public static PortSkyStyle forPage(Identifier page) {
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
return switch (page.getPath()) {
case "sky/overworld" -> OVERWORLD;
case "sky/nether" -> NETHER;
case "sky/end" -> END;
case "sky/static" -> STATIC;
default -> null;
};
}
public static PortSkyStyle fromPages(List<Identifier> pages) {
return pages.stream().map(PortSkyStyle::forPage).filter(java.util.Objects::nonNull)
.findFirst().orElse(OVERWORLD);
}
}
@@ -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,180 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.MapCodec;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import net.dnorberg.rivencraft.mixin.StructurePlacementAccessor;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.HolderOwner;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
/** 1.21.1 structure planning/density rules adapted to modern placement classes. */
public final class PortStructurePages {
private static final Map<Identifier, Identifier> SETS = Map.ofEntries(
vanillaSet("villages", "villages"),
vanillaSet("mineshafts", "mineshafts"),
vanillaSet("strongholds", "strongholds"),
vanillaSet("trial_chambers", "trial_chambers"),
vanillaSet("buried_treasure", "buried_treasures"),
vanillaSet("shipwrecks", "shipwrecks"),
vanillaSet("ocean_ruins", "ocean_ruins"),
vanillaSet("pillager_outposts", "pillager_outposts"),
vanillaSet("ruined_portals", "ruined_portals"),
vanillaSet("nether_fossils", "nether_fossils"),
vanillaSet("ancient_cities", "ancient_cities"),
vanillaSet("trail_ruins", "trail_ruins"),
vanillaSet("abandoned_camps", "abandoned_camp"),
Map.entry(page("nether_fortresses"), Identifier.fromNamespaceAndPath("rivencraft", "fortresses")),
Map.entry(page("bastions"), Identifier.fromNamespaceAndPath("rivencraft", "bastions")));
// Suppress vanilla's combined Nether set so an authored Fortress page does
// not also introduce Bastions (or vice versa).
private static final Set<Identifier> CONTROLLED_SETS = Stream.concat(SETS.values().stream(),
Stream.of(Identifier.withDefaultNamespace("nether_complexes")))
.collect(java.util.stream.Collectors.toUnmodifiableSet());
private static final Set<Identifier> CAVE_ONLY = Set.of(
page("nether_fortresses"), page("bastions"), page("nether_fossils"));
private static final Set<Identifier> SURFACE_ONLY = Set.of(
page("villages"), page("strongholds"), page("buried_treasure"), page("shipwrecks"),
page("ocean_ruins"), page("pillager_outposts"), page("trail_ruins"), page("abandoned_camps"));
private static final Set<Identifier> FLAT_ALLOWED = Set.of(page("villages"), page("strongholds"));
private PortStructurePages() {}
private static Identifier page(String name) {
return Identifier.fromNamespaceAndPath("rivencraft", "feature/" + name);
}
private static Map.Entry<Identifier, Identifier> vanillaSet(String page, String set) {
return Map.entry(page(page), Identifier.withDefaultNamespace(set));
}
public static boolean supports(Identifier page) { return SETS.containsKey(page); }
public static void validateCounts(List<Identifier> pages) {
Map<Identifier, Integer> counts = new HashMap<>();
for (Identifier page : pages) {
if (!supports(page) || counts.merge(page, 1, Integer::sum) > 32)
throw new IllegalArgumentException("Structure plan requires supported pages, at most 32 per feature");
}
}
public static boolean supportsTerrain(Identifier page, PortAgeTerrain terrain) {
if (!supports(page) || terrain == PortAgeTerrain.VOID) return false;
if (terrain == PortAgeTerrain.FLAT && !FLAT_ALLOWED.contains(page)) return false;
if (CAVE_ONLY.contains(page)) return terrain == PortAgeTerrain.CAVE;
return terrain != PortAgeTerrain.CAVE || !SURFACE_ONLY.contains(page);
}
public static void validateRegistry(List<Identifier> pages, HolderLookup.Provider registries) {
var sets = registries.lookupOrThrow(Registries.STRUCTURE_SET);
pages.stream().filter(PortStructurePages::supports).distinct().forEach(page ->
sets.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)))
.orElseThrow(() -> new IllegalArgumentException("Missing structure set for " + page)));
}
private static Map<Identifier, Integer> enabled(List<Identifier> pages) {
Map<Identifier, Integer> result = new HashMap<>();
for (Identifier page : pages) result.merge(SETS.get(page), 1, Integer::sum);
return result;
}
public static boolean authored(Structure structure, HolderLookup.Provider registries, List<Identifier> pages) {
var sets = registries.lookupOrThrow(Registries.STRUCTURE_SET);
return enabled(pages).keySet().stream()
.map(id -> sets.getOrThrow(ResourceKey.create(Registries.STRUCTURE_SET, id)).value())
.flatMap(set -> set.structures().stream()).anyMatch(entry -> entry.structure().value() == structure);
}
/** Extra possible biomes affect planning only; terrain still uses its original source. */
public static BiomeSource planningBiomes(BiomeSource actual, HolderLookup<StructureSet> sets, List<Identifier> pages) {
List<Holder<Biome>> extra = enabled(pages).keySet().stream()
.map(id -> sets.getOrThrow(ResourceKey.create(Registries.STRUCTURE_SET, id)).value())
.flatMap(set -> set.structures().stream()).flatMap(entry -> entry.structure().value().biomes().stream())
.distinct().toList();
return new BiomeSource() {
@Override protected MapCodec<? extends BiomeSource> codec() {
throw new UnsupportedOperationException("Planning biome sources are never serialized");
}
@Override protected Stream<Holder<Biome>> collectPossibleBiomes() {
return Stream.concat(actual.possibleBiomes().stream(), extra.stream());
}
@Override public net.minecraft.world.level.biome.BiomeResolver createResolver(Climate.Sampler sampler) {
return actual.createResolver(sampler);
}
};
}
public static HolderLookup<StructureSet> filtered(HolderLookup<StructureSet> source, List<Identifier> pages,
BiomeSource actual) {
Map<Identifier, Integer> counts = enabled(pages);
Map<ResourceKey<StructureSet>, Holder.Reference<StructureSet>> copies = new HashMap<>();
return new HolderLookup<>() {
@Override public Optional<Holder.Reference<StructureSet>> get(ResourceKey<StructureSet> key) {
if (CONTROLLED_SETS.contains(key.identifier()) && !counts.containsKey(key.identifier())) return Optional.empty();
return source.get(key).filter(holder -> counts.containsKey(key.identifier()) || hasBiome(holder.value(), actual))
.map(holder -> copies.computeIfAbsent(key, ignored -> {
int count = counts.getOrDefault(key.identifier(), 1);
return count == 1 ? holder : new LocalReference(key, scaled(holder.value(), count));
}));
}
@Override public Optional<HolderSet.Named<StructureSet>> get(TagKey<StructureSet> key) { return source.get(key); }
@Override public Stream<Holder.Reference<StructureSet>> listElements() {
return source.listElements().flatMap(holder -> get(holder.key()).stream());
}
@Override public Stream<HolderSet.Named<StructureSet>> listTags() { return source.listTags(); }
};
}
private static boolean hasBiome(StructureSet set, BiomeSource source) {
Set<Holder<Biome>> possible = source.possibleBiomes();
return set.structures().stream().flatMap(entry -> entry.structure().value().biomes().stream()).anyMatch(possible::contains);
}
private static StructureSet scaled(StructureSet set, int count) {
var placement = set.placement();
if (!(placement instanceof RandomSpreadStructurePlacement)
&& !(placement instanceof ConcentricRingsStructurePlacement)) return set;
StructurePlacementAccessor settings = (StructurePlacementAccessor) placement;
if (placement instanceof RandomSpreadStructurePlacement random) {
float original = settings.rivencraft$frequency();
float frequency = Math.min(1.0F, original * count);
double frequencyFactor = original > 0 ? frequency / original : 1;
int spacing = Math.max(1, (int) Math.round(random.spacing() / Math.sqrt(count / frequencyFactor)));
int separation = spacing == 1 ? 0 : Math.min(spacing - 1,
(int) Math.round((double) random.separation() * spacing / random.spacing()));
return new StructureSet(set.structures(), new RandomSpreadStructurePlacement(placement.locateOffset(),
settings.rivencraft$frequencyReductionMethod(), frequency, settings.rivencraft$salt(),
settings.rivencraft$exclusionZone(), spacing, separation, random.spreadType()));
}
if (placement instanceof ConcentricRingsStructurePlacement rings) {
int total = (int) Math.min(4095, (long) rings.count() * count);
int spread = Math.min(total, (int) Math.min(1023, (long) rings.spread() * count));
return new StructureSet(set.structures(), new ConcentricRingsStructurePlacement(placement.locateOffset(),
settings.rivencraft$frequencyReductionMethod(), settings.rivencraft$frequency(),
settings.rivencraft$salt(), settings.rivencraft$exclusionZone(), rings.distance(), spread,
total, rings.preferredBiomes()));
}
return set;
}
private static final class LocalReference extends Holder.Reference<StructureSet> {
private static final HolderOwner<StructureSet> OWNER = new HolderOwner<>() {};
private LocalReference(ResourceKey<StructureSet> key, StructureSet value) { super(Type.STAND_ALONE, OWNER, key, value); }
}
}
@@ -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,95 @@
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, List<Identifier> structures, List<Identifier> carvers) {
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),
Identifier.CODEC.listOf().optionalFieldOf("structures", List.of()).forGetter(PortTerrainFeaturePlan::structures),
Identifier.CODEC.listOf().optionalFieldOf("carvers", List.of()).forGetter(PortTerrainFeaturePlan::carvers)
).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);
structures = List.copyOf(structures);
carvers = List.copyOf(carvers);
PortStructurePages.validateCounts(structures);
PortCarverPages.validateCounts(carvers);
if (denseOres < 0 || denseOres > MAX_PER_FEATURE)
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences");
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty())
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,
boolean starFissures, List<Identifier> structures) {
this(choices, skylands, denseOres, starFissures, structures, List.of());
}
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres, boolean starFissures) {
this(choices, skylands, denseOres, starFissures, List.of());
}
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,56 @@
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 plants and log decorations; not all require surface terrain. */
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 Identifier POPLAR_TREES = page("poplar_trees");
public static final Identifier FALLEN_POPLAR_TREES = page("fallen_poplar_trees");
public static final Identifier RED_SHRUBS = page("red_shrubs");
public static final Identifier SHELF_MUSHROOMS = page("shelf_mushrooms");
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)
|| page.equals(POPLAR_TREES) || page.equals(FALLEN_POPLAR_TREES)
|| page.equals(RED_SHRUBS) || page.equals(SHELF_MUSHROOMS);
}
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");
if (page.equals(POPLAR_TREES)) return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "poplar_trees");
if (page.equals(FALLEN_POPLAR_TREES)) return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "fallen_poplar_trees");
if (page.equals(RED_SHRUBS)) return Identifier.withDefaultNamespace("patch_red_shrub");
throw new IllegalArgumentException("Unsupported vegetation page " + page);
}
public static boolean addsLogs(Identifier page) {
return page.equals(TREES) || page.equals(HUGE_TREES)
|| page.equals(POPLAR_TREES) || page.equals(FALLEN_POPLAR_TREES);
}
public static boolean requiresSurface(Identifier page) {
return isVegetation(page) && !page.equals(SHELF_MUSHROOMS);
}
private static Identifier page(String name) {
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + name);
}
}
@@ -0,0 +1,28 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
/** The six active weather pages in the 1.21.1 writing system. */
public enum PortWeatherMode {
NORMAL, FAST, SLOW, OFF, ALWAYS, STORM;
public static final Codec<PortWeatherMode> CODEC = Codec.STRING.xmap(
name -> valueOf(name.toUpperCase(java.util.Locale.ROOT)),
mode -> mode.name().toLowerCase(java.util.Locale.ROOT));
public static PortWeatherMode forPage(Identifier page) {
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
return switch (page.getPath()) {
case "weather/normal" -> NORMAL;
case "weather/fast" -> FAST;
case "weather/slow" -> SLOW;
case "weather/off" -> OFF;
case "weather/always" -> ALWAYS;
case "weather/storm" -> STORM;
default -> null;
};
}
}
@@ -0,0 +1,24 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.saveddata.SavedData;
import net.minecraft.world.level.saveddata.SavedDataType;
/** Age-local policy; native WeatherData separately persists its live cycle. */
final class PortWeatherSettings extends SavedData {
private static final Codec<PortWeatherSettings> CODEC = PortWeatherMode.CODEC
.fieldOf("mode").codec().xmap(PortWeatherSettings::new, settings -> settings.mode);
static final SavedDataType<PortWeatherSettings> TYPE = new SavedDataType<>(
Identifier.fromNamespaceAndPath("rivencraft", "weather_policy"),
() -> new PortWeatherSettings(PortWeatherMode.NORMAL), CODEC);
private PortWeatherMode mode;
private PortWeatherSettings(PortWeatherMode mode) { this.mode = mode; }
PortWeatherMode mode() { return mode; }
void setMode(PortWeatherMode mode) {
this.mode = mode;
setDirty();
}
}
@@ -34,7 +34,6 @@ import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.storage.DerivedLevelData;
import net.minecraft.world.level.storage.LevelResource;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.common.world.chunk.ForcedChunkManager;
@@ -57,7 +56,8 @@ public final class RuntimeAgeBackend {
for (Map.Entry<Identifier, Long> age : index.entries().entrySet()) {
try {
load(server, age.getKey(), age.getValue(), index.biomePlan(age.getKey()),
index.terrain(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) {
Rivencraft.LOGGER.error("Could not restore Age {}. Its saved index remains intact.", age.getKey(), exception);
}
@@ -75,12 +75,70 @@ public final class RuntimeAgeBackend {
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
return create(server, id, seed, biomePlan, terrain, null);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
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) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, PortWeatherMode.NORMAL);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, weather, PortLighting.NORMAL);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
PortLighting lighting) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, weather, lighting, PortSkyStyle.OVERWORLD);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
PortLighting lighting, PortSkyStyle sky) {
checkServerThread(server);
ageName(id);
AgeLevelIndex index = index(server);
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
ServerLevel level = load(server, id, seed, biomePlan, terrain);
index.add(id, seed, biomePlan, terrain);
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, lighting, sky);
((PortAgeLevel) level).configureWeather(weather);
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures, lighting, sky);
// A newly bound book may be used immediately. Persist its Age identity
// before publishing the catalog so a later crash cannot leave that
// book pointing at a world omitted from the on-disk index.
@@ -100,7 +158,9 @@ public final class RuntimeAgeBackend {
public static ServerLevel reload(MinecraftServer server, Identifier id) {
checkServerThread(server);
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).vegetation(id), index(server).dungeons(id), index(server).lakes(id),
index(server).terrainFeatures(id));
AgeCatalogSync.onCatalogChanged(server);
return level;
}
@@ -112,18 +172,23 @@ public final class RuntimeAgeBackend {
long previousSeed = requireSeed(server, id);
PortBiomePlan biomePlan = index.biomePlan(id);
PortAgeTerrain terrain = index.terrain(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;
boolean loaded = server.getLevel(dimensionKey(id)) != null;
if (loaded) unloadLoaded(server, id);
try {
if (loaded) load(server, id, seed, biomePlan, terrain);
if (loaded) load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
index.reseed(id, seed);
server.overworld().getDataStorage().saveAndJoin();
if (loaded) AgeCatalogSync.onCatalogChanged(server);
} catch (RuntimeException exception) {
if (loaded) {
try {
load(server, id, previousSeed, biomePlan, terrain);
load(server, id, previousSeed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
AgeCatalogSync.onCatalogChanged(server);
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
@@ -141,8 +206,15 @@ public final class RuntimeAgeBackend {
long seed = requireSeed(server, id);
PortBiomePlan biomePlan = index.biomePlan(id);
PortAgeTerrain terrain = index.terrain(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;
ArchivePaths paths = archivePaths(server, name);
PortLighting lighting = index.lighting(id);
PortSkyStyle sky = index.sky(id);
if (loaded) unloadLoaded(server, id);
// Persist the tombstone before moving files: a crash mid-archive must
// not let an old book silently regenerate this identity.
@@ -159,14 +231,15 @@ public final class RuntimeAgeBackend {
}
} catch (IOException | RuntimeException exception) {
try {
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain);
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, lighting, sky);
server.overworld().getDataStorage().saveAndJoin();
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
}
if (loaded) {
try {
load(server, id, seed, biomePlan, terrain);
load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
}
@@ -282,7 +355,17 @@ public final class RuntimeAgeBackend {
}
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures) {
return load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, index(server).lighting(id), index(server).sky(id));
}
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortLighting lighting, PortSkyStyle sky) {
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
@@ -353,11 +436,12 @@ public final class RuntimeAgeBackend {
};
generator = new NoiseBasedChunkGenerator(source, settings);
}
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(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(lighting.dimensionType(server, terrain, overworldStem.type(), sky),
generator, OptionalLong.of(seed));
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
ServerLevel level = new ServerLevel(server, server.executor, server.storageSource,
new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()),
key, stem, server.getWorldData().isDebugWorld(), biomeZoomSeed, List.of(), false);
ServerLevel level = new PortAgeLevel(server, key, stem, biomeZoomSeed, terrain);
// Publish only after the constructor succeeds. If the load event fails,
// roll back the map and close the newly opened chunk/entity stores.
@@ -0,0 +1,66 @@
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.then(arrived -> {
// Departure audio can be discarded when the client switches levels.
// Play after arrival too so the traveler hears the successful return.
arrived.level().playSound(null, arrived.blockPosition(), Rivencraft.LINK_FISSURE.get(),
SoundSource.BLOCKS, 1.0F, 1.0F);
}));
}
}
@@ -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);
}
}
@@ -14,6 +14,12 @@ import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
public final class 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
public static void registerScreens(RegisterMenuScreensEvent event) {
event.register(Rivencraft.BOOK_BINDER_MENU.get(), PortBookBinderScreen::new);
@@ -94,6 +94,8 @@ public final class PortBookBinderScreen extends AbstractContainerScreen<PortBook
} else if (PageItem.isLinkPanel(page)) {
graphics.fill(x + 5, y + 6, x + 22, y + 17, 0xFF000000);
} else {
if (!PortAuthoredWriting.isPageImplemented(page.get(Rivencraft.SYMBOL_PAGE_ID.get())))
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0x88FF3333);
// Render the full-size transparent glyph through the working
// item pipeline, without its small parchment-backed icon.
ItemStack glyphOnly = page.copy();
@@ -0,0 +1,87 @@
package net.dnorberg.rivencraft.client;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.BufferBuilder;
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.MeshData;
import com.mojang.renderpearl.api.buffers.GpuBuffer;
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
import java.util.Optional;
import java.util.OptionalDouble;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.util.RandomSource;
import org.joml.Matrix4f;
/** Legacy's six 48x48 noise faces, uploaded once per animation frame rather than per cell. */
public final class PortStaticSky implements AutoCloseable {
private static final int GRID = 48;
private final DynamicTexture texture = new DynamicTexture("Rivencraft static sky", GRID * 6, GRID, false);
private final GpuBuffer cube = buildCube();
private long uploadedFrame = Long.MIN_VALUE;
public void render(RenderTarget target, GpuBufferSlice fog, long frame) {
if (uploadedFrame != frame) {
RandomSource random = RandomSource.create(frame);
var image = texture.getPixels();
for (int face = 0; face < 6; face++) {
for (int y = 0; y < GRID; y++) {
for (int x = 0; x < GRID; x++) {
int gray = 146 * (65 + random.nextInt(160)) / 255;
image.setPixel(face * GRID + x, y, 0xff000000 | gray << 16 | gray << 8 | gray);
}
}
}
texture.upload();
uploadedFrame = frame;
}
RenderSystem.setShaderFog(fog);
var indices = RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
var indexBuffer = indices.getBuffer(36);
var transform = RenderSystem.getDynamicUniforms().writeTransform(RenderSystem.getModelViewMatrixCopy());
try (var pass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(
() -> "Rivencraft static sky", target.getColorTextureView(), Optional.empty(),
target.getDepthTextureView(), OptionalDouble.empty())) {
pass.setPipeline(RenderSystem.getCompiledPipeline(RenderPipelines.END_SKY));
RenderSystem.bindDefaultUniforms(pass);
pass.setUniform("DynamicTransforms", transform);
pass.setUniform("Sampler0", texture.getTextureView(), texture.getSampler());
pass.setVertexBuffer(0, cube.slice());
pass.setIndexBuffer(indexBuffer, indices.type());
pass.drawIndexed(36, 1, 0, 0, 0);
}
}
private static GpuBuffer buildCube() {
var format = DefaultVertexFormat.POSITION_TEX_COLOR;
try (var bytes = ByteBufferBuilder.exactlySized(24 * format.getVertexSize())) {
var builder = new BufferBuilder(bytes, PrimitiveTopology.QUADS, format);
for (int face = 0; face < 6; face++) {
var pose = new Matrix4f();
switch (face) {
case 1 -> pose.rotationX((float) Math.PI / 2);
case 2 -> pose.rotationX((float) -Math.PI / 2);
case 3 -> pose.rotationX((float) Math.PI);
case 4 -> pose.rotationZ((float) Math.PI / 2);
case 5 -> pose.rotationZ((float) -Math.PI / 2);
}
float u0 = face / 6f, u1 = (face + 1) / 6f;
builder.addVertex(pose, -100, -100, -100).setUv(u0, 0).setColor(-1);
builder.addVertex(pose, -100, -100, 100).setUv(u0, 1).setColor(-1);
builder.addVertex(pose, 100, -100, 100).setUv(u1, 1).setColor(-1);
builder.addVertex(pose, 100, -100, -100).setUv(u1, 0).setColor(-1);
}
try (MeshData mesh = builder.buildOrThrow()) {
return RenderSystem.getDevice().createBuffer(() -> "Rivencraft static sky cube", 40, mesh.vertexBuffer());
}
}
}
@Override public void close() {
cube.close();
texture.close();
}
}
@@ -5,6 +5,7 @@ import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.serialization.MapCodec;
import java.util.function.Consumer;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.rendertype.RenderTypes;
import net.minecraft.client.renderer.special.SpecialModelRenderer;
@@ -18,7 +19,8 @@ public final class PortSymbolPageRenderer implements SpecialModelRenderer<PortSy
public PageArt extractArgument(ItemStack stack) {
Identifier symbol = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
return symbol == null ? null : new PageArt(symbol,
stack.getOrDefault(Rivencraft.PAGE_GLYPH_ONLY.get(), false));
stack.getOrDefault(Rivencraft.PAGE_GLYPH_ONLY.get(), false),
PortAuthoredWriting.isPageImplemented(symbol) ? 0xFFFFFFFF : 0xFFFF6666);
}
@Override
@@ -30,10 +32,10 @@ public final class PortSymbolPageRenderer implements SpecialModelRenderer<PortSy
: PortSymbolPageTextures.forItem(art.symbol());
collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture),
(matrix, vertices) -> {
vertex(vertices, matrix, 0, 0, 0, 1, light, overlay);
vertex(vertices, matrix, 1, 0, 1, 1, light, overlay);
vertex(vertices, matrix, 1, 1, 1, 0, light, overlay);
vertex(vertices, matrix, 0, 1, 0, 0, light, overlay);
vertex(vertices, matrix, 0, 0, 0, 1, light, overlay, art.color());
vertex(vertices, matrix, 1, 0, 1, 1, light, overlay, art.color());
vertex(vertices, matrix, 1, 1, 1, 0, light, overlay, art.color());
vertex(vertices, matrix, 0, 1, 0, 0, light, overlay, art.color());
});
}
@@ -44,12 +46,12 @@ public final class PortSymbolPageRenderer implements SpecialModelRenderer<PortSy
}
private static void vertex(VertexConsumer vertices, PoseStack.Pose pose,
float x, float y, float u, float v, int light, int overlay) {
vertices.addVertex(pose, x, y, 0.5F).setColor(255, 255, 255, 255)
float x, float y, float u, float v, int light, int overlay, int color) {
vertices.addVertex(pose, x, y, 0.5F).setColor(color)
.setUv(u, v).setOverlay(overlay).setLight(light).setNormal(pose, 0, 0, 1);
}
public record PageArt(Identifier symbol, boolean glyphOnly) {}
public record PageArt(Identifier symbol, boolean glyphOnly, int color) {}
public record Unbaked() implements SpecialModelRenderer.Unbaked<PageArt> {
public static final Unbaked INSTANCE = new Unbaked();
@@ -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();
}
}
}
@@ -0,0 +1,7 @@
package net.dnorberg.rivencraft.client;
/** Carries the extracted animation frame without consulting the live world during drawing. */
public interface StaticSkyState {
long rivencraft$staticFrame();
void rivencraft$staticFrame(long frame);
}
@@ -0,0 +1,35 @@
package net.dnorberg.rivencraft.item;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.phys.Vec3;
/** Same successful-link feedback and sound precedence as the 1.21.1 target. */
public final class LinkVisualEffects {
private LinkVisualEffects() {}
public static SoundEvent sound(LinkProperties properties, boolean portal) {
if (properties.has(LinkProperties.DISARM)) return Rivencraft.LINK_DISARM.get();
if (portal) return Rivencraft.LINK_PORTAL.get();
if (properties.has(LinkProperties.INTRA_LINKING)) return Rivencraft.LINK_INTRA.get();
return Rivencraft.LINK.get();
}
public static void play(ServerLevel source, Vec3 departure, ServerPlayer player,
LinkProperties properties, boolean portal) {
SoundEvent event = sound(properties, portal);
burst(source, departure, event);
burst(player.level(), player.position(), event);
}
private static void burst(ServerLevel level, Vec3 position, SoundEvent event) {
level.playSound(null, position.x, position.y, position.z, event,
SoundSource.PLAYERS, 0.8F, level.getRandom().nextFloat() * 0.2F + 0.9F);
level.sendParticles(ParticleTypes.CLOUD, position.x, position.y, position.z,
50, 0.5, 1.0, 0.5, 0.005);
}
}
@@ -78,6 +78,7 @@ public final class LinkingBookItem extends Item {
/** Link Panel effects are shared by Linking and Descriptive Books. */
public static void applySuccessfulLink(ServerPlayer player, LinkProperties properties,
ServerLevel source, Vec3 departure, Vec3 incoming, float destinationYaw) {
LinkVisualEffects.play(source, departure, player, properties, false);
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) {
double speed = Math.hypot(incoming.x, incoming.z);
double yaw = Math.toRadians(destinationYaw);
@@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.item;
import java.util.function.Consumer;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.writing.PortSymbolPages;
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
import net.minecraft.ChatFormatting;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
@@ -12,7 +13,7 @@ import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.component.TooltipDisplay;
/** One Page item with a persistent Link Panel variant; symbol writing is not ported yet. */
/** Shared Page item with persistent Link Panel and authored symbol variants. */
public final class PageItem extends Item {
public PageItem(Properties properties) {
super(properties);
@@ -72,8 +73,10 @@ public final class PageItem extends Item {
? description.copy().withStyle(ChatFormatting.GRAY, ChatFormatting.OBFUSCATED)
: description.copy().withStyle(ChatFormatting.GRAY));
}
tooltip.accept(Component.translatable("item.rivencraft.page.symbol.port_status")
.withStyle(ChatFormatting.DARK_GRAY));
boolean implemented = PortAuthoredWriting.isPageImplemented(symbolId);
tooltip.accept(Component.translatable(implemented
? "item.rivencraft.page.symbol.implemented" : "item.rivencraft.page.symbol.unimplemented")
.withStyle(implemented ? ChatFormatting.DARK_GRAY : ChatFormatting.RED));
return;
}
LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
@@ -0,0 +1,23 @@
package net.dnorberg.rivencraft.mixin;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import net.dnorberg.rivencraft.age.PortLighting;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.lighting.LevelLightEngine;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
/** Ambient light contributes to effective brightness, not fake block/sky propagation. */
@Mixin(LevelLightEngine.class)
public abstract class AgeAmbientLightMixin {
@Shadow @Final protected LevelHeightAccessor levelHeightAccessor;
@ModifyReturnValue(method = "getRawBrightness", at = @At("RETURN"))
private int rivencraft$ambientBrightness(int brightness) {
return levelHeightAccessor instanceof Level level && PortLighting.isBright(level)
? Math.max(brightness, 15) : brightness;
}
}
@@ -0,0 +1,36 @@
package net.dnorberg.rivencraft.mixin;
import com.llamalad7.mixinextras.sugar.Local;
import net.dnorberg.rivencraft.age.PortCarverPages;
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.chunk.CarvingMask;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.NoiseChunk;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.material.rule.MaterialRule;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** Merge authored geometry before vanilla consumes its mask with aquifer/surface rules. */
@Mixin(NoiseBasedChunkGenerator.class)
public abstract class AgeCarversMixin {
@Inject(method = "generateCarvers", at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/level/chunk/CarvingMask;isEmpty()Z"))
private void rivencraft$authoredCarvers(ChunkAccess chunk, Blender blender, NoiseChunk noise,
RandomState random, BiomeManager biomes, WorldGenRegion region, MaterialRule material,
CallbackInfo ci, @Local CarvingMask mask) {
if (region == null) return;
var level = region.getLevel();
if (level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator generator
&& generator.base() == (Object) this && generator.terrainFeatures() != null) {
PortCarverPages.carve((NoiseBasedChunkGenerator) (Object) this, chunk, random,
region.registryAccess(), generator.terrainFeatures().carvers(), level.getSeed(), mask);
}
}
}
@@ -0,0 +1,42 @@
package net.dnorberg.rivencraft.mixin;
import java.util.function.Predicate;
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
import net.dnorberg.rivencraft.age.PortStructurePages;
import net.minecraft.core.Holder;
import net.minecraft.core.RegistryAccess;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.biome.Climate;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
/** Matches 1.21.1's explicit-page biome override, scoped only to authored Age structures. */
@Mixin(Structure.class)
public abstract class AgeStructureBiomeMixin {
// 26.3 jigsaw placement checks biome columns inside findGenerationPoint,
// before the final isValidBiome test. Set the predicate when constructing
// the context so both checks see the explicit-page override.
@ModifyArg(method = "generate", at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/level/levelgen/structure/Structure$GenerationContext;<init>(Lnet/minecraft/core/RegistryAccess;Lnet/minecraft/world/level/chunk/ChunkGenerator;Lnet/minecraft/world/level/biome/BiomeSource;Lnet/minecraft/world/level/biome/Climate$Sampler;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager;JLnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/LevelHeightAccessor;Ljava/util/function/Predicate;)V"),
index = 9)
private Predicate<Holder<Biome>> rivencraft$authoredBiome(
RegistryAccess registries, ChunkGenerator chunkGenerator, BiomeSource biomes,
Climate.Sampler sampler, RandomState random, StructureTemplateManager templates,
long seed, ChunkPos chunk, LevelHeightAccessor height, Predicate<Holder<Biome>> original) {
if (chunkGenerator instanceof PortFeatureChunkGenerator generator
&& generator.terrainFeatures() != null
&& PortStructurePages.authored((Structure) (Object) this, registries,
generator.terrainFeatures().structures())) {
return biome -> true;
}
return original;
}
}
@@ -0,0 +1,53 @@
package net.dnorberg.rivencraft.mixin;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
import net.dnorberg.rivencraft.client.PortStaticSky;
import net.dnorberg.rivencraft.client.StaticSkyState;
import net.minecraft.client.Camera;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.SkyRenderer;
import net.minecraft.client.renderer.state.level.SkyRenderState;
import net.minecraft.world.level.dimension.DimensionType;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(SkyRenderer.class)
public class StaticSkyRendererMixin {
@Shadow @Final private RenderTarget renderTarget;
@Unique private PortStaticSky rivencraft$staticSky;
@Inject(method = "extractRenderState", at = @At("TAIL"))
private void rivencraft$extract(ClientLevel level, float partialTicks, Camera camera,
SkyRenderState state, CallbackInfo ci) {
boolean enabled = level.dimensionTypeRegistration().unwrapKey().map(key ->
key.identifier().getNamespace().equals("rivencraft")
&& key.identifier().getPath().endsWith("_sky_static")).orElse(false);
((StaticSkyState) state).rivencraft$staticFrame(enabled ? level.getGameTime() / 2 : Long.MIN_VALUE);
// Only the extracted presentation uses END to schedule the sky pass. The actual
// dimension has NONE, so it never acquires End flashes or End gameplay behavior.
if (enabled) state.skybox = DimensionType.Skybox.END;
}
@Inject(method = "render", at = @At("HEAD"), cancellable = true)
private void rivencraft$render(GpuBufferSlice fog, SkyRenderState state, CallbackInfo ci) {
long frame = ((StaticSkyState) state).rivencraft$staticFrame();
if (frame == Long.MIN_VALUE) return;
if (rivencraft$staticSky == null) rivencraft$staticSky = new PortStaticSky();
rivencraft$staticSky.render(renderTarget, fog, frame);
ci.cancel();
}
@Inject(method = "close", at = @At("HEAD"))
private void rivencraft$close(CallbackInfo ci) {
if (rivencraft$staticSky != null) {
rivencraft$staticSky.close();
rivencraft$staticSky = null;
}
}
}
@@ -0,0 +1,19 @@
package net.dnorberg.rivencraft.mixin;
import net.dnorberg.rivencraft.client.StaticSkyState;
import net.minecraft.client.renderer.state.level.SkyRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(SkyRenderState.class)
public class StaticSkyStateMixin implements StaticSkyState {
@Unique private long rivencraft$frame = Long.MIN_VALUE;
public long rivencraft$staticFrame() { return rivencraft$frame; }
public void rivencraft$staticFrame(long frame) { rivencraft$frame = frame; }
@Inject(method = "reset", at = @At("TAIL"))
private void rivencraft$reset(CallbackInfo ci) { rivencraft$frame = Long.MIN_VALUE; }
}
@@ -0,0 +1,17 @@
package net.dnorberg.rivencraft.mixin;
import java.util.Optional;
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
/** Read-only placement settings for Age-local copies, never edits global placements. */
@Mixin(AbstractSpreadingStructurePlacement.class)
public interface StructurePlacementAccessor {
@Accessor("frequencyReductionMethod")
AbstractSpreadingStructurePlacement.FrequencyReductionMethod rivencraft$frequencyReductionMethod();
@Accessor("frequency") float rivencraft$frequency();
@Accessor("salt") int rivencraft$salt();
@Accessor("exclusionZone")
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> rivencraft$exclusionZone();
}
@@ -2,7 +2,9 @@ package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.age.PortAgeTravel;
import net.dnorberg.rivencraft.age.PortAgeTerrain;
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
import net.dnorberg.rivencraft.age.PortRandomAgeSelection;
import net.dnorberg.rivencraft.item.LinkBookTarget;
import net.dnorberg.rivencraft.item.LinkProperties;
import net.dnorberg.rivencraft.item.LinkingBookItem;
@@ -125,8 +127,22 @@ public final class PortBookNetworking {
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
}
try {
RuntimeAgeBackend.create(server, id, player.getRandom().nextLong(), biomePlan,
PortAuthoredWriting.terrain(pages));
long seed = player.getRandom().nextLong();
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
if (pages.isEmpty()) {
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
terrain = random.terrain();
biomePlan = random.biomes();
}
var environment = net.dnorberg.rivencraft.age.PortEnvironmentSelection.resolve(pages, seed, terrain);
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
PortAuthoredWriting.rockPlan(pages, seed),
PortAuthoredWriting.vegetationPlan(pages),
PortAuthoredWriting.dungeonCount(pages),
PortAuthoredWriting.lakePlan(pages, seed),
PortAuthoredWriting.terrainFeaturePlan(pages, seed),
environment.weather(), environment.lighting(),
net.dnorberg.rivencraft.age.PortSkyStyle.fromPages(pages));
} catch (IllegalArgumentException | IllegalStateException exception) {
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
return null;
@@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.registry;
import com.mojang.serialization.MapCodec;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.age.PortFlatChunkGenerator;
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.neoforged.bus.api.IEventBus;
@@ -15,6 +16,8 @@ public final class PortChunkGenerators {
static {
GENERATORS.register("age_flat", () -> PortFlatChunkGenerator.CODEC);
// Keep the beta codec ID so previously saved Rocks Ages remain readable.
GENERATORS.register("age_rocks", () -> PortFeatureChunkGenerator.CODEC);
}
private PortChunkGenerators() {}
@@ -2,17 +2,35 @@ package net.dnorberg.rivencraft.writing;
import java.util.List;
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.PortAgeTerrain;
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.age.PortStructurePages;
import net.dnorberg.rivencraft.age.PortCarverPages;
import net.dnorberg.rivencraft.age.PortWeatherMode;
import net.dnorberg.rivencraft.age.PortLighting;
import net.dnorberg.rivencraft.age.PortSkyStyle;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.tags.BiomeTags;
/** Authored noise terrain and biome distribution supported by the 26.3 backend. */
public final class PortAuthoredWriting {
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(
Rivencraft.MOD_ID, "terrain/standard");
private static final Identifier AMPLIFIED_TERRAIN = Identifier.fromNamespaceAndPath(
@@ -25,19 +43,98 @@ public final class PortAuthoredWriting {
Rivencraft.MOD_ID, "terrain/void");
private static final Identifier CAVE_TERRAIN = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "terrain/cave");
private static final Identifier ROCKS = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/rocks");
private static final Identifier DUNGEONS = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/dungeons");
private PortAuthoredWriting() {}
/** Port availability only, not validity of a particular ordered book. */
public static boolean isPageImplemented(Identifier page) {
if (page == null || !page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
var entry = PortSymbolPages.find(page);
if (entry == null) return false;
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null
|| PortLighting.forPage(page) != null || PortSkyStyle.forPage(page) != null) return true;
if (entry.category().equals("rivencraft:biome") && PortRegistryPages.biomeId(page) != null) return true;
if (isBlockPage(page) && (entry.category().equals("rivencraft:block")
|| entry.category().equals("rivencraft:fluid"))) return true;
return page.equals(ROCKS) || page.equals(DUNGEONS) || PortLakePlan.isLake(page)
|| PortVegetationPlan.isVegetation(page) || PortTerrainFeaturePlan.isSupported(page)
|| page.equals(PortTerrainFeaturePlan.SKYLANDS)
|| page.equals(PortTerrainFeaturePlan.DENSE_ORES)
|| page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
|| PortStructurePages.supports(page) || PortCarverPages.supports(page);
}
/** A page never disappears into an Age unless this port can apply its effect. */
public static boolean supports(List<Identifier> pages) {
boolean terrain = false;
int biomes = 0;
int controllers = 0;
String pendingMaterial = null;
int rocks = 0;
int vegetation = 0;
int dungeons = 0;
int lakes = 0;
int skylands = 0;
int denseOres = 0;
int starFissures = 0;
int weather = 0;
int lighting = 0;
int sky = 0;
Map<Identifier, Integer> terrainFeatures = new HashMap<>();
Map<Identifier, Integer> structures = new HashMap<>();
Map<Identifier, Integer> carvers = new HashMap<>();
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) {
terrain = true;
} else if (PortSkyStyle.forPage(page) != null && sky++ == 0) {
// Sky presentation, independent of terrain and illumination.
} else if (PortLighting.forPage(page) != null && lighting++ == 0) {
// A single lighting controller; omitted choices resolve from the Age seed.
} else if (PortWeatherMode.forPage(page) != null && weather++ == 0) {
// A single independent weather policy for this Age.
} else if (controller(page) != null && controllers++ == 0) {
// An explicit controller needs an authored pool.
} else if (isBlockPage(page) && pendingMaterial == null
&& PortSymbolPages.find(page) != null
&& (PortSymbolPages.find(page).category().equals("rivencraft:block")
|| PortSymbolPages.find(page).category().equals("rivencraft:fluid"))) {
pendingMaterial = PortSymbolPages.find(page).category();
} 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 (PortCarverPages.supports(page)
&& carvers.getOrDefault(page, 0) < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
carvers.merge(page, 1, Integer::sum);
} else if (PortStructurePages.supports(page)
&& structures.getOrDefault(page, 0) < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
structures.merge(page, 1, Integer::sum);
} 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)
&& page.getPath().startsWith("biome/") && biomes < MAX_AUTHORED_BIOMES
&& PortSymbolPages.find(page) != null
@@ -46,6 +143,20 @@ public final class PortAuthoredWriting {
biomes++;
} else return false;
}
if (pendingMaterial != null || (rocks > 0 || pages.stream().anyMatch(PortVegetationPlan::requiresSurface))
&& (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 (!carvers.isEmpty() && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
PortAgeTerrain structureTerrain = pages.contains(VOID_TERRAIN) ? PortAgeTerrain.VOID
: pages.contains(FLAT_TERRAIN) ? PortAgeTerrain.FLAT
: pages.contains(CAVE_TERRAIN) ? PortAgeTerrain.CAVE : PortAgeTerrain.STANDARD;
if (structures.keySet().stream().anyMatch(page -> !PortStructurePages.supportsTerrain(page, structureTerrain)))
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) {
PortBiomePlan.Distribution distribution = pages.stream().map(PortAuthoredWriting::controller)
.filter(value -> value != null).findFirst().orElseThrow();
@@ -65,6 +176,10 @@ public final class PortAuthoredWriting {
|| page.equals(VOID_TERRAIN) || page.equals(CAVE_TERRAIN);
}
private static boolean isBlockPage(Identifier page) {
return page.getNamespace().equals(Rivencraft.MOD_ID) && page.getPath().startsWith("block/");
}
private static PortBiomePlan.Distribution controller(Identifier page) {
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
return switch (page.getPath()) {
@@ -91,25 +206,132 @@ public final class PortAuthoredWriting {
return PortAgeTerrain.STANDARD;
}
public static PortRockPlan rockPlan(List<Identifier> pages, long seed) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
List<Identifier> materials = solidMaterials(pages, seed, ROCKS);
return materials.isEmpty() ? null : new PortRockPlan(materials);
}
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);
List<Identifier> structures = pages.stream().filter(PortStructurePages::supports).toList();
List<Identifier> carvers = pages.stream().filter(PortCarverPages::supports).toList();
return choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty() ? null
: new PortTerrainFeaturePlan(choices, skylands, denseOres, starFissures, structures, carvers);
}
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. */
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
if (!supports(pages)) throw new IllegalArgumentException(
"Unsupported terrain or biome-page combination in this port slice");
List<Identifier> biomes = new ArrayList<>();
PortStructurePages.validateRegistry(pages, registries);
PortCarverPages.validateRegistry(pages, registries);
PortBiomePlan.Distribution distribution = null;
boolean caveTerrain = pages.contains(CAVE_TERRAIN);
boolean hasSurfaceBiome = false;
boolean hasOreBiome = false;
for (Identifier page : pages) {
if (isTerrain(page)) continue;
PortBiomePlan.Distribution selected = controller(page);
if (selected != null) { distribution = selected; continue; }
// supports() has already checked the whole book. Resolve only biome
// pages here, not every page absent from a growing exclusion list.
var entry = PortSymbolPages.find(page);
if (entry == null || !entry.category().equals("rivencraft:biome")) continue;
Identifier biome = PortRegistryPages.biomeId(page);
var registry = registries.lookupOrThrow(Registries.BIOME);
var holder = registry.get(ResourceKey.create(Registries.BIOME, biome))
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biome));
if (caveTerrain && !PortRegistryPages.isCaveBiome(holder))
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);
}
if (pages.stream().anyMatch(PortVegetationPlan::requiresSurface)
&& !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)
return new PortBiomePlan(distribution, List.of());
if (biomes.isEmpty()) return null;
@@ -76,13 +76,27 @@ public final class PortRegistryPages {
public static List<ItemStack> materialPages(boolean fluids) {
String category = fluids ? "rivencraft:fluid" : "rivencraft:block";
return BuiltInRegistries.BLOCK.keySet().stream().sorted()
.filter(id -> category.equals(materialCategory(BuiltInRegistries.BLOCK.getValue(id))))
return materialIds(category).stream()
.map(id -> PageItem.createSymbolPage(blockPageId(id))).toList();
}
public static boolean isCaveBiome(net.minecraft.core.Holder.Reference<net.minecraft.world.level.biome.Biome> holder) {
String path = holder.key().identifier().getPath();
/** The same full-block pool used by Creative pages, in stable registry-ID order. */
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)
|| path.equals("deep_dark") || path.endsWith("_caves");
}
@@ -0,0 +1,8 @@
{
"type": "minecraft:weighted_random_selector",
"features": [
{"data": "minecraft:red_poplar_leaf_litter", "weight": 200},
{"data": "minecraft:orange_poplar_leaf_litter", "weight": 240},
{"data": "minecraft:yellow_poplar_leaf_litter", "weight": 90}
]
}
@@ -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 }
}
}
]
}
@@ -0,0 +1,12 @@
{
"feature": "minecraft:fallen_poplar_tree",
"placement": [
{"type": "minecraft:in_square"},
{"type": "minecraft:surface_water_depth_filter", "max_water_depth": 0},
{"type": "minecraft:heightmap", "heightmap": "OCEAN_FLOOR"},
{
"type": "minecraft:block_predicate_filter",
"predicate": {"type": "minecraft:would_survive", "state": "minecraft:poplar_sapling"}
}
]
}
@@ -0,0 +1,9 @@
{
"feature": "rivencraft:poplar_trees",
"placement": [
{"type": "minecraft:count", "count": 6},
{"type": "minecraft:in_square"},
{"type": "minecraft:surface_water_depth_filter", "max_water_depth": 0},
{"type": "minecraft:heightmap", "heightmap": "OCEAN_FLOOR"}
]
}
@@ -0,0 +1,9 @@
{
"required": true,
"minVersion": "0.8",
"package": "net.dnorberg.rivencraft.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin", "AgeAmbientLightMixin"],
"client": ["StaticSkyStateMixin", "StaticSkyRendererMixin"],
"injectors": {"defaultRequire": 1}
}
@@ -4,15 +4,18 @@ issueTrackerURL="https://git.paddedcell.org/dnorberg/rivencraft/issues"
[[mods]]
modId="${mod_id}"
version="${mod_version}"
displayName="${mod_name} 26.3 Alpha"
displayName="${mod_name} 26.3 Beta"
displayURL="https://git.paddedcell.org/dnorberg/rivencraft"
authors="dnorberg and contributors"
description='''
Experimental Minecraft 26.3 alpha. Provides a runtime Age prototype and basic
Linking Book travel. Most Rivencraft gameplay has not yet been ported. Use a
test world and keep backups; this alpha is not compatible with 1.21.1 worlds.
Experimental Minecraft 26.3 beta with books, writing stations and selected
authored Age generation. Full 1.21.1 gameplay parity is still in progress.
Use a test world and keep backups; 1.21.1 worlds are not compatible.
'''
[[mixins]]
config="rivencraft.mixins.json"
[[dependencies.${mod_id}]]
modId="neoforge"
type="required"
+167 -1
View File
@@ -75,6 +75,20 @@ send_and_wait 'rivencraft_port age create smoke_cave 112233 cave' 'Created riven
send_and_wait 'rivencraft_port age create_multi smoke_flat_multi 24681 flat' 'Created rivencraft:age/smoke_flat_multi with seed 24681 terrain=FLAT'
send_and_wait 'rivencraft_port age create_multi smoke_void_multi 13580 void' 'Created rivencraft:age/smoke_void_multi with seed 13580 terrain=VOID'
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_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
# shutdown would hide the interrupted-save regression this checks.
index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/runtime_ages.dat"
@@ -90,6 +104,17 @@ gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_cave'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat_multi'
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_random'
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_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='
@@ -98,12 +123,95 @@ send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave
send_and_wait 'rivencraft_port age sample smoke_flat_multi' 'rivencraft:age/smoke_flat_multi seed=24681 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smoke_void_multi seed=13580 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_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_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_void_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_void_multi 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_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_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks_multi to be force loaded'
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]*$ ]] || {
printf 'Authored Rocks placed no gold blocks: %s\n' "$rocks_before" >&2
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 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'
@@ -119,6 +227,7 @@ alpha_before=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0
beta_before=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
flat_before=$(grep -F 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
void_before=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
random_before=$(grep -F 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
if [[ "$alpha_before" == "$beta_before" ]]; then
printf 'Different Age seeds produced the same terrain probe: %s\n' "$alpha_before" >&2
exit 1
@@ -139,6 +248,34 @@ send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave
send_and_wait 'rivencraft_port age sample smoke_flat_multi' 'rivencraft:age/smoke_flat_multi seed=24681 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smoke_void_multi seed=13580 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_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_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_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'
@@ -146,6 +283,7 @@ alpha_after=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 t
beta_after=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
flat_after=$(grep -F 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
void_after=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
random_after=$(grep -F 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
[[ "$alpha_before" == "$alpha_after" && "$beta_before" == "$beta_after" ]] || {
printf 'Age terrain changed across restart: %s/%s -> %s/%s\n' \
"$alpha_before" "$beta_before" "$alpha_after" "$beta_after" >&2
@@ -156,5 +294,33 @@ void_after=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain
"$flat_before" "$void_before" "$flat_after" "$void_after" >&2
exit 1
}
[[ "$random_before" == "$random_after" ]] || {
printf 'Random Age terrain changed across restart: %s -> %s\n' \
"$random_before" "$random_after" >&2
exit 1
}
[[ "$rocks_before" == "$rocks_after" ]] || {
printf 'Authored Rocks changed across restart: %s -> %s\n' \
"$rocks_before" "$rocks_after" >&2
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
printf '26.3 dedicated-server smoke passed: Standard, Flat, Void and Cave 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'