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

+5 -4
View File
@@ -15,11 +15,12 @@ incomplete **Minecraft 26.3 beta** is available: Java 25, NeoForge 26.3.0.99,
and a first-party Age backend (no Dynamic Dimensions or Bad Packets). It now and a first-party Age backend (no Dynamic Dimensions or Bad Packets). It now
supports authored terrain/biomes, structure and vegetation pages, independent supports authored terrain/biomes, structure and vegetation pages, independent
Age weather/lighting, seeded and colorable Overworld/Nether/End/Static skies, Age weather/lighting, seeded and colorable Overworld/Nether/End/Static skies,
Normal Time length/phase/direction/bearing and sun-size controls, independent Sun/Moon controls, multiple bodies and eclipses, seeded biome and
and a growing set of generation features, feature completion, and random structure selection. Nether/End noise-biome
but does not yet provide full 1.21.1 gameplay or optional-integration parity. materials now follow their native rules. The beta does not yet provide full
1.21.1 gameplay or optional-integration parity.
See the [26.3 beta guide](ports/mc263/README.md) and See the [26.3 beta guide](ports/mc263/README.md) and
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.78-beta.1.md). [latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.93-beta.1.md).
Keep the builds and their saves separate, and back up worlds before updating. Keep the builds and their saves separate, and back up worlds before updating.
## Getting started ## Getting started
Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

