Port celestial systems and seeded Age completion to 26.3 beta
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-11-04 18:08:12 -05:00
1 parent 38e40ddba3
commit 40770841ee
32 files changed
+2792 -99

No files matched your search

+346 -2
View File
@@ -5,8 +5,12 @@ 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, selected material/vegetation/structure features, and
independent Age weather and lighting. Celestial systems, instability, crystal
portals, and much of the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not
independent Age weather and lighting, multiple authored or seeded celestial
bodies and eclipses, biome/material completion, and random features/structures.
Instability, crystal portals, discovery/stations, writing recovery and optional
integration parity remain unfinished. The latest published beta is
[1.0.1.93-beta.1](RELEASE_NOTES_1.0.1.93-beta.1.md).
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
@@ -947,6 +951,346 @@ reload, reseed, restart and unchanged sampled lighting/time. Actual rendered
size remains a manual check. The test server stopped and its world was deleted.
No commit/push/publication is performed until explicitly requested.
The local `1.0.1.79-beta.1` slice adds shared color groups before **Normal
Time**, tinting both its sun and moon as in 1.21.1. Use Number + Red/Green/
Blue/White/Black pages before that controller; Length/Phase/Angle/Size can
appear between the colors and Normal Time without losing the pending group.
Normal Time consumes the group, so it does not tint a later Sky or Ambient
Light Color target. Trailing colors still target the sky. Mixing, seeded
unnumbered amounts and completed-group clamping use the existing color rules.
The tint changes native sun/moon RGB only, retaining sprite transparency,
weather fading, moon phases and existing size/orbit/time settings. It does
not recolor stars, sunrise glow or change gameplay illumination. Older saved
color profiles keep untinted bodies; new profiles save and sync their tint.
Catalog protocol is now 7: use matching client/server builds. Standalone
Sun/Moon controllers and independent per-body colors remain pending.
Test **Link Panel → Zero Length → Zenith Phase → Number 24 → Red → Normal
Time**, optionally with No Weather and Normal Lighting, for a red sun. Use
Nadir Phase to view the same tint on the moon. Mix several colors, add a
separate color group before Sky, and confirm that the Overworld stays normal
after travel. Check glow transparency and persistence after reopening the
world. The op fixture `create_celestial_color <name> <seed>
<rising|zenith|setting|nadir> <red|green|blue|white|black> <0..24>` provides
independently colored sky/ambient plus a frozen body for diagnostics.
Client rendering remains a manual check; nothing is committed or published
without an explicit request.
The local `1.0.1.80-beta.1` slice supports one standalone **Sun** or **Moon**
controller in place of Normal Time. Both accept the existing Color, Length,
Phase, Direction, Angle and Size modifiers (including Number overrides).
Sun omits the paired moon; Moon omits the paired sun and sunrise glow.
Size applies to the selected standalone body, while Normal Time still sizes
only its sun. A missing Sky selects Overworld; explicit Sky remains decisive,
and non-Overworld body rendering is still pending.
Standalone bodies use the root-style linear orbit rather than the native
paired cycle's eased orbit. Zenith puts either body overhead, Nadir puts it
below the world, and Rising/Setting place it at the horizon. Zero Length
freezes that position. Gameplay skylight combines altitude with precipitation
and the root 0.18 moonlight multiplier (overhead sun = 15, overhead moon = 6,
below-horizon body = 4 under clear weather). Bright/Dark rules remain separate.
Native background, stars and moon-phase schedules remain for this slice;
multiple independent bodies, eclipse handling, and full seeded celestial
completion are not yet supported. Catalog protocol is now 8.
Test **Link Panel → Zero Length → Zenith Phase → Large Celestial → Sun**,
then replace Sun with Moon in a new book. Each should show only that body
overhead. Try Number + Color and Size, a moving/reversed cycle, and Nadir;
confirm the paired Normal Time and Overworld are unaffected after travel.
Restart and revisit. The diagnostic `create_body <name> <seed> <sun|moon>
<rising|zenith|setting|nadir>` creates a red, large, east-bearing frozen body.
More than one celestial controller is still rejected, not silently discarded.
Nothing is committed or published without an explicit request.
The local `1.0.1.81-beta.1` slice accepts multiple celestial controllers.
Each Sun/Moon consumes its own preceding Color, Length, Phase, Direction,
Angle and Size group; modifiers reset after that controller. Normal Time
contributes a sun/moon pair with opposite phases and its usual sun-only Size.
The 1.21.1 limit of **ten suns and ten moons** applies, counting pairs toward
both limits. Unsupported duplicate modifiers within one group and dangling
modifiers are still rejected; the full recovery/instability grammar is pending.
Multiple bodies retain independent saved orbits/colors and draw using the
native body resources, with suns before moons. Each has its own linear orbit;
the first controller supplies the shared day timeline and native background/
star/moon-phase schedules. Skylight combines positive-altitude suns (capped at
1), or sums weaker moonlight when no sun contributes, matching the root
aggregation rule. Explicit non-Overworld backgrounds still hide bodies.
Catalog protocol is now 9. Older single-body/paired profiles remain unchanged.
**Eclipse effects are not in this slice:** sky darkening, disc occlusion and
corona effects will be ported next. Visual overlap alone is not an eclipse.
Test two groups in one book: **Zero Length → Zenith Phase → Large Celestial
→ Number 24 → Red → Sun**, then **Zero Length → Rising Phase → Small Celestial
→ Number 24 → Blue → Moon** (with Link Panel first). Each should retain its
own size, color and position. Try a moving/reversed third body, a Normal Time
pair alongside a standalone body, restart, and travel back to an ordinary
Age/Overworld. `/rivencraft_port age create_celestials <name> <seed>` creates a
diagnostic three-body Age. Rendering remains a manual client check. Nothing
is committed or published without an explicit request.
The local `1.0.1.82-beta.1` slice adds eclipse effects to saved multi-body
profiles. Contact is based on the central quarter-size discs, not their
halos. Opposite-kind overlap progressively dims a moon and whitens a sun's
corona. A visible eclipsed sun darkens the sky by up to 65% and reduces its
sunlight contribution by up to 85%, using the 1.21.1 formulas. Other suns can
still supply light; same-kind overlaps do not produce eclipses. Below-horizon
overlap does not darken the visible sky.
Each body's native transparent glow is followed by an ordinary-alpha central
disc, so an eclipsing moon obscures the sun instead of showing it through the
glow. During an eclipse the moon core uses the solid full-moon sprite, matching
root's occluding disc; native phases are retained outside eclipse contact.
Weather fading remains. Meshes are cached, rebuilt after atlas changes and
closed with the sky renderer. No new image assets are needed. Existing Ages
with multiple bodies get these derived effects without rebinding their books.
Test **Link Panel → Zero Length → Zenith Phase → Sun → Zero Length → Zenith
Phase → Large Celestial → Moon** (No Weather and Normal Lighting recommended).
This holds an eclipse overhead. Change the Moon to Half Length in a new book
to watch it pass across the sun. Verify opaque center/transparent glow, sky
and ground darkening, restart and travel back to an ordinary dimension.
The op fixture `/rivencraft_port age create_eclipse <name> <seed>
<aligned|partial|clear|below|moving>` creates controlled comparisons.
The multi-body fixture is now named `create_celestials`, avoiding a collision
with the existing terrain-generation `create_multi` command. Catalog schema
remains protocol 9. Explicit non-Overworld body rendering and broader seeded
completion are still pending. Nothing is committed or published without an
explicit request.
The local `1.0.1.83-beta.1` correction makes standalone and multi-body authored
moons use the **full-moon sprite in both glow and opaque-center passes**, as
the 1.21.1 custom renderer does. Native dark lunar-phase pixels could become
black patches once the new ordinary-alpha center pass made them opaque.
The native Normal Time pair keeps its calendar phases. Eclipse geometry,
darkening and corona formulas are unchanged; genuinely eclipsed moons still
turn dark and obscure their overlapping sun.
Read-only inspection of the reported `celest` save found all four body colors
intact (`-1`, untinted). Its first moon and second sun share period, direction,
phase, origin and bearing, creating a permanent eclipse after eclipse support
was added. This is separate from the phase-sprite rendering correction. No
saved orbits/colors are rewritten, and existing Ages need not be recreated.
Please revisit `celest`, compare a nonaligned authored moon, check an eclipse
and resource reload, then return to Overworld/Normal Time to confirm native
phases are unaffected. This rendering correction still needs a client check.
Nothing is committed or published without an explicit request.
The local `1.0.1.84-beta.1` slice renders authored **Normal Time, Sun, Moon
and multiple bodies over all four Sky styles**, including explicit Nether,
End and animated Static. Backgrounds draw first; the existing authored body
pass follows with its colors, sizes, orbits, opaque centers and eclipse effects.
Nether retains its fog backdrop, End retains its native sky texture, and
Static keeps animating. These are presentation changes only: no Nether/End
terrain, ceiling, coordinate-scale or dragon behavior is introduced.
Eclipses shade End/Static background tint and the Nether-style fog backdrop;
gameplay light still uses the saved body profile. Native Overworld rendering
and no-celestial Ages are unchanged. No new assets, saved fields or catalog
protocol changes are needed. Already-created Ages with explicit backgrounds
and authored celestials gain the rendering on update without rebinding.
Test the same frozen overhead Sun/Moon book with **Nether Sky**, **End Sky**
and **Static Sky**, then test a moving body and eclipse in each. Check body
colors/size, no duplicate bodies, unchanged background style, Static animation,
F3+T, restart and travel to Overworld. The existing diagnostic
`age create_eclipse <name> <seed> <mode> [overworld|nether|end|static]` now
accepts an optional explicit Sky for comparisons. Client rendering remains a
manual check. Nothing is committed or published without an explicit request.
The local `1.0.1.85-beta.1` slice randomizes a **bare Sun or Moon page** when
a new Age is created. Each such controller gets its own deterministic color,
size, bearing, phase, direction and length. Defaults use the root random-Age
palette (mixing 1–3 colors), size 0.75×–1.75×, bearing 0–359°, full-cycle phase,
either direction, and length 0.2–2 days with Legacy's 1-in-7 stopped-cycle
chance. Sun/Moon counts still come from your pages; nothing adds extra bodies.
This applies only when that controller has **no written celestial modifiers**.
If its group includes Color, Size, Angle, Phase, Direction or Length, the
existing explicitly authored behavior is retained, including defaults for
its other fields. Sky/ambient colors consumed by their own target and unrelated
terrain/weather/lighting pages do not make a body authored or reshuffle its
random settings. Normal Time is unchanged. Groups use Age seed plus controller
ordinal; repeated bare pages get independent settings. Choices are saved,
not rerolled on GUI opening, reload/restart or terrain reseeding.
Test a **new** book with **Link Panel → Sun → Moon → Sun**, optionally adding
No Weather and Normal Lighting for visibility. Expect varied colors/sizes/
orbits; some bodies can be below the horizon or stopped. Test an explicitly
modified body in the same book, then restart and revisit. Existing Ages keep
their original saved settings and are not randomized retroactively. The op
fixture `age create_random_celestials <name> <seed>` tests independent seeded
defaults and bounds. Nothing is committed or published without explicit request.
The local `1.0.1.86-beta.1` slice completes the celestial branch for **new
books with no celestial controller pages**, including Link-Panel-only books.
The Age seed chooses **0–3 suns and 0–3 moons**, following the root random
count draws. Each body receives the independent saved settings introduced
in .85. An explicit Sun, Moon or Normal Time suppresses this extra-body
completion entirely; its written controllers remain authoritative.
A valid zero-sun/zero-moon result draws neither body nor sunrise glow, rather
than accidentally falling back to a vanilla pair. This does not force Dark
Lighting: background/star schedules and independent lighting retain their
normal behavior. Random background selection remains independent of body
counts, and all supported Sky styles can render the chosen bodies. Default
sky colors retain their current behavior; this slice completes bodies only.
Choices persist through restart and terrain reseeding. Existing Ages are not
rerolled. Catalog protocol is now **10**, requiring matching client/server
builds. Test several new Link-Panel-only books, plus books that contain only
terrain/weather/lighting/Sky pages. Expect varied counts, including sun-only,
moon-only and empty skies; some bodies may be below the horizon. Compare an
explicit one-Sun book and a Normal Time book, then restart and revisit.
`age create_completed_sky <name> <seed> [overworld|nether|end|static]` provides
clear-weather Flat fixtures and reports a sample zero-body seed. The existing
`age create_random` now also uses the completed celestial plan. Nothing is
committed or published without explicit request.
The local `1.0.1.87-beta.1` slice gives **new Link-Panel-only random Ages**
a saved random sky/fog tint, matching 1.21.1's seeded palette and rounded
mix of 1–3 colors. This completes the sky-color draw already reserved by the
random celestial count selection. Body settings/counts are unchanged.
Books containing symbol pages keep their authored/default color rules; a
black authored sky remains black. Ambient lighting is not randomized by this
change. Existing Ages retain their saved colors. No new saved fields or
catalog protocol change is needed. Create several new Link-Panel-only books,
check sky/fog colors and restart persistence, then compare an explicitly
colored Sky book and an uncolored terrain-only book. Nothing is committed or
published without explicit request.
The local `1.0.1.88-beta.1` slice seed-completes **explicit biome distribution
pages with missing biome pages**. Single Biome alone selects one eligible
biome. Tiled/Grid/region controllers alone choose at least two distinct
biomes, using 1.21.1's seeded continuation rule and the available pool as the
maximum. With one authored biome they fill just the missing second biome;
with two distinct authored biomes they add nothing. Repeated biome pages keep
their weight but do not satisfy the distinct-biome minimum by themselves.
Cave completion draws only recognized Cave/Nether biomes. Void supplies The
Void only when no biome is authored; a multi-biome Void book needs sufficient
explicit biome pages rather than acquiring an unwanted Void biome. Native
still rejects biome pages. No-controller biome behavior is unchanged, and
the broader recovery/instability grammar is not part of this slice.
Test a new Link Panel + Tiny Biome Regions book, Single Biome alone, and Tiny
with one/two specific biome pages. Also try Cave + Tiny and restart/revisit.
The operator fixture is `age create_completed_biomes <name> <seed> <terrain>
<distribution>`; `age biome_pool <name>` inspects the generator without loading
chunks. Existing Age pools do not change. Nothing is committed or published
without explicit request.
The local `1.0.1.89-beta.1` slice supports **unpaired Block/Fluid pages**.
When no feature immediately follows a material, the Age seed and original
page position choose a compatible implied feature, following 1.21.1's model.
Explicit material → feature pairs remain unchanged. Consecutive material
pages each receive their own selection; the resulting choices may repeat.
Only the saved feature plans gain these implied choices, not extra physical
pages in the book.
Solid candidates are Rocks, Floating Islands, Tendrils, Spheres, Spikes and
Obelisks, restricted by terrain. Cave permits Tendrils/Spheres; Void also
permits Floating Islands. Flat excludes Rocks. Fluid candidates are the three
lake types on supported surface noise terrain. An unpaired fluid on Flat,
Void or Cave is still refused because no compatible lake implementation is
available there. Explicit incompatible pairings and existing 32-feature
limits are unchanged; the full recovery/instability grammar remains pending.
Test Link Panel + Glowstone, consecutive Glowstone/Stone pages, and an unpaired
Water page on Standard terrain. Compare an explicit Glowstone → Tendrils
pair, try a solid on Cave/Void, and restart/revisit. Existing Ages retain
their saved features. Operator diagnostics: `age create_implied_material
<name> <seed> <terrain> <material-id>` and `age feature_plan <name>` (no chunk
generation). Nothing is committed or published without explicit request.
The local `1.0.1.90-beta.1` slice adds **random material features to new
Link-Panel-only Ages**. The Age seed rolls 0–2 distinct feature types using
1.21.1's Legacy rank weighting, then chooses a registered compatible solid
or source fluid for each. Candidates are Rocks, Floating Islands, Tendrils,
Spheres, Spikes, Obelisks and the three lake types, filtered by terrain.
The same creation pass rolls rare Skylands (1-in-8 on noise terrain), Crystal
Formations (1-in-10 on exposed surface terrain), and Star Fissures using the
root small-feature grammar. These are additional to the 0–2 material features.
Random structure selection and page-eligibility config remain future port
work. Authored books do not gain unrequested random features, and existing
Age plans are not rerolled. Choices persist through restart and reseed.
Test several new Link-Panel-only books for varied material features, including
Ages with none; compare an explicitly authored book and revisit after restart.
For controlled checks, `age create_random_features <name> <seed> <terrain>`
creates a feature-only fixture and `age feature_plan <name>` reports its
saved generator choices without generating chunks. `age create_random` uses
the same production selector. Nothing is committed or published without
explicit request.
The local `1.0.1.91-beta.1` slice ports **random structure selection** from
1.21.1. New Link-Panel-only Ages roll 0–2 distinct structure families in
addition to .90's material features. Legacy ranks prefer Villages, Strongholds
and Mineshafts, followed by Dungeons/Fortresses; newer structures are rarer.
The 26.3 Abandoned Camps page joins that rare pool. Candidates must support
the terrain and at least one biome in the actual selected biome pool.
A saved structure-control flag distinguishes an intentionally empty selection
from older Ages' native behavior. Unselected page-controlled structure sets
are disabled even when no structures were chosen; other native structure
families remain governed by vanilla biome rules. Authored pages retain their
biome override behavior, and old saved Ages are not changed. Cave Dungeons
remain excluded by the port's existing terrain restriction; this is a known
remaining difference from 1.21.1, not a new root behavior.
Test several new random Ages, including a restart/revisit, and compare an
authored Village book. For focused checks, `age create_random_structures
<name> <seed> <terrain> <biome-id|native>` creates a controlled fixture;
`age feature_plan <name>` reports the frozen selection, including Dungeons.
No catalog protocol change is needed. Nothing is committed or published
without explicit request.
The local `1.0.1.92-beta.1` slice enables **Dungeons on Cave terrain**, including
Nether cave biomes, matching 1.21.1's noise-terrain support. The already-ported
placement JSON is byte-identical to root: eight vanilla monster-room attempts
per page, uniformly from one block above the minimum to one below the maximum
build height. Vanilla still requires a suitable enclosed chamber; an attempt
is not a guaranteed dungeon. Stacking up to 32 pages is unchanged.
Only the combined Cave dungeon/lake rejection was split: lakes remain rejected
there, and Flat/Void still reject Dungeons. New random Cave Ages can now select
Dungeons through .91's normal eligibility/rank rules. Existing saved plans
are unchanged. `age create_dungeons <name> <seed> cave` creates an eight-page
fixture. `age dungeons_probe <name> [chunks]` optionally limits the search to
1–4 chunks per side; omitting it retains the previous 4×4 search. That probe
generates chunks and is reserved for manual placement checks.
Test Cave + Dungeons, optionally stacking pages for visibility; use spectator
mode to find monster rooms and compare a Nether-biome Cave Age. This slice's
server validation checks authoring and persistence without generating a large
region. No saved-field/protocol change is needed. Nothing is committed or
published without explicit request.
The local `1.0.1.93-beta.1` slice fixes **Nether and End biome materials on
noise terrain**, including Nether biomes in Cave Ages. The port had retained
the vanilla terrain's Overworld material rules, which could coat Nether
biomes in grass. It now ports 1.21.1's biome rule layer: the five vanilla End
biomes use the registered End rule, the five vanilla Nether biomes use the
registered Nether rule, and all other biomes retain the terrain's original
rules. Nether coverings include Nylium, Soul Sand/Soil, Basalt and Blackstone
where their native rules select them; End areas use End Stone.
Noise geometry, biome selection, lighting, default fluids, sea level and dry
Cave aquifer settings remain unchanged. This is not a Flat/Void layer rewrite.
The correction applies after reload to **new chunks only** in existing Ages;
already generated grass is not replaced. Test a new Nether-biome Cave Age
and End-biome Standard/Island Age, or explore new chunks after restarting.
Operator fixtures: `age create_biome_surface <name> <seed> <terrain> <biome-id>`
and `age material_probe <name> [chunk_x chunk_z]`. The latter examines one
terrain-stage chunk (default 0,0) without feature, lighting or entity generation. Nothing is committed
or published without explicit request.
From the repository root:
```sh
@@ -0,0 +1,83 @@
# Rivencraft 26.3 beta 1.0.1.93-beta.1
Collects the player-validated port slices .79–.93 since 1.0.1.78-beta.1.
This beta is not yet feature-complete with Minecraft 1.21.1.
## Celestial writing and rendering
- Ported standalone Sun and Moon pages and multiple independently authored
bodies, up to ten of each. Each controller consumes its own ordered Color,
Length, Phase, Direction, Angle and Size modifiers; Number pages fine-tune
supported values. Normal Time retains its paired sun/moon behavior.
- Color groups before Normal Time tint both bodies; independent Sun/Moon
groups can use different colors. Authored moons use full-moon sprites as in
1.21.1; native Normal Time retains calendar moon phases.
- Authored bodies render over Overworld, Nether, End and animated Static
backgrounds. Opaque centers retain transparent glows. Eclipse alignment
affects body color, background shading and gameplay sky light.
- Bare Sun/Moon pages receive independent seeded colors, sizes, bearings,
phases, directions and lengths, including stopped cycles. Explicit modifier
groups retain their authored behavior.
- Books without celestial controllers seed-complete 0–3 suns and 0–3 moons.
Empty skies do not accidentally draw a vanilla pair. Explicit controllers
suppress extra-body completion. Link-Panel-only random Ages also receive
seeded sky/fog colors using the 1.21.1 palette.
- All resolved settings are saved, not rerolled by viewing a book, restarting,
or reseeding future terrain. Existing Age profiles remain unchanged.
## Biome and feature completion
- Explicit biome-distribution pages can now seed-complete missing biome
pools. Single selects one; other nonnative controllers require two distinct
biomes. Fully omitted pools use the seeded continuation rule, bounded by the
available biome pool. Authored pools fill only the missing minimum.
- Supplied biome order and duplicate weights are retained. Two distinct
supplied biomes do not acquire an unwanted third biome. Cave pools use
recognized Cave/Nether biomes; Void does not inject The Void alongside
supplied biome pages.
- Unpaired Block/Fluid pages receive seeded terrain-compatible implied
features. Consecutive materials resolve independently; explicit pairs remain
authoritative. Unsupported pairings and the current feature caps remain.
- Link-Panel-only Ages roll 0–2 distinct material feature types with Legacy
rank weighting and compatible registered solids/source fluids. Additional
rare rolls cover Skylands, Crystal Formations and Star Fissures.
- Random Ages also roll 0–2 distinct structure families, filtered by terrain,
registry availability and the chosen biome pool. Newer structures, including
26.3 Abandoned Camps, are rare candidates. An intentionally empty selection
disables page-controlled structure sets rather than enabling all of them.
- Cave terrain now supports authored and randomly selected Dungeons, matching
1.21.1. Dungeon placement retains eight monster-room attempts per page and
modern build-height anchors. Flat/Void Dungeons and Cave lakes remain
unsupported.
## Nether and End material correction
- Ported 1.21.1's biome-specific material-rule layer for noise terrain. Nether
biomes in Cave Ages now use their native Netherrack, Nylium, Soul Sand/Soil,
Basalt and Blackstone rules instead of Overworld grass. End biomes use End
Stone. Other biomes retain the original terrain rules.
- Geometry, biome selection, dry Cave aquifers and other environment settings
are preserved. Flat/Void layers and authored feature materials are unchanged.
- Existing Ages pick up the correction after reload, but only new chunks
change. Already generated grass is not replaced.
## Compatibility and validation
- Requires Minecraft **26.3**, NeoForge **26.3.0.99**, and **Java 25**. Dynamic
Dimensions and Bad Packets are not required. Minecraft 1.21.1 is unchanged.
- Catalog protocol is **10**: use matching client/server builds.
- Player in-game checks passed for all included slices. Builds and targeted
disposable-server checks covered writing, seeded completion, codecs/catalog
round trips, terrain materials, reload, reseed and restart. Temporary servers
and test worlds were removed. This is not a full automated GameTest run.
- Full writing recovery, discovery/stations, instability, portals and optional
integration parity remain unfinished. Random-page eligibility config also
remains pending; valuable/hazardous random materials currently lack the
finished version's instability scoring. Creative availability does not imply
full page support. Number-controlled Static resolution (#109) and time-page
naming (#110) remain separate work.
- Beta save compatibility is not guaranteed. Back up worlds and keep the two
Minecraft version tracks and saves separate.
Report issues and help improve the wiki at
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
+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.78-beta.1
mod_version=1.0.1.93-beta.1
+29 -25
View File
@@ -9,33 +9,35 @@
"item.rivencraft.page.symbol.implemented": "Implemented in 26.3; book and terrain requirements still apply.",
"symbol.rivencraft.weather.normal.description": "Use an independent normal weather cycle. Cave terrain keeps weather off.",
"symbol.rivencraft.sky.overworld.description": "Use the native Overworld sky. Terrain and lighting remain independently authored.",
"symbol.rivencraft.sky.nether.description": "Use a Nether-style sky and reddish fog, without Nether terrain or gameplay rules.",
"symbol.rivencraft.sky.end.description": "Use the native End sky backdrop, without End terrain or a dragon fight.",
"symbol.rivencraft.sky.static.description": "Animated static surrounds the Age. Preceding color pages tint it; otherwise it is gray.",
"symbol.rivencraft.sky.nether.description": "Use a Nether-style reddish fog backdrop. Authored celestials render over it; terrain and gameplay rules remain independent.",
"symbol.rivencraft.sky.end.description": "Use the native End sky backdrop with authored celestials over it, without End terrain or a dragon fight.",
"symbol.rivencraft.sky.static.description": "Animated static surrounds the Age, behind authored celestials. Preceding colors tint it; otherwise it is gray.",
"symbol.rivencraft.lighting.ambient_color": "Ambient Light Color",
"symbol.rivencraft.lighting.ambient_color.description": "Apply preceding shared colors to ambient light. Use Bright separately for full effective ambient lighting.",
"symbol.rivencraft.number.description": "Before Color: value × 10 per channel. Before Length: value × 0.08 days. Before Phase/Angle: value × 15 degrees. Before Size: 0.25× to 3× (0-24).",
"symbol.rivencraft.size.small.description": "Before Normal Time: render the sun at 0.5× size. Its paired moon stays normal-sized, matching 1.21.1. A preceding Number overrides the size.",
"symbol.rivencraft.size.normal.description": "Before Normal Time: render the sun at 1× size. A preceding Number selects 0.25× through 3× (0-24). Its paired moon is unchanged.",
"symbol.rivencraft.size.large.description": "Before Normal Time: render the sun at 2× size. A preceding Number selects 0.25× through 3× (0-24). Its paired moon is unchanged.",
"symbol.rivencraft.angle.north.description": "Before Normal Time: set the native celestial orbit bearing to 0 degrees. A preceding Number overrides Angle in 15-degree steps.",
"symbol.rivencraft.angle.east.description": "Before Normal Time: set the native celestial orbit bearing to 90 degrees. A preceding Number overrides Angle in 15-degree steps.",
"symbol.rivencraft.angle.south.description": "Before Normal Time: set the native celestial orbit bearing to 180 degrees. A preceding Number overrides Angle in 15-degree steps.",
"symbol.rivencraft.angle.west.description": "Before Normal Time: set the native celestial orbit bearing to 270 degrees. A preceding Number overrides Angle in 15-degree steps.",
"symbol.rivencraft.phase.rising.description": "Before Normal Time: start this Age's native cycle at sunrise. A preceding Number overrides Phase in 15-degree steps from midnight.",
"symbol.rivencraft.phase.zenith.description": "Before Normal Time: start this Age's native cycle at noon. With Zero Length it remains noon.",
"symbol.rivencraft.phase.setting.description": "Before Normal Time: start this Age's native cycle at sunset. A preceding Number overrides Phase in 15-degree steps from midnight.",
"symbol.rivencraft.phase.nadir.description": "Before Normal Time: start this Age's native cycle at midnight. With Zero Length it remains midnight.",
"symbol.rivencraft.direction.clockwise.description": "Before Normal Time: advance this Age's native day cycle in its usual direction.",
"symbol.rivencraft.direction.counterclockwise.description": "Before Normal Time: run this Age's native day cycle in reverse. Does not change the Overworld clock.",
"symbol.rivencraft.celestial.normal_time.description": "Use the native sun/moon cycle with Length, Phase, Direction, Angle, and sun Size modifiers. Without a Length, use one normal day.",
"symbol.rivencraft.period.zero.description": "Before Normal Time: freeze the Age's day timelines at the written Phase, or a saved seeded time if Phase is omitted. A preceding Number overrides the Length.",
"symbol.rivencraft.period.half.description": "Before Normal Time: a half-length day (twice as fast). A preceding Number sets Length to value × 0.08 days.",
"symbol.rivencraft.period.full.description": "Before Normal Time: a normal-length day. A preceding Number sets Length to value × 0.08 days.",
"symbol.rivencraft.period.double.description": "Before Normal Time: a double-length day (half as fast). A preceding Number sets Length to value × 0.08 days.",
"symbol.rivencraft.color.red.description": "Add red to the next sky or Ambient Light Color target; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
"symbol.rivencraft.color.green.description": "Add green to the next sky or Ambient Light Color target; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
"symbol.rivencraft.color.blue.description": "Add blue to the next sky or Ambient Light Color target; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
"symbol.rivencraft.size.small.description": "Before Sun/Moon: 0.5× body size. Before Normal Time: size the sun only. A preceding Number overrides the size.",
"symbol.rivencraft.size.normal.description": "Before Sun/Moon: 1× body size. Before Normal Time: size the sun only. A preceding Number selects 0.25× through 3× (0-24).",
"symbol.rivencraft.size.large.description": "Before Sun/Moon: 2× body size. Before Normal Time: size the sun only. A preceding Number selects 0.25× through 3× (0-24).",
"symbol.rivencraft.angle.north.description": "Before Normal Time, Sun or Moon: set orbit bearing to 0 degrees. A preceding Number overrides Angle in 15-degree steps.",
"symbol.rivencraft.angle.east.description": "Before Normal Time, Sun or Moon: set orbit bearing to 90 degrees. A preceding Number overrides Angle in 15-degree steps.",
"symbol.rivencraft.angle.south.description": "Before Normal Time, Sun or Moon: set orbit bearing to 180 degrees. A preceding Number overrides Angle in 15-degree steps.",
"symbol.rivencraft.angle.west.description": "Before Normal Time, Sun or Moon: set orbit bearing to 270 degrees. A preceding Number overrides Angle in 15-degree steps.",
"symbol.rivencraft.phase.rising.description": "Before a celestial controller: start the sun or standalone moon at rising. A preceding Number overrides Phase in 15-degree steps from Nadir.",
"symbol.rivencraft.phase.zenith.description": "Before a celestial controller: start the sun or standalone moon overhead. Zero Length holds this position.",
"symbol.rivencraft.phase.setting.description": "Before a celestial controller: start the sun or standalone moon at setting. A preceding Number overrides Phase in 15-degree steps from Nadir.",
"symbol.rivencraft.phase.nadir.description": "Before a celestial controller: start the sun or standalone moon below the world. Zero Length holds this position.",
"symbol.rivencraft.direction.clockwise.description": "Before Normal Time, Sun or Moon: advance this Age's celestial cycle in its usual direction.",
"symbol.rivencraft.direction.counterclockwise.description": "Before Normal Time, Sun or Moon: reverse this Age's celestial cycle. Does not change the Overworld clock.",
"symbol.rivencraft.celestial.normal_time.description": "Add a sun/moon pair. Preceding modifiers apply to this pair; Size affects its sun only. Colors tint both. Limit ten suns and ten moons per Age.",
"symbol.rivencraft.celestial.sun.description": "Add a sun. A bare page gets saved seeded color, size and orbit controls. Written modifiers preserve authored behavior and reset per controller. Moon alignment causes eclipses; limit ten suns.",
"symbol.rivencraft.celestial.moon.description": "Add a moon with weaker light. A bare page gets saved seeded color, size and orbit controls. Written modifiers preserve authored behavior and reset per controller. Sun alignment causes eclipses; limit ten moons.",
"symbol.rivencraft.period.zero.description": "Before Normal Time, Sun or Moon: freeze at the written Phase, or a saved seeded time if omitted. A preceding Number overrides the Length.",
"symbol.rivencraft.period.half.description": "Before Normal Time, Sun or Moon: a half-length cycle (twice as fast). A preceding Number sets Length to value × 0.08 days.",
"symbol.rivencraft.period.full.description": "Before Normal Time, Sun or Moon: a normal-length cycle. A preceding Number sets Length to value × 0.08 days.",
"symbol.rivencraft.period.double.description": "Before Normal Time, Sun or Moon: a double-length cycle (half as fast). A preceding Number sets Length to value × 0.08 days.",
"symbol.rivencraft.color.red.description": "Add red to the next Sky, Ambient Light Color or celestial controller; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
"symbol.rivencraft.color.green.description": "Add green to the next Sky, Ambient Light Color or celestial controller; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
"symbol.rivencraft.color.blue.description": "Add blue to the next Sky, Ambient Light Color or celestial controller; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
"symbol.rivencraft.color.white.description": "Add equally to all RGB channels. Precede with a Number, or use a seeded amount.",
"symbol.rivencraft.color.black.description": "Subtract equally from all RGB channels. Precede with a Number, or use a seeded amount.",
"symbol.rivencraft.lighting.normal.description": "Use normal skylight and ambient brightness. Cave terrain remains without skylight.",
@@ -82,8 +84,10 @@
"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": "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.block.generated.description": "Modify the next compatible feature. Without a following feature, choose a compatible feature from the Age seed.",
"symbol.rivencraft.biome.native.description": "Use the terrain's native biome source; do not include Biome pages.",
"symbol.rivencraft.feature.dungeons.description": "Add monster-room dungeon attempts throughout surface noise or Cave terrain, including Nether cave biomes. Not Flat or Void.",
"symbol.rivencraft.biome.single.description": "Use one biome across the Age. If omitted, choose an eligible biome from the Age seed.",
"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.",
@@ -21,7 +21,7 @@ public final class AgeCatalogSync {
}
public static void registerPayloads(RegisterPayloadHandlersEvent event) {
var registrar = event.registrar("6");
var registrar = event.registrar("10");
registrar.playToClient(AgeCatalogPayload.TYPE, AgeCatalogPayload.STREAM_CODEC);
registrar.playToServer(AgeCatalogAckPayload.TYPE, AgeCatalogAckPayload.STREAM_CODEC,
(payload, context) -> {
@@ -14,22 +14,27 @@ import org.joml.Vector3f;
import org.joml.Vector3fc;
/** Frozen shared-color writing, adapted from 1.21.1 AgeSkyProfile. -1 means native. */
public record PortAgeColors(int sky, int ambient) {
public record PortAgeColors(int sky, int ambient, int celestial) {
public static final PortAgeColors NATIVE = new PortAgeColors(-1, -1);
public static final Codec<PortAgeColors> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.intRange(-1, 0xffffff).fieldOf("sky").forGetter(PortAgeColors::sky),
Codec.intRange(-1, 0xffffff).fieldOf("ambient").forGetter(PortAgeColors::ambient)
Codec.intRange(-1, 0xffffff).fieldOf("ambient").forGetter(PortAgeColors::ambient),
Codec.intRange(-1, 0xffffff).optionalFieldOf("celestial", -1).forGetter(PortAgeColors::celestial)
).apply(instance, PortAgeColors::new));
public static final StreamCodec<RegistryFriendlyByteBuf, PortAgeColors> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.INT, PortAgeColors::sky, ByteBufCodecs.INT, PortAgeColors::ambient, PortAgeColors::new);
ByteBufCodecs.INT, PortAgeColors::sky, ByteBufCodecs.INT, PortAgeColors::ambient,
ByteBufCodecs.INT, PortAgeColors::celestial, PortAgeColors::new);
public static final Identifier AMBIENT = Identifier.fromNamespaceAndPath("rivencraft", "lighting/ambient_color");
private static volatile Map<Identifier, PortAgeColors> clientColors = Map.of();
public PortAgeColors {
if (sky < -1 || sky > 0xffffff || ambient < -1 || ambient > 0xffffff)
if (sky < -1 || sky > 0xffffff || ambient < -1 || ambient > 0xffffff
|| celestial < -1 || celestial > 0xffffff)
throw new IllegalArgumentException("Invalid Age color");
}
public PortAgeColors(int sky, int ambient) { this(sky, ambient, -1); }
public static void updateClient(Map<Identifier, PortAgeColors> colors) { clientColors = Map.copyOf(colors); }
public static PortAgeColors forLevel(Level level) {
@@ -65,7 +70,7 @@ public record PortAgeColors(int sky, int ambient) {
public static PortAgeColors compile(List<Identifier> pages, long seed) {
if (!validNumbers(pages)) throw new IllegalArgumentException("Number pages must precede a supported Color, Length, Phase, Angle, or Size modifier");
int red = 0, green = 0, blue = 0, sky = -1, ambient = -1;
int red = 0, green = 0, blue = 0, sky = -1, ambient = -1, celestial = -1;
boolean written = false;
for (int i = 0; i < pages.size(); i++) {
Identifier page = pages.get(i);
@@ -81,17 +86,43 @@ public record PortAgeColors(int sky, int ambient) {
case "color/white" -> { red += amount; green += amount; blue += amount; }
case "color/black" -> { red -= amount; green -= amount; blue -= amount; }
}
} else if (PortSkyStyle.forPage(page) != null || page.equals(AMBIENT)) {
} else if (PortSkyStyle.forPage(page) != null || page.equals(AMBIENT)
|| PortDayCycle.isController(page)) {
if (PortDayCycle.isController(page)) celestial = -1;
if (written) {
int rgb = clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
if (page.equals(AMBIENT)) ambient = rgb; else sky = rgb;
if (page.equals(AMBIENT)) ambient = rgb;
else if (PortDayCycle.isController(page)) celestial = rgb;
else sky = rgb;
}
red = green = blue = 0;
written = false;
}
}
if (written) sky = clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
return new PortAgeColors(sky, ambient);
return new PortAgeColors(sky, ambient, celestial);
}
/** Only pageless random Ages receive a random sky tint, as in 1.21.1. */
public static PortAgeColors resolve(List<Identifier> pages, long seed) {
if (!pages.isEmpty()) return compile(pages, seed);
var random = new Random(seed ^ 0x534B5950524F464CL);
random.nextInt(5); // Root sky-style draw precedes the sky-color draw.
return new PortAgeColors(randomColor(random, true), -1);
}
/** Shared palette; retain .85's truncation for already-defined body defaults. */
static int randomColor(Random random, boolean rounded) {
int[] palette = {0xB74A50, 0xD9834B, 0xDBC574, 0x5AAB80, 0x6FA8DF, 0x9870B9, 0xD5DBE1, 0x252531};
int count = 1 + random.nextInt(3), red = 0, green = 0, blue = 0;
for (int i = 0; i < count; i++) {
int color = palette[random.nextInt(palette.length)];
red += color >> 16 & 255;
green += color >> 8 & 255;
blue += color & 255;
}
int offset = rounded ? count / 2 : 0;
return (red + offset) / count << 16 | (green + offset) / count << 8 | (blue + offset) / count;
}
private static int clamp(int value) { return Math.max(0, Math.min(255, value)); }
@@ -8,6 +8,7 @@ import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.LinkBookTarget;
import net.minecraft.commands.Commands;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.arguments.IdentifierArgument;
import net.minecraft.core.registries.Registries;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
@@ -68,6 +69,85 @@ public final class PortAgeCommands {
return 1;
})))
.then(Commands.literal("age")
.then(Commands.literal("create_biome_surface")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.then(Commands.argument("biome", IdentifierArgument.id())
.executes(PortAgeCommands::createBiomeSurface))))))
.then(Commands.literal("material_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(context -> probeBiomeMaterials(context, 0, 0))
.then(Commands.argument("chunk_x", IntegerArgumentType.integer())
.then(Commands.argument("chunk_z", IntegerArgumentType.integer())
.executes(context -> probeBiomeMaterials(context,
IntegerArgumentType.getInteger(context, "chunk_x"),
IntegerArgumentType.getInteger(context, "chunk_z")))))))
.then(Commands.literal("create_random_structures")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.then(Commands.argument("biome", IdentifierArgument.id())
.executes(PortAgeCommands::createRandomStructures))))))
.then(Commands.literal("create_random_features")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::createRandomFeatures)))))
.then(Commands.literal("create_implied_material")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.then(Commands.argument("material", IdentifierArgument.id())
.executes(PortAgeCommands::createImpliedMaterial))))))
.then(Commands.literal("feature_plan")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeFeaturePlan)))
.then(Commands.literal("create_completed_biomes")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.then(Commands.argument("distribution", StringArgumentType.word())
.executes(PortAgeCommands::createCompletedBiomes))))))
.then(Commands.literal("biome_pool")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeBiomePool)))
.then(Commands.literal("create_completed_sky")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createCompletedSky(context, null))
.then(Commands.argument("sky", StringArgumentType.word())
.executes(context -> createCompletedSky(context,
StringArgumentType.getString(context, "sky")))))))
.then(Commands.literal("create_random_celestials")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(PortAgeCommands::createRandomCelestialsAge))))
.then(Commands.literal("create_eclipse")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("mode", StringArgumentType.word())
.executes(PortAgeCommands::createEclipseAge)
.then(Commands.argument("sky", StringArgumentType.word())
.executes(context -> createEclipseAge(context,
StringArgumentType.getString(context, "sky"))))))))
.then(Commands.literal("create_celestials")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(PortAgeCommands::createMultiBodyAge))))
.then(Commands.literal("create_body")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("body", StringArgumentType.word())
.then(Commands.argument("phase", StringArgumentType.word())
.executes(PortAgeCommands::createBodyAge))))))
.then(Commands.literal("create_celestial_color")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("phase", StringArgumentType.word())
.then(Commands.argument("color", StringArgumentType.word())
.then(Commands.argument("number", IntegerArgumentType.integer(0, 24))
.executes(PortAgeCommands::createCelestialColorAge)))))))
.then(Commands.literal("create_size")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
@@ -209,8 +289,13 @@ public final class PortAgeCommands {
PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select(
context.getSource().getServer(), seed);
var environment = PortEnvironmentSelection.resolve(java.util.List.of(), seed, selected.terrain());
var cycle = PortDayCycle.resolve(java.util.List.of(), seed,
PortDayCycle.nativeTicks(context.getSource().getServer().overworld()));
var features = PortRandomFeatureSelection.select(context.getSource().getServer(),
seed, selected.terrain(), selected.biomes());
int created = createAge(context, selected.terrain(), selected.biomes(),
null, null, 0, null, null, environment);
features.rocks(), null, features.dungeons(), features.lakes(), features.terrain(), environment, cycle,
PortAgeColors.resolve(java.util.List.of(), seed));
if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
"Random Age plan: terrain=" + selected.terrain()
+ " biomes=" + selected.biomes()
@@ -244,7 +329,9 @@ public final class PortAgeCommands {
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
null, null, null, testDungeons())))))
null, null, null, testDungeons()))
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::createDungeonTerrain)))))
.then(Commands.literal("create_lakes")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
@@ -378,7 +465,10 @@ public final class PortAgeCommands {
.executes(PortAgeCommands::probeVegetation)))
.then(Commands.literal("dungeons_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeDungeons)))
.executes(context -> probeDungeons(context, 4))
.then(Commands.argument("chunks", IntegerArgumentType.integer(1, 4))
.executes(context -> probeDungeons(context,
IntegerArgumentType.getInteger(context, "chunks"))))))
.then(Commands.literal("lakes_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeLakes)))
@@ -586,12 +676,659 @@ public final class PortAgeCommands {
var pos = new net.minecraft.core.BlockPos(0, 80, 0);
var sun = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.SUN_ANGLE, pos);
var light = level.environmentAttributes().getDimensionValue(net.minecraft.world.attribute.EnvironmentAttributes.SKY_LIGHT_LEVEL);
var moon = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.MOON_ANGLE, pos);
if (cycle.customLight()) {
float expectedAngle = cycle.bodyAngle(source.totalTicks());
float weather = (1F - level.getRainLevel(1F) * 5F / 16F)
* (1F - level.getThunderLevel(1F) * 5F / 16F);
float expectedLight = 15F - (int) ((1F - cycle.bodyLight(source.totalTicks()) * weather) * 11F);
if (cycle.singleBody() && (Math.abs(sun - expectedAngle) > 0.01F || Math.abs(moon - expectedAngle) > 0.01F)
|| Math.abs(light - expectedLight) > 0.01F)
throw new IllegalStateException("Single-body attribute mismatch");
}
context.getSource().sendSuccess(() -> Component.literal("Cycle probe " + ageId(context)
+ " saved=" + cycle + " nativeTicks=" + source.totalTicks() + " ageTicks=" + sample.totalTicks()
+ " sunAngle=" + sun + " skyLight=" + light), false);
+ " sunAngle=" + sun + " moonAngle=" + moon + " skyLight=" + light), false);
return 1;
}
private static int createBiomeSurface(CommandContext<CommandSourceStack> context) {
try {
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var biome = IdentifierArgument.getId(context, "biome");
var plan = new PortBiomePlan(PortBiomePlan.Distribution.SINGLE, java.util.List.of(biome));
var server = context.getSource().getServer();
var registryOps = net.minecraft.resources.RegistryOps.create(com.mojang.serialization.JsonOps.INSTANCE, server.registryAccess());
for (var key : java.util.List.of(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.OVERWORLD,
net.minecraft.world.level.levelgen.NoiseGeneratorSettings.AMPLIFIED,
net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES,
net.minecraft.world.level.levelgen.NoiseGeneratorSettings.FLOATING_ISLANDS)) {
var base = server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS).getOrThrow(key).value();
var wrapped = PortBiomeMaterialRules.apply(server.registryAccess(), base).value();
if (!wrapped.noiseSettings().equals(base.noiseSettings()) || wrapped.noiseRouter() != base.noiseRouter()
|| wrapped.defaultBlock() != base.defaultBlock() || wrapped.defaultFluid() != base.defaultFluid()
|| wrapped.seaLevel() != base.seaLevel() || !wrapped.aquifers().equals(base.aquifers()))
throw new IllegalStateException("Biome material rules changed terrain/aquifer settings");
var codec = net.minecraft.world.level.levelgen.material.rule.MaterialRule.HOLDER_CODEC;
var encoded = codec.encodeStart(registryOps, wrapped.materialRule()).getOrThrow();
var decoded = codec.parse(registryOps, encoded).getOrThrow();
if (!encoded.equals(codec.encodeStart(registryOps, decoded).getOrThrow()))
throw new IllegalStateException("Biome material rule codec round trip failed");
}
return createAge(context, terrain, plan);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
/** One terrain-stage chunk only: no feature/lighting/entity generation. */
private static int probeBiomeMaterials(CommandContext<CommandSourceStack> context, int chunkX, int chunkZ) {
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded"));
return 0;
}
var chunk = level.getChunk(chunkX, chunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.TERRAIN);
var counts = new TreeMap<String, Integer>();
var pos = new net.minecraft.core.BlockPos.MutableBlockPos();
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++)
for (int y = chunk.getMinY(); y < chunk.getMaxY(); y++) {
var state = chunk.getBlockState(pos.set(chunk.getPos().getMinBlockX() + x, y, chunk.getPos().getMinBlockZ() + z));
if (!state.isAir()) counts.merge(net.minecraft.core.registries.BuiltInRegistries.BLOCK
.getKey(state.getBlock()).toString(), 1, Integer::sum);
}
context.getSource().sendSuccess(() -> Component.literal("Terrain materials " + ageId(context)
+ " chunk=" + chunkX + "," + chunkZ + " " + counts), false);
return 1;
}
private static int createRandomStructures(CommandContext<CommandSourceStack> context) {
try {
var server = context.getSource().getServer();
verifyRandomStructures(server);
long seed = LongArgumentType.getLong(context, "seed");
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
Identifier biome = IdentifierArgument.getId(context, "biome");
var plan = biome.equals(Identifier.withDefaultNamespace("native")) ? null : new PortBiomePlan(PortBiomePlan.Distribution.SINGLE,
java.util.List.of(biome));
var selected = PortRandomFeatureSelection.select(server, seed, terrain, plan);
int result = createAge(context, terrain, plan, selected.rocks(), null, selected.dungeons(), selected.lakes(), selected.terrain());
return result == 0 ? 0 : probeFeaturePlan(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static void verifyRandomStructures(net.minecraft.server.MinecraftServer server) {
boolean empty = false, one = false, two = false, dungeons = false, caveDungeons = false, modern = false;
for (var terrain : PortAgeTerrain.values()) for (long seed = 0; seed < 64; seed++) {
var selected = PortRandomStructureSelection.select(server, seed, terrain, null);
int count = selected.structures().size() + selected.dungeons();
if (count > 2 || selected.dungeons() > 1
|| new java.util.HashSet<>(selected.structures()).size() != selected.structures().size()
|| !selected.equals(PortRandomStructureSelection.select(server, seed, terrain, null))
|| terrain == PortAgeTerrain.VOID && count != 0)
throw new IllegalStateException("Random structure count/terrain/determinism failed");
empty |= count == 0;
one |= count == 1;
two |= count == 2;
dungeons |= selected.dungeons() != 0;
caveDungeons |= terrain == PortAgeTerrain.CAVE && selected.dungeons() != 0;
modern |= selected.structures().stream().anyMatch(page -> page.getPath().equals("feature/trial_chambers"));
for (var feature : selected.structures())
if (!PortStructurePages.supportsTerrain(feature, terrain))
throw new IllegalStateException("Random structure violated terrain eligibility");
for (String biome : java.util.List.of("plains", "the_end", "basalt_deltas")) {
var biomeId = Identifier.withDefaultNamespace(biome);
var fixed = new PortBiomePlan(PortBiomePlan.Distribution.SINGLE, java.util.List.of(biomeId));
var chosen = PortRandomStructureSelection.select(server, seed, terrain, fixed);
for (var feature : chosen.structures())
if (!PortStructurePages.randomEligible(feature, terrain, java.util.Set.of(biomeId), server.registryAccess()))
throw new IllegalStateException("Random structure violated biome eligibility");
}
}
if (!empty || !one || !two || !dungeons || !caveDungeons || !modern)
throw new IllegalStateException("Random structure fixture coverage failed");
var sets = server.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET);
var source = server.overworld().getChunkSource().getGenerator().getBiomeSource();
var villages = ResourceKey.create(Registries.STRUCTURE_SET, Identifier.withDefaultNamespace("villages"));
var page = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/villages");
if (PortStructurePages.filtered(sets, java.util.List.of(), source).get(villages).isPresent()
|| PortStructurePages.filtered(sets, java.util.List.of(page), source).get(villages).isEmpty()
|| PortStructurePages.randomEligible(page, PortAgeTerrain.STANDARD,
java.util.Set.of(Identifier.withDefaultNamespace("the_end")), server.registryAccess()))
throw new IllegalStateException("Random empty-selection/biome gate failed");
var merged = PortRandomFeatureSelection.select(server, 91, PortAgeTerrain.STANDARD, null);
var base = PortRandomFeatureSelection.select(91, PortAgeTerrain.STANDARD);
var encoded = PortTerrainFeaturePlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, merged.terrain()).getOrThrow();
if (!merged.terrain().structureControl()
|| !java.util.Objects.equals(merged.rocks(), base.rocks()) || !java.util.Objects.equals(merged.lakes(), base.lakes())
|| !merged.terrain().equals(PortTerrainFeaturePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
throw new IllegalStateException("Random structure merge/persistence failed");
var older = PortTerrainFeaturePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
com.google.gson.JsonParser.parseString("{\"choices\":[],\"skylands\":true}")).getOrThrow();
if (older.structureControl()) throw new IllegalStateException("Old Age acquired structure control");
}
private static int createRandomFeatures(CommandContext<CommandSourceStack> context) {
try {
verifyRandomFeatures();
long seed = LongArgumentType.getLong(context, "seed");
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var features = PortRandomFeatureSelection.select(seed, terrain);
int result = createAge(context, terrain, null, features.rocks(), null, 0, features.lakes(), features.terrain());
return result == 0 ? 0 : probeFeaturePlan(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static void verifyRandomFeatures() {
var seen = new java.util.HashSet<Identifier>();
boolean empty = false, single = false, doubleFeature = false, skylands = false, crystals = false, fissures = false;
for (var terrain : PortAgeTerrain.values()) for (long seed = 0; seed < 64; seed++) {
var selected = PortRandomFeatureSelection.select(seed, terrain);
if (!selected.equals(PortRandomFeatureSelection.select(seed, terrain)))
throw new IllegalStateException("Random feature completion is not deterministic");
var materialPages = new java.util.ArrayList<Identifier>();
if (selected.rocks() != null) {
materialPages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/rocks"));
var encoded = PortRockPlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, selected.rocks()).getOrThrow();
if (!selected.rocks().equals(PortRockPlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
throw new IllegalStateException("Random Rocks codec mismatch");
}
if (selected.lakes() != null) {
materialPages.addAll(selected.lakes().choices().stream().map(PortLakePlan.Choice::page).toList());
var encoded = PortLakePlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, selected.lakes()).getOrThrow();
if (!selected.lakes().equals(PortLakePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
throw new IllegalStateException("Random lake codec mismatch");
}
if (selected.terrain() != null) {
var plan = selected.terrain();
materialPages.addAll(plan.choices().stream().map(PortTerrainFeaturePlan.Choice::page)
.filter(page -> !page.equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS)).toList());
skylands |= plan.skylands();
crystals |= plan.choices().stream().anyMatch(choice -> choice.page().equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS));
fissures |= plan.starFissures();
var encoded = PortTerrainFeaturePlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, plan).getOrThrow();
if (!plan.equals(PortTerrainFeaturePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
throw new IllegalStateException("Random terrain feature codec mismatch");
}
int count = materialPages.size();
int expected = new java.util.Random(seed ^ 0x4D4154455249414CL).nextInt(3);
if (count != expected || new java.util.HashSet<>(materialPages).size() != count)
throw new IllegalStateException("Random material feature count/uniqueness failed");
var terrainPage = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT));
for (Identifier feature : materialPages)
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(java.util.List.of(terrainPage, feature)))
throw new IllegalStateException("Random feature is incompatible with its terrain");
seen.addAll(materialPages);
empty |= count == 0;
single |= count == 1;
doubleFeature |= count == 2;
}
if (seen.size() != 9 || !empty || !single || !doubleFeature || !skylands || !crystals || !fissures)
throw new IllegalStateException("Random feature seed coverage failed");
}
private static int createImpliedMaterial(CommandContext<CommandSourceStack> context) {
try {
verifyMaterialCompletion();
var server = context.getSource().getServer();
long seed = LongArgumentType.getLong(context, "seed");
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var material = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
IdentifierArgument.getId(context, "material"));
var pages = java.util.List.of(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
"terrain/" + terrain.name().toLowerCase(Locale.ROOT)), material);
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server, seed);
int result = createAge(context, terrain, biomes,
net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(pages, seed), null, 0,
net.dnorberg.rivencraft.writing.PortAuthoredWriting.lakePlan(pages, seed),
net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed));
return result == 0 ? 0 : probeFeaturePlan(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static void verifyMaterialCompletion() {
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
var solid = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(Identifier.withDefaultNamespace("glowstone"));
var fluid = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(Identifier.withDefaultNamespace("water"));
var seen = new java.util.HashSet<Identifier>();
for (var terrain : PortAgeTerrain.values()) for (long seed = 0; seed < 64; seed++) {
var terrainPage = symbol.apply("terrain/" + terrain.name().toLowerCase(Locale.ROOT));
var pages = java.util.List.of(terrainPage, solid, solid);
var completed = net.dnorberg.rivencraft.writing.PortMaterialCompletion.complete(pages, seed);
if (completed.size() != 5 || !completed.get(1).equals(solid) || !completed.get(3).equals(solid)
|| !net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(completed)
|| !completed.equals(net.dnorberg.rivencraft.writing.PortMaterialCompletion.complete(pages, seed))
|| !completed.equals(net.dnorberg.rivencraft.writing.PortMaterialCompletion.complete(completed, seed)))
throw new IllegalStateException("Implied material completion lost order, compatibility or determinism");
seen.add(completed.get(2));
var explicit = java.util.List.of(terrainPage, solid, PortTerrainFeaturePlan.TENDRILS);
if (!explicit.equals(net.dnorberg.rivencraft.writing.PortMaterialCompletion.complete(explicit, seed)))
throw new IllegalStateException("Explicit material target was replaced");
boolean wetTerrain = terrain == PortAgeTerrain.STANDARD || terrain == PortAgeTerrain.AMPLIFIED || terrain == PortAgeTerrain.ISLAND;
var wet = java.util.List.of(terrainPage, fluid);
if (net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(wet) != wetTerrain)
throw new IllegalStateException("Implied fluid terrain validation failed");
if (wetTerrain) {
var lakes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.lakePlan(wet, seed);
if (lakes == null || lakes.choices().size() != 1
|| !lakes.choices().getFirst().fluid().equals(Identifier.withDefaultNamespace("water")))
throw new IllegalStateException("Implied lake discarded its authored fluid");
}
}
if (seen.size() < 6 || net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(
java.util.List.of(fluid, PortTerrainFeaturePlan.TENDRILS)))
throw new IllegalStateException("Implied feature coverage/explicit incompatibility check failed");
}
private static int probeFeaturePlan(CommandContext<CommandSourceStack> context) {
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded"));
return 0;
}
if (!(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)) {
context.getSource().sendSuccess(() -> Component.literal("Feature plan " + ageId(context) + ": none"), false);
return 1;
}
context.getSource().sendSuccess(() -> Component.literal("Feature plan " + ageId(context)
+ " rocks=" + features.rocks() + " lakes=" + features.lakes() + " dungeons=" + features.dungeons()
+ " terrain=" + features.terrainFeatures()), false);
return 1;
}
private static int createCompletedBiomes(CommandContext<CommandSourceStack> context) {
try {
var server = context.getSource().getServer();
long seed = LongArgumentType.getLong(context, "seed");
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var distribution = PortBiomePlan.Distribution.valueOf(
StringArgumentType.getString(context, "distribution").toUpperCase(Locale.ROOT));
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
var pages = java.util.List.of(symbol.apply("terrain/" + terrain.name().toLowerCase(Locale.ROOT)),
symbol.apply("biome/" + distribution.name().toLowerCase(Locale.ROOT)));
var plan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server, seed);
// Controller-only and partial pools complete; already-sufficient pools never gain extras.
var plains = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("plains"));
var forest = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("forest"));
for (var mode : PortBiomePlan.Distribution.values()) {
var controller = symbol.apply("biome/" + mode.name().toLowerCase(Locale.ROOT));
for (long s = 0; s < 32; s++) {
var selected = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(controller), server, s);
if (!selected.equals(net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(controller), server, s))
|| selected.distribution() != mode)
throw new IllegalStateException("Biome completion is not deterministic");
}
if (mode == PortBiomePlan.Distribution.NATIVE || mode == PortBiomePlan.Distribution.SINGLE) continue;
var partial = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(controller, plains), server, seed);
var full = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(controller, plains, forest), server, seed);
var repeated = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(controller, plains, plains), server, seed);
if (partial.biomes().size() != 2 || !partial.biomes().getFirst().equals(Identifier.withDefaultNamespace("plains"))
|| !full.biomes().equals(java.util.List.of(Identifier.withDefaultNamespace("plains"), Identifier.withDefaultNamespace("forest")))
|| repeated.biomes().size() != 3 || new java.util.HashSet<>(repeated.biomes()).size() != 2)
throw new IllegalStateException("Authored pool completion changed weights or added unnecessary biomes");
}
var encoded = PortBiomePlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, plan).getOrThrow();
if (!plan.equals(PortBiomePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
throw new IllegalStateException("Completed biome plan codec mismatch");
int created = createAge(context, terrain, plan);
if (created != 0) context.getSource().sendSuccess(() -> Component.literal("Completed biome plan: " + plan), false);
return created == 0 ? 0 : probeBiomePool(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeBiomePool(CommandContext<CommandSourceStack> context) {
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded"));
return 0;
}
var biomes = level.getChunkSource().getGenerator().getBiomeSource().possibleBiomes().stream()
.flatMap(holder -> holder.unwrapKey().stream()).map(key -> key.identifier()).sorted().toList();
context.getSource().sendSuccess(() -> Component.literal("Biome pool " + ageId(context) + ": " + biomes), false);
return 1;
}
private static int createCompletedSky(CommandContext<CommandSourceStack> context, String sky) {
try {
verifyRandomSkyColors();
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
var pages = new java.util.ArrayList<Identifier>(java.util.stream.Stream.of("terrain/flat", "lighting/normal", "weather/off").map(symbol).toList());
if (sky != null) {
PortSkyStyle.valueOf(sky.toUpperCase(Locale.ROOT));
pages.add(symbol.apply("sky/" + sky.toLowerCase(Locale.ROOT)));
}
var server = context.getSource().getServer();
long seed = LongArgumentType.getLong(context, "seed"), origin = PortDayCycle.nativeTicks(server.overworld());
var cycle = PortDayCycle.resolve(pages, seed, origin);
int zeroSeed = -1;
boolean sunOnly = false, moonOnly = false, mixed = false;
for (int s = 0; s < 256; s++) {
var selected = PortDayCycle.resolve(java.util.List.of(), s, origin);
long suns = selected.bodies().stream().filter(body -> body.kind() == PortDayCycle.SUN).count();
long moons = selected.bodies().size() - suns;
if (suns > 3 || moons > 3 || !selected.authored()
|| !selected.equals(PortDayCycle.resolve(pages, s, origin)))
throw new IllegalStateException("Missing-celestial completion bounds/determinism failed");
if (suns == 0 && moons == 0) {
zeroSeed = zeroSeed < 0 ? s : zeroSeed;
if (selected.bodyMode() != PortDayCycle.NONE || selected.customLight())
throw new IllegalStateException("Empty sky incorrectly acquires a hidden native pair");
}
sunOnly |= suns > 0 && moons == 0;
moonOnly |= moons > 0 && suns == 0;
mixed |= suns > 0 && moons > 0;
}
for (String controller : java.util.List.of("celestial/sun", "celestial/moon", "celestial/normal_time")) {
var explicit = java.util.List.of(symbol.apply(controller));
if (!PortDayCycle.resolve(explicit, seed, origin).equals(PortDayCycle.compile(explicit, seed, origin)))
throw new IllegalStateException("Explicit controller gained additional bodies");
}
var older = PortDayCycle.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
com.google.gson.JsonParser.parseString("{\"authored\":false,\"period\":1,\"stopped_time\":0}")).getOrThrow();
if (zeroSeed < 0 || !sunOnly || !moonOnly || !mixed || !older.equals(PortDayCycle.NATIVE))
throw new IllegalStateException("Completion coverage/old-profile default failed");
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
RuntimeAgeBackend.create(server, ageId(context), seed, biomes, PortAgeTerrain.FLAT,
null, null, 0, null, null, environment.weather(), environment.lighting(), environment.sky(),
PortAgeColors.compile(pages, seed), cycle);
final int sample = zeroSeed;
context.getSource().sendSuccess(() -> Component.literal("Completed sky; sample zero-body seed=" + sample), false);
return probeDayCycle(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int createRandomCelestialsAge(CommandContext<CommandSourceStack> context) {
try {
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
var pages = java.util.stream.Stream.of("celestial/sun", "celestial/moon", "celestial/sun",
"weather/off", "lighting/normal", "terrain/flat").map(symbol).toList();
var server = context.getSource().getServer();
long seed = LongArgumentType.getLong(context, "seed"), origin = PortDayCycle.nativeTicks(server.overworld());
var cycle = PortDayCycle.compile(pages, seed, origin);
var decorated = new java.util.ArrayList<Identifier>(java.util.stream.Stream.of("weather/off", "sky/overworld",
"terrain/flat", "number/10", "color/red", "lighting/ambient_color").map(symbol).toList());
decorated.addAll(pages);
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages)
|| cycle.bodies().size() != 3 || cycle.bodies().get(0).equals(cycle.bodies().get(2))
|| !cycle.equals(PortDayCycle.compile(pages, seed, origin))
|| !cycle.bodies().equals(PortDayCycle.compile(decorated, seed, origin).bodies()))
throw new IllegalStateException("Random body determinism/independence failed");
var oneSun = PortDayCycle.compile(java.util.List.of(symbol.apply("celestial/sun")), seed, origin);
var oneMoon = PortDayCycle.compile(java.util.List.of(symbol.apply("celestial/moon")), seed, origin);
if (oneSun.bodies().size() != 1 || oneMoon.bodies().size() != 1
|| !oneSun.bodies().getFirst().equals(cycle.bodies().getFirst())
|| oneMoon.bodies().getFirst().kind() != PortDayCycle.MOON)
throw new IllegalStateException("Standalone bare body compilation failed");
int stopped = 0, moving = 0, forward = 0, reverse = 0;
for (int s = 0; s < 128; s++) {
var random = PortDayCycle.compile(pages, s, origin);
for (var body : random.bodies()) {
if (body.period() == 0) stopped++; else moving++;
if (body.clockwise()) forward++; else reverse++;
if (body.period() != 0 && (body.period() < 0.2 || body.period() > 2)
|| body.size() < 0.75 || body.size() >= 1.75 || body.bearing() < 0
|| body.bearing() >= 360 || body.phaseTick() < 0 || body.phaseTick() >= 24000
|| body.color() < 0 || body.color() > 0xffffff)
throw new IllegalStateException("Random body bounds failed");
}
}
if (stopped == 0 || moving == 0 || forward == 0 || reverse == 0)
throw new IllegalStateException("Random body pool coverage failed");
var explicit = PortDayCycle.compile(java.util.List.of(symbol.apply("size/small"), symbol.apply("celestial/sun")), seed, origin);
var normal = PortDayCycle.compile(java.util.List.of(PortDayCycle.NORMAL_TIME), seed, origin);
if (!explicit.bodies().isEmpty() || explicit.sunSize() != 0.5F || explicit.period() != 1 || explicit.bearing() != -1
|| !normal.bodies().isEmpty() || normal.period() != 1 || normal.sunSize() != 1)
throw new IllegalStateException("Explicit/Normal Time default changed");
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
RuntimeAgeBackend.create(server, ageId(context), seed, biomes, PortAgeTerrain.FLAT,
null, null, 0, null, null, environment.weather(), environment.lighting(), environment.sky(),
PortAgeColors.compile(pages, seed), cycle);
return probeDayCycle(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int createEclipseAge(CommandContext<CommandSourceStack> context) {
return createEclipseAge(context, null);
}
private static int createEclipseAge(CommandContext<CommandSourceStack> context, String sky) {
try {
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
String mode = StringArgumentType.getString(context, "mode");
if (!java.util.Set.of("aligned", "partial", "clear", "below", "moving").contains(mode))
throw new IllegalArgumentException("Use aligned, partial, clear, below or moving");
long seed = LongArgumentType.getLong(context, "seed");
var server = context.getSource().getServer();
long origin = PortDayCycle.nativeTicks(server.overworld());
var pages = new java.util.ArrayList<Identifier>();
pages.add(symbol.apply("period/zero"));
if (mode.equals("partial")) pages.add(symbol.apply("number/13"));
pages.add(symbol.apply(mode.equals("below") ? "phase/nadir" : "phase/zenith"));
if (mode.equals("partial")) { pages.add(symbol.apply("number/24")); pages.add(symbol.apply("size/large")); }
pages.addAll(java.util.stream.Stream.of("number/24", "color/red", "celestial/sun",
mode.equals("moving") ? "period/half" : "period/zero",
mode.equals("below") || mode.equals("clear") ? "phase/nadir" : "phase/zenith").map(symbol).toList());
if (mode.equals("partial")) pages.add(symbol.apply("number/24"));
pages.addAll(java.util.stream.Stream.of("size/large", "celestial/moon", "weather/off", "lighting/normal", "terrain/flat").map(symbol).toList());
if (sky != null) {
PortSkyStyle.valueOf(sky.toUpperCase(Locale.ROOT));
pages.add(symbol.apply("sky/" + sky.toLowerCase(Locale.ROOT)));
}
var cycle = PortDayCycle.compile(pages, seed, origin);
var bodies = cycle.bodies();
float eclipse = PortCelestialBody.eclipseAt(bodies, origin);
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages)
|| (mode.equals("aligned") || mode.equals("moving")) && (Math.abs(eclipse - 1) > 0.0001F
|| Math.abs(cycle.bodyLight(origin) - 0.15F) > 0.0001F)
|| mode.equals("partial") && !(eclipse > 0 && eclipse < 1)
|| (mode.equals("clear") || mode.equals("below")) && eclipse != 0)
throw new IllegalStateException("Eclipse fixture failed: " + mode);
var sun = bodies.get(0);
var moon = bodies.get(1);
if (sun.eclipse(java.util.List.of(sun, sun), origin) != 0
|| sun.eclipseColor(1) != 0xfccccc || moon.eclipseColor(1) != 0
|| mode.equals("moving") && (PortCelestialBody.eclipseAt(bodies, origin + 100) >= eclipse
|| PortCelestialBody.eclipseAt(bodies, origin + 800) != 0))
throw new IllegalStateException("Eclipse color/movement invariant failed");
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
RuntimeAgeBackend.create(server, ageId(context), seed, biomes, PortAgeTerrain.FLAT,
null, null, 0, null, null, environment.weather(), environment.lighting(), environment.sky(),
PortAgeColors.compile(pages, seed), cycle);
context.getSource().sendSuccess(() -> Component.literal("Eclipse fixture " + mode + " overlap=" + eclipse
+ " bodyLight=" + cycle.bodyLight(origin) + " skyShade=" + (1F - 0.65F * eclipse)), false);
return probeDayCycle(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int createMultiBodyAge(CommandContext<CommandSourceStack> context) {
try {
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
var pages = java.util.stream.Stream.of("period/zero", "phase/zenith", "size/large", "number/24", "color/red", "celestial/sun",
"period/zero", "phase/nadir", "size/small", "number/24", "color/blue", "celestial/moon",
"period/half", "phase/rising", "angle/east", "direction/counterclockwise", "number/12", "color/green", "celestial/sun",
"weather/off", "lighting/normal", "terrain/flat").map(symbol).toList();
var server = context.getSource().getServer();
long seed = LongArgumentType.getLong(context, "seed"), origin = PortDayCycle.nativeTicks(server.overworld());
var cycle = PortDayCycle.compile(pages, seed, origin);
var bodies = cycle.bodies();
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages) || bodies.size() != 3
|| bodies.get(0).kind() != PortDayCycle.SUN || bodies.get(0).color() != 0xf00000 || bodies.get(0).size() != 2F
|| bodies.get(1).kind() != PortDayCycle.MOON || bodies.get(1).color() != 0x0000f0 || bodies.get(1).size() != 0.5F
|| bodies.get(2).color() != 0x007800 || bodies.get(2).size() != 1F || bodies.get(2).bearing() != 90
|| Math.abs(bodies.get(2).angle(origin + 100) + 93F) > 0.001F
|| cycle.bodyLight(origin) != 1F)
throw new IllegalStateException("Independent body fixture failed");
var limits = new java.util.ArrayList<Identifier>();
for (int i = 0; i < 10; i++) limits.add(PortDayCycle.NORMAL_TIME);
var atLimit = PortDayCycle.compile(limits, seed, origin);
if (!PortDayCycle.validWriting(limits) || atLimit.bodies().size() != 20)
throw new IllegalStateException("Body limit rejected valid writing");
limits.add(symbol.apply("celestial/sun"));
if (PortDayCycle.validWriting(limits)) throw new IllegalStateException("Body limit not enforced");
var reset = PortDayCycle.compile(java.util.stream.Stream.of("number/24", "color/red", "size/large", "celestial/sun",
"size/normal", "celestial/moon", "celestial/normal_time").map(symbol).toList(), seed, origin).bodies();
if (reset.size() != 4 || reset.get(1).color() != -1 || reset.get(1).size() != 1F
|| reset.get(2).color() != -1 || reset.get(3).size() != 1F
|| Math.abs(Math.abs(reset.get(2).angle(origin) - reset.get(3).angle(origin)) - 180F) > 0.001F)
throw new IllegalStateException("Body modifier reset/paired expansion failed");
var moonlight = PortDayCycle.compile(java.util.stream.Stream.of("period/zero", "phase/zenith", "celestial/moon",
"period/zero", "phase/zenith", "celestial/moon").map(symbol).toList(), seed, origin);
var sunlight = PortDayCycle.compile(java.util.stream.Stream.of("period/zero", "phase/zenith", "celestial/sun",
"period/zero", "phase/zenith", "celestial/sun").map(symbol).toList(), seed, origin);
if (Math.abs(moonlight.bodyLight(origin) - 0.36F) > 0.0001F || sunlight.bodyLight(origin) != 1F)
throw new IllegalStateException("Multi-body light aggregation failed");
var encoded = PortDayCycle.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, cycle).getOrThrow();
if (!cycle.equals(PortDayCycle.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
throw new IllegalStateException("Body profile codec round trip failed");
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
RuntimeAgeBackend.create(server, ageId(context), seed, biomes, PortAgeTerrain.FLAT,
null, null, 0, null, null, environment.weather(), environment.lighting(), environment.sky(),
PortAgeColors.compile(pages, seed), cycle);
return probeDayCycle(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int createBodyAge(CommandContext<CommandSourceStack> context) {
try {
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
String body = StringArgumentType.getString(context, "body");
Identifier controller = symbol.apply("celestial/" + body);
Identifier phase = symbol.apply("phase/" + StringArgumentType.getString(context, "phase"));
if ((!body.equals("sun") && !body.equals("moon")) || !PortDayCycle.isPhase(phase))
throw new IllegalArgumentException("Use sun/moon and rising/zenith/setting/nadir");
var pages = java.util.stream.Stream.of("period/zero", phase.getPath(), "angle/east",
"size/large", "number/24", "color/red", controller.getPath(), "weather/off",
"lighting/normal", "terrain/flat").map(symbol).toList();
var server = context.getSource().getServer();
long seed = LongArgumentType.getLong(context, "seed"), origin = PortDayCycle.nativeTicks(server.overworld());
var cycle = PortDayCycle.compile(pages, seed, origin);
var colors = PortAgeColors.compile(pages, seed);
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages)
|| !cycle.singleBody() || colors.celestial() != 0xf00000 || colors.sky() != -1
|| cycle.sunSize() != 2 || cycle.bearing() != 90)
throw new IllegalStateException("Single-body fixture failed");
for (String kind : java.util.List.of("sun", "moon")) {
var target = symbol.apply("celestial/" + kind);
int mode = kind.equals("sun") ? PortDayCycle.SUN : PortDayCycle.MOON;
for (var entry : java.util.Map.of("rising", 0, "zenith", 6000, "setting", 12000, "nadir", 18000).entrySet()) {
var selected = PortDayCycle.compile(java.util.List.of(symbol.apply("period/zero"),
symbol.apply("phase/" + entry.getKey()), target), seed, origin);
float expectedLight = entry.getKey().equals("zenith") ? (mode == PortDayCycle.MOON ? 0.18F : 1F) : 0F;
if (selected.bodyMode() != mode || selected.ticks(origin) != entry.getValue()
|| Math.abs(selected.bodyLight(origin) - expectedLight) > 0.0001F)
throw new IllegalStateException("Single-body phase/light invariant failed");
}
var moving = PortDayCycle.compile(java.util.stream.Stream.of("period/half", "phase/zenith",
"direction/counterclockwise", "number/12", "size/large", target.getPath()).map(symbol).toList(), seed, origin);
if (moving.sunSize() != 1.625F || moving.ticks(origin + 100) != 5800
|| Math.abs(moving.bodyAngle(origin + 100) + 3F) > 0.0001F
|| !PortDayCycle.validWriting(java.util.List.of(target, PortDayCycle.NORMAL_TIME))
|| !PortDayCycle.validWriting(java.util.List.of(target, target))
|| PortDayCycle.validWriting(java.util.List.of(target, symbol.apply("size/large"))))
throw new IllegalStateException("Single-body modifier/order invariant failed");
}
var older = PortDayCycle.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
com.google.gson.JsonParser.parseString("{\"authored\":true,\"period\":1,\"stopped_time\":0,\"sun_size\":2}")).getOrThrow();
if (older.bodyMode() != PortDayCycle.PAIRED || older.singleBody())
throw new IllegalStateException("Old paired body default failed");
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
if (environment.sky() != PortSkyStyle.OVERWORLD)
throw new IllegalStateException("Single body selected an incompatible sky");
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
RuntimeAgeBackend.create(server, ageId(context), seed, biomes, PortAgeTerrain.FLAT,
null, null, 0, null, null, environment.weather(), environment.lighting(), environment.sky(), colors, cycle);
return probeColors(context) == 1 ? probeDayCycle(context) : 0;
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int createCelestialColorAge(CommandContext<CommandSourceStack> context) {
try {
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
Identifier phase = symbol.apply("phase/" + StringArgumentType.getString(context, "phase"));
Identifier color = symbol.apply("color/" + StringArgumentType.getString(context, "color"));
if (!PortDayCycle.isPhase(phase) || !PortAgeColors.isColor(color))
throw new IllegalArgumentException("Use rising/zenith/setting/nadir and red/green/blue/white/black");
var pages = java.util.List.of(symbol.apply("number/10"), symbol.apply("color/blue"),
symbol.apply("sky/overworld"), symbol.apply("number/20"), symbol.apply("color/white"),
PortAgeColors.AMBIENT, symbol.apply("period/zero"), phase,
symbol.apply("number/" + IntegerArgumentType.getInteger(context, "number")), color,
PortDayCycle.NORMAL_TIME, symbol.apply("weather/off"), symbol.apply("lighting/normal"),
symbol.apply("terrain/flat"));
long seed = LongArgumentType.getLong(context, "seed");
var colors = PortAgeColors.compile(pages, seed);
int amount = IntegerArgumentType.getInteger(context, "number") * 10;
int expected = switch (color.getPath()) {
case "color/red" -> amount << 16;
case "color/green" -> amount << 8;
case "color/blue" -> amount;
case "color/white" -> amount * 0x010101;
default -> 0;
};
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages)
|| !colors.equals(new PortAgeColors(0x000064, 0xc8c8c8, expected)))
throw new IllegalStateException("Celestial color fixture failed");
// Color groups persist across intervening Length/Phase/Size modifiers, then
// are consumed by Normal Time rather than leaking into a following target.
var mixed = PortAgeColors.compile(java.util.stream.Stream.of("number/24", "color/red",
"number/24", "color/red", "number/24", "color/black", "size/large",
"celestial/normal_time", "sky/overworld", "lighting/ambient_color")
.map(symbol).toList(), seed);
var trailing = PortAgeColors.compile(java.util.stream.Stream.of("number/10", "color/red",
"celestial/normal_time", "number/5", "color/green").map(symbol).toList(), seed);
var unnumbered = java.util.List.of(symbol.apply("color/blue"), PortDayCycle.NORMAL_TIME);
var older = PortAgeColors.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
com.google.gson.JsonParser.parseString("{\"sky\":100,\"ambient\":-1}")).getOrThrow();
if (!mixed.equals(new PortAgeColors(-1, -1, 0xf00000))
|| !trailing.equals(new PortAgeColors(0x003200, -1, 0x640000))
|| !older.equals(new PortAgeColors(100, -1))
|| !PortAgeColors.compile(unnumbered, seed).equals(PortAgeColors.compile(unnumbered, seed))
|| PortAgeColors.compile(unnumbered, seed).sky() != -1)
throw new IllegalStateException("Celestial color grouping/default invariant failed");
var server = context.getSource().getServer();
var cycle = PortDayCycle.compile(pages, seed, PortDayCycle.nativeTicks(server.overworld()));
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
RuntimeAgeBackend.create(server, ageId(context), seed, biomes, PortAgeTerrain.FLAT,
null, null, 0, null, null, environment.weather(), environment.lighting(), environment.sky(), colors, cycle);
return probeColors(context) == 1 ? probeDayCycle(context) : 0;
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int createColorAge(CommandContext<CommandSourceStack> context) {
try {
String sky = StringArgumentType.getString(context, "sky").toLowerCase(Locale.ROOT);
@@ -641,7 +1378,8 @@ public final class PortAgeCommands {
var sky = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.SKY_COLOR, pos);
var fog = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.FOG_COLOR, pos);
var ambient = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.AMBIENT_LIGHT_COLOR, pos);
if (colors.sky() >= 0 && (!sky.equals(PortAgeColors.vector(colors.sky())) || !fog.equals(sky))
var expectedFog = colors.sky() < 0 ? fog : new org.joml.Vector3f(PortAgeColors.vector(colors.sky())).mul(PortSkyStyle.fogShade(level));
if (colors.sky() >= 0 && (!sky.equals(PortAgeColors.vector(colors.sky())) || !fog.equals(expectedFog))
|| colors.ambient() >= 0 && !ambient.equals(PortAgeColors.vector(colors.ambient()))) {
context.getSource().sendFailure(Component.literal("Color attribute mismatch"));
return 0;
@@ -651,6 +1389,29 @@ public final class PortAgeCommands {
return 1;
}
private static void verifyRandomSkyColors() {
long[] seeds = {0, 1, 2, 14, 86, 87};
int[] expected = {0x8abac0, 0x8cb39c, 0x6e3841, 0xcda38c, 0xb7a6cd, 0x406859};
for (int i = 0; i < seeds.length; i++) {
var colors = PortAgeColors.resolve(java.util.List.of(), seeds[i]);
var encoded = PortAgeColors.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, colors).getOrThrow();
if (colors.sky() != expected[i] || colors.ambient() != -1 || colors.celestial() != -1
|| !colors.equals(PortAgeColors.resolve(java.util.List.of(), seeds[i]))
|| !colors.equals(PortAgeColors.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
throw new IllegalStateException("Random sky root-vector/persistence mismatch");
}
for (var paths : java.util.List.of(java.util.List.of("sky/end"), java.util.List.of("celestial/sun"),
java.util.List.of("terrain/flat"), java.util.List.of("number/12", "color/red", "sky/overworld"),
java.util.List.of("number/0", "color/black"))) {
var pages = paths.stream().map(path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path)).toList();
if (!PortAgeColors.resolve(pages, 87).equals(PortAgeColors.compile(pages, 87)))
throw new IllegalStateException("Authored sky colors were randomized");
}
var older = PortAgeColors.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
com.google.gson.JsonParser.parseString("{\"sky\":-1,\"ambient\":-1}")).getOrThrow();
if (!older.equals(PortAgeColors.NATIVE)) throw new IllegalStateException("Old sky-color defaults changed");
}
private static void verifyCatalogRoundTrip(net.minecraft.server.MinecraftServer server) {
var payload = new AgeCatalogPayload(1, server.levelKeys().stream().map(key -> key.identifier()).toList(),
RuntimeAgeBackend.savedColors(server), RuntimeAgeBackend.savedDayCycles(server));
@@ -925,7 +1686,41 @@ public final class PortAgeCommands {
java.util.Collections.nCopies(8, page));
}
private static int probeDungeons(CommandContext<CommandSourceStack> context) {
private static int createDungeonTerrain(CommandContext<CommandSourceStack> context) {
try {
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var terrainPage = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
"terrain/" + terrain.name().toLowerCase(Locale.ROOT));
var dungeon = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dungeons");
var cave = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/cave");
var pages = new java.util.ArrayList<Identifier>(java.util.Collections.nCopies(8, dungeon));
pages.addFirst(terrainPage);
var maximum = new java.util.ArrayList<Identifier>(java.util.Collections.nCopies(32, dungeon));
maximum.addFirst(cave);
if (net.dnorberg.rivencraft.writing.PortAuthoredWriting.dungeonCount(maximum) != 32)
throw new IllegalStateException("Cave Dungeons stacking failed");
maximum.add(dungeon);
if (net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(maximum)
|| net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(java.util.List.of(cave,
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/surface_lakes"))))
throw new IllegalStateException("Dungeon cap/lake restriction changed");
for (String unsupported : java.util.List.of("flat", "void"))
if (net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(java.util.List.of(dungeon,
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + unsupported))))
throw new IllegalStateException("Dungeons unexpectedly accepted unsupported terrain");
var server = context.getSource().getServer();
int count = net.dnorberg.rivencraft.writing.PortAuthoredWriting.dungeonCount(pages);
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server,
LongArgumentType.getLong(context, "seed"));
int created = createAge(context, terrain, biomes, null, null, count);
return created == 0 ? 0 : probeFeaturePlan(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeDungeons(CommandContext<CommandSourceStack> context, int chunks) {
Identifier id = ageId(context);
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)) {
@@ -934,7 +1729,7 @@ public final class PortAgeCommands {
}
int count = features.dungeons();
int spawners = 0;
for (int chunkX = 0; chunkX < 4; chunkX++) for (int chunkZ = 0; chunkZ < 4; chunkZ++) {
for (int chunkX = 0; chunkX < chunks; chunkX++) for (int chunkZ = 0; chunkZ < chunks; chunkZ++) {
var chunk = level.getChunk(chunkX, chunkZ);
for (var pos : chunk.getBlockEntitiesPos()) {
if (chunk.getBlockState(pos).is(Blocks.SPAWNER)) spawners++;
@@ -942,7 +1737,7 @@ public final class PortAgeCommands {
}
int placed = spawners;
context.getSource().sendSuccess(() -> Component.literal(
"Dungeons probe " + id + " pages=" + count + " spawners=" + placed), false);
"Dungeons probe " + id + " pages=" + count + " chunks=" + chunks + "x" + chunks + " spawners=" + placed), false);
return placed;
}
@@ -1092,6 +1887,24 @@ public final class PortAgeCommands {
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures,
PortEnvironmentSelection environment) {
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
environment, PortDayCycle.NATIVE);
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures,
PortEnvironmentSelection environment, PortDayCycle cycle) {
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
environment, cycle, PortAgeColors.NATIVE);
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures,
PortEnvironmentSelection environment, PortDayCycle cycle, PortAgeColors colors) {
String name = StringArgumentType.getString(context, "name");
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
long seed = LongArgumentType.getLong(context, "seed");
@@ -1099,7 +1912,7 @@ public final class PortAgeCommands {
try {
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
environment.weather(), environment.lighting(), environment.sky());
environment.weather(), environment.lighting(), environment.sky(), colors, cycle);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
@@ -0,0 +1,31 @@
package net.dnorberg.rivencraft.age;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.Registries;
import net.minecraft.data.worldgen.material.EndMaterialRules;
import net.minecraft.data.worldgen.material.NetherMaterialRules;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
import net.minecraft.world.level.levelgen.material.MaterialRules;
/** Root AgeDimensionService.withBiomeMaterialRules adapted to 26.3's rule registry. */
final class PortBiomeMaterialRules {
private PortBiomeMaterialRules() {}
static Holder<NoiseGeneratorSettings> apply(HolderLookup.Provider registries, NoiseGeneratorSettings base) {
var biomes = registries.lookupOrThrow(Registries.BIOME);
var rules = registries.lookupOrThrow(Registries.MATERIAL_RULE);
var materials = MaterialRules.sequence(
MaterialRules.ifTrue(MaterialRules.isBiome(biomes, Biomes.THE_END, Biomes.END_HIGHLANDS,
Biomes.END_MIDLANDS, Biomes.SMALL_END_ISLANDS, Biomes.END_BARRENS),
MaterialRules.getRule(rules, EndMaterialRules.END)),
MaterialRules.ifTrue(MaterialRules.isBiome(biomes, Biomes.NETHER_WASTES, Biomes.SOUL_SAND_VALLEY,
Biomes.CRIMSON_FOREST, Biomes.WARPED_FOREST, Biomes.BASALT_DELTAS),
MaterialRules.getRule(rules, NetherMaterialRules.NETHER)),
base.materialRule().value());
return Holder.direct(new NoiseGeneratorSettings(base.noiseSettings(), base.defaultBlock(), base.defaultFluid(),
base.noiseRouter(), Holder.direct(materials), base.spawnTarget(), base.seaLevel(), base.disableMobGeneration(),
base.aquifers(), base.useLegacyRandomSource(), base.debugFunctions()));
}
}
@@ -0,0 +1,96 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
/** One frozen authored orbit, using the same angular motion as 1.21.1 Body. */
public record PortCelestialBody(int kind, double period, int stoppedTime, long originTick,
int phaseTick, boolean clockwise, int bearing, float size, int color) {
public static final Codec<PortCelestialBody> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.intRange(1, 2).fieldOf("kind").forGetter(PortCelestialBody::kind),
Codec.doubleRange(0, 2).fieldOf("period").forGetter(PortCelestialBody::period),
Codec.intRange(0, 23999).fieldOf("stopped_time").forGetter(PortCelestialBody::stoppedTime),
Codec.LONG.fieldOf("origin_tick").forGetter(PortCelestialBody::originTick),
Codec.intRange(0, 23999).fieldOf("phase_tick").forGetter(PortCelestialBody::phaseTick),
Codec.BOOL.fieldOf("clockwise").forGetter(PortCelestialBody::clockwise),
Codec.intRange(-1, 359).fieldOf("bearing").forGetter(PortCelestialBody::bearing),
Codec.floatRange(0.25F, 3F).fieldOf("size").forGetter(PortCelestialBody::size),
Codec.intRange(-1, 0xffffff).fieldOf("color").forGetter(PortCelestialBody::color)
).apply(instance, PortCelestialBody::new));
public static final StreamCodec<RegistryFriendlyByteBuf, PortCelestialBody> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.VAR_INT, PortCelestialBody::kind, ByteBufCodecs.DOUBLE, PortCelestialBody::period,
ByteBufCodecs.VAR_INT, PortCelestialBody::stoppedTime, ByteBufCodecs.VAR_LONG, PortCelestialBody::originTick,
ByteBufCodecs.VAR_INT, PortCelestialBody::phaseTick, ByteBufCodecs.BOOL, PortCelestialBody::clockwise,
ByteBufCodecs.INT, PortCelestialBody::bearing, ByteBufCodecs.FLOAT, PortCelestialBody::size,
ByteBufCodecs.INT, PortCelestialBody::color, PortCelestialBody::new);
public PortCelestialBody {
if (kind != PortDayCycle.SUN && kind != PortDayCycle.MOON || !Double.isFinite(period)
|| period < 0 || period > 2 || stoppedTime < 0 || stoppedTime >= 24000
|| phaseTick < 0 || phaseTick >= 24000 || bearing < -1 || bearing > 359
|| !Float.isFinite(size) || size < 0.25F || size > 3 || color < -1 || color > 0xffffff)
throw new IllegalArgumentException("Invalid celestial body");
}
public static PortCelestialBody of(PortDayCycle cycle, int kind, int color, boolean pairedMoon) {
int offset = pairedMoon ? 12000 : 0;
return new PortCelestialBody(kind, cycle.period(), (cycle.stoppedTime() + offset) % 24000,
cycle.originTick(), (cycle.phaseTick() + offset) % 24000, cycle.clockwise(),
cycle.bearing(), pairedMoon ? 1F : cycle.sunSize(), color);
}
public float angle(double nativeTicks) {
double time = period == 0 ? stoppedTime
: (clockwise ? 1 : -1) * (nativeTicks - originTick) / period + phaseTick;
return (float) (((time - 6000D) % 24000D) * (360D / 24000D));
}
public float light(long nativeTicks) {
return Math.max(0F, (float) Math.cos(Math.toRadians(angle(nativeTicks))))
* (kind == PortDayCycle.MOON ? 0.18F : 1F);
}
public net.minecraft.world.phys.Vec3 direction(double ticks) {
double orbit = Math.toRadians(angle(ticks)), axis = Math.toRadians(bearing < 0 ? -90 : bearing);
double lateral = Math.sin(orbit);
return new net.minecraft.world.phys.Vec3(Math.sin(axis) * lateral, Math.cos(orbit), Math.cos(axis) * lateral);
}
/** Root AgeSkyProfile.eclipseFor: contact uses the central quarter-size disc, not its halo. */
public float eclipse(java.util.List<PortCelestialBody> bodies, double ticks) {
float strongest = 0;
var direction = direction(ticks);
double radius = Math.atan((kind == PortDayCycle.SUN ? 30D : 20D) * size * 0.25D / 100D);
for (var other : bodies) {
if (other.kind == kind) continue;
double separation = Math.acos(Math.max(-1D, Math.min(1D, direction.dot(other.direction(ticks)))));
double contact = radius + Math.atan((other.kind == PortDayCycle.SUN ? 30D : 20D) * other.size * 0.25D / 100D);
strongest = Math.max(strongest, (float) Math.max(0D, 1D - separation / contact));
}
return strongest;
}
public static float eclipseAt(java.util.List<PortCelestialBody> bodies, double ticks) {
float strongest = 0;
for (var body : bodies) if (body.kind == PortDayCycle.SUN && body.direction(ticks).y > 0D)
strongest = Math.max(strongest, body.eclipse(bodies, ticks));
return strongest;
}
/** Shared client/server-checkable RGB rule; alpha is handled by the renderer. */
public int eclipseColor(float eclipse) {
int rgb = color < 0 ? 0xffffff : color;
float corona = kind == PortDayCycle.SUN ? (float) Math.pow(eclipse, 8D) * 0.8F : 0F;
int red = channel(rgb >> 16 & 255, eclipse, corona);
int green = channel(rgb >> 8 & 255, eclipse, corona);
int blue = channel(rgb & 255, eclipse, corona);
return red << 16 | green << 8 | blue;
}
private int channel(int value, float eclipse, float corona) {
return Math.round(kind == PortDayCycle.MOON ? value * (1F - eclipse) : value + (255 - value) * corona);
}
}
@@ -19,7 +19,9 @@ import net.minecraft.world.level.Level;
/** Authored native celestial controls. Never mutates the shared clock. */
public record PortDayCycle(boolean authored, double period, int stoppedTime,
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize) {
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize, int bodyMode,
List<PortCelestialBody> bodies) {
public static final int PAIRED = 0, SUN = 1, MOON = 2, NONE = 3;
public static final PortDayCycle NATIVE = new PortDayCycle(false, 1, 0);
public static final Identifier NORMAL_TIME = Identifier.fromNamespaceAndPath("rivencraft", "celestial/normal_time");
public static final Codec<PortDayCycle> CODEC = RecordCodecBuilder.create(instance -> instance.group(
@@ -30,20 +32,40 @@ public record PortDayCycle(boolean authored, double period, int stoppedTime,
Codec.intRange(0, 23999).optionalFieldOf("phase_tick", 0).forGetter(PortDayCycle::phaseTick),
Codec.BOOL.optionalFieldOf("clockwise", true).forGetter(PortDayCycle::clockwise),
Codec.intRange(-1, 359).optionalFieldOf("bearing", -1).forGetter(PortDayCycle::bearing),
Codec.floatRange(0.25F, 3F).optionalFieldOf("sun_size", 1F).forGetter(PortDayCycle::sunSize)
Codec.floatRange(0.25F, 3F).optionalFieldOf("sun_size", 1F).forGetter(PortDayCycle::sunSize),
Codec.intRange(0, 3).optionalFieldOf("body_mode", PAIRED).forGetter(PortDayCycle::bodyMode),
PortCelestialBody.CODEC.listOf().optionalFieldOf("bodies", List.of()).forGetter(PortDayCycle::bodies)
).apply(instance, PortDayCycle::new));
public static final StreamCodec<RegistryFriendlyByteBuf, PortDayCycle> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.BOOL, PortDayCycle::authored, ByteBufCodecs.DOUBLE, PortDayCycle::period,
ByteBufCodecs.VAR_INT, PortDayCycle::stoppedTime, ByteBufCodecs.VAR_LONG, PortDayCycle::originTick,
ByteBufCodecs.VAR_INT, PortDayCycle::phaseTick, ByteBufCodecs.BOOL, PortDayCycle::clockwise,
ByteBufCodecs.INT, PortDayCycle::bearing, ByteBufCodecs.FLOAT, PortDayCycle::sunSize, PortDayCycle::new);
ByteBufCodecs.INT, PortDayCycle::bearing, ByteBufCodecs.FLOAT, PortDayCycle::sunSize,
ByteBufCodecs.VAR_INT, PortDayCycle::bodyMode,
PortCelestialBody.STREAM_CODEC.apply(ByteBufCodecs.list()), PortDayCycle::bodies, PortDayCycle::new);
private static volatile Map<Identifier, PortDayCycle> clientCycles = Map.of();
public PortDayCycle {
bodies = List.copyOf(bodies);
if (!Double.isFinite(period) || period < 0 || period > 2 || stoppedTime < 0 || stoppedTime >= 24000
|| phaseTick < 0 || phaseTick >= 24000 || bearing < -1 || bearing >= 360
|| !Float.isFinite(sunSize) || sunSize < 0.25F || sunSize > 3F)
|| !Float.isFinite(sunSize) || sunSize < 0.25F || sunSize > 3F
|| bodyMode < PAIRED || bodyMode > NONE
|| bodyMode == NONE && (!authored || !bodies.isEmpty()))
throw new IllegalArgumentException("Invalid day cycle");
if (bodies.stream().filter(body -> body.kind() == SUN).count() > 10
|| bodies.stream().filter(body -> body.kind() == MOON).count() > 10)
throw new IllegalArgumentException("An Age may have at most ten suns and ten moons");
}
public PortDayCycle(boolean authored, double period, int stoppedTime,
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize, int bodyMode) {
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, bearing, sunSize, bodyMode, List.of());
}
public PortDayCycle(boolean authored, double period, int stoppedTime,
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize) {
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, bearing, sunSize, PAIRED);
}
public PortDayCycle(boolean authored, double period, int stoppedTime) {
@@ -87,7 +109,14 @@ public record PortDayCycle(boolean authored, double period, int stoppedTime,
}
public static boolean supports(Identifier page) {
return page.equals(NORMAL_TIME) || isPeriod(page) || isPhase(page) || isDirection(page) || isAngle(page) || isSize(page);
return isController(page) || isPeriod(page) || isPhase(page) || isDirection(page) || isAngle(page) || isSize(page);
}
public static boolean isController(Identifier page) {
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
case "celestial/normal_time", "celestial/sun", "celestial/moon" -> true;
default -> false;
};
}
public static boolean isSize(Identifier page) {
@@ -98,25 +127,74 @@ public record PortDayCycle(boolean authored, double period, int stoppedTime,
}
public static boolean validWriting(List<Identifier> pages) {
int target = pages.indexOf(NORMAL_TIME), periods = 0, phases = 0, directions = 0, angles = 0, sizes = 0;
if (target != pages.lastIndexOf(NORMAL_TIME)) return false;
for (int i = 0; i < pages.size(); i++) {
if (isPeriod(pages.get(i)) && (++periods > 1 || i > target)) return false;
if (isPhase(pages.get(i)) && (++phases > 1 || i > target)) return false;
if (isDirection(pages.get(i)) && (++directions > 1 || i > target)) return false;
if (isAngle(pages.get(i)) && (++angles > 1 || i > target)) return false;
if (isSize(pages.get(i)) && (++sizes > 1 || i > target)) return false;
int periods = 0, phases = 0, directions = 0, angles = 0, sizes = 0, suns = 0, moons = 0;
for (Identifier page : pages) {
if (isPeriod(page) && ++periods > 1 || isPhase(page) && ++phases > 1
|| isDirection(page) && ++directions > 1 || isAngle(page) && ++angles > 1
|| isSize(page) && ++sizes > 1) return false;
if (isController(page)) {
if (!page.getPath().equals("celestial/moon")) suns++;
if (!page.getPath().equals("celestial/sun")) moons++;
if (suns > 10 || moons > 10) return false;
periods = phases = directions = angles = sizes = 0;
}
}
return true;
return periods + phases + directions + angles + sizes == 0;
}
public static PortDayCycle compile(List<Identifier> pages, long seed) {
return compile(pages, seed, 0);
}
/** Creation-only completion. Exact diagnostic fixtures can still use compile. */
public static PortDayCycle resolve(List<Identifier> pages, long seed, long nativeTicks) {
if (!validWriting(pages) || !PortAgeColors.validNumbers(pages))
throw new IllegalArgumentException("Invalid celestial modifier order");
if (pages.stream().anyMatch(PortDayCycle::isController)) return compile(pages, seed, nativeTicks);
// Match root AgeSkyProfile.random's count draw positions: sky style,
// mixed sky color, then independent 0..3 sun and moon counts.
var random = new Random(seed ^ 0x534B5950524F464CL);
random.nextInt(5);
randomColor(random);
int suns = random.nextInt(4), moons = random.nextInt(4);
var bodies = new java.util.ArrayList<PortCelestialBody>();
for (int i = 0; i < suns + moons; i++)
bodies.addAll(randomUnmodified(seed, i, nativeTicks, i < suns ? SUN : MOON).bodies);
if (bodies.isEmpty()) return new PortDayCycle(true, 1, 0, 0, 0, true, -1, 1F, NONE);
var first = bodies.getFirst();
return new PortDayCycle(true, first.period(), first.stoppedTime(), first.originTick(), first.phaseTick(),
first.clockwise(), first.bearing(), first.size(), first.kind(), bodies);
}
public static PortDayCycle compile(List<Identifier> pages, long seed, long nativeTicks) {
if (!validWriting(pages)) throw new IllegalArgumentException("Place at most one Length, Phase, Direction, Angle, and Size before Normal Time");
if (!pages.contains(NORMAL_TIME)) return NATIVE;
if (!validWriting(pages)) throw new IllegalArgumentException("Place at most one of each celestial modifier before its controller; limit ten suns and ten moons");
if (pages.stream().filter(PortDayCycle::isController).count() <= 1)
return compileOne(pages, seed, nativeTicks, 0);
var bodies = new java.util.ArrayList<PortCelestialBody>();
PortDayCycle first = null;
int start = 0, ordinal = 0;
for (int i = 0; i < pages.size(); i++) if (isController(pages.get(i))) {
var cycle = compileOne(pages.subList(start, i + 1), seed, nativeTicks, ordinal++);
if (first == null) first = cycle;
// Prefix retains original page indices for seeded unnumbered colors.
int color = PortAgeColors.compile(pages.subList(0, i + 1), seed).celestial();
if (!cycle.bodies.isEmpty()) bodies.addAll(cycle.bodies);
else {
if (cycle.bodyMode != MOON) bodies.add(PortCelestialBody.of(cycle, SUN, color, false));
if (cycle.bodyMode != SUN) bodies.add(PortCelestialBody.of(cycle, MOON, color, cycle.bodyMode == PAIRED));
}
start = i + 1;
}
return new PortDayCycle(first.authored, first.period, first.stoppedTime, first.originTick,
first.phaseTick, first.clockwise, first.bearing, first.sunSize, first.bodyMode, bodies);
}
private static PortDayCycle compileOne(List<Identifier> pages, long seed, long nativeTicks, int ordinal) {
var controller = pages.stream().filter(PortDayCycle::isController).findFirst().orElse(null);
if (controller == null) return NATIVE;
int mode = controller.getPath().equals("celestial/sun") ? SUN
: controller.getPath().equals("celestial/moon") ? MOON : PAIRED;
if (mode != PAIRED && !hasBodyModifiers(pages)) return randomUnmodified(seed, ordinal, nativeTicks, mode);
double period = 1;
Integer phase = null;
boolean clockwise = true;
@@ -160,7 +238,59 @@ public record PortDayCycle(boolean authored, double period, int stoppedTime,
int stopped = period == 0 ? phase != null ? phase
: new Random(seed ^ 0x4441594359434C45L).nextInt(24000) : 0;
return new PortDayCycle(true, period, stopped, phase != null ? nativeTicks : 0,
phase == null ? 0 : phase, clockwise, bearing, size);
phase == null ? 0 : phase, clockwise, bearing, size, mode);
}
private static boolean hasBodyModifiers(List<Identifier> pages) {
boolean color = false, controls = false;
for (var page : pages) {
if (isController(page)) return controls || color;
if (PortAgeColors.isColor(page)) color = true;
else if (PortSkyStyle.forPage(page) != null || page.equals(PortAgeColors.AMBIENT)) color = false;
if (isPeriod(page) || isPhase(page) || isDirection(page) || isAngle(page) || isSize(page)) controls = true;
}
return false;
}
private static PortDayCycle randomUnmodified(long seed, int ordinal, long nativeTicks, int mode) {
// Independent per-controller stream: unrelated terrain/sky/weather pages
// cannot reshuffle the bodies. Use root random-Age bounds, not native defaults.
var random = new Random(seed ^ 0x43454C4553544941L ^ (ordinal + 1L) * 0x9E3779B97F4A7C15L);
double period = random.nextInt(7) == 0 ? 0D : 0.2D + random.nextDouble() * 1.8D;
int phase = random.nextInt(24000), bearing = random.nextInt(360);
float size = 0.75F + random.nextFloat();
boolean clockwise = random.nextBoolean();
int color = randomColor(random);
var cycle = new PortDayCycle(true, period, period == 0 ? phase : 0, nativeTicks,
phase, clockwise, bearing, size, mode);
return new PortDayCycle(true, period, cycle.stoppedTime, nativeTicks, phase, clockwise, bearing, size,
mode, List.of(PortCelestialBody.of(cycle, mode, color, false)));
}
private static int randomColor(Random random) {
return PortAgeColors.randomColor(random, false);
}
public boolean singleBody() { return authored && (bodyMode == SUN || bodyMode == MOON); }
public boolean customLight() { return singleBody() || !bodies.isEmpty(); }
/** Linear authored orbit, as in 1.21.1 Body; zero degrees is Zenith. */
public float bodyAngle(long nativeTicks) {
return (float) (((time(nativeTicks) - 6000D) % 24000D) * (360D / 24000D));
}
public float bodyLight(long nativeTicks) {
if (bodyMode == NONE) return 0;
if (!bodies.isEmpty()) {
float sun = 0, moon = 0;
for (var body : bodies) {
if (body.kind() == SUN) sun += body.light(nativeTicks) * (1F - 0.85F * body.eclipse(bodies, nativeTicks));
else moon += body.light(nativeTicks);
}
return Math.min(1F, sun > 0 ? sun : moon);
}
float altitude = Math.max(0F, (float) Math.cos(Math.toRadians(bodyAngle(nativeTicks))));
return altitude * (bodyMode == MOON ? 0.18F : 1F);
}
public long ticks(long nativeTicks) {
@@ -113,7 +113,7 @@ public final class PortFeatureChunkGenerator extends ChunkGenerator {
}
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
RandomState random, long seed) {
if (hasStructurePages()) {
if (controlsStructures()) {
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)
@@ -124,13 +124,13 @@ public final class PortFeatureChunkGenerator extends ChunkGenerator {
return base.createState(sets, random, seed);
}
private boolean hasStructurePages() {
return terrainFeatures != null && !terrainFeatures.structures().isEmpty();
private boolean controlsStructures() {
return terrainFeatures != null && (terrainFeatures.structureControl() || !terrainFeatures.structures().isEmpty());
}
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
StructureManager structures, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> dimension) {
if (hasStructurePages()) super.createStructures(registries, state, structures, chunk, templates, dimension);
if (controlsStructures()) 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) {
@@ -54,7 +54,8 @@ public final class PortRandomAgeSelection {
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
}
private static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
public static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
if (terrain == PortAgeTerrain.VOID) return List.of(Identifier.withDefaultNamespace("the_void"));
if (terrain == PortAgeTerrain.CAVE) {
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
.filter(PortRegistryPages::isCaveBiome)
@@ -0,0 +1,85 @@
package net.dnorberg.rivencraft.age;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
import net.dnorberg.rivencraft.writing.PortRegistryPages;
import net.minecraft.resources.Identifier;
/** Frozen page-free material features and rare formations from 1.21.1. */
public final class PortRandomFeatureSelection {
private PortRandomFeatureSelection() {}
public record Selection(PortRockPlan rocks, PortLakePlan lakes, PortTerrainFeaturePlan terrain, int dungeons) {}
public static Selection select(net.minecraft.server.MinecraftServer server, long seed,
PortAgeTerrain terrain, PortBiomePlan biomes) {
var base = select(seed, terrain);
var structures = PortRandomStructureSelection.select(server, seed, terrain, biomes);
var plan = base.terrain();
plan = plan == null ? new PortTerrainFeaturePlan(List.of(), false, 0, false, structures.structures(), List.of(), true)
: new PortTerrainFeaturePlan(plan.choices(), plan.skylands(), plan.denseOres(), plan.starFissures(),
structures.structures(), plan.carvers(), true);
return new Selection(base.rocks(), base.lakes(), plan, structures.dungeons());
}
public static Selection select(long seed, PortAgeTerrain terrain) {
var random = new Random(seed ^ 0x4D4154455249414CL);
int count = random.nextInt(3);
var terrainPage = id("terrain/" + terrain.name().toLowerCase(java.util.Locale.ROOT));
var candidates = new ArrayList<PortRankedChoice.Option<Identifier>>();
for (String name : List.of("rocks", "floating_islands", "tendrils", "spheres", "spikes", "obelisks",
"surface_lakes", "deep_lakes", "lava_lakes")) {
var page = id("feature/" + name);
if (PortAuthoredWriting.supports(List.of(terrainPage, page)))
candidates.add(new PortRankedChoice.Option<>(page, rank(name)));
}
var pages = new ArrayList<Identifier>();
pages.add(terrainPage);
List<Identifier> solids = PortRegistryPages.solidMaterialIds(), fluids = PortRegistryPages.fluidMaterialIds();
for (int selected = 0; selected < count && !candidates.isEmpty();) {
Identifier feature = PortRankedChoice.choose(random, candidates);
candidates.removeIf(option -> option.value().equals(feature));
var materials = PortLakePlan.isLake(feature) ? fluids : solids;
if (materials.isEmpty()) continue;
pages.add(PortRegistryPages.blockPageId(materials.get(random.nextInt(materials.size()))));
pages.add(feature);
selected++;
}
if (terrain != PortAgeTerrain.FLAT && terrain != PortAgeTerrain.VOID
&& Math.floorMod(mix(seed ^ 0x534B594C414E4453L), 8) == 0)
pages.add(PortTerrainFeaturePlan.SKYLANDS);
if (terrain != PortAgeTerrain.CAVE && terrain != PortAgeTerrain.VOID && random.nextInt(10) == 0)
pages.add(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS);
if (randomStarFissure(seed)) pages.add(PortTerrainFeaturePlan.STAR_FISSURES);
return new Selection(PortAuthoredWriting.rockPlan(pages, seed), PortAuthoredWriting.lakePlan(pages, seed),
PortAuthoredWriting.terrainFeaturePlan(pages, seed), 0);
}
private static int rank(String name) {
return switch (name) {
case "surface_lakes", "deep_lakes" -> 1;
case "lava_lakes" -> 2;
case "spheres", "spikes", "obelisks" -> 3;
default -> 4;
};
}
private static boolean randomStarFissure(long seed) {
var random = new Random(mix(seed ^ 0x535441524752414DL));
for (int step = 0; step < 27; step++) {
int branch = random.nextInt(5);
if (branch == 0) return false;
if (random.nextInt(9) == 0) return true;
if (branch == 1) return false;
}
return false;
}
private static Identifier id(String path) { return Identifier.fromNamespaceAndPath("rivencraft", path); }
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
}
@@ -0,0 +1,69 @@
package net.dnorberg.rivencraft.age;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Set;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.dimension.LevelStem;
/** Creation-only structure completion, adapted from 1.21.1 AgeStructureSelection. */
public final class PortRandomStructureSelection {
private static final Identifier DUNGEONS = page("dungeons");
private PortRandomStructureSelection() {}
public record Selection(int dungeons, List<Identifier> structures) {
public Selection { structures = List.copyOf(structures); }
}
public static Selection select(MinecraftServer server, long seed, PortAgeTerrain terrain, PortBiomePlan plan) {
Set<Identifier> biomes = actualBiomes(server, terrain, plan);
var eligible = new ArrayList<PortRankedChoice.Option<Identifier>>();
for (String name : List.of("dungeons", "mineshafts", "nether_fortresses", "strongholds", "villages",
"trial_chambers", "bastions", "buried_treasure", "shipwrecks", "ocean_ruins", "pillager_outposts",
"ruined_portals", "nether_fossils", "ancient_cities", "trail_ruins", "abandoned_camps")) {
Identifier feature = page(name);
boolean allowed = feature.equals(DUNGEONS)
? net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(List.of(
Identifier.fromNamespaceAndPath("rivencraft", "terrain/" + terrain.name().toLowerCase(java.util.Locale.ROOT)), feature))
&& server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
.get(ResourceKey.create(Registries.PLACED_FEATURE,
Identifier.fromNamespaceAndPath("rivencraft", "dungeons"))).isPresent()
: PortStructurePages.randomEligible(feature, terrain, biomes, server.registryAccess());
if (allowed) eligible.add(new PortRankedChoice.Option<>(feature, rank(name)));
}
var random = new Random(seed ^ 0x5354525543545552L);
int count = random.nextInt(3), dungeons = 0;
var structures = new ArrayList<Identifier>();
for (int selected = 0; selected < count && !eligible.isEmpty(); selected++) {
var feature = PortRankedChoice.choose(random, eligible);
if (feature.equals(DUNGEONS)) dungeons++;
else structures.add(feature);
eligible.removeIf(option -> option.value().equals(feature));
}
return new Selection(dungeons, structures);
}
private static Set<Identifier> actualBiomes(MinecraftServer server, PortAgeTerrain terrain, PortBiomePlan plan) {
if (plan != null && plan.distribution() != PortBiomePlan.Distribution.NATIVE)
return Set.copyOf(plan.biomes());
if (terrain == PortAgeTerrain.FLAT) return Set.of(Identifier.withDefaultNamespace("plains"));
if (terrain == PortAgeTerrain.VOID) return Set.of(Identifier.withDefaultNamespace("the_void"));
if (terrain == PortAgeTerrain.CAVE) return Set.of(Identifier.withDefaultNamespace("lush_caves"));
return server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM).getOrThrow(LevelStem.OVERWORLD)
.value().generator().getBiomeSource().possibleBiomes().stream()
.flatMap(holder -> holder.unwrapKey().stream()).map(key -> key.identifier())
.collect(java.util.stream.Collectors.toUnmodifiableSet());
}
private static int rank(String name) {
return switch (name) {
case "villages", "strongholds", "mineshafts" -> 1;
case "dungeons", "nether_fortresses" -> 2;
default -> 3;
};
}
private static Identifier page(String name) { return Identifier.fromNamespaceAndPath("rivencraft", "feature/" + name); }
}
@@ -0,0 +1,36 @@
package net.dnorberg.rivencraft.age;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
/** The 1.21.1 LegacyRankChoice rule, retained for seeded grammar completion. */
final class PortRankedChoice {
private PortRankedChoice() {}
record Option<T>(T value, int rank) {}
static <T> T choose(Random random, List<Option<T>> options) {
if (options.isEmpty()) throw new IllegalArgumentException("No ranked choices");
var counts = new HashMap<Integer, Integer>();
int highest = 0;
for (var option : options) {
counts.merge(option.rank(), 1, Integer::sum);
highest = Math.max(highest, option.rank());
}
var weights = new HashMap<Integer, Integer>();
int weight = 1, previousTotal = 0;
for (int rank = highest; rank >= 0; rank--) {
int count = counts.getOrDefault(rank, 0);
if (weight != 1 && count > 0) weight = Math.max(weight, previousTotal / count + 1);
weights.put(rank, weight);
previousTotal = count * weight;
weight++;
}
int ticket = random.nextInt(options.stream().mapToInt(option -> weights.get(option.rank())).sum());
for (var option : options) {
ticket -= weights.get(option.rank());
if (ticket < 0) return option.value();
}
throw new IllegalStateException("Ranked choice ticket was not consumed");
}
}
@@ -31,11 +31,20 @@ public enum PortSkyStyle {
? OVERWORLD : random(seed));
}
/** Nether-style Ages use their fog clear color as the visible backdrop. */
public static float fogShade(net.minecraft.world.level.Level level) {
boolean nether = level.dimensionTypeRegistration().unwrapKey().map(key ->
key.identifier().getNamespace().equals("rivencraft")
&& key.identifier().getPath().endsWith("_sky_nether")).orElse(false);
var cycle = PortDayCycle.forLevel(level);
return nether && !cycle.bodies().isEmpty()
? 1F - 0.65F * PortCelestialBody.eclipseAt(cycle.bodies(), PortDayCycle.nativeTicks(level)) : 1F;
}
private static boolean isCelestial(Identifier page) {
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
return switch (page.getPath()) {
// Only Overworld currently draws bodies in this port. Recognize the
// Sun/Moon intentions too, without marking those pages implemented.
// Preserve the established compatible-default rule for written controllers.
case "celestial/normal_time", "celestial/sun", "celestial/moon" -> true;
default -> false;
};
@@ -88,6 +88,20 @@ public final class PortStructurePages {
.orElseThrow(() -> new IllegalArgumentException("Missing structure set for " + page)));
}
/** Random choices need at least one normally eligible biome; authored pages may override it. */
public static boolean randomEligible(Identifier page, PortAgeTerrain terrain, Set<Identifier> biomes,
HolderLookup.Provider registries) {
if (!supportsTerrain(page, terrain)) return false;
var set = registries.lookupOrThrow(Registries.STRUCTURE_SET)
.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)));
if (set.isEmpty()) return false;
var compatible = set.get().value().structures().stream()
.flatMap(entry -> entry.structure().value().biomes().stream())
.flatMap(holder -> holder.unwrapKey().stream()).map(key -> key.identifier())
.collect(java.util.stream.Collectors.toSet());
return compatible.isEmpty() || biomes.stream().anyMatch(compatible::contains);
}
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);
@@ -9,7 +9,8 @@ import net.minecraft.resources.Identifier;
/** Frozen terrain filters and ordered solid/surface features for an Age. */
public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
boolean starFissures, List<Identifier> structures, List<Identifier> carvers) {
boolean starFissures, List<Identifier> structures, List<Identifier> carvers,
boolean structureControl) {
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");
@@ -30,7 +31,8 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
.forGetter(PortTerrainFeaturePlan::denseOres),
Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures),
Identifier.CODEC.listOf().optionalFieldOf("structures", List.of()).forGetter(PortTerrainFeaturePlan::structures),
Identifier.CODEC.listOf().optionalFieldOf("carvers", List.of()).forGetter(PortTerrainFeaturePlan::carvers)
Identifier.CODEC.listOf().optionalFieldOf("carvers", List.of()).forGetter(PortTerrainFeaturePlan::carvers),
Codec.BOOL.optionalFieldOf("structure_control", false).forGetter(PortTerrainFeaturePlan::structureControl)
).apply(instance, PortTerrainFeaturePlan::new));
public record Choice(Identifier page, Identifier material) {
@@ -55,7 +57,7 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
PortCarverPages.validateCounts(carvers);
if (denseOres < 0 || denseOres > MAX_PER_FEATURE)
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences");
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty())
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty() && !structureControl)
throw new IllegalArgumentException("Terrain feature plan must not be empty");
var counts = new HashMap<Identifier, Integer>();
for (Choice choice : choices) {
@@ -64,6 +66,11 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
}
}
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
boolean starFissures, List<Identifier> structures, List<Identifier> carvers) {
this(choices, skylands, denseOres, starFissures, structures, carvers, false);
}
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
boolean starFissures, List<Identifier> structures) {
this(choices, skylands, denseOres, starFissures, structures, List.of());
@@ -465,7 +465,7 @@ public final class RuntimeAgeBackend {
case CAVE -> dryCaveSettings(server);
case FLAT, VOID -> throw new IllegalStateException("Flat terrain should use FlatLevelSource");
};
generator = new NoiseBasedChunkGenerator(source, settings);
generator = new NoiseBasedChunkGenerator(source, PortBiomeMaterialRules.apply(server.registryAccess(), settings.value()));
}
if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
@@ -0,0 +1,85 @@
package net.dnorberg.rivencraft.client;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.BufferBuilder;
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.renderpearl.api.buffers.GpuBuffer;
import com.mojang.renderpearl.api.commands.RenderPass;
import com.mojang.renderpearl.api.pipeline.BlendFunction;
import com.mojang.renderpearl.api.pipeline.ColorTargetState;
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
import com.mojang.renderpearl.api.pipeline.RenderPipeline;
import java.util.IdentityHashMap;
import java.util.Map;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.renderer.texture.TextureAtlas;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.MoonPhase;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.client.event.RegisterRenderPipelinesEvent;
import org.joml.Matrix4f;
import org.joml.Vector4f;
/** Legacy's opaque central disc, separate from the native transparent glow. */
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
public final class PortCelestialDiscs implements AutoCloseable {
private static final Identifier SUN = Identifier.withDefaultNamespace("sun");
private static final RenderPipeline CORE = RenderPipelines.CELESTIAL.toBuilder()
.withLocation(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "pipeline/celestial_core"))
.withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)).build();
private final Map<TextureAtlasSprite, GpuBuffer> cores = new IdentityHashMap<>();
private TextureAtlasSprite atlasGeneration;
@SubscribeEvent public static void register(RegisterRenderPipelinesEvent event) { event.registerPipeline(CORE); }
public void draw(RenderPass pass, TextureAtlas atlas, boolean moon, MoonPhase phase,
Matrix4f transform, int color, float alpha) {
if (alpha <= 0) return;
var generation = atlas.getSprite(SUN);
if (generation != atlasGeneration) {
close();
atlasGeneration = generation;
}
var sprite = moon ? atlas.getSprite(Identifier.withDefaultNamespace("moon/" + phase.getSerializedName())) : generation;
var buffer = cores.computeIfAbsent(sprite, ignored -> build(sprite, moon));
var indices = RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
var indexBuffer = indices.getBuffer(6);
var rgb = net.dnorberg.rivencraft.age.PortAgeColors.vector(color);
var uniforms = RenderSystem.getDynamicUniforms().writeTransform(transform, new Vector4f(rgb, alpha));
pass.setPipeline(RenderSystem.getCompiledPipeline(CORE));
RenderSystem.bindDefaultUniforms(pass);
pass.setUniform("DynamicTransforms", uniforms);
pass.setUniform("Sampler0", atlas.getTextureView(), atlas.getSampler());
pass.setVertexBuffer(0, buffer.slice());
pass.setIndexBuffer(indexBuffer, indices.type());
pass.drawIndexed(6, 1, 0, 0, 0);
}
private static GpuBuffer build(TextureAtlasSprite sprite, boolean moon) {
var format = DefaultVertexFormat.POSITION_TEX;
try (var bytes = ByteBufferBuilder.exactlySized(4 * format.getVertexSize())) {
var builder = new BufferBuilder(bytes, PrimitiveTopology.QUADS, format);
// Same 8/32 central area as root's ordinary-alpha second pass.
float u0 = sprite.getU(0.375F), u1 = sprite.getU(0.625F);
float v0 = sprite.getV(0.375F), v1 = sprite.getV(0.625F);
builder.addVertex(-0.25F, 0, -0.25F).setUv(moon ? u1 : u0, moon ? v1 : v0);
builder.addVertex(0.25F, 0, -0.25F).setUv(moon ? u0 : u1, moon ? v1 : v0);
builder.addVertex(0.25F, 0, 0.25F).setUv(moon ? u0 : u1, moon ? v0 : v1);
builder.addVertex(-0.25F, 0, 0.25F).setUv(moon ? u1 : u0, moon ? v0 : v1);
try (var mesh = builder.buildOrThrow()) {
return RenderSystem.getDevice().createBuffer(() -> "Rivencraft celestial core", 40, mesh.vertexBuffer());
}
}
}
@Override public void close() {
cores.values().forEach(GpuBuffer::close);
cores.clear();
atlasGeneration = null;
}
}
@@ -2,6 +2,11 @@ package net.dnorberg.rivencraft.client;
/** Carries extracted sky presentation values without live-world reads during drawing. */
public interface StaticSkyState {
int rivencraft$bodyOverlay();
void rivencraft$bodyOverlay(int mode);
record Body(int kind, float angle, int bearing, float size, int color, float eclipse) {}
java.util.List<Body> rivencraft$bodies();
void rivencraft$bodies(java.util.List<Body> bodies);
long rivencraft$staticFrame();
void rivencraft$staticFrame(long frame);
int rivencraft$skyTint();
@@ -10,4 +15,8 @@ public interface StaticSkyState {
void rivencraft$orbitBearing(int bearing);
float rivencraft$sunSize();
void rivencraft$sunSize(float size);
int rivencraft$celestialTint();
void rivencraft$celestialTint(int color);
int rivencraft$bodyMode();
void rivencraft$bodyMode(int mode);
}
@@ -22,7 +22,9 @@ public class AgeColorAttributesMixin {
});
builder.addTimeBasedLayer(EnvironmentAttributes.FOG_COLOR, (base, tick) -> {
int rgb = PortAgeColors.forLevel(level).sky();
return rgb < 0 ? base : PortAgeColors.vector(rgb);
var color = rgb < 0 ? base : PortAgeColors.vector(rgb);
float shade = net.dnorberg.rivencraft.age.PortSkyStyle.fogShade(level);
return shade == 1F ? color : new org.joml.Vector3f(color).mul(shade);
});
builder.addTimeBasedLayer(EnvironmentAttributes.AMBIENT_LIGHT_COLOR, (base, tick) -> {
int rgb = PortAgeColors.forLevel(level).ambient();
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.mixin;
import net.dnorberg.rivencraft.age.PortDayCycle;
import net.minecraft.world.attribute.EnvironmentAttributeSystem;
import net.minecraft.world.attribute.EnvironmentAttributes;
import net.minecraft.world.attribute.WeatherAttributes;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
@@ -11,6 +12,30 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(EnvironmentAttributeSystem.Builder.class)
public class AgeDayTimelineMixin {
@Inject(method = "addDefaultLayers", at = @At("RETURN"))
private void rivencraft$singleBody(Level level, CallbackInfoReturnable<EnvironmentAttributeSystem.Builder> cir) {
var id = level.dimension().identifier();
if (!id.getNamespace().equals("rivencraft") || !id.getPath().startsWith("age/")) return;
var builder = cir.getReturnValue();
builder.addTimeBasedLayer(EnvironmentAttributes.SUN_ANGLE, (base, tick) -> {
var cycle = PortDayCycle.forLevel(level);
return cycle.singleBody() ? cycle.bodyAngle(PortDayCycle.nativeTicks(level)) : base;
});
builder.addTimeBasedLayer(EnvironmentAttributes.MOON_ANGLE, (base, tick) -> {
var cycle = PortDayCycle.forLevel(level);
return cycle.singleBody() ? cycle.bodyAngle(PortDayCycle.nativeTicks(level)) : base;
});
builder.addTimeBasedLayer(EnvironmentAttributes.SKY_LIGHT_LEVEL, (base, tick) -> {
var cycle = PortDayCycle.forLevel(level);
if (!cycle.customLight()) return base;
// Match root AgeSkyProfile.lightAt and LevelSkyBrightnessMixin's
// single skylight channel. Bright/Dark and no-skylight geometry remain separate.
float weather = (1F - level.getRainLevel(1F) * 5F / 16F)
* (1F - level.getThunderLevel(1F) * 5F / 16F);
return 15F - (int) ((1F - cycle.bodyLight(PortDayCycle.nativeTicks(level)) * weather) * 11F);
});
}
@Inject(method = "addDynamicLayers", at = @At("HEAD"), cancellable = true)
private void rivencraft$dayCycle(Level level, CallbackInfoReturnable<EnvironmentAttributeSystem.Builder> cir) {
var id = level.dimension().identifier();
@@ -26,6 +26,24 @@ public class StaticSkyRendererMixin {
@Unique private int rivencraft$endTint = -1;
@Unique private int rivencraft$orbitBearing = -1;
@Unique private float rivencraft$sunSize = 1F;
@Unique private int rivencraft$celestialTint = -1;
@Unique private int rivencraft$bodyMode;
@Unique private java.util.List<StaticSkyState.Body> rivencraft$bodies = java.util.List.of();
@Unique private net.dnorberg.rivencraft.client.PortCelestialDiscs rivencraft$discs;
@Shadow @Final private net.minecraft.client.renderer.texture.TextureAtlas celestialsAtlas;
@Shadow private org.joml.Matrix4f applyCelestialBodyTransform(com.mojang.blaze3d.vertex.PoseStack pose,
float height, float size) { throw new AssertionError(); }
@Shadow private void renderSun(com.mojang.renderpearl.api.commands.RenderPass pass, float alpha,
com.mojang.blaze3d.vertex.PoseStack pose) { throw new AssertionError(); }
@Shadow private void renderMoon(com.mojang.renderpearl.api.commands.RenderPass pass,
net.minecraft.world.level.MoonPhase phase, float alpha,
com.mojang.blaze3d.vertex.PoseStack pose) { throw new AssertionError(); }
@Shadow private void renderStars(com.mojang.renderpearl.api.commands.RenderPass pass, float brightness,
com.mojang.blaze3d.vertex.PoseStack pose) { throw new AssertionError(); }
@Shadow private void renderSunMoonAndStars(com.mojang.renderpearl.api.commands.RenderPass pass,
com.mojang.blaze3d.vertex.PoseStack pose, float sunAngle,
float moonAngle, float starAngle, net.minecraft.world.level.MoonPhase phase,
float alpha, float stars) { throw new AssertionError(); }
@Inject(method = "extractRenderState", at = @At("TAIL"))
private void rivencraft$extract(ClientLevel level, float partialTicks, Camera camera,
@@ -36,11 +54,66 @@ public class StaticSkyRendererMixin {
((StaticSkyState) state).rivencraft$staticFrame(enabled ? level.getGameTime() / 2 : Long.MIN_VALUE);
((StaticSkyState) state).rivencraft$skyTint(net.dnorberg.rivencraft.age.PortAgeColors.forLevel(level).sky());
var cycle = net.dnorberg.rivencraft.age.PortDayCycle.forLevel(level);
// 0=native Overworld/no authored overlay, 1=fog-only backdrop,
// 2=native End backdrop, 3=custom Static backdrop.
int overlay = !cycle.authored() || cycle.bodyMode() == net.dnorberg.rivencraft.age.PortDayCycle.NONE
|| state.skybox == DimensionType.Skybox.OVERWORLD ? 0
: enabled ? 3 : state.skybox == DimensionType.Skybox.NONE ? 1 : 2;
((StaticSkyState) state).rivencraft$bodyOverlay(overlay);
((StaticSkyState) state).rivencraft$orbitBearing(cycle.authored() ? cycle.bearing() : -1);
((StaticSkyState) state).rivencraft$sunSize(cycle.authored() ? cycle.sunSize() : 1F);
((StaticSkyState) state).rivencraft$celestialTint(
net.dnorberg.rivencraft.age.PortAgeColors.forLevel(level).celestial());
((StaticSkyState) state).rivencraft$bodyMode(cycle.authored() ? cycle.bodyMode() : 0);
// Root's authored Moon always samples the full-moon sprite. Preserve
// native calendar phases only for the native Normal Time pair.
if (cycle.singleBody() && cycle.bodyMode() == net.dnorberg.rivencraft.age.PortDayCycle.MOON)
state.moonPhase = net.minecraft.world.level.MoonPhase.FULL_MOON;
var authoredBodies = cycle.bodies();
if (authoredBodies.isEmpty() && overlay != 0) {
var bodies = new java.util.ArrayList<net.dnorberg.rivencraft.age.PortCelestialBody>();
int tint = net.dnorberg.rivencraft.age.PortAgeColors.forLevel(level).celestial();
if (cycle.bodyMode() != net.dnorberg.rivencraft.age.PortDayCycle.MOON)
bodies.add(net.dnorberg.rivencraft.age.PortCelestialBody.of(cycle, 1, tint, false));
if (cycle.bodyMode() != net.dnorberg.rivencraft.age.PortDayCycle.SUN)
bodies.add(net.dnorberg.rivencraft.age.PortCelestialBody.of(cycle, 2, tint, cycle.bodyMode() == 0));
authoredBodies = java.util.List.copyOf(bodies);
}
if (authoredBodies.isEmpty()) ((StaticSkyState) state).rivencraft$bodies(java.util.List.of());
else {
var clock = level.clockManager().getInstance(level.registryAccess().lookupOrThrow(
net.minecraft.core.registries.Registries.WORLD_CLOCK).getOrThrow(net.minecraft.world.clock.WorldClocks.OVERWORLD));
double ticks = clock.totalTicks() + clock.partialTick();
var sources = authoredBodies;
var bodies = sources.stream().map(body -> {
float eclipse = body.eclipse(sources, ticks);
return new StaticSkyState.Body(body.kind(), body.angle(ticks), body.bearing(), body.size(),
body.eclipseColor(eclipse), eclipse);
}).toList();
((StaticSkyState) state).rivencraft$bodies(bodies);
float eclipse = 0;
for (var body : bodies) if (body.kind() == 1 && Math.cos(Math.toRadians(body.angle())) > 0)
eclipse = Math.max(eclipse, body.eclipse());
if (state.skybox == DimensionType.Skybox.OVERWORLD) {
state.skyColor = new org.joml.Vector3f(state.skyColor).mul(1F - 0.65F * eclipse);
} else if (eclipse > 0 && overlay >= 2) {
int tint = ((StaticSkyState) state).rivencraft$skyTint();
if (tint < 0) tint = enabled ? 0x929292 : 0xffffff;
((StaticSkyState) state).rivencraft$skyTint(rivencraft$shade(tint, 1F - 0.65F * eclipse));
}
}
// Only the extracted presentation uses END to schedule the sky pass. The actual
// dimension has NONE, so it never acquires End flashes or End gameplay behavior.
if (enabled) state.skybox = DimensionType.Skybox.END;
if (enabled || overlay == 1) {
state.skybox = DimensionType.Skybox.END;
state.endFlashIntensity = 0;
}
if (overlay != 0) state.rainBrightness = 1F - level.getRainLevel(partialTicks);
}
@Unique private static int rivencraft$shade(int rgb, float shade) {
return Math.round((rgb >> 16 & 255) * shade) << 16
| Math.round((rgb >> 8 & 255) * shade) << 8 | Math.round((rgb & 255) * shade);
}
@Inject(method = "render", at = @At("HEAD"), cancellable = true)
@@ -49,9 +122,82 @@ public class StaticSkyRendererMixin {
rivencraft$endTint = ((StaticSkyState) state).rivencraft$skyTint();
rivencraft$orbitBearing = ((StaticSkyState) state).rivencraft$orbitBearing();
rivencraft$sunSize = ((StaticSkyState) state).rivencraft$sunSize();
rivencraft$celestialTint = ((StaticSkyState) state).rivencraft$celestialTint();
rivencraft$bodyMode = ((StaticSkyState) state).rivencraft$bodyMode();
rivencraft$bodies = ((StaticSkyState) state).rivencraft$bodies();
if (((StaticSkyState) state).rivencraft$bodyOverlay() == 1) {
rivencraft$overlay(fog, state);
ci.cancel();
return;
}
if (frame == Long.MIN_VALUE) return;
if (rivencraft$staticSky == null) rivencraft$staticSky = new PortStaticSky();
rivencraft$staticSky.render(renderTarget, fog, frame, rivencraft$endTint);
if (((StaticSkyState) state).rivencraft$bodyOverlay() == 3) rivencraft$overlay(fog, state);
ci.cancel();
}
@Inject(method = "render", at = @At("RETURN"))
private void rivencraft$afterBackground(GpuBufferSlice fog, SkyRenderState state, CallbackInfo ci) {
// RETURN includes the native End branch's early return, after its pass closes.
if (((StaticSkyState) state).rivencraft$bodyOverlay() == 2) rivencraft$overlay(fog, state);
}
@Unique private void rivencraft$overlay(GpuBufferSlice fog, SkyRenderState state) {
if (rivencraft$bodies.isEmpty()) return;
com.mojang.blaze3d.systems.RenderSystem.setShaderFog(fog);
try (var pass = com.mojang.blaze3d.systems.RenderSystem.getDevice().createCommandEncoder().createRenderPass(
() -> "Rivencraft celestial overlay", renderTarget.getColorTextureView(), java.util.Optional.empty(),
renderTarget.getDepthTextureView(), java.util.OptionalDouble.empty())) {
renderSunMoonAndStars(pass, new com.mojang.blaze3d.vertex.PoseStack(), 0, 0, 0,
net.minecraft.world.level.MoonPhase.FULL_MOON, state.rainBrightness, 0);
}
}
@Inject(method = "renderSunMoonAndStars", at = @At("HEAD"), cancellable = true)
private void rivencraft$drawBodies(com.mojang.renderpearl.api.commands.RenderPass pass,
com.mojang.blaze3d.vertex.PoseStack pose, float sunAngle,
float moonAngle, float starAngle, net.minecraft.world.level.MoonPhase phase,
float alpha, float stars, CallbackInfo ci) {
if (rivencraft$bodies.isEmpty()) return;
int savedMode = rivencraft$bodyMode, savedTint = rivencraft$celestialTint;
float savedSize = rivencraft$sunSize;
try {
// Stars belong behind opaque discs, not on top of a dark eclipsing moon.
if (stars > 0) {
pose.pushPose();
try {
pose.rotateDegrees(com.mojang.math.Axis.YP, -90);
pose.rotate(com.mojang.math.Axis.XP, starAngle);
renderStars(pass, stars, pose);
} finally { pose.popPose(); }
}
// Same ordering as 1.21.1: all suns, then all moons; each opaque
// central disc is drawn after its transparent glow.
for (int kind = 1; kind <= 2; kind++) for (var body : rivencraft$bodies) {
if (body.kind() != kind) continue;
rivencraft$bodyMode = kind;
rivencraft$celestialTint = body.color();
rivencraft$sunSize = body.size();
pose.pushPose();
try {
pose.rotateDegrees(com.mojang.math.Axis.YP, body.bearing() < 0 ? -90 : body.bearing());
pose.rotateDegrees(com.mojang.math.Axis.XP, body.angle());
float glowAlpha = alpha * (kind == 1 ? 0.84F : 0.88F + 0.12F * body.eclipse());
var bodyPhase = net.minecraft.world.level.MoonPhase.FULL_MOON;
if (kind == 1) renderSun(pass, glowAlpha, pose); else renderMoon(pass, bodyPhase, glowAlpha, pose);
if (rivencraft$discs == null) rivencraft$discs = new net.dnorberg.rivencraft.client.PortCelestialDiscs();
// Both passes must use the same sprite. Native dark phase pixels
// become black patches under ordinary alpha, unlike the old glow-only pass.
var transform = applyCelestialBodyTransform(pose, 100F, (kind == 1 ? 30F : 20F) * body.size());
rivencraft$discs.draw(pass, celestialsAtlas, kind == 2, bodyPhase, transform, body.color(), alpha);
} finally { pose.popPose(); }
}
} finally {
rivencraft$bodyMode = savedMode;
rivencraft$celestialTint = savedTint;
rivencraft$sunSize = savedSize;
}
ci.cancel();
}
@@ -68,10 +214,48 @@ public class StaticSkyRendererMixin {
return nativeSize * rivencraft$sunSize;
}
@Inject(method = "renderSunriseAndSunset", at = @At("HEAD"))
@ModifyArg(method = "renderMoon", at = @At(value = "INVOKE", target =
"Lnet/minecraft/client/renderer/SkyRenderer;applyCelestialBodyTransform(Lcom/mojang/blaze3d/vertex/PoseStack;FF)Lorg/joml/Matrix4f;"), index = 2)
private float rivencraft$scaleMoon(float nativeSize) {
return rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.MOON
? nativeSize * rivencraft$sunSize : nativeSize;
}
@Inject(method = "renderSun", at = @At("HEAD"), cancellable = true)
private void rivencraft$hideSun(com.mojang.renderpearl.api.commands.RenderPass pass, float alpha,
com.mojang.blaze3d.vertex.PoseStack pose, CallbackInfo ci) {
if (rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.MOON
|| rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.NONE) ci.cancel();
}
@Inject(method = "renderMoon", at = @At("HEAD"), cancellable = true)
private void rivencraft$hideMoon(com.mojang.renderpearl.api.commands.RenderPass pass,
net.minecraft.world.level.MoonPhase phase, float alpha,
com.mojang.blaze3d.vertex.PoseStack pose, CallbackInfo ci) {
if (rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.SUN
|| rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.NONE) ci.cancel();
}
@ModifyArg(method = {"renderSun", "renderMoon"}, at = @At(value = "INVOKE", target =
"Lnet/minecraft/client/renderer/DynamicGpuData;writeTransform(Lorg/joml/Matrix4f;Lorg/joml/Vector4f;)Lcom/mojang/renderpearl/api/buffers/GpuBufferSlice;"), index = 1)
private org.joml.Vector4f rivencraft$tintCelestial(org.joml.Vector4f nativeColor) {
// Normal Time consumes one shared color group for its sun and moon in 1.21.1.
// Keep native alpha (including weather fading) and the sprite's transparent halo.
if (rivencraft$celestialTint < 0) return nativeColor;
var tint = net.dnorberg.rivencraft.age.PortAgeColors.vector(rivencraft$celestialTint);
return new org.joml.Vector4f(nativeColor.x * tint.x(), nativeColor.y * tint.y(),
nativeColor.z * tint.z(), nativeColor.w);
}
@Inject(method = "renderSunriseAndSunset", at = @At("HEAD"), cancellable = true)
private void rivencraft$rotateHorizon(com.mojang.renderpearl.api.commands.RenderPass pass,
com.mojang.blaze3d.vertex.PoseStack pose, float angle,
org.joml.Vector4fc color, CallbackInfo ci) {
if (rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.MOON
|| rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.NONE) {
ci.cancel();
return;
}
if (rivencraft$orbitBearing < 0) return;
pose.pushPose();
pose.rotateDegrees(com.mojang.math.Axis.YP, rivencraft$orbitBearing + 90);
@@ -81,7 +265,9 @@ public class StaticSkyRendererMixin {
private void rivencraft$restoreHorizon(com.mojang.renderpearl.api.commands.RenderPass pass,
com.mojang.blaze3d.vertex.PoseStack pose, float angle,
org.joml.Vector4fc color, CallbackInfo ci) {
if (rivencraft$orbitBearing >= 0) pose.popPose();
if (rivencraft$bodyMode != net.dnorberg.rivencraft.age.PortDayCycle.MOON
&& rivencraft$bodyMode != net.dnorberg.rivencraft.age.PortDayCycle.NONE
&& rivencraft$orbitBearing >= 0) pose.popPose();
}
@Redirect(method = "renderEndSky", at = @At(value = "INVOKE", target =
@@ -94,6 +280,10 @@ public class StaticSkyRendererMixin {
@Inject(method = "close", at = @At("HEAD"))
private void rivencraft$close(CallbackInfo ci) {
if (rivencraft$discs != null) {
rivencraft$discs.close();
rivencraft$discs = null;
}
if (rivencraft$staticSky != null) {
rivencraft$staticSky.close();
rivencraft$staticSky = null;
@@ -14,6 +14,14 @@ public class StaticSkyStateMixin implements StaticSkyState {
@Unique private int rivencraft$tint = -1;
@Unique private int rivencraft$bearing = -1;
@Unique private float rivencraft$sunSize = 1F;
@Unique private int rivencraft$celestialTint = -1;
@Unique private int rivencraft$bodyMode;
@Unique private int rivencraft$bodyOverlay;
public int rivencraft$bodyOverlay() { return rivencraft$bodyOverlay; }
public void rivencraft$bodyOverlay(int mode) { rivencraft$bodyOverlay = mode; }
@Unique private java.util.List<StaticSkyState.Body> rivencraft$bodies = java.util.List.of();
public java.util.List<StaticSkyState.Body> rivencraft$bodies() { return rivencraft$bodies; }
public void rivencraft$bodies(java.util.List<StaticSkyState.Body> bodies) { rivencraft$bodies = java.util.List.copyOf(bodies); }
public long rivencraft$staticFrame() { return rivencraft$frame; }
public void rivencraft$staticFrame(long frame) { rivencraft$frame = frame; }
public int rivencraft$skyTint() { return rivencraft$tint; }
@@ -22,6 +30,10 @@ public class StaticSkyStateMixin implements StaticSkyState {
public void rivencraft$orbitBearing(int bearing) { rivencraft$bearing = bearing; }
public float rivencraft$sunSize() { return rivencraft$sunSize; }
public void rivencraft$sunSize(float size) { rivencraft$sunSize = size; }
public int rivencraft$celestialTint() { return rivencraft$celestialTint; }
public void rivencraft$celestialTint(int color) { rivencraft$celestialTint = color; }
public int rivencraft$bodyMode() { return rivencraft$bodyMode; }
public void rivencraft$bodyMode(int mode) { rivencraft$bodyMode = mode; }
@Inject(method = "reset", at = @At("TAIL"))
private void rivencraft$reset(CallbackInfo ci) {
@@ -29,5 +41,9 @@ public class StaticSkyStateMixin implements StaticSkyState {
rivencraft$tint = -1;
rivencraft$bearing = -1;
rivencraft$sunSize = 1F;
rivencraft$celestialTint = -1;
rivencraft$bodyMode = 0;
rivencraft$bodyOverlay = 0;
rivencraft$bodies = java.util.List.of();
}
}
@@ -108,9 +108,10 @@ public final class PortBookNetworking {
public static Identifier createAge(ServerPlayer player, String title, List<Identifier> pages) {
if (title == null || title.isBlank() || title.length() > MAX_BOOK_TITLE_LENGTH) return null;
long seed = player.getRandom().nextLong();
net.dnorberg.rivencraft.age.PortBiomePlan biomePlan;
try {
biomePlan = PortAuthoredWriting.biomePlan(pages, player.registryAccess());
biomePlan = PortAuthoredWriting.biomePlan(pages, player.level().getServer(), seed);
} catch (IllegalArgumentException exception) {
player.sendOverlayMessage(Component.literal("Age writing unavailable: " + exception.getMessage()));
return null;
@@ -127,7 +128,6 @@ public final class PortBookNetworking {
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
}
try {
long seed = player.getRandom().nextLong();
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
if (pages.isEmpty()) {
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
@@ -135,15 +135,17 @@ public final class PortBookNetworking {
biomePlan = random.biomes();
}
var environment = net.dnorberg.rivencraft.age.PortEnvironmentSelection.resolve(pages, seed, terrain);
var randomFeatures = pages.isEmpty()
? net.dnorberg.rivencraft.age.PortRandomFeatureSelection.select(server, seed, terrain, biomePlan) : null;
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
PortAuthoredWriting.rockPlan(pages, seed),
randomFeatures == null ? PortAuthoredWriting.rockPlan(pages, seed) : randomFeatures.rocks(),
PortAuthoredWriting.vegetationPlan(pages),
PortAuthoredWriting.dungeonCount(pages),
PortAuthoredWriting.lakePlan(pages, seed),
PortAuthoredWriting.terrainFeaturePlan(pages, seed),
randomFeatures == null ? PortAuthoredWriting.dungeonCount(pages) : randomFeatures.dungeons(),
randomFeatures == null ? PortAuthoredWriting.lakePlan(pages, seed) : randomFeatures.lakes(),
randomFeatures == null ? PortAuthoredWriting.terrainFeaturePlan(pages, seed) : randomFeatures.terrain(),
environment.weather(), environment.lighting(), environment.sky(),
net.dnorberg.rivencraft.age.PortAgeColors.compile(pages, seed),
net.dnorberg.rivencraft.age.PortDayCycle.compile(pages, seed,
net.dnorberg.rivencraft.age.PortAgeColors.resolve(pages, seed),
net.dnorberg.rivencraft.age.PortDayCycle.resolve(pages, seed,
net.dnorberg.rivencraft.age.PortDayCycle.nativeTicks(server.overworld())));
} catch (IllegalArgumentException | IllegalStateException exception) {
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
@@ -72,6 +72,12 @@ public final class PortAuthoredWriting {
/** A page never disappears into an Age unless this port can apply its effect. */
public static boolean supports(List<Identifier> pages) {
try {
return supportsExplicit(PortMaterialCompletion.complete(pages, 0));
} catch (IllegalArgumentException exception) { return false; }
}
private static boolean supportsExplicit(List<Identifier> pages) {
if (!net.dnorberg.rivencraft.age.PortAgeColors.validNumbers(pages)) return false;
if (!net.dnorberg.rivencraft.age.PortDayCycle.validWriting(pages)) return false;
boolean terrain = false;
@@ -97,7 +103,7 @@ public final class PortAuthoredWriting {
|| pendingMaterial.equals("rivencraft:fluid") && PortLakePlan.isLake(page))) return false;
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) {
// Shared color groups can precede a sky/ambient target or tint the sky at the end.
// Shared colors target Sky, Ambient Light Color or Normal Time; trailing colors tint the sky.
} else if (isTerrain(page) && !terrain) {
terrain = true;
} else if (PortSkyStyle.forPage(page) != null && sky++ == 0) {
@@ -107,7 +113,7 @@ public final class PortAuthoredWriting {
} 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.
// Missing biome slots are seed-completed when the Age is created.
} else if (isBlockPage(page) && pendingMaterial == null
&& PortSymbolPages.find(page) != null
&& (PortSymbolPages.find(page).category().equals("rivencraft:block")
@@ -152,7 +158,8 @@ public final class PortAuthoredWriting {
}
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)
if (dungeons > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
if (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;
@@ -169,8 +176,8 @@ public final class PortAuthoredWriting {
.filter(value -> value != null).findFirst().orElseThrow();
boolean valid = switch (distribution) {
case NATIVE -> biomes == 0;
case SINGLE -> biomes == 1;
default -> biomes >= 2;
case SINGLE -> biomes <= 1;
default -> true;
};
return valid;
}
@@ -214,13 +221,14 @@ public final class PortAuthoredWriting {
}
public static PortRockPlan rockPlan(List<Identifier> pages, long seed) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
pages = completedMaterials(pages, seed);
List<Identifier> materials = solidMaterials(pages, seed, ROCKS);
return materials.isEmpty() ? null : new PortRockPlan(materials);
}
public static PortTerrainFeaturePlan terrainFeaturePlan(List<Identifier> pages, long seed) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
pages = completedMaterials(pages, seed);
final List<Identifier> completed = pages;
Map<Identifier, List<Identifier>> materials = new HashMap<>();
Map<Identifier, Integer> occurrences = new HashMap<>();
List<PortTerrainFeaturePlan.Choice> choices = new ArrayList<>();
@@ -230,7 +238,7 @@ public final class PortAuthoredWriting {
choices.add(new PortTerrainFeaturePlan.Choice(page, PortTerrainFeaturePlan.CRYSTAL_MATERIAL));
continue;
}
List<Identifier> selected = materials.computeIfAbsent(page, feature -> solidMaterials(pages, seed, feature));
List<Identifier> selected = materials.computeIfAbsent(page, feature -> solidMaterials(completed, seed, feature));
int occurrence = occurrences.merge(page, 1, Integer::sum) - 1;
choices.add(new PortTerrainFeaturePlan.Choice(page, selected.get(occurrence)));
}
@@ -262,7 +270,7 @@ public final class PortAuthoredWriting {
}
public static PortLakePlan lakePlan(List<Identifier> pages, long seed) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
pages = completedMaterials(pages, seed);
if (pages.stream().noneMatch(PortLakePlan::isLake)) return null;
List<Identifier> fluidPool = PortRegistryPages.fluidMaterialIds();
if (fluidPool.isEmpty()) throw new IllegalStateException("No source-fluid materials are registered");
@@ -288,6 +296,12 @@ public final class PortAuthoredWriting {
return new PortLakePlan(choices);
}
private static List<Identifier> completedMaterials(List<Identifier> pages, long seed) {
var completed = PortMaterialCompletion.complete(pages, seed);
if (!supportsExplicit(completed)) throw new IllegalArgumentException("Unsupported authored Age pages");
return completed;
}
public static PortVegetationPlan vegetationPlan(List<Identifier> pages) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
List<Identifier> vegetation = pages.stream().filter(PortVegetationPlan::isVegetation).toList();
@@ -307,6 +321,17 @@ public final class PortAuthoredWriting {
/** Resolves all authored biomes from the active registry before creating a level. */
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
return biomePlan(pages, registries, 0, null);
}
/** Creation-only completion; the saved plan never rerolls on load or reseed. */
public static PortBiomePlan biomePlan(List<Identifier> pages, net.minecraft.server.MinecraftServer server, long seed) {
return biomePlan(pages, server.registryAccess(), seed,
net.dnorberg.rivencraft.age.PortRandomAgeSelection.eligibleBiomes(server, terrain(pages)));
}
private static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries,
long seed, List<Identifier> available) {
if (!supports(pages)) throw new IllegalArgumentException(
"Unsupported terrain or biome-page combination in this port slice");
List<Identifier> biomes = new ArrayList<>();
@@ -341,6 +366,25 @@ public final class PortAuthoredWriting {
throw new IllegalArgumentException("Dense Ores needs at least one biome with registered ore placements");
if (distribution == PortBiomePlan.Distribution.NATIVE)
return new PortBiomePlan(distribution, List.of());
if (distribution != null && available != null) {
int minimum = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
int target = minimum;
if (biomes.isEmpty() && distribution != PortBiomePlan.Distribution.SINGLE) {
var random = new Random(seed ^ 0x42494F4D45434F55L);
while (target < available.size() && random.nextInt(3) != 0) target++;
}
var distinct = new java.util.HashSet<>(biomes);
// Void may retain authored weather/season biomes, but must not gain
// an unrequested The Void biome just to fill a multi-biome controller.
var filler = new ArrayList<>(pages.contains(VOID_TERRAIN) && !biomes.isEmpty() ? List.<Identifier>of() : available);
Collections.shuffle(filler, new Random(seed ^ 0x42494F4D45504F4FL));
for (Identifier biome : filler) {
if (distinct.size() >= target) break;
if (distinct.add(biome)) biomes.add(biome);
}
if (distinct.size() < minimum) throw new IllegalArgumentException(
distribution + " requires " + minimum + " distinct biomes; supply more biome pages for this terrain");
}
if (biomes.isEmpty()) return null;
if (distribution == null) distribution = biomes.size() == 1
? PortBiomePlan.Distribution.SINGLE : biomes.size() == 2
@@ -0,0 +1,68 @@
package net.dnorberg.rivencraft.writing;
import java.util.ArrayList;
import java.util.List;
import net.dnorberg.rivencraft.age.PortCarverPages;
import net.dnorberg.rivencraft.age.PortLakePlan;
import net.dnorberg.rivencraft.age.PortStructurePages;
import net.dnorberg.rivencraft.age.PortTerrainFeaturePlan;
import net.dnorberg.rivencraft.age.PortVegetationPlan;
import net.minecraft.resources.Identifier;
/** Creation-time implied features, adapted from 1.21.1 AgeFeatureSelection. */
public final class PortMaterialCompletion {
private PortMaterialCompletion() {}
public static List<Identifier> complete(List<Identifier> pages, long seed) {
var result = new ArrayList<Identifier>();
for (int index = 0; index < pages.size(); index++) {
Identifier page = pages.get(index);
result.add(page);
var entry = PortSymbolPages.find(page);
if (entry == null || !(entry.category().equals("rivencraft:block")
|| entry.category().equals("rivencraft:fluid"))) continue;
if (index + 1 < pages.size() && isFeature(pages.get(index + 1))) continue;
Identifier material = PortRegistryPages.blockId(page);
var candidates = candidates(pages, entry.category().equals("rivencraft:fluid"));
if (candidates.isEmpty()) throw new IllegalArgumentException(
"No compatible feature for material " + material + " on this terrain");
long mixed = mix(seed ^ (long) material.hashCode() * 0x9E3779B97F4A7C15L ^ index);
result.add(candidates.get(Math.floorMod(mixed, candidates.size())));
}
return List.copyOf(result);
}
private static List<Identifier> candidates(List<Identifier> pages, boolean fluid) {
boolean cave = pages.contains(id("terrain/cave"));
boolean empty = pages.contains(id("terrain/void"));
boolean flat = pages.contains(id("terrain/flat"));
if (fluid) return cave || empty || flat ? List.of()
: List.of(id("feature/surface_lakes"), id("feature/deep_lakes"), id("feature/lava_lakes"));
var choices = new ArrayList<Identifier>();
if (!cave && !empty && !flat) choices.add(id("feature/rocks"));
if (!cave) choices.add(PortTerrainFeaturePlan.FLOATING_ISLANDS);
choices.add(PortTerrainFeaturePlan.TENDRILS);
choices.add(PortTerrainFeaturePlan.SPHERES);
if (!cave && !empty) {
choices.add(PortTerrainFeaturePlan.SPIKES);
choices.add(PortTerrainFeaturePlan.OBELISKS);
}
return choices;
}
private static boolean isFeature(Identifier page) {
return PortTerrainFeaturePlan.isSupported(page) || PortLakePlan.isLake(page)
|| PortVegetationPlan.isVegetation(page) || PortStructurePages.supports(page)
|| PortCarverPages.supports(page) || page.equals(id("feature/rocks"))
|| page.equals(id("feature/dungeons")) || page.equals(PortTerrainFeaturePlan.SKYLANDS)
|| page.equals(PortTerrainFeaturePlan.DENSE_ORES) || page.equals(PortTerrainFeaturePlan.STAR_FISSURES);
}
private static Identifier id(String path) { return Identifier.fromNamespaceAndPath("rivencraft", path); }
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
}