Port structures, weather, vegetation and safe surface arrivals to 26.3 beta
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-11-02 09:30:22 -05:00
1 parent c223f3fdca
commit 48fbae9be9
38 files changed
+1474 -93

No files matched your search

+3 -2
View File
@@ -13,10 +13,11 @@ the 1.12.2 mod.
The gameplay guide below describes the **Minecraft 1.21.1 release**. A separate,
incomplete **Minecraft 26.3 beta** is available: Java 25, NeoForge 26.3.0.99,
and a first-party Age backend (no Dynamic Dimensions or Bad Packets). It now
supports authored terrain/biomes and a growing set of generation features,
supports authored terrain/biomes, structure and vegetation pages, independent
Age weather, and a growing set of generation features,
but does not yet provide full 1.21.1 gameplay or optional-integration parity.
See the [26.3 beta guide](ports/mc263/README.md) and
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.54-beta.1.md).
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.65-beta.1.md).
Keep the builds and their saves separate, and back up worlds before updating.
## Getting started
+219
View File
@@ -681,6 +681,225 @@ 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.
## Verification of this first slice
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
+210 -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. Celestial/lighting 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
@@ -351,6 +351,213 @@ 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.
From the repository root:
```sh
@@ -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).
+4 -1
View File
@@ -124,9 +124,12 @@ sourceSets.main.resources.srcDir generatePortLanguage
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/linking/link-fissure.ogg'
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.54-beta.1
mod_version=1.0.1.65-beta.1
+35 -8
View File
@@ -6,19 +6,26 @@
"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.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 +40,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."
}
@@ -59,9 +59,17 @@ public final class Rivencraft {
private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MOD_ID);
private static final DeferredRegister<SoundEvent> SOUNDS =
DeferredRegister.create(Registries.SOUND_EVENT, MOD_ID);
public static final Supplier<SoundEvent> LINK_FISSURE = SOUNDS.register(
"linking.link-fissure", () -> SoundEvent.createVariableRangeEvent(
Identifier.fromNamespaceAndPath(MOD_ID, "linking.link-fissure")));
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 =
@@ -170,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);
@@ -68,6 +68,22 @@ public final class PortAgeCommands {
return 1;
})))
.then(Commands.literal("age")
.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())
@@ -302,6 +318,62 @@ public final class PortAgeCommands {
.executes(PortAgeCommands::sampleBiome)))))));
}
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);
}
@@ -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 {
@@ -28,7 +26,9 @@ public final class PortAgeTravel {
boolean fissure = target.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
&& features.terrainFeatures() != null && features.terrainFeatures().starFissures();
BlockPos landing = fissure ? PortStarFissureFeature.savedSpawn(target) : null;
if (landing != null) landing = findFootedDestination(target, landing).orElse(null);
if (landing != null) landing = 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));
@@ -38,8 +38,6 @@ public final class PortAgeTravel {
}
private static BlockPos findLanding(ServerLevel target) {
net.minecraft.world.level.chunk.ChunkGenerator generator = target.getChunkSource().getGenerator();
if (generator instanceof PortFeatureChunkGenerator features) generator = features.base();
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
@@ -57,36 +55,40 @@ 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 (generator) {
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) {
@@ -119,8 +121,13 @@ 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()
&& passable(target, foot) && passable(target, foot.above());
@@ -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);
}
}
}
}
}
@@ -113,12 +113,25 @@ public final class PortFeatureChunkGenerator extends ChunkGenerator {
}
@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) {
base.createStructures(registries, state, structures, chunk, templates, 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);
@@ -250,19 +263,27 @@ public final class PortFeatureChunkGenerator extends ChunkGenerator {
int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 8);
Holder<Biome> biome = level.getBiome(new BlockPos(
chunkPos.getMinBlockX() + 8, surface, chunkPos.getMinBlockZ() + 8));
if (biome.is(BiomeTags.IS_END) || PortRegistryPages.isCaveBiome(biome)) return;
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());
RandomSource random = RandomSource.create(chunkSeed
^ (long) page.hashCode() * 0x9E3779B97F4A7C15L
^ (long) occurrence * 0xD1B54A32D192ED03L);
authored.place(level, this, random, origin);
if (PortVegetationPlan.addsLogs(page)) mushroomHosts = null;
}
}
}
@@ -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,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); }
}
}
@@ -8,7 +8,8 @@ import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
/** Frozen terrain filters and ordered solid/surface features for an Age. */
public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres, boolean starFissures) {
public 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");
@@ -27,7 +28,9 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
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)
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) {
@@ -46,9 +49,13 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
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)
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) {
@@ -57,6 +64,15 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
}
}
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);
}
@@ -6,11 +6,15 @@ import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
/** Ordered, frozen authored vegetation pages for surface Ages. */
/** 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));
@@ -22,16 +26,30 @@ public record PortVegetationPlan(List<Identifier> pages) {
}
public static boolean isVegetation(Identifier page) {
return page.equals(FLOWERS) || page.equals(TREES) || page.equals(HUGE_TREES);
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,33 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import java.util.List;
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;
};
}
public static PortWeatherMode fromPages(List<Identifier> pages) {
return pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
.findFirst().orElse(NORMAL);
}
}
@@ -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;
@@ -106,11 +105,20 @@ public final class RuntimeAgeBackend {
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) {
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, rocks, vegetation, dungeons, lakes, terrainFeatures);
((PortAgeLevel) level).configureWeather(weather);
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
// A newly bound book may be used immediately. Persist its Age identity
// before publishing the catalog so a later crash cannot leave that
@@ -403,9 +411,7 @@ public final class RuntimeAgeBackend {
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
LevelStem stem = new LevelStem(overworldStem.type(), 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.
@@ -56,6 +56,11 @@ public final class StarFissureBlock extends EndPortalBlock {
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);
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);
}));
}
}
@@ -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();
@@ -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,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,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,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();
}
@@ -139,7 +139,8 @@ public final class PortBookNetworking {
PortAuthoredWriting.vegetationPlan(pages),
PortAuthoredWriting.dungeonCount(pages),
PortAuthoredWriting.lakePlan(pages, seed),
PortAuthoredWriting.terrainFeaturePlan(pages, seed));
PortAuthoredWriting.terrainFeaturePlan(pages, seed),
net.dnorberg.rivencraft.age.PortWeatherMode.fromPages(pages));
} catch (IllegalArgumentException | IllegalStateException exception) {
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
return null;
@@ -12,6 +12,9 @@ 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.Rivencraft;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.Registries;
@@ -45,6 +48,23 @@ public final class PortAuthoredWriting {
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) 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;
@@ -58,13 +78,18 @@ public final class PortAuthoredWriting {
int skylands = 0;
int denseOres = 0;
int starFissures = 0;
int weather = 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 (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
@@ -91,6 +116,12 @@ public final class PortAuthoredWriting {
} 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) {
@@ -103,11 +134,17 @@ public final class PortAuthoredWriting {
biomes++;
} else return false;
}
if (pendingMaterial != null || (rocks > 0 || vegetation > 0)
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;
@@ -184,8 +221,10 @@ public final class PortAuthoredWriting {
boolean skylands = pages.contains(PortTerrainFeaturePlan.SKYLANDS);
int denseOres = (int) pages.stream().filter(PortTerrainFeaturePlan.DENSE_ORES::equals).count();
boolean starFissures = pages.contains(PortTerrainFeaturePlan.STAR_FISSURES);
return choices.isEmpty() && !skylands && denseOres == 0 && !starFissures ? null
: new PortTerrainFeaturePlan(choices, skylands, denseOres, starFissures);
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) {
@@ -255,19 +294,19 @@ public final class PortAuthoredWriting {
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) || page.equals(ROCKS) || page.equals(DUNGEONS)
|| page.equals(PortTerrainFeaturePlan.SKYLANDS)
|| page.equals(PortTerrainFeaturePlan.DENSE_ORES)
|| page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
|| PortLakePlan.isLake(page) || PortTerrainFeaturePlan.isSupported(page) || isBlockPage(page)
|| PortVegetationPlan.isVegetation(page)) continue;
PortBiomePlan.Distribution selected = controller(page);
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))
@@ -279,7 +318,7 @@ public final class PortAuthoredWriting {
if (net.dnorberg.rivencraft.age.PortDenseOres.hasOres(holder)) hasOreBiome = true;
biomes.add(biome);
}
if (pages.stream().anyMatch(PortVegetationPlan::isVegetation)
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)
@@ -1,5 +0,0 @@
{
"linking.link-fissure": {
"sounds": [{"name": "rivencraft:linking/link-fissure"}]
}
}
@@ -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,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,8 @@
{
"required": true,
"minVersion": "0.8",
"package": "net.dnorberg.rivencraft.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin"],
"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"