+498
View File
@@ -1303,6 +1303,504 @@ unchanged moon, and travel isolation still require the user's client check.
The server stopped cleanly and its temporary world was deleted. Nothing is The server stopped cleanly and its temporary world was deleted. Nothing is
committed, pushed or published without an explicit request. committed, pushed or published without an explicit request.
## Normal Time shared celestial color
`1.0.1.79-beta.1` ports the root `AgeSkyProfile` behavior that consumes a
pending shared color group at Normal Time and applies it to both paired
bodies. `PortAgeColors` now saves an optional `celestial` RGB value, default
-1 (native/untinted), with catalog protocol 7 for synchronization. Existing
sky/ambient fields and old profiles retain their values. Color accumulation
passes through non-color celestial modifiers, clamps at the target, resets
after consumption, and leaves trailing colors assigned to sky as before.
Extraction copies the saved tint into `SkyRenderState`. A narrowly scoped
native sun/moon uniform modification multiplies RGB while retaining alpha;
there are no live world reads during drawing, new textures, pipelines or GPU
buffers. Native weather fading, transparent halos, moon phases, stars and
sunrise glow remain unchanged. Time and effective illumination are not tied
to RGB. State reset/defaults prevent cross-dimension tint leakage. Independent
Sun/Moon pages, multiple bodies and per-body colors remain unported.
The `create_celestial_color` fixture checks three separate target groups,
completed-group clamping, modifier passthrough, consumed-group reset, seeded
unnumbered colors, old codec defaults and catalog round trips. Client checks
must cover a frozen Zenith sun, Nadir moon, mixed colors, transparent halos,
restart and return to an untinted dimension. Server probes validate saved
profiles and attributes, not rendered output. Build passed; disposable-server
fixtures retained separate sky/ambient/body colors through reload, reseed and
restart, including zero/black colors and untouched native defaults. Sampled
time/light remained unchanged. The server stopped cleanly and its temporary
world was deleted. No commit/push/publication is performed without an explicit
request.
## Standalone Sun and Moon controllers
`1.0.1.80-beta.1` extends the existing cycle profile with optional `body_mode`
(paired=0, Sun=1, Moon=2); old profiles default to the paired renderer.
Catalog protocol 8 includes this field. One celestial controller is accepted
per book, with existing Length/Phase/Direction/Angle/Size and shared Color
groups. Multiple controllers and dangling/duplicate modifiers retain the
interim strict rejection rather than losing written pages.
Standalone bodies use a linear angular orbit as in root `AgeSkyProfile.Body`.
Both native body-angle attributes point at that orbit; render-state mode
suppresses the absent counterpart. The selected Moon consumes Size itself;
Normal Time retains its original sun-only sizing. Moon-only sunrise glow is
suppressed before any horizon pose is pushed. Shared RGB/alpha handling and
native buffers/pipelines are reused, with extraction/reset defaults protecting
other dimensions. Missing Sky completion uses Overworld, as before. Explicit
Nether/End/Static still does not draw bodies in this native-renderer slice.
The gameplay skylight attribute follows root `lightAt` and
`LevelSkyBrightnessMixin`: positive cosine altitude, 0.18 for a moon, weather
attenuation, then the 11-level sky-darkening range. It does not infer light
from color or size, and does not bypass Bright/Dark or no-skylight terrain.
Native background/star/moon-phase schedules remain, rather than claiming a
full custom sky port. Seeded default phase/bearing completion, multiple
independent bodies and eclipses remain future work.
The op `create_body` fixture checks all named phases for both bodies, light
weights, numeric size, reverse motion, order rejection, old-profile defaults,
compatible missing Sky and catalog synchronization. Actual body visibility,
moon size, glow transparency and travel isolation require manual client
validation. Build and disposable-server probes passed: Zenith sun/moon
skylight was 15/6, Nadir and horizon were 4; named phase angles were correct.
The paired fixture retained its opposite moon and native lighting. Reload,
reseed and full restart preserved body mode, color, size, bearing and phase.
The test server stopped and its temporary world was deleted. No commit/push/
publication is performed without an explicit request.
## Multiple independent authored celestials
`1.0.1.81-beta.1` adds an optional immutable `PortCelestialBody` list to the
cycle profile and catalog protocol 9. Existing profiles default to an empty
list and use their validated native paired/single path. Books with multiple
controllers compile ordered modifier groups into independent body orbits;
Normal Time expands into opposite-phase sun/moon entries. Colors are consumed
at each controller, with original page indices retained for seeded amounts.
Uncolored bodies reset to native white rather than inheriting the previous
body's tint. Size/phase/length/bearing/direction also reset per group.
The profile constructor and authoring gate enforce the root ten-sun/ten-moon
limit, with Normal Time counting toward both. Duplicate modifiers within a
group and dangling modifiers remain strict errors until grammar recovery is
ported. The first controller continues to govern the shared day timeline and
native background/star/moon-phase schedules. Positive-altitude sunlight sums
to at most 1; only if no sunlight contributes is summed 0.18-weight moonlight
used. Weather and Bright/Dark remain separate. No eclipse attenuation is
included yet.
Extraction resolves body angles into immutable presentation snapshots. The
native body pass is replaced only for a nonempty multi-body list: draw suns
first, then moons with their own color, size and bearing, then native stars
once. Native buffers, pipelines and alpha are reused. Temporary renderer
parameters and pose transforms restore in finally blocks; no world lookup is
performed during drawing. Explicit Nether/End/Static rendering remains a
separate limitation. Eclipse disc occlusion, corona and sky/light attenuation
will be the following slice, not a claim of this implementation.
`create_celestials` checks independent motion/color/size, modifier reset, Normal
Time expansion, ten-pair acceptance/overflow rejection, profile codecs and
catalog round trips. Manual checks cover body placement/alpha, multiple
controllers, restart and ordinary-dimension isolation. Build passed, as did
disposable-server profile/catalog checks, combined-light invariants, reload,
reseed and full restart. Single-body, paired and tinted fixtures retained
their earlier behavior. The test server stopped and its temporary world was
deleted. No commit/push/release is performed without explicit instruction.
## Eclipse effects
`1.0.1.82-beta.1` adapts root `AgeSkyProfile.eclipseFor`, `eclipseAt` and
`lightAt`, plus `RivencraftSkyEffects.drawBody`. The dot-product separation
of normalized orbit directions is compared with the sum of quarter-disc
angular radii; each body takes the strongest opposite-kind contact. This is
root's radial proximity formula, not a new physical disc-area model. Bearing,
phase, size, motion and direction all affect contact. Same-kind and isolated
bodies have no eclipse; only an above-horizon sun contributes global shading.
Sunlight is multiplied by `1 - 0.85 * overlap` before the existing light
aggregation and weather handling. Extraction shades sky RGB by
`1 - 0.65 * strongestVisibleSolarOverlap`. Moon RGB scales by `1 - overlap`;
sun RGB moves toward white by `pow(overlap, 8) * 0.8`. Values are derived from
saved profiles, requiring neither a new saved schema nor packet fields.
Existing multi-body Ages gain the effects on update; single/paired native
paths stay unchanged. Overlap calculations are bounded by the 20-body limit,
and extracted presentation values are reused without live-world drawing reads.
Native transparent glows draw before each body's central ordinary-alpha disc.
A registered pipeline cloned from native Celestial uses conventional alpha
blending; cached center-cropped atlas meshes retain native UV orientation.
An eclipsed moon uses the full-moon core to avoid transparent phase pixels
exposing the sun (root always drew that full-moon center). Weather alpha is
retained. The helper drops meshes on atlas-generation change and closes them
with the renderer. There is no generated art or per-frame mesh allocation.
Explicit Nether/End/Static body rendering remains outside this slice.
`create_eclipse` supplies aligned, partial, clear, below-horizon and moving
fixtures; it checks overlap/color/light constants, same-kind exclusion and
motion out of contact. The multi-body op fixture is renamed
`create_celestials` to avoid merging with the unrelated existing terrain
`create_multi` command. Manual checks must confirm disc opacity, halo,
corona/sky darkening, resource reload and travel isolation. Build passed.
Disposable-server fixtures passed with overlap 1/0.293/0 for aligned/partial/
clear cases and no global shading for below-horizon alignment. Aligned
sunlight was 0.15, clear sunlight 1; sampled skylight was 6/12/15 for aligned/
partial/clear. Moving contact ended as expected. Reload, reseed, catalog
round trips and full restart retained profiles and derived light behavior;
earlier standalone/paired fixtures remained unchanged. The test server stopped
and its temporary world was deleted. No commit/push/publication is performed
without an explicit request.
## Authored moon sprite correction
`1.0.1.83-beta.1` removes the mixed native-phase/full-core adaptation introduced
with the opaque eclipse pass. Standalone and multi-body authored moons now
draw full-moon glow and center consistently, matching root's custom renderer,
which always samples the first (full-moon) atlas cell. Under ordinary alpha,
native dark phase pixels can produce black opaque centers; the old overlay
glow had made those dark pixels effectively invisible. The ordinary native
Normal Time pair retains its calendar phases. No eclipse math, saved profile,
catalog protocol or world data changes are needed.
The user's `celest` Age was read without modification. Its body colors are
all -1 (native white), not corrupted black values. Its first Moon and second
Sun both have period 1, phase 0, origin 0, clockwise direction and native
bearing -1; they remain exactly coincident. That moon's full eclipse and its
occlusion of the sun are expected under the newly added root overlap rules,
independent of the phase-sprite correction. Do not silently alter player-authored
orbits to conceal the intentional alignment. Manual checks cover nonaligned
moons, the known permanent eclipse, Native Normal Time, resource reload and
cross-dimension isolation. No user save was modified, and nothing is committed,
pushed or published without explicit instruction.
## Celestials over explicit non-Overworld skies
`1.0.1.84-beta.1` removes the presentation exclusion that hid authored bodies
over Nether/End/Static. Root `RivencraftSkyEffects` draws its body list after
any chosen background. The native 26.3 paths instead returned after End or
skipped a NONE sky entirely. An extracted overlay-mode flag now schedules an
authored body layer without changing the actual dimension type or gameplay.
Single Sun/Moon and Normal Time profiles expand into temporary presentation
bodies on these backgrounds; saved profiles are not rewritten. Existing
Overworld single/paired/multi-body paths stay unchanged.
End uses a RETURN hook that includes the native early return, after its pass
is closed. Static closes its own background pass before drawing the overlay.
Fog-only Nether presentation schedules an extracted END sky pass, suppresses
its End background/flash drawing, and retains the fog clear color. Overlay
passes load existing targets without clearing, reuse native body resources
and suppress additional stars/horizon glow on non-Overworld backgrounds.
Weather alpha is extracted even when the native NONE/END branch omits it.
All presentation flags reset when the render state is reused.
Eclipse shading modulates End/Static background tint; Nether-style fog color
uses the same root shade factor through an environment attribute layer.
Native untinted fog and authored sky colors are both supported; other
backgrounds retain their existing fog behavior. Body lighting and eclipse
geometry do not depend on style. Missing Sky selection still chooses
Overworld for authored celestials. No schema/protocol changes or new assets
are required. The eclipse fixture accepts an optional explicit Sky; server
checks cover saved choices, fog/light behavior and persistence, while visual
layering, no duplication, resource reload and travel isolation require manual
client checks. Build and disposable-server checks passed for all four sky
styles: aligned eclipse skylight stayed 6, clear stayed 15, and Nether fog
scaled by 0.35 only during overlap. No-celestial and tinted-Nether fixtures
retained their expected colors. Saved profiles, style, light and fog survived
reload/reseed and full restart. The test server stopped and its temporary
world was deleted. No commit/push/publication is performed without explicit
request.
## Bare celestial seeded randomization
`1.0.1.85-beta.1` adds per-controller randomization specifically for bare
Sun/Moon groups, as requested for easier testing. This deliberately uses the
root **random-Age** ranges rather than its authored compile defaults (which
reuse shared seeded phase/bearing and default size/color). A dedicated stream
mixes Age seed with controller ordinal, independent of unrelated page indices,
terrain/weather/lighting RNG and sky selection. Palette/mixing and 0.75–1.75
size come from `AgeSkyProfile.random`; 0.2–2 periods with 1/7 stopped chance
come from `AgeTraits.select`. Bearing and phase use the port's existing degree/
tick granularity. Direction is random per body. Counts and body-kind limits
still follow explicitly written controller pages.
The compiler recognizes whether a controller has any written celestial
modifier. Colors already consumed by Sky/Ambient do not suppress randomization.
Any written celestial modifier retains the previous authored path as a whole,
not just individual field overrides; partially authored groups do not gain
new random missing fields in this slice. Normal Time and absent controllers
are unchanged. Bare controllers materialize immutable full body profiles,
including a one-body list for standalone cases. Multi-body compilation carries
their colors/orbits without replacing them with native defaults. No new
saved schema, protocol or client renderer code is needed.
Only newly created Ages compile these defaults. Older saved profiles load
unchanged; GUI opening, restart and terrain reseeding do not reroll them.
`create_random_celestials` checks reproducibility, ordinal independence,
unrelated-page stability, 128-seed bounds/pool coverage and unchanged authored/
Normal Time behavior. The regular fixtures still exercise explicit groups.
Manual checks cover visible variation, moving/stopped bodies, mixed authored
groups and restart. Build and disposable-server checks passed, including
single bare bodies, multi-body/128-seed coverage, catalog round trips,
unrelated-page stability, explicit/eclipsed fixtures, reload, terrain reseed
and full restart. Saved random parameters remained unchanged while moving
orbits continued with the clock. The server stopped and its temporary world
was deleted. No commit/push/publication occurs without explicit request.
## Missing celestial branch completion
`1.0.1.86-beta.1` adds creation-only `PortDayCycle.resolve`. Written controllers
delegate to the validated compiler unchanged. If none exist, the root
`AgeSkyProfile.random` draw positions select 0–3 suns and 0–3 moons: consume
the sky-style draw and mixed sky-color draws, then the two counts. Body
settings reuse .85's independent per-controller streams. This matches root's
unrestricted count model, not bit-identical per-body values; the .85 per-body
length/direction independence is retained. Missing sky tint completion and
random-page policy parity are not claimed by this slice.
The shared resolver is used by player book creation and the `create_random`
operator command. Other exact diagnostic fixtures continue using `compile`
or native profiles to isolate their subject. A saved NONE body mode (3)
represents a resolved zero-body sky. Its sun/moon/horizon drawing is suppressed
without switching to Dark lighting or changing the native background/star
timeline. Old missing profiles still decode to NATIVE rather than being
retroactively completed. The mode-domain extension uses catalog protocol 10.
No generation runs or rerolls happen during GUI rendering, load or reseed.
`create_completed_sky` tests 256-seed count bounds and coverage of empty,
sun-only, moon-only and mixed results; reproducibility and unrelated-page
stability; explicit-controller precedence; old defaults; and catalog round
trips. Manual checks cover varied new books, empty skies, explicit-controller
counts, all backgrounds, restart and Overworld isolation. No commit/push/
publication is performed without explicit instruction.
The .86 build and disposable dedicated-server checks passed: four background
fixtures, the seed-14 empty sky, random Age creation, explicit controllers,
catalog round trips, unload/reload, terrain reseeding and full restart. Saved
body parameters survived unchanged. The test server was stopped and its world
deleted; client visual confirmation remains a manual check.
## Random Age sky-color completion
`1.0.1.87-beta.1` uses creation-only `PortAgeColors.resolve` for player books
and `age create_random`. Empty symbol lists choose a sky tint from root
`AgeSkyProfile.random`'s palette and draw order, including its rounded channel
averages. Nonempty symbol lists delegate to the existing compiler unchanged.
This intentionally distinguishes wholly random Ages from authored books with
an omitted sky-color branch, as 1.21.1 does. The existing .85 per-body random
color truncation is preserved by a shared palette helper; celestial count and
body random streams do not change.
Colors are stored in the existing profile and catalog fields. Old native
colors are not retroactively randomized; reload, restart and terrain reseed
do not rerun completion. No protocol update is necessary. The completed-sky
diagnostic checks six root seed/color vectors, codec persistence, authored
precedence (including explicit black) and old-profile defaults. Random-page
blocklist parity remains separate pending work.
Build and disposable dedicated-server checks passed: root vectors and authored
precedence, color attributes/catalog round trips for all four background
styles, explicit sky/ambient colors, unload/reload, reseed and full restart.
Saved random colors stayed unchanged after reseeding and restart. The test
server was stopped and its disposable world deleted. Client rendering is
still a manual check. No commit, push or publication was performed.
## Authored biome branch completion
`1.0.1.88-beta.1` ports the explicit-controller missing-pool behavior from
1.21.1 `AgeBiomeSelection.compile`: Single requires one biome; other nonnative
controllers require two distinct biomes. An entirely omitted pool uses the
seeded 2:1 continuation/stop draw up to the eligible pool size. Authored pools
fill only to the minimum, preserve order and duplicate weights, and never add
an unrequested third biome once the minimum is met. Native still disallows
biome pages. The binder's availability validation accepts incomplete pools;
the server resolves actual registry eligibility before creating the Age.
Player creation now passes the actual Age seed to biome compilation. Exact
older diagnostics retain the registry-only compiler; the new completion
fixture exercises the production seeded path. Cave uses recognized Cave and
Nether biomes. Void does not inject The Void alongside authored biomes and
reports a clear distinct-biome error when completion is impossible. Pools
are stored in existing `PortBiomePlan` fields and never rerolled on restart
or terrain reseed. No protocol change is needed. Broader writing recovery,
structure-driven missing-biome selection and random-page policies remain
separate parity work.
The build and disposable dedicated-server checks passed. The completion
fixture checked every distribution over 32 seeds, partial/full/weighted pools
and codec round trips. Created Standard Single, Flat Tiny, Cave Tiny and Void
Single plans; Cave included the new 26.3 Sulfur Caves biome. An impossible
controller-only Void Tiny pool was refused with the distinct-biome message.
Generator pools survived unload/reload, reseed and full server restart.
The server was stopped and the temporary world deleted. Binder interaction
and in-game biome layouts still require manual validation. No commit, push
or publication was performed.
## Unpaired material completion
`1.0.1.89-beta.1` adapts 1.21.1 `AgeFeatureSelection.implied`. A shared pure
normalizer expands an unpaired material into an internal material/feature
pair, keyed by Age seed, material ID and its original book index. Rock, lake
and terrain-feature compilers all use the same completed view. Original book
pages stay unchanged and no random selection happens at render/load time.
Explicit feature targets suppress completion, including incompatible targets
that must still be rejected by the current strict grammar.
Candidate ordering and terrain restrictions follow root's material-feature
list: Rocks/Floating Islands/Tendrils/Spheres/Spikes/Obelisks for solids and
the three lake types for fluids, filtered by supported terrain. Cave only
permits the two terrain-independent solid features; Void also permits floating
islands; Flat excludes Rocks. No fallback fluid feature exists on Flat, Cave
or Void. This does not add page-free random features, nor remove the existing
per-feature caps or introduce instability recovery. Resulting plans use their
existing save codecs, so no catalog/protocol change is needed.
The operator fixture checks 64 seeds on each terrain: independent consecutive
materials, deterministic/idempotent completion, explicit precedence, candidate
coverage, fluid retention and incompatible-fluid rejection. Persistence and
actual generator plans are inspected with `feature_plan` without generating
chunks. Client binder interaction and feature visibility remain manual checks.
The .89 build and disposable server checks passed: seeded fixture assertions,
Standard Glowstone Spikes, Cave Diamond Spheres, Void Stone Tendrils, Flat
Glowstone Obelisks and Standard Water Deep Lakes. Cave Water was refused.
Feature plans and materials survived unload/reload, terrain reseed and full
restart. The server was stopped and its temporary world deleted. No commit,
push or publication was performed.
## Page-free random material features
`1.0.1.90-beta.1` adapts the material and rare-formation portion of 1.21.1
`AgeFeatureSelection.randomChoices`. A new creation-only selector shares the
production authored plan compilers rather than duplicating generator logic.
It rolls 0–2 unique material feature types, uses root's candidate ordering and
Legacy rank chooser, removes chosen types, and selects full-collision solids
or source fluids from the registered sorted pools. Terrain eligibility uses
the current port validator. The separate seeded Skylands and Star Fissure
rolls and the 1-in-10 Crystal Formations roll follow root's rules.
Player Link-Panel-only creation and `age create_random` share this selector.
Authored books still use their own feature compilers, including implied
materials from .89. Plans are frozen in existing save fields, without extra
book pages or a protocol change. Old Ages keep their original plans.
Random structures, random-page policies and instability are not claimed by
this slice. Material values can be valuable/hazardous, matching the root pool;
the instability port must account for that before stable release.
`create_random_features` checks 64 seeds across all terrain types: seeded
determinism, counts 0/1/2, unique feature types, all nine candidate families,
terrain compatibility, rare-feature coverage, and plan codec round trips.
`feature_plan` treats an absent feature generator as a valid empty result.
Client feature visibility remains a manual check.
The .90 build and disposable server checks passed. The seeded matrix covered
all six terrains and all nine material feature families. Created surface,
Cave, Flat and Void fixtures, including lakes, rare Skylands and a Star
Fissure, and verified the production `create_random` plan. Seed 0 Standard
correctly produced no features; seed 1 produced a single lake. Saved plans
survived unload/reload, reseed and full restart. The server was stopped and
its temporary world deleted. Nothing was committed, pushed or published.
## Page-free random structures
`1.0.1.91-beta.1` ports `AgeStructureSelection.randomChoices`: an independent
seed stream rolls 0–2 unique candidates using Legacy ranks. Terrain and
registry availability are checked, then the selected biome pool must intersect
the structure set's normal biome eligibility. This is deliberately stricter
than authored pages, which retain their existing biome overrides. Native
Flat uses Plains, Native Cave uses the backend's Lush Caves fallback, and
other native noise terrain uses the Overworld LevelStem biome source. A
non-Native plan uses its saved biome IDs. Abandoned Camps are added after
the root candidate list at rank 3. Cave Dungeons remain excluded by current
port support, whereas root permits that combination; it needs a separate
placement validation before enabling it.
The combined random feature resolver preserves .90 material/rare choices,
adds Dungeons separately, and merges structure pages into the saved terrain
feature plan. A new optional `structure_control` field defaults false for
old profiles and is true for new random Ages, even for an empty selection.
The generator applies its existing controlled-set filter when that flag is
true. This prevents zero choices from accidentally retaining all vanilla
controlled sets. Other native structure families still use their normal
biome gate. Existing authored/old plans are not rewritten, and no catalog
protocol change is needed.
The operator fixture checks seeded counts/uniqueness, all terrains, fixed
Plains/End/Nether biome pools, native defaults, biome eligibility, the empty
controlled-set gate, explicit Village precedence, merge/codec persistence,
and old-profile defaults. Actual placement remains an in-game check.
The .91 build and disposable server checks passed. Fixtures exercised Plains,
Native Flat, Nether-biome Cave, End-biome surface and empty Void selection,
plus the production random-Age command. Village, Fortress and Dungeon plans,
and an intentionally empty controlled plan, survived unload/reload, reseed
and full restart. Namespaced biome arguments were checked. The server was
stopped and its temporary world deleted. No commit, push or publication was
performed; generated structure placement remains a manual client check.
## Cave Dungeon parity
`1.0.1.92-beta.1` closes the Cave Dungeon terrain exclusion documented in .91.
Root `AgeStructureSelection.error` permits Dungeons on noise terrain, which
includes Cave. The 26.3 feature and placement implementation were already in
place; the port's shared lake/dungeon Cave rejection blocked authoring and,
through the availability check, random selection. Splitting that validation
permits Cave Dungeons without allowing Cave lakes or Flat/Void Dungeons.
The two targets' `worldgen/placed_feature/dungeons.json` files compare exactly:
vanilla monster_room, count 8, in-square and full-height uniform placement
using above-bottom/below-top anchors. No legacy Y=0 assumption is introduced.
Vanilla's solid floor/ceiling and opening-count requirements still determine
successful placement. The 32-page cap and per-occurrence deterministic seed
remain unchanged. New random Cave Ages now consider Dungeons automatically;
old frozen plans are not changed.
The extended `create_dungeons` fixture checks Cave stacking/cap and the
remaining lake/Flat/Void restrictions. The random-structure matrix now requires
Cave Dungeon coverage. `dungeons_probe` accepts an optional 1–4 chunk-side
bound for manual tests; it is not run automatically, avoiding a large sync
chunk-generation scan. Actual dungeon placement is handed to the client check.
The .92 build and disposable server checks passed: Cave and surface authored
fixtures, 32-page stacking/overflow, retained lake and Flat/Void rejection,
and random-structure coverage including a Nether Cave Dungeon selection.
Dungeon counts survived unload/reload and full restart; frozen random plans
survived reseed. Placement JSON matched the 1.21.1 file byte-for-byte. The
server was stopped and its temporary world deleted. No commit, push or
publication was performed. No dungeon chunk scan was run automatically.
## Biome-specific Nether and End materials
`1.0.1.93-beta.1` ports root `AgeDimensionService.withBiomeMaterialRules`.
The backend was using `caves.materialRule()` unchanged, which selects
`minecraft:overworld_caves` and can apply Overworld grass to Nether biomes.
The new noise-settings wrapper sequences the five vanilla End biomes' registered
End rule, then the five vanilla Nether biomes' registered Nether rule, then
the original terrain rule. This fixes Cave Nether coverings and the equivalent
End/Nether mismatch in authored Standard, Amplified and Island noise Ages.
It uses the active rule registry rather than copying vanilla material tables.
All non-material settings are preserved. Dry Cave's AIR default fluid, empty
aquifer config and minimum-relative sea level remain intact. Native Nether
surface rules can still place their own lava patches; this does not restore
flooded Cave aquifers. Flat/Void layer materials and custom feature materials
are not rewritten. Old saved Ages pick up the rule when reloaded, but only
newly generated chunks change; no generated blocks are retroactively edited.
No profile field or network protocol change is required.
The creation fixture verifies unchanged geometry/router/default blocks and
fluids, aquifers and sea level for four noise presets, and round-trips the
composed material rule with registry-aware codecs. `material_probe` limits
generation to one terrain-stage chunk and reports actual block counts.
Build, registry-aware rule codec tests and disposable server terrain checks
passed. All five Nether Cave fixtures generated their native material families
without grass/dirt: Nylium, Soul Sand/Soil, Basalt/Blackstone or Netherrack.
Standard The End and Island End Highlands generated End Stone; Lush Cave's
original Overworld materials remained present. Reload retained the rules, and
fresh chunks after full restart again used Nylium/End Stone. No full feature
or entity chunk scans were run. The server was stopped and temporary world
deleted. Nothing was committed, pushed or published.
## Verification of this first slice ## Verification of this first slice
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
+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 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 author Standard, Amplified, Island, Flat, Void, or Cave terrain with supported
biome-distribution pages, selected material/vegetation/structure features, and biome-distribution pages, selected material/vegetation/structure features, and
independent Age weather and lighting. Celestial systems, instability, crystal independent Age weather and lighting, multiple authored or seeded celestial
portals, and much of the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not 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. 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 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. size remains a manual check. The test server stopped and its world was deleted.
No commit/push/publication is performed until explicitly requested. 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: From the repository root:
```sh ```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_id=rivencraft
mod_name=Rivencraft mod_name=Rivencraft
mod_license=LGPL-3.0-or-later mod_license=LGPL-3.0-or-later
mod_version=1.0.1.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.", "item.rivencraft.page.symbol.implemented": "Implemented in 26.3; book and terrain requirements still apply.",
"symbol.rivencraft.weather.normal.description": "Use an independent normal weather cycle. Cave terrain keeps weather off.", "symbol.rivencraft.weather.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.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.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, without End terrain or a dragon fight.", "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. Preceding color pages tint it; otherwise it is gray.", "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": "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.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.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.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 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.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 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.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: set the native celestial orbit bearing to 0 degrees. A preceding Number overrides Angle in 15-degree steps.", "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: set the native celestial orbit bearing to 90 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: set the native celestial orbit bearing to 180 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: set the native celestial orbit bearing to 270 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 Normal Time: start this Age's native cycle at sunrise. A preceding Number overrides Phase in 15-degree steps from midnight.", "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 Normal Time: start this Age's native cycle at noon. With Zero Length it remains noon.", "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 Normal Time: start this Age's native cycle at sunset. A preceding Number overrides Phase in 15-degree steps from midnight.", "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 Normal Time: start this Age's native cycle at midnight. With Zero Length it remains midnight.", "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: advance this Age's native day cycle in its usual direction.", "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: run this Age's native day cycle in reverse. Does not change the Overworld clock.", "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": "Use the native sun/moon cycle with Length, Phase, Direction, Angle, and sun Size modifiers. Without a Length, use one normal day.", "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.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.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.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.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.full.description": "Before Normal Time: a normal-length day. A preceding Number sets Length to value × 0.08 days.", "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.double.description": "Before Normal Time: a double-length day (half as fast). A preceding Number sets Length to value × 0.08 days.", "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.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.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.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.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.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.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.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.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.", "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.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": "Abandoned Camps",
"symbol.rivencraft.feature.abandoned_camps.description": "Allow abandoned camps on surface noise terrain, including outside their usual biomes.", "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.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.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.floating_islands.description": "Add high floating islands alongside noise, Flat, or Void terrain; not Cave terrain.",
"symbol.rivencraft.feature.caves.description": "Add extra cave carving to noise terrain, including Cave; not Flat or Void.", "symbol.rivencraft.feature.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) { public static void registerPayloads(RegisterPayloadHandlersEvent event) {
var registrar = event.registrar("6"); var registrar = event.registrar("10");
registrar.playToClient(AgeCatalogPayload.TYPE, AgeCatalogPayload.STREAM_CODEC); registrar.playToClient(AgeCatalogPayload.TYPE, AgeCatalogPayload.STREAM_CODEC);
registrar.playToServer(AgeCatalogAckPayload.TYPE, AgeCatalogAckPayload.STREAM_CODEC, registrar.playToServer(AgeCatalogAckPayload.TYPE, AgeCatalogAckPayload.STREAM_CODEC,
(payload, context) -> { (payload, context) -> {
@@ -14,22 +14,27 @@ import org.joml.Vector3f;
import org.joml.Vector3fc; import org.joml.Vector3fc;
/** Frozen shared-color writing, adapted from 1.21.1 AgeSkyProfile. -1 means native. */ /** 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 PortAgeColors NATIVE = new PortAgeColors(-1, -1);
public static final Codec<PortAgeColors> CODEC = RecordCodecBuilder.create(instance -> instance.group( 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("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)); ).apply(instance, PortAgeColors::new));
public static final StreamCodec<RegistryFriendlyByteBuf, PortAgeColors> STREAM_CODEC = StreamCodec.composite( 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"); public static final Identifier AMBIENT = Identifier.fromNamespaceAndPath("rivencraft", "lighting/ambient_color");
private static volatile Map<Identifier, PortAgeColors> clientColors = Map.of(); private static volatile Map<Identifier, PortAgeColors> clientColors = Map.of();
public PortAgeColors { 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"); 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 void updateClient(Map<Identifier, PortAgeColors> colors) { clientColors = Map.copyOf(colors); }
public static PortAgeColors forLevel(Level level) { 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) { 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"); 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; boolean written = false;
for (int i = 0; i < pages.size(); i++) { for (int i = 0; i < pages.size(); i++) {
Identifier page = pages.get(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/white" -> { red += amount; green += amount; blue += amount; }
case "color/black" -> { 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) { if (written) {
int rgb = clamp(red) << 16 | clamp(green) << 8 | clamp(blue); 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; red = green = blue = 0;
written = false; written = false;
} }
} }
if (written) sky = clamp(red) << 16 | clamp(green) << 8 | clamp(blue); 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)); } 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.dnorberg.rivencraft.item.LinkBookTarget;
import net.minecraft.commands.Commands; import net.minecraft.commands.Commands;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.arguments.IdentifierArgument;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.core.component.DataComponents; import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
@@ -68,6 +69,85 @@ public final class PortAgeCommands {
return 1; return 1;
}))) })))
.then(Commands.literal("age") .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.literal("create_size")
.then(Commands.argument("name", StringArgumentType.word()) .then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg()) .then(Commands.argument("seed", LongArgumentType.longArg())
@@ -209,8 +289,13 @@ public final class PortAgeCommands {
PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select( PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select(
context.getSource().getServer(), seed); context.getSource().getServer(), seed);
var environment = PortEnvironmentSelection.resolve(java.util.List.of(), seed, selected.terrain()); 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(), 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( if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
"Random Age plan: terrain=" + selected.terrain() "Random Age plan: terrain=" + selected.terrain()
+ " biomes=" + selected.biomes() + " biomes=" + selected.biomes()
@@ -244,7 +329,9 @@ public final class PortAgeCommands {
.then(Commands.argument("name", StringArgumentType.word()) .then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg()) .then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.STANDARD, .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.literal("create_lakes")
.then(Commands.argument("name", StringArgumentType.word()) .then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg()) .then(Commands.argument("seed", LongArgumentType.longArg())
@@ -378,7 +465,10 @@ public final class PortAgeCommands {
.executes(PortAgeCommands::probeVegetation))) .executes(PortAgeCommands::probeVegetation)))
.then(Commands.literal("dungeons_probe") .then(Commands.literal("dungeons_probe")
.then(Commands.argument("name", StringArgumentType.word()) .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.literal("lakes_probe")
.then(Commands.argument("name", StringArgumentType.word()) .then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeLakes))) .executes(PortAgeCommands::probeLakes)))
@@ -586,12 +676,659 @@ public final class PortAgeCommands {
var pos = new net.minecraft.core.BlockPos(0, 80, 0); var pos = new net.minecraft.core.BlockPos(0, 80, 0);
var sun = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.SUN_ANGLE, pos); 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 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) context.getSource().sendSuccess(() -> Component.literal("Cycle probe " + ageId(context)
+ " saved=" + cycle + " nativeTicks=" + source.totalTicks() + " ageTicks=" + sample.totalTicks() + " saved=" + cycle + " nativeTicks=" + source.totalTicks() + " ageTicks=" + sample.totalTicks()
+ " sunAngle=" + sun + " skyLight=" + light), false); + " sunAngle=" + sun + " moonAngle=" + moon + " skyLight=" + light), false);
return 1; 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) { private static int createColorAge(CommandContext<CommandSourceStack> context) {
try { try {
String sky = StringArgumentType.getString(context, "sky").toLowerCase(Locale.ROOT); 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 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 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); 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()))) { || colors.ambient() >= 0 && !ambient.equals(PortAgeColors.vector(colors.ambient()))) {
context.getSource().sendFailure(Component.literal("Color attribute mismatch")); context.getSource().sendFailure(Component.literal("Color attribute mismatch"));
return 0; return 0;
@@ -651,6 +1389,29 @@ public final class PortAgeCommands {
return 1; 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) { private static void verifyCatalogRoundTrip(net.minecraft.server.MinecraftServer server) {
var payload = new AgeCatalogPayload(1, server.levelKeys().stream().map(key -> key.identifier()).toList(), var payload = new AgeCatalogPayload(1, server.levelKeys().stream().map(key -> key.identifier()).toList(),
RuntimeAgeBackend.savedColors(server), RuntimeAgeBackend.savedDayCycles(server)); RuntimeAgeBackend.savedColors(server), RuntimeAgeBackend.savedDayCycles(server));
@@ -925,7 +1686,41 @@ public final class PortAgeCommands {
java.util.Collections.nCopies(8, page)); 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); Identifier id = ageId(context);
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id)); ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)) { if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)) {
@@ -934,7 +1729,7 @@ public final class PortAgeCommands {
} }
int count = features.dungeons(); int count = features.dungeons();
int spawners = 0; 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); var chunk = level.getChunk(chunkX, chunkZ);
for (var pos : chunk.getBlockEntitiesPos()) { for (var pos : chunk.getBlockEntitiesPos()) {
if (chunk.getBlockState(pos).is(Blocks.SPAWNER)) spawners++; if (chunk.getBlockState(pos).is(Blocks.SPAWNER)) spawners++;
@@ -942,7 +1737,7 @@ public final class PortAgeCommands {
} }
int placed = spawners; int placed = spawners;
context.getSource().sendSuccess(() -> Component.literal( 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; return placed;
} }
@@ -1092,6 +1887,24 @@ public final class PortAgeCommands {
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures, int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures,
PortEnvironmentSelection environment) { 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"); String name = StringArgumentType.getString(context, "name");
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name); Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
long seed = LongArgumentType.getLong(context, "seed"); long seed = LongArgumentType.getLong(context, "seed");
@@ -1099,7 +1912,7 @@ public final class PortAgeCommands {
try { try {
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan, level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures, terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
environment.weather(), environment.lighting(), environment.sky()); environment.weather(), environment.lighting(), environment.sky(), colors, cycle);
} catch (IllegalArgumentException | IllegalStateException exception) { } catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage())); context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0; return 0;
@@ -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. */ /** Authored native celestial controls. Never mutates the shared clock. */
public record PortDayCycle(boolean authored, double period, int stoppedTime, 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 PortDayCycle NATIVE = new PortDayCycle(false, 1, 0);
public static final Identifier NORMAL_TIME = Identifier.fromNamespaceAndPath("rivencraft", "celestial/normal_time"); public static final Identifier NORMAL_TIME = Identifier.fromNamespaceAndPath("rivencraft", "celestial/normal_time");
public static final Codec<PortDayCycle> CODEC = RecordCodecBuilder.create(instance -> instance.group( 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.intRange(0, 23999).optionalFieldOf("phase_tick", 0).forGetter(PortDayCycle::phaseTick),
Codec.BOOL.optionalFieldOf("clockwise", true).forGetter(PortDayCycle::clockwise), Codec.BOOL.optionalFieldOf("clockwise", true).forGetter(PortDayCycle::clockwise),
Codec.intRange(-1, 359).optionalFieldOf("bearing", -1).forGetter(PortDayCycle::bearing), 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)); ).apply(instance, PortDayCycle::new));
public static final StreamCodec<RegistryFriendlyByteBuf, PortDayCycle> STREAM_CODEC = StreamCodec.composite( public static final StreamCodec<RegistryFriendlyByteBuf, PortDayCycle> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.BOOL, PortDayCycle::authored, ByteBufCodecs.DOUBLE, PortDayCycle::period, ByteBufCodecs.BOOL, PortDayCycle::authored, ByteBufCodecs.DOUBLE, PortDayCycle::period,
ByteBufCodecs.VAR_INT, PortDayCycle::stoppedTime, ByteBufCodecs.VAR_LONG, PortDayCycle::originTick, ByteBufCodecs.VAR_INT, PortDayCycle::stoppedTime, ByteBufCodecs.VAR_LONG, PortDayCycle::originTick,
ByteBufCodecs.VAR_INT, PortDayCycle::phaseTick, ByteBufCodecs.BOOL, PortDayCycle::clockwise, 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(); private static volatile Map<Identifier, PortDayCycle> clientCycles = Map.of();
public PortDayCycle { public PortDayCycle {
bodies = List.copyOf(bodies);
if (!Double.isFinite(period) || period < 0 || period > 2 || stoppedTime < 0 || stoppedTime >= 24000 if (!Double.isFinite(period) || period < 0 || period > 2 || stoppedTime < 0 || stoppedTime >= 24000
|| phaseTick < 0 || phaseTick >= 24000 || bearing < -1 || bearing >= 360 || 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"); 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) { 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) { 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) { 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) { public static boolean validWriting(List<Identifier> pages) {
int target = pages.indexOf(NORMAL_TIME), periods = 0, phases = 0, directions = 0, angles = 0, sizes = 0; int periods = 0, phases = 0, directions = 0, angles = 0, sizes = 0, suns = 0, moons = 0;
if (target != pages.lastIndexOf(NORMAL_TIME)) return false; for (Identifier page : pages) {
for (int i = 0; i < pages.size(); i++) { if (isPeriod(page) && ++periods > 1 || isPhase(page) && ++phases > 1
if (isPeriod(pages.get(i)) && (++periods > 1 || i > target)) return false; || isDirection(page) && ++directions > 1 || isAngle(page) && ++angles > 1
if (isPhase(pages.get(i)) && (++phases > 1 || i > target)) return false; || isSize(page) && ++sizes > 1) return false;
if (isDirection(pages.get(i)) && (++directions > 1 || i > target)) return false; if (isController(page)) {
if (isAngle(pages.get(i)) && (++angles > 1 || i > target)) return false; if (!page.getPath().equals("celestial/moon")) suns++;
if (isSize(pages.get(i)) && (++sizes > 1 || i > target)) return false; 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) { public static PortDayCycle compile(List<Identifier> pages, long seed) {
return compile(pages, seed, 0); 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) { 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 (!validWriting(pages)) throw new IllegalArgumentException("Place at most one of each celestial modifier before its controller; limit ten suns and ten moons");
if (!pages.contains(NORMAL_TIME)) return NATIVE; 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; double period = 1;
Integer phase = null; Integer phase = null;
boolean clockwise = true; boolean clockwise = true;
@@ -160,7 +238,59 @@ public record PortDayCycle(boolean authored, double period, int stoppedTime,
int stopped = period == 0 ? phase != null ? phase int stopped = period == 0 ? phase != null ? phase
: new Random(seed ^ 0x4441594359434C45L).nextInt(24000) : 0; : new Random(seed ^ 0x4441594359434C45L).nextInt(24000) : 0;
return new PortDayCycle(true, period, stopped, phase != null ? nativeTicks : 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) { public long ticks(long nativeTicks) {
@@ -113,7 +113,7 @@ public final class PortFeatureChunkGenerator extends ChunkGenerator {
} }
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets, @Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
RandomState random, long seed) { RandomState random, long seed) {
if (hasStructurePages()) { if (controlsStructures()) {
var lookup = PortStructurePages.filtered(sets, terrainFeatures.structures(), getBiomeSource()); var lookup = PortStructurePages.filtered(sets, terrainFeatures.structures(), getBiomeSource());
var planning = PortStructurePages.planningBiomes(getBiomeSource(), sets, terrainFeatures.structures()); var planning = PortStructurePages.planningBiomes(getBiomeSource(), sets, terrainFeatures.structures());
if (base instanceof PortFlatChunkGenerator || base instanceof net.minecraft.world.level.levelgen.FlatLevelSource) 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); return base.createState(sets, random, seed);
} }
private boolean hasStructurePages() { private boolean controlsStructures() {
return terrainFeatures != null && !terrainFeatures.structures().isEmpty(); return terrainFeatures != null && (terrainFeatures.structureControl() || !terrainFeatures.structures().isEmpty());
} }
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state, @Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
StructureManager structures, ChunkAccess chunk, StructureManager structures, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> dimension) { 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); else base.createStructures(registries, state, structures, chunk, templates, dimension);
} }
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) { @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))); 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) { if (terrain == PortAgeTerrain.CAVE) {
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements() List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
.filter(PortRegistryPages::isCaveBiome) .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)); ? 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) { private static boolean isCelestial(Identifier page) {
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return false; if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
return switch (page.getPath()) { return switch (page.getPath()) {
// Only Overworld currently draws bodies in this port. Recognize the // Preserve the established compatible-default rule for written controllers.
// Sun/Moon intentions too, without marking those pages implemented.
case "celestial/normal_time", "celestial/sun", "celestial/moon" -> true; case "celestial/normal_time", "celestial/sun", "celestial/moon" -> true;
default -> false; default -> false;
}; };
@@ -88,6 +88,20 @@ public final class PortStructurePages {
.orElseThrow(() -> new IllegalArgumentException("Missing structure set for " + page))); .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) { private static Map<Identifier, Integer> enabled(List<Identifier> pages) {
Map<Identifier, Integer> result = new HashMap<>(); Map<Identifier, Integer> result = new HashMap<>();
for (Identifier page : pages) result.merge(SETS.get(page), 1, Integer::sum); 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. */ /** Frozen terrain filters and ordered solid/surface features for an Age. */
public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres, 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( public static final Identifier FLOATING_ISLANDS = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/floating_islands"); Rivencraft.MOD_ID, "feature/floating_islands");
public static final Identifier TENDRILS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/tendrils"); 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), .forGetter(PortTerrainFeaturePlan::denseOres),
Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures), Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures),
Identifier.CODEC.listOf().optionalFieldOf("structures", List.of()).forGetter(PortTerrainFeaturePlan::structures), Identifier.CODEC.listOf().optionalFieldOf("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)); ).apply(instance, PortTerrainFeaturePlan::new));
public record Choice(Identifier page, Identifier material) { public record Choice(Identifier page, Identifier material) {
@@ -55,7 +57,7 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
PortCarverPages.validateCounts(carvers); PortCarverPages.validateCounts(carvers);
if (denseOres < 0 || denseOres > MAX_PER_FEATURE) if (denseOres < 0 || denseOres > MAX_PER_FEATURE)
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences"); 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"); throw new IllegalArgumentException("Terrain feature plan must not be empty");
var counts = new HashMap<Identifier, Integer>(); var counts = new HashMap<Identifier, Integer>();
for (Choice choice : choices) { 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, public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
boolean starFissures, List<Identifier> structures) { boolean starFissures, List<Identifier> structures) {
this(choices, skylands, denseOres, starFissures, structures, List.of()); this(choices, skylands, denseOres, starFissures, structures, List.of());
@@ -465,7 +465,7 @@ public final class RuntimeAgeBackend {
case CAVE -> dryCaveSettings(server); case CAVE -> dryCaveSettings(server);
case FLAT, VOID -> throw new IllegalStateException("Flat terrain should use FlatLevelSource"); 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) if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed); 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. */ /** Carries extracted sky presentation values without live-world reads during drawing. */
public interface StaticSkyState { 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(); long rivencraft$staticFrame();
void rivencraft$staticFrame(long frame); void rivencraft$staticFrame(long frame);
int rivencraft$skyTint(); int rivencraft$skyTint();
@@ -10,4 +15,8 @@ public interface StaticSkyState {
void rivencraft$orbitBearing(int bearing); void rivencraft$orbitBearing(int bearing);
float rivencraft$sunSize(); float rivencraft$sunSize();
void rivencraft$sunSize(float size); 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) -> { builder.addTimeBasedLayer(EnvironmentAttributes.FOG_COLOR, (base, tick) -> {
int rgb = PortAgeColors.forLevel(level).sky(); 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) -> { builder.addTimeBasedLayer(EnvironmentAttributes.AMBIENT_LIGHT_COLOR, (base, tick) -> {
int rgb = PortAgeColors.forLevel(level).ambient(); int rgb = PortAgeColors.forLevel(level).ambient();
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.mixin;
import net.dnorberg.rivencraft.age.PortDayCycle; import net.dnorberg.rivencraft.age.PortDayCycle;
import net.minecraft.world.attribute.EnvironmentAttributeSystem; import net.minecraft.world.attribute.EnvironmentAttributeSystem;
import net.minecraft.world.attribute.EnvironmentAttributes;
import net.minecraft.world.attribute.WeatherAttributes; import net.minecraft.world.attribute.WeatherAttributes;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
@@ -11,6 +12,30 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(EnvironmentAttributeSystem.Builder.class) @Mixin(EnvironmentAttributeSystem.Builder.class)
public class AgeDayTimelineMixin { 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) @Inject(method = "addDynamicLayers", at = @At("HEAD"), cancellable = true)
private void rivencraft$dayCycle(Level level, CallbackInfoReturnable<EnvironmentAttributeSystem.Builder> cir) { private void rivencraft$dayCycle(Level level, CallbackInfoReturnable<EnvironmentAttributeSystem.Builder> cir) {
var id = level.dimension().identifier(); var id = level.dimension().identifier();
@@ -26,6 +26,24 @@ public class StaticSkyRendererMixin {
@Unique private int rivencraft$endTint = -1; @Unique private int rivencraft$endTint = -1;
@Unique private int rivencraft$orbitBearing = -1; @Unique private int rivencraft$orbitBearing = -1;
@Unique private float rivencraft$sunSize = 1F; @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")) @Inject(method = "extractRenderState", at = @At("TAIL"))
private void rivencraft$extract(ClientLevel level, float partialTicks, Camera camera, 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$staticFrame(enabled ? level.getGameTime() / 2 : Long.MIN_VALUE);
((StaticSkyState) state).rivencraft$skyTint(net.dnorberg.rivencraft.age.PortAgeColors.forLevel(level).sky()); ((StaticSkyState) state).rivencraft$skyTint(net.dnorberg.rivencraft.age.PortAgeColors.forLevel(level).sky());
var cycle = net.dnorberg.rivencraft.age.PortDayCycle.forLevel(level); 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$orbitBearing(cycle.authored() ? cycle.bearing() : -1);
((StaticSkyState) state).rivencraft$sunSize(cycle.authored() ? cycle.sunSize() : 1F); ((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 // 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. // 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) @Inject(method = "render", at = @At("HEAD"), cancellable = true)
@@ -49,9 +122,82 @@ public class StaticSkyRendererMixin {
rivencraft$endTint = ((StaticSkyState) state).rivencraft$skyTint(); rivencraft$endTint = ((StaticSkyState) state).rivencraft$skyTint();
rivencraft$orbitBearing = ((StaticSkyState) state).rivencraft$orbitBearing(); rivencraft$orbitBearing = ((StaticSkyState) state).rivencraft$orbitBearing();
rivencraft$sunSize = ((StaticSkyState) state).rivencraft$sunSize(); 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 (frame == Long.MIN_VALUE) return;
if (rivencraft$staticSky == null) rivencraft$staticSky = new PortStaticSky(); if (rivencraft$staticSky == null) rivencraft$staticSky = new PortStaticSky();
rivencraft$staticSky.render(renderTarget, fog, frame, rivencraft$endTint); 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(); ci.cancel();
} }
@@ -68,10 +214,48 @@ public class StaticSkyRendererMixin {
return nativeSize * rivencraft$sunSize; 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, private void rivencraft$rotateHorizon(com.mojang.renderpearl.api.commands.RenderPass pass,
com.mojang.blaze3d.vertex.PoseStack pose, float angle, com.mojang.blaze3d.vertex.PoseStack pose, float angle,
org.joml.Vector4fc color, CallbackInfo ci) { 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; if (rivencraft$orbitBearing < 0) return;
pose.pushPose(); pose.pushPose();
pose.rotateDegrees(com.mojang.math.Axis.YP, rivencraft$orbitBearing + 90); 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, private void rivencraft$restoreHorizon(com.mojang.renderpearl.api.commands.RenderPass pass,
com.mojang.blaze3d.vertex.PoseStack pose, float angle, com.mojang.blaze3d.vertex.PoseStack pose, float angle,
org.joml.Vector4fc color, CallbackInfo ci) { 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 = @Redirect(method = "renderEndSky", at = @At(value = "INVOKE", target =
@@ -94,6 +280,10 @@ public class StaticSkyRendererMixin {
@Inject(method = "close", at = @At("HEAD")) @Inject(method = "close", at = @At("HEAD"))
private void rivencraft$close(CallbackInfo ci) { private void rivencraft$close(CallbackInfo ci) {
if (rivencraft$discs != null) {
rivencraft$discs.close();
rivencraft$discs = null;
}
if (rivencraft$staticSky != null) { if (rivencraft$staticSky != null) {
rivencraft$staticSky.close(); rivencraft$staticSky.close();
rivencraft$staticSky = null; rivencraft$staticSky = null;
@@ -14,6 +14,14 @@ public class StaticSkyStateMixin implements StaticSkyState {
@Unique private int rivencraft$tint = -1; @Unique private int rivencraft$tint = -1;
@Unique private int rivencraft$bearing = -1; @Unique private int rivencraft$bearing = -1;
@Unique private float rivencraft$sunSize = 1F; @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 long rivencraft$staticFrame() { return rivencraft$frame; }
public void rivencraft$staticFrame(long frame) { rivencraft$frame = frame; } public void rivencraft$staticFrame(long frame) { rivencraft$frame = frame; }
public int rivencraft$skyTint() { return rivencraft$tint; } 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 void rivencraft$orbitBearing(int bearing) { rivencraft$bearing = bearing; }
public float rivencraft$sunSize() { return rivencraft$sunSize; } public float rivencraft$sunSize() { return rivencraft$sunSize; }
public void rivencraft$sunSize(float size) { rivencraft$sunSize = size; } 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")) @Inject(method = "reset", at = @At("TAIL"))
private void rivencraft$reset(CallbackInfo ci) { private void rivencraft$reset(CallbackInfo ci) {
@@ -29,5 +41,9 @@ public class StaticSkyStateMixin implements StaticSkyState {
rivencraft$tint = -1; rivencraft$tint = -1;
rivencraft$bearing = -1; rivencraft$bearing = -1;
rivencraft$sunSize = 1F; 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) { public static Identifier createAge(ServerPlayer player, String title, List<Identifier> pages) {
if (title == null || title.isBlank() || title.length() > MAX_BOOK_TITLE_LENGTH) return null; if (title == null || title.isBlank() || title.length() > MAX_BOOK_TITLE_LENGTH) return null;
long seed = player.getRandom().nextLong();
net.dnorberg.rivencraft.age.PortBiomePlan biomePlan; net.dnorberg.rivencraft.age.PortBiomePlan biomePlan;
try { try {
biomePlan = PortAuthoredWriting.biomePlan(pages, player.registryAccess()); biomePlan = PortAuthoredWriting.biomePlan(pages, player.level().getServer(), seed);
} catch (IllegalArgumentException exception) { } catch (IllegalArgumentException exception) {
player.sendOverlayMessage(Component.literal("Age writing unavailable: " + exception.getMessage())); player.sendOverlayMessage(Component.literal("Age writing unavailable: " + exception.getMessage()));
return null; return null;
@@ -127,7 +128,6 @@ public final class PortBookNetworking {
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name); id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
} }
try { try {
long seed = player.getRandom().nextLong();
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages); PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
if (pages.isEmpty()) { if (pages.isEmpty()) {
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed); PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
@@ -135,15 +135,17 @@ public final class PortBookNetworking {
biomePlan = random.biomes(); biomePlan = random.biomes();
} }
var environment = net.dnorberg.rivencraft.age.PortEnvironmentSelection.resolve(pages, seed, terrain); 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, RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
PortAuthoredWriting.rockPlan(pages, seed), randomFeatures == null ? PortAuthoredWriting.rockPlan(pages, seed) : randomFeatures.rocks(),
PortAuthoredWriting.vegetationPlan(pages), PortAuthoredWriting.vegetationPlan(pages),
PortAuthoredWriting.dungeonCount(pages), randomFeatures == null ? PortAuthoredWriting.dungeonCount(pages) : randomFeatures.dungeons(),
PortAuthoredWriting.lakePlan(pages, seed), randomFeatures == null ? PortAuthoredWriting.lakePlan(pages, seed) : randomFeatures.lakes(),
PortAuthoredWriting.terrainFeaturePlan(pages, seed), randomFeatures == null ? PortAuthoredWriting.terrainFeaturePlan(pages, seed) : randomFeatures.terrain(),
environment.weather(), environment.lighting(), environment.sky(), environment.weather(), environment.lighting(), environment.sky(),
net.dnorberg.rivencraft.age.PortAgeColors.compile(pages, seed), net.dnorberg.rivencraft.age.PortAgeColors.resolve(pages, seed),
net.dnorberg.rivencraft.age.PortDayCycle.compile(pages, seed, net.dnorberg.rivencraft.age.PortDayCycle.resolve(pages, seed,
net.dnorberg.rivencraft.age.PortDayCycle.nativeTicks(server.overworld()))); net.dnorberg.rivencraft.age.PortDayCycle.nativeTicks(server.overworld())));
} catch (IllegalArgumentException | IllegalStateException exception) { } catch (IllegalArgumentException | IllegalStateException exception) {
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage())); player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
@@ -72,6 +72,12 @@ public final class PortAuthoredWriting {
/** A page never disappears into an Age unless this port can apply its effect. */ /** A page never disappears into an Age unless this port can apply its effect. */
public static boolean supports(List<Identifier> pages) { 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.PortAgeColors.validNumbers(pages)) return false;
if (!net.dnorberg.rivencraft.age.PortDayCycle.validWriting(pages)) return false; if (!net.dnorberg.rivencraft.age.PortDayCycle.validWriting(pages)) return false;
boolean terrain = false; boolean terrain = false;
@@ -97,7 +103,7 @@ public final class PortAuthoredWriting {
|| pendingMaterial.equals("rivencraft:fluid") && PortLakePlan.isLake(page))) return false; || pendingMaterial.equals("rivencraft:fluid") && PortLakePlan.isLake(page))) return false;
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page) if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(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) { } else if (isTerrain(page) && !terrain) {
terrain = true; terrain = true;
} else if (PortSkyStyle.forPage(page) != null && sky++ == 0) { } else if (PortSkyStyle.forPage(page) != null && sky++ == 0) {
@@ -107,7 +113,7 @@ public final class PortAuthoredWriting {
} else if (PortWeatherMode.forPage(page) != null && weather++ == 0) { } else if (PortWeatherMode.forPage(page) != null && weather++ == 0) {
// A single independent weather policy for this Age. // A single independent weather policy for this Age.
} else if (controller(page) != null && controllers++ == 0) { } 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 } else if (isBlockPage(page) && pendingMaterial == null
&& PortSymbolPages.find(page) != null && PortSymbolPages.find(page) != null
&& (PortSymbolPages.find(page).category().equals("rivencraft:block") && (PortSymbolPages.find(page).category().equals("rivencraft:block")
@@ -152,7 +158,8 @@ public final class PortAuthoredWriting {
} }
if (pendingMaterial != null || (rocks > 0 || pages.stream().anyMatch(PortVegetationPlan::requiresSurface)) if (pendingMaterial != null || (rocks > 0 || pages.stream().anyMatch(PortVegetationPlan::requiresSurface))
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false; && (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; || pages.contains(CAVE_TERRAIN))) return false;
if (denseOres > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_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; 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(); .filter(value -> value != null).findFirst().orElseThrow();
boolean valid = switch (distribution) { boolean valid = switch (distribution) {
case NATIVE -> biomes == 0; case NATIVE -> biomes == 0;
case SINGLE -> biomes == 1; case SINGLE -> biomes <= 1;
default -> biomes >= 2; default -> true;
}; };
return valid; return valid;
} }
@@ -214,13 +221,14 @@ public final class PortAuthoredWriting {
} }
public static PortRockPlan rockPlan(List<Identifier> pages, long seed) { 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); List<Identifier> materials = solidMaterials(pages, seed, ROCKS);
return materials.isEmpty() ? null : new PortRockPlan(materials); return materials.isEmpty() ? null : new PortRockPlan(materials);
} }
public static PortTerrainFeaturePlan terrainFeaturePlan(List<Identifier> pages, long seed) { 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, List<Identifier>> materials = new HashMap<>();
Map<Identifier, Integer> occurrences = new HashMap<>(); Map<Identifier, Integer> occurrences = new HashMap<>();
List<PortTerrainFeaturePlan.Choice> choices = new ArrayList<>(); List<PortTerrainFeaturePlan.Choice> choices = new ArrayList<>();
@@ -230,7 +238,7 @@ public final class PortAuthoredWriting {
choices.add(new PortTerrainFeaturePlan.Choice(page, PortTerrainFeaturePlan.CRYSTAL_MATERIAL)); choices.add(new PortTerrainFeaturePlan.Choice(page, PortTerrainFeaturePlan.CRYSTAL_MATERIAL));
continue; 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; int occurrence = occurrences.merge(page, 1, Integer::sum) - 1;
choices.add(new PortTerrainFeaturePlan.Choice(page, selected.get(occurrence))); 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) { 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; if (pages.stream().noneMatch(PortLakePlan::isLake)) return null;
List<Identifier> fluidPool = PortRegistryPages.fluidMaterialIds(); List<Identifier> fluidPool = PortRegistryPages.fluidMaterialIds();
if (fluidPool.isEmpty()) throw new IllegalStateException("No source-fluid materials are registered"); if (fluidPool.isEmpty()) throw new IllegalStateException("No source-fluid materials are registered");
@@ -288,6 +296,12 @@ public final class PortAuthoredWriting {
return new PortLakePlan(choices); 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) { public static PortVegetationPlan vegetationPlan(List<Identifier> pages) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages"); if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
List<Identifier> vegetation = pages.stream().filter(PortVegetationPlan::isVegetation).toList(); 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. */ /** Resolves all authored biomes from the active registry before creating a level. */
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) { 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( if (!supports(pages)) throw new IllegalArgumentException(
"Unsupported terrain or biome-page combination in this port slice"); "Unsupported terrain or biome-page combination in this port slice");
List<Identifier> biomes = new ArrayList<>(); 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"); throw new IllegalArgumentException("Dense Ores needs at least one biome with registered ore placements");
if (distribution == PortBiomePlan.Distribution.NATIVE) if (distribution == PortBiomePlan.Distribution.NATIVE)
return new PortBiomePlan(distribution, List.of()); return new PortBiomePlan(distribution, List.of());
if (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 (biomes.isEmpty()) return null;
if (distribution == null) distribution = biomes.size() == 1 if (distribution == null) distribution = biomes.size() == 1
? PortBiomePlan.Distribution.SINGLE : biomes.size() == 2 ? 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;
}
}