Compare commits

..
Author SHA1 Message Date
dnorberg 38e40ddba3 Port seeded sky colors and Normal Time celestial controls to 26.3
Build / build (push) Canceled after 0s
2026-11-03 16:56:07 -05:00
dnorberg a7a73d3c17 Port Age lighting and animated sky styles to 26.3 beta
Build / build (push) Canceled after 0s
2026-11-02 15:49:58 -05:00
33 changed files with 2452 additions and 28 deletions

No files matched your search

+4 -2
View File
@@ -14,10 +14,12 @@ The gameplay guide below describes the **Minecraft 1.21.1 release**. A separate,
incomplete **Minecraft 26.3 beta** is available: Java 25, NeoForge 26.3.0.99,
and a first-party Age backend (no Dynamic Dimensions or Bad Packets). It now
supports authored terrain/biomes, structure and vegetation pages, independent
Age weather, and a growing set of generation features,
Age weather/lighting, seeded and colorable Overworld/Nether/End/Static skies,
Normal Time length/phase/direction/bearing and sun-size controls,
and a growing set of generation features,
but does not yet provide full 1.21.1 gameplay or optional-integration parity.
See the [26.3 beta guide](ports/mc263/README.md) and
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.65-beta.1.md).
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.78-beta.1.md).
Keep the builds and their saves separate, and back up worlds before updating.
## Getting started
+403
View File
@@ -900,6 +900,409 @@ loaded; that fixture is not claimed as tested. The server shut down cleanly,
and its temporary world was deleted. Actual player arrival in the affected
Age and the unchanged Cave/Linking Book paths remain manual checks.
The local `1.0.1.66-beta.1` slice ports the three 1.21.1 lighting controls.
Normal surface types keep the host Overworld holder; Dark disables skylight,
Bright sets ambient light to 0.25. Cave Bright is explicitly supported with
0.25 ambient and no skylight; Cave Normal/Dark have 0.0 ambient and no skylight.
The native vanilla registry-sync path supplies types to clients before travel.
Generated data derives three variants from the exact 26.3 vanilla Overworld
resource, preserving every key except has_skylight and ambient_light. The
generator has declared inputs/outputs and depends on Minecraft artifacts.
The Age index has an optional lighting map, defaulting old entries to Normal.
Creation supplies the choice before constructing the level and saving the
index. Restore/reload/reseed read it from the index; retirement removes it and
failed retirement restores it before reloading. Pages share grammar/tint and
the positive biome-page filter, avoiding the weather-only compiler regression.
Missing lighting stays Normal until the random-trait port; Bright instability
is pending with the instability system. No sky/celestial renderer is added.
The static variants derive vanilla data, not arbitrary data-pack overrides
to the host's Overworld type; broader custom type inheritance is not claimed.
Build/diff checks and generated-data equality checks pass (only the two
lighting fields differ from vanilla). A disposable server with a normal
Overworld compiled all six surface/Cave lighting-page combinations and
reported the expected type, skylight and ambient values. Cave Bright survived
unload/reload and reseeding, and all six choices survived a full restart.
Overworld skylight/ambient stayed unchanged. An earlier Superflat-host fixture
could test surface variants but could not create Cave terrain because the
existing backend requires a noise-based host generator; it was discarded and
the Cave checks ran on a fresh normal-host fixture. Test servers stopped
cleanly and both temporary worlds were deleted. Client-side appearance,
authoring, travel, placed lights and multiplayer registry behavior are manual
checks, not inferred from the server probes. No commit/push/publication.
The local `1.0.1.67-beta.1` corrects the visual failure found in slice 66:
LightmapRenderStateExtractor and lightmap.fsh use AMBIENT_LIGHT_COLOR, while
dimension ambient_light only affects other brightness queries. Merely porting
1.21.1's 0.25 field was insufficient. Bright types now provide white native
ambient color and ambient_light 1.0; Cave Bright retains no skylight. A scoped
NeoForge spawn-placement handler denies natural/chunk-generation MONSTER
spawns in Bright runtime Ages, leaving passive/existing mobs, spawners,
commands/eggs and event spawns untouched. It does not manufacture propagated
block light or alter crop light checks. This supersedes slice 66's Bright
appearance and natural-spawn limitation; existing Bright types update on boot.
Requested follow-up for the color-page port: add ambient-light color as a
separate authorable target using minecraft:visual/ambient_light_color. Keep
the existing number/color mixing semantics and white Bright default; do not
confuse ambient tint with sky/celestial color or its server spawn policy.
Color pages themselves remain unported in this slice.
Build/diff and generated attribute checks pass. A disposable dedicated server
verified the registered event handler denies a successful-default natural
zombie placement in surface/Cave Bright, leaves spawner zombies and natural
cows unchanged, and leaves Normal unaffected. Those are isolated event-policy
probes, not a long-running natural-spawn census. The server shut down and its
temporary world was deleted. Actual client appearance awaits player validation.
The local `1.0.1.68-beta.1` removes the requested-against MobSpawnEvent veto.
An Age-scoped LevelLightEngine.getRawBrightness return modifier contributes
Bright's full ambient minimum of 15 to combined brightness on both sides.
Vanilla Monster.isDarkEnoughToSpawn now sees that light, rather than being
overridden or filtered by mob category/spawn reason. Light-ignoring mobs and
special spawns retain vanilla rules; no spawner exemptions are introduced.
Block/sky storage and propagation remain physical, while combined-light
consumers (including plant checks) see ambient light. Cave Bright retains
no skylight, and the native white render attribute remains. This supersedes
slice 67's separate spawn policy and unchanged-crop-light behavior.
The lighting probe now calls vanilla's darkness predicate and combined
brightness instead of constructing synthetic spawn events. Ambient-color
authoring remains a planned part of the color-page port.
Build/diff checks pass. A disposable dedicated server loaded the required
mixin and reported effective brightness 15 / vanilla darkness false for
surface/Cave Bright, versus 0 / true for Dark. These are direct brightness/
vanilla-predicate probes, not a natural-spawn census or a fully generated
underground-room test; an unloaded Normal column returned native sky light 15.
The server stopped cleanly and its temporary world was deleted. Client visual
validation remains with the player; no publication is performed.
The local `1.0.1.69-beta.1` slice completes missing weather/lighting choices
at book binding. PortEnvironmentSelection resolves explicit supported pages
first and uses independent seed-salted Legacy-rank rolls for omissions.
Normal is rank 1, remaining supported modes rank 2: 3:1:1 lighting and
6:1:1:1:1:1 weather. These weights derive from the active enum pool and reuse
the existing weighted ticket routine, rather than duplicating random-choice
logic or enabling unported Cloudy/Rain/Snow modes. It is rank-policy parity,
not exact seed-stream parity with 1.21.1's broader trait pool.
The selected weather/lighting are passed through the existing save-before-
catalog creation path. Restoring Ages never calls the resolver, so old Ages
retain their saved values. Cave weather is always Off; Cave lighting remains
independent and supports Bright. Explicit developer fixtures remain controlled,
but create_random now uses the same resolver as pageless books. A read-only
environment probe checks seed repeatability and explicit overrides. Superseded
Normal-only fromPages helpers are removed. Random feature, sky/celestial,
blocklist policy and instability work remain pending.
Build/diff checks pass. Disposable dedicated-server probes repeated seeds
0/4/7, verified explicit overrides and independent one-field completion, and
confirmed Cave weather Off with Bright lighting. Three random Ages saved
Always/Normal, Normal/Dark and Fast/Bright respectively; weather/lighting
choices and live weather timers persisted through a full server restart.
Overworld weather/type stayed unchanged. The server stopped cleanly and its
temporary world was deleted. Player book-authoring/visual checks remain
manual. No commit, push or publication is performed.
The local `1.0.1.70-beta.1` slice ports native Overworld/Nether/End sky styles.
The shared grammar accepts one style and the biome compiler ignores it by
category. Sky is saved in an optional Age-index map with Overworld fallback;
restore/reseed/retirement rollback use the saved choice. It does not alter
the missing-weather/lighting seed-completion rules, add random sky selection,
or enable Static Sky/color/individual-celestial pages prematurely.
The dimension-data generator now produces 11 sky/lighting variants from the
vanilla Overworld baseline. It changes visual skybox/fog/cloud settings only
in addition to lighting. Geometry, scale, ceiling, monster settings, default
clock and gameplay attributes are retained; no Nether gameplay or End dragon
fight is inherited. A derived native day timeline omits only sky/fog/cloud
color tracks for non-Overworld styles, preserving schedules and illumination.
Native End/no-sky rendering needs no optional renderer or new artwork. The
Legacy animated Static Sky renderer cannot be replaced by native flat gray
without losing its behavior, so its page remains red/unimplemented.
Bright detection includes all registered sky variants, preserving both white
ambient rendering and the effective brightness floor for surface/Cave Bright.
Data equality checks confirm all 11 variants preserve geometry/gameplay and
the day timeline keeps native tracks except the three excluded visual colors.
Its global time markers remain defined only by minecraft:day, not duplicated
in the derived timeline. The grouped timeline list is registered as a tag.
Client appearance and multiplayer travel validation remain manual.
Registry validation initially caught an invalid inline tag reference and
duplicate global clock markers in the derived day timeline; both were fixed
before handoff. The sky diagnostic also now supplies a position when sampling
the spatial fog attribute. A disposable dedicated server then restored all
sampled types after restart, including Nether normal/Bright and End normal/
Cave Bright, with the expected reddish/purple fog at the sample position,
unchanged geometry/gameplay flags, and effective light 15 on Bright variants.
Cave Bright End retained its type after unload/reload; a reseeded Nether Age
retained its style after restart. Servers stopped cleanly and temporary test
directories/worlds were deleted. These are server/data checks, not a rendered
client pass. No commit/push/publication is performed.
## Static Sky renderer slice
`1.0.1.71-beta.1` ports the 1.21.1 Legacy-style animated Static Sky, superseding
the previous unsupported/red status. The six independent 48×48 noise faces
use the existing default gray tint (146 per channel), noise range 65–224,
and a two-world-tick refresh. A cached 24-vertex cube and a single 288×48
dynamic texture use the native textured sky pipeline instead of rebuilding
thousands of colored cells every frame. No new asset or dependency is needed.
Client-only mixins carry the animation frame in extracted sky state, reset
it with the state, and own/close GPU resources with SkyRenderer. Static types
have native skybox NONE; the extracted state alone requests a sky pass and
the custom renderer replaces it. End flashes and End gameplay are not enabled.
Normal/Dark/Bright/Cave Bright variants preserve the existing lighting policy,
geometry and gameplay. Existing persistence handles STATIC as a sky enum value.
Random sky selection, color pages, and individual celestial bodies remain
separate unfinished work; this slice does not claim complete sky parity.
Verification: port build passed (no automated unit tests configured). A
disposable dedicated server created Static Standard Normal and Static Cave
Bright Ages, confirmed their gray fog, NONE native skybox and correct skylight/
ambient values, then restored both choices after restart. It stopped cleanly;
the temporary server/world was deleted. Client animation, travel between sky
styles, and restart appearance still require manual validation. No user world
was modified, and nothing was committed, pushed or published.
## Seeded sky completion slice
`1.0.1.72-beta.1` completes missing sky controllers using the first seeded
selection from 1.21.1 `AgeSkyProfile.random` (`0x534B5950524F464C` salt).
Inherited and Overworld currently map to the same native Overworld style;
the effective Overworld/Nether/End/Static weights are 2:1:1:1. This deliberately
ports only style selection, not random colors or celestial-body lists.
The player book path and `create_random` fixture now share one environment
record containing weather, lighting and sky. Explicit pages win. Separate
seed streams keep controllers independent, including when one is supplied
explicitly, and Cave weather remains Off. Existing Age index values/defaults
remain authoritative; this does not reroll old Ages or reseeded Age skies.
The one-purpose controlled fixtures keep their explicit Overworld defaults.
The environment probe checks repeatability and every explicit sky override
with and without explicit lighting/weather. Manual seeds 0–11 and -1 covered
all four sky choices, Cave and Void, without changing established weather/
lighting results; signed-long boundary seeds also passed. Disposable-server
fixtures created all four random styles plus Bright Static and an explicit
Overworld override. Static survived unload/reload; Nether retained its saved
style after reseeding to an End-selecting seed. All six choices survived a
server restart. The server stopped cleanly and its temporary world was deleted.
The port build passed (no unit-test sources are configured). Player rendering
still needs validation of new book creation and travel. No user save is changed
and nothing is committed or published.
## Shared colors and ambient-light target slice
`1.0.1.73-beta.1` adapts the 1.21.1 `AgeSkyProfile.ColorAccumulator` rules:
Number 0–24 followed by Red/Green/Blue adds ten times that amount to one
channel, White adds to all, Black subtracts from all, and final channels are
clamped. Unnumbered colors use the same seeded index-salt approach. Color
groups preceding Sky pages, or trailing the book, tint the sky. No colors
means native rendering; omitted channels within an authored group are zero.
The new port-only `lighting/ambient_color` target consumes the preceding group
as ambient color instead. It appears in the existing Lighting creative tab
and uses the shared procedural glyph/item compositor, not generated artwork.
Profiles are frozen in the Age index with native defaults, retained on reload/
reseed/retirement rollback, and included in the full catalog. The catalog
protocol is now 2; both peers need the matching beta. The client replaces its
profile map before catalog acknowledgment, invalidates the current attribute
cache and clears the map on disconnect. Age-only default-layer hooks apply
the native sky/fog/ambient RGB attributes dynamically; vanilla dimensions are
not changed. No runtime dimension registry mutation or RGB variant explosion
is involved. Client sky state carries tint separately from the live world:
Static cells use that tint, and the native End texture uses a color modulator.
Native celestial rendering is otherwise unchanged; authorable bodies remain
pending. Explicit ambient tints do not alter the effective Bright floor or
fabricate block/sky light; native lightmap tinting controls their appearance.
Numbers are supported only before shared colors in this slice. Celestial
numeric modifiers and variable Static resolution (#109) remain unsupported.
Random sky color completion and deprecated legacy fixed-color aliases remain
pending; this is not a claim of full celestial/color parity. Rendering needs
manual checks of authored groups, all sky types, ambient color, no leakage on
travel/reconnect, and persistence. Dedicated-server fixtures exercise known
RGB results, zero/upper amounts, clamping, seeded repeatability, rejection of
dangling numbers, and a catalog encode/decode round trip. All four colored
styles and an uncolored native fixture restored correctly after restart with
the expected sampled RGB attributes. The server stopped cleanly and its
temporary world was deleted. Port build/static checks passed; there are no
unit-test sources configured. Client rendering and live multiplayer sync
remain manual checks. No commit, push, or publication was requested.
## Authored native day-cycle slice
`1.0.1.74-beta.1` ports Normal Time with Full/Half/Double/Zero Length modifiers,
using the fixed values from the 1.21.1 symbol definitions. Number pages before
Length apply the existing `CelestialPageValues` rule (`number × 0.08` days).
Zero chooses a seeded time once and saves it. Missing Normal Time retains the
native cycle. The strict interim grammar requires at most one Length preceding
one Normal Time; broader ordered recovery and multiple celestial bodies remain
separate work. Number targets other than colors and lengths are still unported.
The Age index persists a native-default day-cycle profile. Lifecycle load,
reseed, retirement/rollback and catalog sync include it. Catalog protocol 3
carries the new profile map; clients replace it before acknowledgment, clear it
on logout and invalidate current attributes/sky brightness. Both peers need
the same beta build. A small Age-only Builder mixin preserves native dynamic
timeline ordering and weather layers, but supplies a read-only wrapped clock
manager for this Age's timeline samples. Its clock instances are cached per
definition, read profiles lazily (including during level construction), and
delegate non-Overworld clocks untouched. No global clock mutation occurs.
Native day shading, sun/moon tracks, moon phase, and gameplay attributes in
those timelines share the scaled sample. Zero has a constant integer sample,
rate zero, and partial tick zero, preventing the prior stopped-time bounce.
This does not accelerate blocks/entities, change the native `/time query`
result, make a Nether/End/Static sky show bodies, or port custom bodies. Global
time commands still affect moving samples. Authored RGB overrides and existing
Bright/Dark behavior remain independent. Separate Sun/Moon, angle/phase/size/
direction, eclipses, and randomized celestial completion are still pending.
Fixtures validate parser eligibility, known half/double/zero clock values,
Number overrides, deterministic stopped time, and the shared catalog codec.
At shared native time 18022 the sampled Full time was 18022 (sky light 4),
Half 36044 (sky light ~13.92), Double 9011 (sky light 15), and Zero stayed at
11611 with its unchanged sun angle. An unmodified Age matched Full exactly.
Zero survived unload/reload and reseeding without rerolling; the color fixture
retained its RGB values. All authored profiles, numeric endpoints 1/24, zero
override, native fallback, and the color fixture restored after server restart.
The final port build passed (no unit-test sources configured). Test servers
stopped cleanly; their temporary world was deleted. Visual/client reconnect
checks remain manual. No commit/push/publication is performed without an
explicit request.
## Native cycle phase and direction slice
`1.0.1.75-beta.1` extends the existing day-cycle profile with a saved native
clock origin, phase offset and direction. Phase conversion is adapted directly
from 1.21.1 `AgeDimensionService.applyInitialCelestialPhase`: the symbolic
angle is measured from Nadir, then shifted 18000 native ticks. Rising/Zenith/
Setting/Nadir give 0/6000/12000/18000, and preceding Number pages override a
Phase using the `CelestialPageValues` 15-degree rule (24 wraps to 0). Length
and phase remain independent. Explicit phase starts at Age creation, not at
the current Overworld position divided by Length.
Moving samples are `direction * (nativeTicks - origin) / period + phase`;
Counterclockwise reverses the native day timelines, including moon-phase and
gameplay tracks. Stopped samples remain constant and prefer written phase,
otherwise the previous saved seeded stopping time. Existing profiles without
the new optional fields retain their former behavior. Saved/profile lifecycle
and catalog synchronization already carry the extended codec. Catalog protocol
4 requires matching client/server beta versions. No registry types, textures,
or global clock mutations are added. Angle/size/per-body color and multiple
celestial rendering remain pending, as do eclipses and random celestial
completion. #110's Time/Normal Length naming changes remain separate work.
The interim grammar accepts at most one Length, Phase and Direction preceding
one Normal Time; numbers currently target Color/Length/Phase only. Fixtures
check all phase mappings at a shared origin, forward/reverse deltas, stopped
behavior, parsing of the earlier three-field profile, and full catalog codec
round trips. At native tick 1518, profiles sharing origin 496 and noon offset
sampled 7022 forward and 4978 backward, with sun angles ~10.756° and ~349.244°.
Zero Zenith stayed at 6000 / 0° / sky light 15; Zero Nadir stayed at 18000 /
180° / sky light 4. Number 6 yielded sunrise; Number 24 yielded midnight.
The reverse profile survived unload/reload; reseeding retained frozen midnight.
All sampled profiles restored after a server restart, retaining their origins,
direction and fixed phases; the old-style seeded Zero fixture also retained
its saved stopping time. Build checks passed (no unit-test sources configured).
The test server stopped cleanly and its temporary world was deleted. Rendered
motion and reconnect behavior remain manual. No commit/push/publication is
performed without explicit direction.
## Native orbital bearing slice
`1.0.1.76-beta.1` ports the four Angle pages and the existing 1.21.1
`CelestialPageValues` number rule (15° per number, modulo 360). North/East/
South/West map to 0/90/180/270. The interim grammar accepts one Angle before
Normal Time, independent of Length, Phase and Direction. Missing Angle keeps
the native orientation rather than enabling random-bearing completion.
The cycle codec adds an optional bearing defaulting to -1 (native). Existing
origin/phase/direction defaults and lifecycle persistence remain unchanged.
Catalog protocol 5 carries the extended profile; older peers must update.
Extracted sky state carries/resets bearing. Native SkyRenderer's Y rotation
for the shared sun/moon/star path uses the explicit value; the sunrise/sunset
pose receives the corresponding delta from the native -90° body bearing, with
balanced pose-stack restoration. No live-world lookup occurs during drawing.
Colors, native geometry/textures, phase timing, illumination, and direction
are retained. Angle changes only horizontal orientation, not altitude. Body
counts/sizes/colors and eclipses remain pending; no extra rendering backend,
asset or registry variant is introduced. #110 is deliberately not implemented
as part of this port slice.
The port build passes (no unit-test sources configured), and the targeted
PoseStack invocation signature was checked against the patched native class.
Disposable-server fixtures exercise cardinal values, all 0–24 Number values,
six-field profile fallback, and the complete catalog stream codec. All four
frozen-sunrise fixtures sampled the same time 0 and sky light ~14.19; only
their saved bearings differ. Number 3 gave 45°, Number 24 gave 0°. Unwritten
bearing retained -1. East survived unload/reload; the 45° profile retained its
bearing after reseed. Client rendering/glow alignment and travel isolation
still require manual validation; server probes cannot prove the drawn result.
All six explicit bearings and the native fallback restored correctly after
restart. The test server stopped cleanly and its temporary world was deleted.
No commit/push/publication is performed without an explicit request.
## Celestial-compatible missing-Sky fallback
`1.0.1.77-beta.1` restricts the missing-Sky fallback for celestial writing to
the current renderer's compatible style, Overworld. Normal Time no longer
inherits an unsuitable seeded Nether/End/Static background. The resolver
also recognizes Sun/Moon intentions for future support without enabling
those pages prematurely. Explicit Sky pages always win; no-celestial random
selection, terrain, and independent weather/lighting streams are unchanged.
No packet or saved-profile format changes are needed. Existing Ages retain
their saved style; tests of the fix must create new Ages.
1.21.1 comparison: `AgeSkyProfile.compile` starts with inherited sky style;
the bundled Age profiles all use the Overworld dimension type. Authored Sun/
Moon selects `AgeEnvironmentCompiler.CUSTOM_SKY_EFFECTS`, whose renderer draws
the body list after its background without a sky-style exclusion. Thus the
bundled 1.21.1 paths already avoid this visibility regression, and no follow-up
issue or root-code change is needed for them. This is not a claim about every
possible third-party datapack profile.
The environment diagnostic checks missing/explicit Sky for Normal Time, Sun,
and Moon, including both explicit-page orders, and verifies other environment
choices are unchanged. The day-cycle fixture now supplies no Sky page and
uses the same environment resolver as player books. Client creation/travel
checks remain manual. Build and disposable-server creation checks passed:
Normal Time without Sky selected native Overworld for seeds 0, 6 and 8,
while the same seeds without celestials retained Nether, Static and End.
Explicit-Sky precedence and independent environment choices passed the
diagnostic checks, including Cave terrain. The server stopped cleanly and
its temporary world was deleted. No commit/push/publication is performed
without an explicit request.
## Normal Time sun size
`1.0.1.78-beta.1` ports Small/Normal/Large Celestial Size modifiers to the
native Normal Time sun. Presets are 0.5×/1×/2×. An immediately preceding Number
uses the root compiler's `0.25 + number * (2.75 / 24)` rule, covering 0.25×
through 3×. Root Normal Time creates a size-modified sun and a 1× moon; this
slice preserves that distinction rather than resizing both. Standalone Sun/
Moon controllers, multiple bodies, per-body colors and eclipses remain pending.
No random size completion is added yet.
`PortDayCycle` persists optional `sun_size`, defaults old profiles to 1×,
validates the supported range and transmits it in catalog protocol 6. Size
is extracted into the sky render state and multiplies only the native sun
transform. Native texture/glow and rendering pipeline remain; no new assets
or buffers are allocated. State resets/defaults prevent size leaking to other
dimensions. Clock sampling, illumination and orbit bearing are unaffected.
The `create_size` op fixture freezes the sun at Zenith with Zero Length and
omits Sky to exercise compatible completion. Diagnostic checks cover all
25 numeric sizes, presets, older profile defaults, unchanged timing and
catalog round trips. Disposable dedicated-server checks produced sizes
0.5/1/2/0.25/1.625/3 with the same noon angle and sky light; reload, reseed
and full restart retained them. Build passed. Visual size/glow alignment,
unchanged moon, and travel isolation still require the user's client check.
The server stopped cleanly and its temporary world was deleted. Nothing is
committed, pushed or published without an explicit request.
## Verification of this first slice
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
+390 -1
View File
@@ -5,7 +5,7 @@ with the Minecraft 1.21.1 release. The 26.3 backend persists separate Ages and
their seeds. Books, the Book Binder, and the Ink Mixer work, and the Binder can
author Standard, Amplified, Island, Flat, Void, or Cave terrain with supported
biome-distribution pages, selected material/vegetation/structure features, and
independent Age weather. Celestial/lighting systems, instability, crystal
independent Age weather and lighting. Celestial systems, instability, crystal
portals, and much of the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not
substitute this JAR for the 1.21.1 mod or import 1.21.1 saves.
@@ -558,6 +558,395 @@ Operator-only `/rivencraft_port age surface_probe <name> <x> <z>` reports the
actual highest obstruction and a safe nearby surface without creating a
platform or teleporting. It can generate chunks while inspecting the region.
The local `1.0.1.66-beta.1` slice ports Normal, Dark, and Bright Lighting.
One lighting page is accepted per book and its choice is saved in the Age
index before catalog publication. Missing lighting currently defaults to
Normal; random lighting selection and Bright's instability cost await those
systems' later ports. Lighting pages should no longer be tinted red.
Normal surface Ages retain the Overworld type. Dark removes skylight with
ambient light 0.0; Bright uses ambient light 0.25 and retains surface skylight,
matching 1.21.1. **Cave + Bright is supported:** it has ambient light 0.25
without skylight. Cave Normal/Dark use no skylight and ambient light 0.0.
Placed light sources continue working; ambient light is not a placed light
level or a guarantee that monsters cannot spawn.
The build derives three registered dimension-type variants from the current
vanilla Overworld JSON, changing only has_skylight and ambient_light. This
preserves native attributes/timelines and synchronizes types through the
normal login registry exchange; no dynamic client registry mutation is used.
These variants currently use the vanilla type, not custom runtime changes to
the host Overworld dimension type made by another data pack.
Saved lighting survives reload, reseed, restart and retirement rollback.
Existing non-Cave Ages without a choice remain Normal; old Cave Ages now use
the no-skylight baseline. Use new Ages for clear visual comparisons and avoid
confusing previously lit chunks with the current dimension settings.
Manual check: create Normal/Dark/Bright surface books and Cave + Bright;
compare an unlit enclosed room, check placed torches, revisit/restart and
confirm the Overworld stays unchanged. Operator diagnostics:
`/rivencraft_port age create_lighting <name> <seed> <mode> <terrain>` and
`/rivencraft_port age lighting_probe <name>` report the registered type,
skylight and ambient light. Test JAR: `1.0.1.66-beta.1`.
The local `1.0.1.67-beta.1` correction supersedes slice 66's Bright rendering:
26.3's lightmap uses `minecraft:visual/ambient_light_color`, not the old
dimension `ambient_light` field. Bright surface/Cave types now set that native
attribute to white and ambient_light to 1.0. Cave still has no skylight.
Bright Ages reject natural/chunk-generation hostile spawns independently of
stored light levels. Existing mobs, passive mobs, spawners, eggs, commands
and special-event spawns are not deleted or globally disabled. Actual block
light propagation and plant light requirements are unchanged. Existing Bright
Ages receive the corrected registered types after restart; no new Age needed.
Planned color-page target: **ambient light color**, alongside sky/celestial
colors. Reuse the native 26.3 attribute for authored tints; Bright defaults to
neutral white until the color-page port. This does not enable color pages yet.
The local `1.0.1.68-beta.1` replaces slice 67's special spawn veto with actual
effective ambient brightness. The spawn event handler is removed. Bright's
combined light-engine reading now has a minimum of 15 on server and client,
including after sky darkening. Its white rendering attribute is retained.
Vanilla darkness checks therefore reject light-sensitive monsters naturally;
light-ignoring mobs, spawners and other spawn sources use their native rules,
not Rivencraft exemptions or bans. This does not remove existing mobs.
Ambient light is not stored as emitted block light or fabricated skylight.
Consumers of combined brightness, including plant light checks, now see the
ambient contribution. Normal/Dark and non-Rivencraft dimensions are unchanged.
Cave Bright remains without skylight. Existing Bright Ages update on restart.
This supersedes slice 67's statement that crop light checks are unchanged.
The local `1.0.1.69-beta.1` slice seed-completes missing weather and lighting
pages for newly bound books, including Link-Panel-only random Ages. Explicit
pages take precedence. Cave weather remains Off, while Cave lighting can
still choose Bright. Old Ages retain their saved choices; they are not rerolled.
The supported pool uses the 1.21.1 Legacy-rank rule: Normal is rank 1, other
choices rank 2. Lighting weights are 3:1:1 for Normal/Dark/Bright; weather
weights are 6:1:1:1:1:1 for Normal/Fast/Slow/Off/Always/Storm. The common weight
is derived from the supported pool size. Unported Cloudy/Rain/Snow modes are
not selected. Independent seed salts keep a missing lighting roll unchanged
when weather is explicitly written, and vice versa. This matches the rank
policy, not the exact 1.21.1 seed stream/pool. No random feature list, sky or
celestial choices are added by this slice.
`/rivencraft_port age create_random <name> <seed>` uses the same environment
resolver and logs its frozen plan. `/rivencraft_port age environment_probe
<seed> <terrain>` inspects deterministic defaults and explicit Storm/Bright
precedence without creating an Age. Other explicit operator fixtures keep
their Normal defaults to isolate the feature under test.
For manual checking, create several Link-Panel-only books and verify weather/
lighting varies across Ages and persists on revisit/restart. Then author an
explicit Normal Weather + Normal Lighting book, and a Cave + Bright book.
Include explicit Normal pages when comparing terrain without random lighting
or weather changes. Random blocklist policy and instability remain pending.
The local `1.0.1.70-beta.1` slice ports the Overworld, Nether, and End Sky
pages. One sky style is accepted per book and persisted independently of
terrain, weather and lighting. Missing sky pages still use Overworld; random
sky selection is not part of this slice. Static Sky remains unsupported/red
until its Legacy animated renderer is ported, rather than being replaced by
a flat gray backdrop. Color and individual celestial pages are also pending.
Nether Sky uses the native no-sky backdrop and reddish fog; End Sky uses the
native End backdrop. Neither changes dimension height, coordinate scale,
terrain, Nether water/bed rules, or enables an End dragon fight. All lighting
choices work with these styles, including Cave + Bright. Bright's render
attribute and effective ambient level 15 are retained on each variant.
The registered variants derive the vanilla Overworld type and preserve its
gameplay attributes. Non-Overworld skies use a derived day timeline without
the three sky/fog/cloud tint tracks, retaining native gameplay schedules and
illumination while avoiding Overworld day tint on those backdrops. Their
cloud color is transparent. Biome/weather fog modifiers still apply. Types
sync through normal registry login; no new rendering dependency is required.
Sky choices survive reload/reseed/restart and retirement rollback. Old Ages
without a sky choice retain Overworld style.
For visual checks, author Plains + Normal Lighting + No Weather with each
sky page; compare the sky/fog and verify the same terrain and normal bed/water
behavior. Then try Cave + Bright + End Sky and revisit after restarting.
Static Sky should remain red. Operator fixtures:
`/rivencraft_port age create_sky <name> <seed> <sky> <lighting> <terrain>` and
`/rivencraft_port age sky_probe <name>`. Use new books to author a sky choice;
this does not rewrite the sky choice of already-bound books.
The local `1.0.1.71-beta.1` slice adds the previously pending Static Sky.
Its client renderer adapts the 1.21.1 Legacy-style background: six independent
48×48 noise faces, default gray tint `#929292`, and noise intensity 65–224.
The noise updates every two world ticks. A single small dynamic texture and
cached cube replace thousands of per-cell draw vertices; resources are closed
with the sky renderer. No artwork or rendering dependency is added.
Static Sky is now supported rather than red, and persists through the same
Age index as other sky styles. Four registered lighting variants retain the
existing terrain/gameplay rules. Their native skybox is NONE (no End flashes);
only the extracted client state schedules the custom background sky pass.
Animation state resets between frames and does not consult a different live
world while rendering. Color pages, celestial pages, and random sky selection
remain pending.
Build and disposable dedicated-server creation/restart checks passed for
Static + Normal + Standard and Static + Bright + Cave. These do not validate
client rendering. For manual testing, bind a new Static Sky + Normal Lighting
+ No Weather book, look around/above/below for animated gray static, then
visit an Overworld/End Sky Age and return to check there is no sky-state leak.
Revisit after restarting. Also try Static Sky + Cave + Bright (look outside
terrain or in spectator mode). The fixture accepts `static` as its sky value.
The dedicated test server was stopped and its temporary world deleted.
Changes remain local; no commit/push/publication was requested.
The local `1.0.1.72-beta.1` slice completes seeded default sky selection for
new Ages. Missing sky pages now choose Overworld, Nether, End, or Static
instead of always Overworld. This applies both to Link-Panel-only books and
authored books without a sky controller. An explicit sky page always wins.
The selection uses the first random draw and seed salt from 1.21.1's
`AgeSkyProfile.random`. Its equally likely Inherited and Overworld slots both
use the native Overworld presentation in this port, giving effective weights
2:1:1:1. Sky uses an independent random stream, so adding sky/weather/lighting
controllers does not reroll the other choices, terrain, or biomes. Colors,
individual celestial bodies, and random feature completion remain pending.
The chosen sky is saved at Age creation and is not recalculated on travel,
reload, restart, or reseed. Existing Ages (including ones with no saved sky
entry) keep their previous presentation. Explicit diagnostic fixtures retain
their controlled defaults; `create_random` uses the same complete environment
resolver as player books. `environment_probe` now also checks every explicit
sky override and cross-controller independence.
For manual checking, create several new Link-Panel-only books and compare
their sky styles. Also author a terrain/biome book without a sky page, and one
with an explicit Overworld Sky page. Revisit/restart to check persistence.
For repeatable operator fixtures, `create_random <name> 0` selects Nether,
seed 2 selects Overworld, seed 6 Static, and seed 8 End. These seeds only test
the current port selection, not complete 1.21.1 world-generation equivalence.
No commit/push/publication is performed until explicitly requested.
The local `1.0.1.73-beta.1` slice ports shared Red/Green/Blue/White/Black
colors and their Number modifiers using the 1.21.1 additive mixing rules.
Numbers 0–24 directly before a color contribute `number × 10`; White adds to
all three channels and Black subtracts from them. Missing channels begin at
zero, groups can be written in any channel order, and the completed result is
clamped to 0–255 per channel. A color without a number gets a deterministic
seeded amount. Numbers currently require a following color: celestial numeric
targets and Static Sky resolution (#109) are not implemented by this slice.
Color groups preceding a Sky page tint that sky; a trailing color group also
tints the sky, matching the existing 1.21.1 behavior. The new **Ambient Light
Color** page (Lighting creative category) consumes the preceding color group
for ambient illumination instead. Author Bright separately when full effective
light level 15 is wanted; this color target does not change stored block light
or the selected lighting controller. A target without a color group leaves
that color native. For example:
`Number 24 → Blue → Overworld Sky → Number 24 → White → Ambient Light Color → Bright`
This gives a blue sky/fog with white ambient illumination. Do not use an
unmodified Blue page when an exact amount is desired: its amount is seeded.
Explicit colors are saved once; they survive reload/reseed/restart. Existing
Ages without a color profile remain native. There are no random color groups
for books that do not contain color pages yet.
Sky colors override the native sky/fog color attributes, tint Static Sky's
animated cells, and multiply the native End background texture. End texture
detail/brightness remains native; a tint does not make it a flat solid color.
Ambient color uses 26.3's native ambient-light attribute. Both profiles sync in
the Age catalog before travel, not through unbounded RGB dimension types.
Client state is replaced with each catalog and cleared on disconnect. Use the
same beta JAR on server and client: the catalog protocol has changed.
Build and dedicated-server checks cover compiled color groups, native fallback,
all four sky types, catalog encoding/decoding, reload and reseed. Client color
rendering and page availability still need manual checking. Test the example
above, a tinted Static/End Sky, return to an uncolored Age/Overworld, and revisit
after restarting. `create_colors <name> <seed> <sky>` creates a repeatable Flat,
Bright fixture with sky `#643296` and ambient `#6496c8`; `color_probe <name>`
checks saved colors against sampled attributes and round-trips the catalog.
No commit/push/publication is performed until explicitly requested.
The local `1.0.1.74-beta.1` slice adds authored day-cycle lengths through the
existing **Normal Time** page. Put one **Half Length**, **Full Length**,
**Double Length**, or **Zero Length** before Normal Time. These use the
1.21.1 period values 0.5, 1, 2, and 0 normal days respectively. Half runs twice
as fast; Double runs half as fast. A Number immediately before a Length page
overrides it with `number × 0.08` days (0–24), including Number 0 for stopped
time. Normal Time without a Length uses one normal day.
Zero Length freezes the native day timelines at a deterministic, saved random
time. It does not snap back to noon on reload/reseed, and contributes no
fractional clock interpolation. A book with no Normal Time page retains the
existing native cycle; missing periods are not randomly completed yet.
Until the full grammar port, a Length requires one following Normal Time page;
repeated Length/Normal Time pages are unsupported.
The implementation wraps only this Age's native Overworld-clock timeline
sampling, on server and client. It does not change the global clock or tick
rate. Native sun/moon angles, sky/fog/day shading, moon phase, and gameplay
attributes supplied by those timelines use the sampled time. Weather layers
and authored colors remain intact. Explicit RGB sky/fog overrides remain
constant rather than acquiring day tint. Global `/time` changes still shift
moving Ages' underlying clock; Zero remains fixed. `/time query` reports the
global clock, not the scaled sample; use `cycle_probe` to inspect the latter.
This controls the existing native pair only. Separate Sun/Moon pages, custom
angles/phases/directions/sizes, per-body colors, eclipses, and random celestial
completion remain pending. Normal Time does not add visible bodies to End,
Nether or Static skies; include Overworld Sky for the visual cycle test.
Client/server must use the same beta: the catalog protocol is now 3.
For manual testing, bind **Link Panel → Half Length → Normal Time → Overworld
Sky** (optionally add Normal Lighting and No Weather), compare with Full and
Double, then try Zero Length and check that the sky stays still without
bouncing. Revisit after restarting. For numeric authoring, put Number 12 before
Full Length for a 0.96-day cycle. Existing Ages are not rewritten. Operator
fixtures: `create_cycle <name> <seed> <half|full|double|zero> [number]` and
`cycle_probe <name>` under `/rivencraft_port age`.
Port build and disposable-server probes passed for all four lengths and
numeric overrides, native fallback, real sun-angle/light differences, catalog
round trips, unload/reload, and reseed preservation. Rendering/travel/reconnect
checks still require manual validation. Changes remain local until explicitly
requested for commit/push/publication.
The local `1.0.1.75-beta.1` slice adds **Rising**, **Zenith**, **Setting**, and
**Nadir** Phase pages and **Clockwise/Counterclockwise** Direction pages to the
native Normal Time cycle. Put these before Normal Time, alongside an optional
Length. This does not implement separate Sun/Moon bodies or rename Normal
Time/Full Length; the naming cleanup is tracked separately in #110.
The phase conversion matches 1.21.1's `applyInitialCelestialPhase`: written
angles start at midnight, while native day time zero is sunrise. Rising,
Zenith, Setting, and Nadir select native times 0, 6000, 12000, and 18000.
A Number immediately before a Phase overrides it with `number × 15°` from
midnight; Number 6 means sunrise, 12 noon, 18 sunset, and 0 or 24 midnight.
These are native day-time positions, not uneased geometric sun angles.
Written phase is anchored when the Age is created, independently of the
current Overworld time and Length. Moving cycles then progress from that
saved point. Counterclockwise runs the cycle backward; Clockwise is the
default. This reverses the Age's day timelines, including their moon-phase
and gameplay attributes, not just a visual disc. The global clock is untouched.
As before, global `/time` changes can shift moving samples.
With Zero Length, written Phase replaces the seeded random stopping time:
Zero + Zenith stays at noon, Zero + Nadir at midnight. Direction has no motion
to reverse in a stopped cycle. Without a Phase, old behavior and saved Ages
are preserved. Origins, phase offsets, and direction survive reload, restart,
and reseed. Old profile fields default to origin/offset zero and forward
motion. The catalog protocol is now 4; server and client need the same beta.
Interim grammar permits at most one each of Length, Phase and Direction before
one Normal Time target. Angle/bearing, size, multiple bodies and eclipses remain
unsupported. No random phase/direction completion was added. Example visual
checks (include Link Panel, Overworld Sky, Normal Lighting and No Weather):
- `Zero Length → Zenith → Normal Time`: frozen noon, including after restart.
- `Zero Length → Nadir → Normal Time`: frozen midnight.
- `Half Length → Zenith → Counterclockwise → Normal Time`: starts at noon and
travels backward, twice as fast. Compare with Clockwise.
- `Zero Length → Number 6 → Nadir → Normal Time`: Number overrides Nadir to sunrise.
Operator fixture: `/rivencraft_port age create_phase <name> <seed> <length>
<rising|zenith|setting|nadir> <clockwise|counterclockwise> [phaseNumber]`.
`cycle_probe` reports both the saved origin and current native/scaled samples.
Build and disposable-server checks cover named/numeric phases, opposite motion,
fixed day/night light levels, older profile defaults, catalog round trips,
reload and reseed. Client rendering/reconnect checks remain manual. Changes
remain local until explicitly requested for commit/push/publication.
The local `1.0.1.76-beta.1` slice ports **North/East/South/West Angle** pages
for the native Normal Time cycle. Their bearings match the 1.21.1 symbol
values: 0°, 90°, 180°, and 270°. Put at most one Angle before Normal Time.
A Number immediately before it overrides the bearing with `number × 15°`,
wrapping at 360°: Number 3 is 45°, Number 12 is 180°, and Number 24 returns
to 0°. Angle is the orbit's horizontal bearing, distinct from Phase (position
along that orbit) and Direction (which way time progresses).
The client uses the extracted saved bearing to rotate the native sun/moon
path, star field, and sunrise/sunset glow together. The native buffers,
textures, phase, and lighting tracks are retained. Bearing does not change
altitude, day length, light levels, or clock direction. Without an Angle page,
the existing native orientation is preserved; old profiles use the same
fallback. No seeded random bearing completion was added. This does not add
bodies to Nether/End/Static skies or port custom Sun/Moon counts, sizes,
per-body colors, or eclipses. #110 naming work remains separate.
Bearing persists in the existing cycle profile and catalog; the catalog
protocol is now 5, requiring the same beta on server and client. Extracted
bearing resets with sky state and is replaced at each render, preventing an
Age's orientation from leaking into the next world. Sunrise/sunset glow is
rotated relative to the native -90° body-path orientation, not around a
different axis. No new textures or dimension variants are required.
Visual check: bind **Link Panel → Zero Length → Rising Phase → North Angle →
Normal Time → Overworld Sky** (Normal Lighting/No Weather help comparison).
Compare with East/South/West Angle, then put Number 3 before North Angle for
a 45° intermediate bearing. Use Rising/Setting rather than Zenith when
checking bearing: a body directly overhead stays overhead under a horizontal
rotation. Check the glow stays aligned, then travel to an unmodified Age or
Overworld and revisit after restart.
Operator fixture: `/rivencraft_port age create_angle <name> <seed>
<north|east|south|west> [number]` creates a frozen sunrise for comparison.
The fixture validates all 25 numeric overrides, cardinal values, timing
independence, older profile defaults and catalog codec round trips. Dedicated
checks cover persistence and unchanged sampled day lighting; actual rendered
orientation remains a manual check. No commit/push/publication is performed
until explicitly requested.
The local `1.0.1.77-beta.1` slice fixes automatic sky selection for authored
celestials. **Normal Time without a Sky page now selects Overworld Sky**,
the current port's celestial-capable style, instead of sometimes selecting
Nether/End/Static and hiding the native bodies. Explicit Sky pages retain
precedence. Books without celestial pages retain the existing seeded sky
distribution, and lighting/weather choices are unchanged. The resolver also
recognizes Sun/Moon page intentions for their eventual port; this does not
make those still-unported body pages bindable.
This matches the visible outcome of the current bundled 1.21.1 Age path:
its native fallback is Overworld, and its custom sky renderer draws authored
suns/moons over its backgrounds. No 1.21.1 change or follow-up issue is needed
for those bundled paths. The fix applies at creation of new 26.3 Ages; it
does not rewrite existing saved sky choices. Recreate an older affected book
to test the corrected automatic choice. Weather and terrain can still obscure
celestials naturally; this is a compatible-sky rule, not a guarantee of a clear
view from underground or during storms.
Test **Link Panel → Normal Time**, optionally with No Weather and Normal
Lighting, but without any Sky page. The celestial view should be present.
Also test Length/Phase/Angle/Direction + Normal Time without a Sky page and
confirm that an explicit Sky page still wins. `create_cycle` now exercises
this missing-Sky path rather than forcing an explicit sky in its fixture.
`environment_probe` verifies all three celestial intentions, explicit Sky
precedence in either order, and unchanged lighting/weather selection.
No commit/push/publication is performed until explicitly requested.
The local `1.0.1.78-beta.1` slice ports **Small/Normal/Large Celestial**
Size modifiers for Normal Time: 0.5×, 1× and 2× respectively. A Number page
immediately before Size overrides that preset with the existing 1.21.1
0.25×–3× range (Number 0 = 0.25×, 12 = 1.625×, 24 = 3×).
As in 1.21.1, Size before Normal Time sizes its sun; the paired moon stays
normal-sized. Standalone Sun/Moon and independent body sizing remain unported.
Size changes neither day length nor illumination, phase or bearing. Older
profiles default to 1×; new profiles retain size through reload/restart/reseed.
The catalog protocol is now 6, so clients and servers must use matching builds.
Rendering retains the native sun texture/pipeline without new GPU buffers.
Test **Link Panel → Zero Length → Zenith Phase → Large Celestial → Normal
Time**, optionally with No Weather and Normal Lighting. Compare Small and
Normal, then Number 0/12/24 immediately before Size. No Sky page is needed.
The sun should change size, while the moon and Overworld remain unchanged.
Reopen the world to check persistence. `create_size <name> <seed>
<small|normal|large> [number]` creates a frozen-noon fixture without explicit
Sky; `cycle_probe <name>` reports its saved size and catalog round trip.
Build and disposable-server checks passed, including all numeric sizes,
reload, reseed, restart and unchanged sampled lighting/time. Actual rendered
size remains a manual check. The test server stopped and its world was deleted.
No commit/push/publication is performed until explicitly requested.
From the repository root:
```sh
@@ -0,0 +1,59 @@
# Rivencraft 26.3 beta 1.0.1.71-beta.1
This beta collects the player-tested lighting and sky port slices since
1.0.1.65-beta.1. It is not yet feature-complete with Minecraft 1.21.1.
## Lighting and seeded environments
- Added Normal, Dark, and Bright lighting pages with saved per-Age settings.
- Bright works in Cave terrain too, without adding skylight or weather.
- Bright supplies effective ambient light level 15 to native combined-light
checks and the white ambient rendering attribute. Vanilla darkness-based
monster checks respond to that lighting; there is no separate mob-spawn
veto. Mobs that ignore light retain their normal rules.
- Missing weather and lighting controllers now resolve deterministically from
the Age seed, using independent choices and Legacy-style Normal weighting.
Explicit pages win; Cave terrain still forces weather Off. Existing Ages
keep their saved choices rather than rerolling on load.
## Sky styles
- Added Overworld, Nether, End, and animated Static Sky pages, independently
of terrain, weather, and lighting. Choices persist on reload and restart.
- Nether Sky uses a reddish fog backdrop; End Sky uses the native End sky.
These are visual styles, not Nether/End terrain or gameplay conversions:
dimension geometry, coordinate scale, bed/water rules, and dragon behavior
are not changed.
- Static Sky adapts the existing Legacy-style renderer: six independent
48×48 animated gray noise faces. A cached cube and small dynamic texture
avoid rebuilding thousands of per-cell vertices each frame. Client-only
rendering resources are released with the sky renderer.
- Non-Overworld backgrounds have transparent clouds and a derived day
timeline that avoids Overworld sky/fog/cloud color tracks while retaining
gameplay and illumination schedules.
- Updated supported-page feedback and added operator sky/environment probes.
## Current limits and testing
- Missing Sky pages still default to Overworld; random sky selection, color
pages and individual celestial bodies remain pending. Number-controlled
Static Sky resolution is tracked separately in issue #109 for both versions.
- The writing/discovery loop, instability, portals, and optional-integration
parity are still incomplete. Creative availability is not a support claim;
check page tooltips and the beta guide for supported combinations.
- Previously generated chunks are not regenerated. Save compatibility during
the beta is not guaranteed; keep backups and keep both version tracks apart.
The player reported in-game passes for the included slices, including Static
Sky. Build checks passed for both version tracks. Targeted disposable-server
checks covered environment defaults, explicit choices, registered sky/lighting
variants, persistence and restart, including Cave + Bright and Static Sky.
This is not a claim of complete dedicated-server or optional-mod coverage.
Test servers were stopped and temporary worlds removed.
Requires Minecraft **26.3**, NeoForge **26.3.0.99** and **Java 25**. Dynamic
Dimensions and Bad Packets are not required by this build. The 1.21.1 release
and its dependencies are unchanged.
Report issues and help improve the wiki at
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
@@ -0,0 +1,53 @@
# Rivencraft 26.3 beta 1.0.1.78-beta.1
Collects the player-validated port slices since 1.0.1.71-beta.1. This beta is
not yet feature-complete with the Minecraft 1.21.1 version.
## Sky and color writing
- Missing Sky pages now select a deterministic seeded style. Books with
Normal Time and no explicit Sky select celestial-compatible Overworld Sky.
Explicit Sky pages always win; weather and lighting choices stay independent.
- Ported Red, Green, Blue, Black and White color pages. Preceding Number pages
control component strength; unnumbered colors use seeded values. Colors mix
before clamping the completed group. Colors before Sky tint that sky; a
trailing color group also targets the sky.
- Added an Ambient Color controller for an independent ambient-light tint.
Color alone does not change effective illumination or monster-spawn rules.
- Native, End and animated Static backgrounds use saved Age color settings.
## Normal Time celestial controls
- Length pages control the native day cycle; a preceding Number fine-tunes
length. Zero Length freezes the cycle at a seeded time or an authored Phase,
without interpolation bounce or changing the shared Overworld clock.
- Rising, Zenith, Setting and Nadir Phase pages choose the starting phase.
Clockwise and Counterclockwise control direction. North, East, South and
West Angle pages choose orbit bearing. Preceding Number pages fine-tune
Phase and Angle in 15-degree increments.
- Small, Normal and Large Celestial size Normal Time's sun to 0.5×, 1× and 2×.
A preceding Number overrides size from 0.25× to 3×. As in 1.21.1, the paired
moon remains normal-sized. Size does not alter illumination or day length.
- Settings persist through reload/restart and remain stable when reseeding
future terrain. Older profiles retain native/default values.
- Added operator diagnostics for environment, color and celestial profiles.
## Compatibility and testing
- Catalog protocol is now 6: use matching client/server builds.
- Player in-game checks passed for the included slices. Build checks and
targeted disposable-server diagnostics covered numeric modifiers, catalog
round trips, unchanged sampled lighting, reload, reseed and restart.
- Standalone Sun/Moon controllers, multiple bodies, per-body colors, eclipses,
full writing/discovery, instability, portals and optional-integration parity
remain incomplete. Creative availability does not imply full page support.
- Number-controlled Static Sky resolution remains tracked in issue #109;
time-page naming improvements remain tracked in issue #110.
- Existing chunks are not regenerated. Save compatibility is not guaranteed
during beta; keep backups and keep the two Minecraft version tracks apart.
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.
Report issues and help improve the wiki at
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
+65
View File
@@ -121,6 +121,71 @@ def generatePortLanguage = tasks.register('generatePortLanguage', GeneratePortLa
}
sourceSets.main.resources.srcDir generatePortLanguage
// Derive registered presentation variants without duplicating vanilla gameplay settings.
abstract class GeneratePortDimensionTypes extends DefaultTask {
@InputFile abstract RegularFileProperty getMinecraftJar()
@OutputDirectory abstract DirectoryProperty getOutputDir()
@TaskAction void generate() {
new java.util.zip.ZipFile(minecraftJar.get().asFile).withCloseable { archive ->
def entry = archive.getEntry('data/minecraft/dimension_type/overworld.json')
if (entry == null) throw new GradleException('Missing vanilla Overworld dimension type')
String template = archive.getInputStream(entry).withCloseable { it.getText('UTF-8') }
def dayEntry = archive.getEntry('data/minecraft/timeline/day.json')
if (dayEntry == null) throw new GradleException('Missing vanilla day timeline')
def day = archive.getInputStream(dayEntry).withCloseable {
new groovy.json.JsonSlurper().parseText(it.getText('UTF-8'))
}
// Keep gameplay schedules and illumination, but do not tint these
// native non-Overworld sky backgrounds blue/black with the day cycle.
['fog_color', 'sky_color', 'cloud_color'].each { day.tracks.remove("minecraft:visual/${it}".toString()) }
// Markers are global per clock and already owned by minecraft:day.
day.remove('time_markers')
File dayFile = new File(outputDir.get().asFile, 'data/rivencraft/timeline/age_sky_day.json')
dayFile.parentFile.mkdirs()
dayFile.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(day)) + '\n'
File timelineTag = new File(outputDir.get().asFile, 'data/rivencraft/tags/timeline/age_sky.json')
timelineTag.parentFile.mkdirs()
timelineTag.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson([
values: ['#minecraft:universal', 'rivencraft:age_sky_day',
'minecraft:moon', 'minecraft:early_game']])) + '\n'
['overworld', 'nether', 'end', 'static'].each { sky ->
['age_normal', 'age_dark', 'age_bright', 'age_cave_bright'].each { lighting ->
if (sky == 'overworld' && lighting == 'age_normal') return
String name = lighting + (sky == 'overworld' ? '' : "_sky_${sky}")
def definition = new groovy.json.JsonSlurper().parseText(template)
definition.has_skylight = lighting in ['age_normal', 'age_bright']
definition.ambient_light = lighting in ['age_bright', 'age_cave_bright'] ? 1.0 : 0.0
// 26.3's lightmap reads this attribute, not ambient_light.
if (definition.ambient_light == 1.0)
definition.attributes['minecraft:visual/ambient_light_color'] = '#ffffff'
if (sky != 'overworld') {
definition.skybox = sky == 'end' ? 'end' : 'none'
definition.attributes['minecraft:visual/fog_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#181318' : '#6b2725'
definition.attributes['minecraft:visual/sky_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#000000' : '#6b2725'
definition.attributes['minecraft:visual/cloud_color'] = '#00000000'
if (sky == 'nether') {
definition.attributes['minecraft:visual/fog_start_distance'] = 10.0
definition.attributes['minecraft:visual/fog_end_distance'] = 96.0
}
definition.timelines = '#rivencraft:age_sky'
}
File result = new File(outputDir.get().asFile, "data/rivencraft/dimension_type/${name}.json")
result.parentFile.mkdirs()
result.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(definition)) + '\n'
}
}
}
}
}
def generatePortDimensionTypes = tasks.register('generatePortDimensionTypes', GeneratePortDimensionTypes) {
dependsOn 'createMinecraftArtifacts'
minecraftJar.set(layout.buildDirectory.file("moddev/artifacts/minecraft-patched-${neo_version}.jar"))
outputDir.set(layout.buildDirectory.dir('generated/resources/dimensionTypes'))
}
sourceSets.main.resources.srcDir generatePortDimensionTypes
tasks.named('processResources', ProcessResources).configure {
// Reuse the existing sprite and base model without copying binary assets.
from('../../src/main/resources') {
+1 -1
View File
@@ -7,4 +7,4 @@ neo_version=26.3.0.99
mod_id=rivencraft
mod_name=Rivencraft
mod_license=LGPL-3.0-or-later
mod_version=1.0.1.65-beta.1
mod_version=1.0.1.78-beta.1
+33
View File
@@ -8,6 +8,39 @@
"item.rivencraft.page.unresolved": "Unknown Symbol Page",
"item.rivencraft.page.symbol.implemented": "Implemented in 26.3; book and terrain requirements still apply.",
"symbol.rivencraft.weather.normal.description": "Use an independent normal weather cycle. Cave terrain keeps weather off.",
"symbol.rivencraft.sky.overworld.description": "Use the native Overworld sky. Terrain and lighting remain independently authored.",
"symbol.rivencraft.sky.nether.description": "Use a Nether-style sky and reddish fog, without Nether terrain or gameplay rules.",
"symbol.rivencraft.sky.end.description": "Use the native End sky backdrop, without End terrain or a dragon fight.",
"symbol.rivencraft.sky.static.description": "Animated static surrounds the Age. Preceding color pages tint it; otherwise it is gray.",
"symbol.rivencraft.lighting.ambient_color": "Ambient Light Color",
"symbol.rivencraft.lighting.ambient_color.description": "Apply preceding shared colors to ambient light. Use Bright separately for full effective ambient lighting.",
"symbol.rivencraft.number.description": "Before Color: value × 10 per channel. Before Length: value × 0.08 days. Before Phase/Angle: value × 15 degrees. Before Size: 0.25× to 3× (0-24).",
"symbol.rivencraft.size.small.description": "Before Normal Time: render the sun at 0.5× size. Its paired moon stays normal-sized, matching 1.21.1. A preceding Number overrides the size.",
"symbol.rivencraft.size.normal.description": "Before Normal Time: render the sun at 1× size. A preceding Number selects 0.25× through 3× (0-24). Its paired moon is unchanged.",
"symbol.rivencraft.size.large.description": "Before Normal Time: render the sun at 2× size. A preceding Number selects 0.25× through 3× (0-24). Its paired moon is unchanged.",
"symbol.rivencraft.angle.north.description": "Before Normal Time: set the native celestial orbit bearing to 0 degrees. A preceding Number overrides Angle in 15-degree steps.",
"symbol.rivencraft.angle.east.description": "Before Normal Time: set the native celestial orbit bearing to 90 degrees. A preceding Number overrides Angle in 15-degree steps.",
"symbol.rivencraft.angle.south.description": "Before Normal Time: set the native celestial orbit bearing to 180 degrees. A preceding Number overrides Angle in 15-degree steps.",
"symbol.rivencraft.angle.west.description": "Before Normal Time: set the native celestial orbit bearing to 270 degrees. A preceding Number overrides Angle in 15-degree steps.",
"symbol.rivencraft.phase.rising.description": "Before Normal Time: start this Age's native cycle at sunrise. A preceding Number overrides Phase in 15-degree steps from midnight.",
"symbol.rivencraft.phase.zenith.description": "Before Normal Time: start this Age's native cycle at noon. With Zero Length it remains noon.",
"symbol.rivencraft.phase.setting.description": "Before Normal Time: start this Age's native cycle at sunset. A preceding Number overrides Phase in 15-degree steps from midnight.",
"symbol.rivencraft.phase.nadir.description": "Before Normal Time: start this Age's native cycle at midnight. With Zero Length it remains midnight.",
"symbol.rivencraft.direction.clockwise.description": "Before Normal Time: advance this Age's native day cycle in its usual direction.",
"symbol.rivencraft.direction.counterclockwise.description": "Before Normal Time: run this Age's native day cycle in reverse. Does not change the Overworld clock.",
"symbol.rivencraft.celestial.normal_time.description": "Use the native sun/moon cycle with Length, Phase, Direction, Angle, and sun Size modifiers. Without a Length, use one normal day.",
"symbol.rivencraft.period.zero.description": "Before Normal Time: freeze the Age's day timelines at the written Phase, or a saved seeded time if Phase is omitted. A preceding Number overrides the Length.",
"symbol.rivencraft.period.half.description": "Before Normal Time: a half-length day (twice as fast). A preceding Number sets Length to value × 0.08 days.",
"symbol.rivencraft.period.full.description": "Before Normal Time: a normal-length day. A preceding Number sets Length to value × 0.08 days.",
"symbol.rivencraft.period.double.description": "Before Normal Time: a double-length day (half as fast). A preceding Number sets Length to value × 0.08 days.",
"symbol.rivencraft.color.red.description": "Add red to the next sky or Ambient Light Color target; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
"symbol.rivencraft.color.green.description": "Add green to the next sky or Ambient Light Color target; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
"symbol.rivencraft.color.blue.description": "Add blue to the next sky or Ambient Light Color target; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
"symbol.rivencraft.color.white.description": "Add equally to all RGB channels. Precede with a Number, or use a seeded amount.",
"symbol.rivencraft.color.black.description": "Subtract equally from all RGB channels. Precede with a Number, or use a seeded amount.",
"symbol.rivencraft.lighting.normal.description": "Use normal skylight and ambient brightness. Cave terrain remains without skylight.",
"symbol.rivencraft.lighting.dark.description": "Remove skylight from this Age; placed light sources still work.",
"symbol.rivencraft.lighting.bright.description": "Provide full ambient brightness and effective light level 15. Vanilla light-based spawn rules apply, including in Cave terrain without skylight.",
"symbol.rivencraft.weather.fast.description": "Use the faster Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
"symbol.rivencraft.weather.slow.description": "Use the slower Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
"symbol.rivencraft.weather.off.description": "Keep this Age clear without changing Overworld weather.",
@@ -1,6 +1,8 @@
package net.dnorberg.rivencraft.age;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
@@ -9,17 +11,23 @@ import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.Identifier;
/** Full server level catalog, sent on login and after a runtime Age is added. */
public record AgeCatalogPayload(int version, List<Identifier> dimensions) implements CustomPacketPayload {
public record AgeCatalogPayload(int version, List<Identifier> dimensions,
Map<Identifier, PortAgeColors> colors,
Map<Identifier, PortDayCycle> dayCycles) implements CustomPacketPayload {
public static final Type<AgeCatalogPayload> TYPE = new Type<>(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age_catalog"));
public static final StreamCodec<RegistryFriendlyByteBuf, AgeCatalogPayload> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.VAR_INT, AgeCatalogPayload::version,
Identifier.STREAM_CODEC.apply(ByteBufCodecs.list()), AgeCatalogPayload::dimensions,
ByteBufCodecs.map(HashMap::new, Identifier.STREAM_CODEC, PortAgeColors.STREAM_CODEC), AgeCatalogPayload::colors,
ByteBufCodecs.map(HashMap::new, Identifier.STREAM_CODEC, PortDayCycle.STREAM_CODEC), AgeCatalogPayload::dayCycles,
AgeCatalogPayload::new
);
public AgeCatalogPayload {
dimensions = List.copyOf(dimensions);
colors = Map.copyOf(colors);
dayCycles = Map.copyOf(dayCycles);
}
@Override
@@ -21,7 +21,7 @@ public final class AgeCatalogSync {
}
public static void registerPayloads(RegisterPayloadHandlersEvent event) {
var registrar = event.registrar("1");
var registrar = event.registrar("6");
registrar.playToClient(AgeCatalogPayload.TYPE, AgeCatalogPayload.STREAM_CODEC);
registrar.playToServer(AgeCatalogAckPayload.TYPE, AgeCatalogAckPayload.STREAM_CODEC,
(payload, context) -> {
@@ -77,7 +77,8 @@ public final class AgeCatalogSync {
State state = state(server);
state.acknowledged.remove(player.getUUID());
var dimensions = server.levelKeys().stream().map(key -> key.identifier()).sorted().toList();
PacketDistributor.sendToPlayer(player, new AgeCatalogPayload(state.version, dimensions));
PacketDistributor.sendToPlayer(player, new AgeCatalogPayload(state.version, dimensions,
RuntimeAgeBackend.savedColors(server), RuntimeAgeBackend.savedDayCycles(server)));
}
private static void acknowledge(ServerPlayer player, int version) {
@@ -34,7 +34,15 @@ final class AgeLevelIndex extends SavedData {
Codec.unboundedMap(Identifier.CODEC, PortLakePlan.CODEC)
.optionalFieldOf("lakes", Map.of()).forGetter(index -> index.lakes),
Codec.unboundedMap(Identifier.CODEC, PortTerrainFeaturePlan.CODEC)
.optionalFieldOf("terrain_features", Map.of()).forGetter(index -> index.terrainFeatures)
.optionalFieldOf("terrain_features", Map.of()).forGetter(index -> index.terrainFeatures),
Codec.unboundedMap(Identifier.CODEC, PortLighting.CODEC)
.optionalFieldOf("lighting", Map.of()).forGetter(index -> index.lighting),
Codec.unboundedMap(Identifier.CODEC, PortSkyStyle.CODEC)
.optionalFieldOf("sky", Map.of()).forGetter(index -> index.sky),
Codec.unboundedMap(Identifier.CODEC, PortAgeColors.CODEC)
.optionalFieldOf("colors", Map.of()).forGetter(index -> index.colors),
Codec.unboundedMap(Identifier.CODEC, PortDayCycle.CODEC)
.optionalFieldOf("day_cycles", Map.of()).forGetter(index -> index.dayCycles)
).apply(instance, AgeLevelIndex::new));
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
@@ -53,6 +61,10 @@ final class AgeLevelIndex extends SavedData {
private final Map<Identifier, Integer> dungeons;
private final Map<Identifier, PortLakePlan> lakes;
private final Map<Identifier, PortTerrainFeaturePlan> terrainFeatures;
private final Map<Identifier, PortLighting> lighting;
private final Map<Identifier, PortSkyStyle> sky;
private final Map<Identifier, PortAgeColors> colors;
private final Map<Identifier, PortDayCycle> dayCycles;
private AgeLevelIndex() {
this.ages = new LinkedHashMap<>();
@@ -65,6 +77,10 @@ final class AgeLevelIndex extends SavedData {
this.dungeons = new LinkedHashMap<>();
this.lakes = new LinkedHashMap<>();
this.terrainFeatures = new LinkedHashMap<>();
this.lighting = new LinkedHashMap<>();
this.sky = new LinkedHashMap<>();
this.colors = new LinkedHashMap<>();
this.dayCycles = new LinkedHashMap<>();
}
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
@@ -75,7 +91,10 @@ final class AgeLevelIndex extends SavedData {
Map<Identifier, PortVegetationPlan> vegetation,
Map<Identifier, Integer> dungeons,
Map<Identifier, PortLakePlan> lakes,
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures) {
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures,
Map<Identifier, PortLighting> lighting,
Map<Identifier, PortSkyStyle> sky, Map<Identifier, PortAgeColors> colors,
Map<Identifier, PortDayCycle> dayCycles) {
this.ages = new LinkedHashMap<>(ages);
this.retired = new LinkedHashSet<>(retired);
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
@@ -86,6 +105,10 @@ final class AgeLevelIndex extends SavedData {
this.dungeons = new LinkedHashMap<>(dungeons);
this.lakes = new LinkedHashMap<>(lakes);
this.terrainFeatures = new LinkedHashMap<>(terrainFeatures);
this.lighting = new LinkedHashMap<>(lighting);
this.sky = new LinkedHashMap<>(sky);
this.colors = new LinkedHashMap<>(colors);
this.dayCycles = new LinkedHashMap<>(dayCycles);
}
Map<Identifier, Long> entries() {
@@ -116,10 +139,17 @@ final class AgeLevelIndex extends SavedData {
int dungeons(Identifier id) { return dungeons.getOrDefault(id, 0); }
PortLakePlan lakes(Identifier id) { return lakes.get(id); }
PortTerrainFeaturePlan terrainFeatures(Identifier id) { return terrainFeatures.get(id); }
PortLighting lighting(Identifier id) { return lighting.getOrDefault(id, PortLighting.NORMAL); }
PortSkyStyle sky(Identifier id) { return sky.getOrDefault(id, PortSkyStyle.OVERWORLD); }
PortAgeColors colors(Identifier id) { return colors.getOrDefault(id, PortAgeColors.NATIVE); }
Map<Identifier, PortAgeColors> colors() { return Map.copyOf(colors); }
PortDayCycle dayCycle(Identifier id) { return dayCycles.getOrDefault(id, PortDayCycle.NATIVE); }
Map<Identifier, PortDayCycle> dayCycles() { return Map.copyOf(dayCycles); }
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, int dungeonCount,
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light, PortSkyStyle skyStyle,
PortAgeColors colorProfile, PortDayCycle dayCycle) {
if (contains(id)) {
throw new IllegalArgumentException("Age already exists: " + id);
}
@@ -131,6 +161,10 @@ final class AgeLevelIndex extends SavedData {
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
if (lakePlan != null) lakes.put(id, lakePlan);
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
if (light != PortLighting.NORMAL) lighting.put(id, light);
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
if (!colorProfile.equals(PortAgeColors.NATIVE)) colors.put(id, colorProfile);
if (!dayCycle.equals(PortDayCycle.NATIVE)) dayCycles.put(id, dayCycle);
setDirty();
}
@@ -144,6 +178,10 @@ final class AgeLevelIndex extends SavedData {
dungeons.remove(id);
lakes.remove(id);
terrainFeatures.remove(id);
lighting.remove(id);
sky.remove(id);
colors.remove(id);
dayCycles.remove(id);
retired.add(id);
setDirty();
}
@@ -151,7 +189,8 @@ final class AgeLevelIndex extends SavedData {
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
PortAgeTerrain ageTerrain, PortRockPlan rockPlan,
PortVegetationPlan vegetationPlan, int dungeonCount,
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light,
PortSkyStyle skyStyle, PortAgeColors colorProfile, PortDayCycle dayCycle) {
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
ages.put(id, seed);
if (biomePlan != null) biomePlans.put(id, biomePlan);
@@ -161,6 +200,10 @@ final class AgeLevelIndex extends SavedData {
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
if (lakePlan != null) lakes.put(id, lakePlan);
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
if (light != PortLighting.NORMAL) lighting.put(id, light);
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
if (!colorProfile.equals(PortAgeColors.NATIVE)) colors.put(id, colorProfile);
if (!dayCycle.equals(PortDayCycle.NATIVE)) dayCycles.put(id, dayCycle);
setDirty();
}
@@ -0,0 +1,101 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import java.util.Map;
import java.util.Random;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.Level;
import org.joml.Vector3f;
import org.joml.Vector3fc;
/** Frozen shared-color writing, adapted from 1.21.1 AgeSkyProfile. -1 means native. */
public record PortAgeColors(int sky, int ambient) {
public static final PortAgeColors NATIVE = new PortAgeColors(-1, -1);
public static final Codec<PortAgeColors> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.intRange(-1, 0xffffff).fieldOf("sky").forGetter(PortAgeColors::sky),
Codec.intRange(-1, 0xffffff).fieldOf("ambient").forGetter(PortAgeColors::ambient)
).apply(instance, PortAgeColors::new));
public static final StreamCodec<RegistryFriendlyByteBuf, PortAgeColors> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.INT, PortAgeColors::sky, ByteBufCodecs.INT, PortAgeColors::ambient, PortAgeColors::new);
public static final Identifier AMBIENT = Identifier.fromNamespaceAndPath("rivencraft", "lighting/ambient_color");
private static volatile Map<Identifier, PortAgeColors> clientColors = Map.of();
public PortAgeColors {
if (sky < -1 || sky > 0xffffff || ambient < -1 || ambient > 0xffffff)
throw new IllegalArgumentException("Invalid Age color");
}
public static void updateClient(Map<Identifier, PortAgeColors> colors) { clientColors = Map.copyOf(colors); }
public static PortAgeColors forLevel(Level level) {
if (level instanceof PortAgeLevel age) return age.colors();
return level.isClientSide() ? clientColors.getOrDefault(level.dimension().identifier(), NATIVE) : NATIVE;
}
public static boolean isColor(Identifier page) {
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
case "color/red", "color/green", "color/blue", "color/white", "color/black" -> true;
default -> false;
};
}
public static int number(Identifier page) {
if (!page.getNamespace().equals("rivencraft") || !page.getPath().startsWith("number/")) return -1;
try {
int value = Integer.parseInt(page.getPath().substring(7));
return value >= 0 && value <= 24 ? value : -1;
} catch (NumberFormatException exception) { return -1; }
}
public static boolean isModifier(Identifier page) { return isColor(page) || number(page) >= 0 || page.equals(AMBIENT); }
public static boolean validNumbers(List<Identifier> pages) {
for (int i = 0; i < pages.size(); i++)
if (number(pages.get(i)) >= 0 && (i + 1 == pages.size()
|| !isColor(pages.get(i + 1)) && !PortDayCycle.isPeriod(pages.get(i + 1))
&& !PortDayCycle.isPhase(pages.get(i + 1)) && !PortDayCycle.isAngle(pages.get(i + 1))
&& !PortDayCycle.isSize(pages.get(i + 1)))) return false;
return true;
}
public static PortAgeColors compile(List<Identifier> pages, long seed) {
if (!validNumbers(pages)) throw new IllegalArgumentException("Number pages must precede a supported Color, Length, Phase, Angle, or Size modifier");
int red = 0, green = 0, blue = 0, sky = -1, ambient = -1;
boolean written = false;
for (int i = 0; i < pages.size(); i++) {
Identifier page = pages.get(i);
if (isColor(page)) {
int amount = i > 0 ? number(pages.get(i - 1)) : -1;
if (amount < 0) amount = new Random(seed ^ (0x9E3779B97F4A7C15L * (i + 1L))).nextInt(25);
amount *= 10;
written = true;
switch (page.getPath()) {
case "color/red" -> red += amount;
case "color/green" -> green += amount;
case "color/blue" -> blue += amount;
case "color/white" -> { red += amount; green += amount; blue += amount; }
case "color/black" -> { red -= amount; green -= amount; blue -= amount; }
}
} else if (PortSkyStyle.forPage(page) != null || page.equals(AMBIENT)) {
if (written) {
int rgb = clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
if (page.equals(AMBIENT)) ambient = rgb; else sky = rgb;
}
red = green = blue = 0;
written = false;
}
}
if (written) sky = clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
return new PortAgeColors(sky, ambient);
}
private static int clamp(int value) { return Math.max(0, Math.min(255, value)); }
public static Vector3fc vector(int rgb) {
return new Vector3f((rgb >> 16 & 255) / 255f, (rgb >> 8 & 255) / 255f, (rgb & 255) / 255f);
}
}
@@ -68,6 +68,77 @@ public final class PortAgeCommands {
return 1;
})))
.then(Commands.literal("age")
.then(Commands.literal("create_size")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("size", StringArgumentType.word())
.executes(context -> createSizeAge(context, -1))
.then(Commands.argument("number", IntegerArgumentType.integer(0, 24))
.executes(context -> createSizeAge(context,
IntegerArgumentType.getInteger(context, "number"))))))))
.then(Commands.literal("create_angle")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("bearing", StringArgumentType.word())
.executes(context -> createAngleAge(context, -1))
.then(Commands.argument("number", IntegerArgumentType.integer(0, 24))
.executes(context -> createAngleAge(context,
IntegerArgumentType.getInteger(context, "number"))))))))
.then(Commands.literal("create_phase")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("length", StringArgumentType.word())
.then(Commands.argument("phase", StringArgumentType.word())
.then(Commands.argument("direction", StringArgumentType.word())
.executes(context -> createPhaseAge(context, -1))
.then(Commands.argument("number", IntegerArgumentType.integer(0, 24))
.executes(context -> createPhaseAge(context,
IntegerArgumentType.getInteger(context, "number"))))))))))
.then(Commands.literal("create_cycle")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("length", StringArgumentType.word())
.executes(context -> createDayCycleAge(context, -1))
.then(Commands.argument("number", IntegerArgumentType.integer(0, 24))
.executes(context -> createDayCycleAge(context,
IntegerArgumentType.getInteger(context, "number"))))))))
.then(Commands.literal("cycle_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeDayCycle)))
.then(Commands.literal("create_colors")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("sky", StringArgumentType.word())
.executes(PortAgeCommands::createColorAge)))))
.then(Commands.literal("color_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeColors)))
.then(Commands.literal("create_sky")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("sky", StringArgumentType.word())
.then(Commands.argument("lighting", StringArgumentType.word())
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::createSkyAge)))))))
.then(Commands.literal("sky_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeSky)))
.then(Commands.literal("environment_probe")
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::probeEnvironment))))
.then(Commands.literal("create_lighting")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("mode", StringArgumentType.word())
.suggests((context, builder) -> net.minecraft.commands.SharedSuggestionProvider.suggest(
java.util.Arrays.stream(PortLighting.values())
.map(mode -> mode.name().toLowerCase(Locale.ROOT)), builder))
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::createLightingAge))))))
.then(Commands.literal("lighting_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeLighting)))
.then(Commands.literal("surface_probe")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("x", IntegerArgumentType.integer())
@@ -137,10 +208,13 @@ public final class PortAgeCommands {
long seed = LongArgumentType.getLong(context, "seed");
PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select(
context.getSource().getServer(), seed);
int created = createAge(context, selected.terrain(), selected.biomes());
var environment = PortEnvironmentSelection.resolve(java.util.List.of(), seed, selected.terrain());
int created = createAge(context, selected.terrain(), selected.biomes(),
null, null, 0, null, null, environment);
if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
"Random Age plan: terrain=" + selected.terrain()
+ " biomes=" + selected.biomes()), false);
+ " biomes=" + selected.biomes()
+ " environment=" + environment), false);
return created;
}))))
.then(Commands.literal("create_rocks")
@@ -318,6 +392,413 @@ public final class PortAgeCommands {
.executes(PortAgeCommands::sampleBiome)))))));
}
private static int createSizeAge(CommandContext<CommandSourceStack> context, int number) {
try {
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
var pages = new java.util.ArrayList<Identifier>();
pages.add(symbol.apply("period/zero"));
pages.add(symbol.apply("phase/zenith"));
if (number >= 0) pages.add(symbol.apply("number/" + number));
pages.add(symbol.apply("size/" + StringArgumentType.getString(context, "size")));
pages.add(PortDayCycle.NORMAL_TIME);
pages.addAll(java.util.stream.Stream.of("lighting/normal", "weather/off", "terrain/flat").map(symbol).toList());
var server = context.getSource().getServer();
long seed = LongArgumentType.getLong(context, "seed");
long origin = PortDayCycle.nativeTicks(server.overworld());
var cycle = PortDayCycle.compile(pages, seed, origin);
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages))
throw new IllegalArgumentException("Unsupported size fixture writing");
for (var size : java.util.Map.of("small", 0.5F, "normal", 1F, "large", 2F).entrySet()) {
var selected = PortDayCycle.compile(java.util.List.of(symbol.apply("size/" + size.getKey()),
PortDayCycle.NORMAL_TIME), seed, origin);
if (selected.sunSize() != size.getValue() || selected.ticks(origin) != origin)
throw new IllegalStateException("Size mapping changed cycle timing");
}
for (int n = 0; n <= 24; n++) {
var selected = PortDayCycle.compile(java.util.List.of(symbol.apply("number/" + n),
symbol.apply("size/large"), PortDayCycle.NORMAL_TIME), seed, origin);
float expected = (float) (0.25D + n * (2.75D / 24D));
if (selected.sunSize() != expected || n == 0 && expected != 0.25F
|| n == 12 && expected != 1.625F || n == 24 && expected != 3F)
throw new IllegalStateException("Numeric size override failed");
}
var older = PortDayCycle.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
com.google.gson.JsonParser.parseString("{\"authored\":true,\"period\":1,\"stopped_time\":0,"
+ "\"origin_tick\":100,\"phase_tick\":6000,\"clockwise\":false,\"bearing\":90}")).getOrThrow();
if (!older.equals(new PortDayCycle(true, 1, 0, 100, 6000, false, 90)))
throw new IllegalStateException("Old cycle size fallback failed");
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
if (environment.sky() != PortSkyStyle.OVERWORLD)
throw new IllegalStateException("Sized celestial selected an incompatible sky");
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 createAngleAge(CommandContext<CommandSourceStack> context, int number) {
try {
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
var pages = new java.util.ArrayList<Identifier>();
pages.add(symbol.apply("period/zero"));
pages.add(symbol.apply("phase/rising"));
if (number >= 0) pages.add(symbol.apply("number/" + number));
pages.add(symbol.apply("angle/" + StringArgumentType.getString(context, "bearing")));
pages.add(PortDayCycle.NORMAL_TIME);
pages.addAll(java.util.stream.Stream.of("sky/overworld", "lighting/normal", "weather/off", "terrain/flat")
.map(symbol).toList());
var server = context.getSource().getServer();
long seed = LongArgumentType.getLong(context, "seed");
long origin = PortDayCycle.nativeTicks(server.overworld());
var cycle = PortDayCycle.compile(pages, seed, origin);
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages))
throw new IllegalArgumentException("Unsupported angle fixture writing");
for (var bearing : java.util.Map.of("north", 0, "east", 90, "south", 180, "west", 270).entrySet()) {
var selected = PortDayCycle.compile(java.util.List.of(symbol.apply("angle/" + bearing.getKey()),
PortDayCycle.NORMAL_TIME), seed, origin);
if (selected.bearing() != bearing.getValue() || selected.ticks(origin) != origin)
throw new IllegalStateException("Angle mapping changed cycle timing");
}
for (int n = 0; n <= 24; n++) {
var selected = PortDayCycle.compile(java.util.List.of(symbol.apply("number/" + n),
symbol.apply("angle/west"), PortDayCycle.NORMAL_TIME), seed, origin);
if (selected.bearing() != n * 15 % 360)
throw new IllegalStateException("Numeric angle override failed");
}
var older = PortDayCycle.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
com.google.gson.JsonParser.parseString("{\"authored\":true,\"period\":1,\"stopped_time\":0,"
+ "\"origin_tick\":100,\"phase_tick\":6000,\"clockwise\":false}")).getOrThrow();
if (!older.equals(new PortDayCycle(true, 1, 0, 100, 6000, false)))
throw new IllegalStateException("Old cycle bearing fallback failed");
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, PortWeatherMode.OFF, PortLighting.NORMAL, PortSkyStyle.OVERWORLD,
PortAgeColors.compile(pages, seed), cycle);
return probeDayCycle(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int createPhaseAge(CommandContext<CommandSourceStack> context, int number) {
try {
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
var pages = new java.util.ArrayList<Identifier>();
pages.add(symbol.apply("period/" + StringArgumentType.getString(context, "length")));
if (number >= 0) pages.add(symbol.apply("number/" + number));
pages.add(symbol.apply("phase/" + StringArgumentType.getString(context, "phase")));
pages.add(symbol.apply("direction/" + StringArgumentType.getString(context, "direction")));
pages.add(PortDayCycle.NORMAL_TIME);
pages.addAll(java.util.stream.Stream.of("sky/overworld", "lighting/normal", "weather/off", "terrain/flat")
.map(symbol).toList());
var server = context.getSource().getServer();
long seed = LongArgumentType.getLong(context, "seed");
long origin = PortDayCycle.nativeTicks(server.overworld());
var cycle = PortDayCycle.compile(pages, seed, origin);
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages))
throw new IllegalArgumentException("Unsupported phase fixture writing");
for (var phase : java.util.Map.of("rising", 0, "zenith", 6000, "setting", 12000, "nadir", 18000).entrySet()) {
var forward = PortDayCycle.compile(java.util.List.of(symbol.apply("phase/" + phase.getKey()),
PortDayCycle.NORMAL_TIME), seed, origin);
var reverse = PortDayCycle.compile(java.util.List.of(symbol.apply("phase/" + phase.getKey()),
symbol.apply("direction/counterclockwise"), PortDayCycle.NORMAL_TIME), seed, origin);
var stopped = PortDayCycle.compile(java.util.List.of(symbol.apply("period/zero"),
symbol.apply("phase/" + phase.getKey()), PortDayCycle.NORMAL_TIME), seed, origin);
if (forward.ticks(origin) != phase.getValue() || reverse.ticks(origin) != phase.getValue()
|| forward.ticks(origin + 1000) != phase.getValue() + 1000
|| reverse.ticks(origin + 1000) != phase.getValue() - 1000
|| stopped.ticks(origin + 123456) != phase.getValue())
throw new IllegalStateException("Phase/direction fixture failed");
}
var legacy = PortDayCycle.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
com.google.gson.JsonParser.parseString("{\"authored\":true,\"period\":0,\"stopped_time\":321}"))
.getOrThrow();
if (!legacy.equals(new PortDayCycle(true, 0, 321)))
throw new IllegalStateException("Old day-cycle defaults changed");
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, PortWeatherMode.OFF, PortLighting.NORMAL, PortSkyStyle.OVERWORLD,
PortAgeColors.compile(pages, seed), cycle);
return probeDayCycle(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int createDayCycleAge(CommandContext<CommandSourceStack> context, int number) {
try {
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
var pages = new java.util.ArrayList<Identifier>();
if (number >= 0) pages.add(symbol.apply("number/" + number));
pages.add(symbol.apply("period/" + StringArgumentType.getString(context, "length")));
pages.add(PortDayCycle.NORMAL_TIME);
pages.addAll(java.util.stream.Stream.of("lighting/normal", "weather/off", "terrain/flat")
.map(symbol).toList());
long seed = LongArgumentType.getLong(context, "seed");
var cycle = PortDayCycle.compile(pages, seed);
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages)
|| !cycle.equals(PortDayCycle.compile(pages, seed))
|| new PortDayCycle(true, 0.5, 0).ticks(12000) != 24000
|| new PortDayCycle(true, 2, 0).ticks(12000) != 6000
|| new PortDayCycle(true, 0, 321).ticks(-123456) != 321
|| new PortDayCycle(true, 0, 321).ticks(123456) != 321
|| PortDayCycle.validWriting(java.util.List.of(symbol.apply("period/half")))
|| PortDayCycle.validWriting(java.util.List.of(PortDayCycle.NORMAL_TIME, symbol.apply("period/half"))))
throw new IllegalStateException("Day cycle fixture failed");
var server = context.getSource().getServer();
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
if (environment.sky() != PortSkyStyle.OVERWORLD)
throw new IllegalStateException("Celestial-only writing selected an incompatible sky");
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 probeDayCycle(CommandContext<CommandSourceStack> context) {
var server = context.getSource().getServer();
var level = server.getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded"));
return 0;
}
verifyCatalogRoundTrip(server);
var definition = server.registryAccess().lookupOrThrow(Registries.WORLD_CLOCK)
.getOrThrow(net.minecraft.world.clock.WorldClocks.OVERWORLD);
var source = level.clockManager().getInstance(definition);
var sample = PortDayCycle.timelines(level, level.clockManager()).getInstance(definition);
var cycle = PortDayCycle.forLevel(level);
if (cycle.ticks(source.totalTicks()) != sample.totalTicks()
|| cycle.authored() && cycle.period() == 0 && (sample.partialTick() != 0 || sample.rate() != 0))
throw new IllegalStateException("Day cycle clock invariant failed");
var pos = new net.minecraft.core.BlockPos(0, 80, 0);
var sun = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.SUN_ANGLE, pos);
var light = level.environmentAttributes().getDimensionValue(net.minecraft.world.attribute.EnvironmentAttributes.SKY_LIGHT_LEVEL);
context.getSource().sendSuccess(() -> Component.literal("Cycle probe " + ageId(context)
+ " saved=" + cycle + " nativeTicks=" + source.totalTicks() + " ageTicks=" + sample.totalTicks()
+ " sunAngle=" + sun + " skyLight=" + light), false);
return 1;
}
private static int createColorAge(CommandContext<CommandSourceStack> context) {
try {
String sky = StringArgumentType.getString(context, "sky").toLowerCase(Locale.ROOT);
PortSkyStyle style = PortSkyStyle.valueOf(sky.toUpperCase(Locale.ROOT));
var pages = java.util.stream.Stream.of("number/10", "color/red", "number/5", "color/green",
"number/15", "color/blue", "sky/" + sky, "number/20", "color/white",
"number/10", "color/black", "number/5", "color/green", "number/10", "color/blue",
"lighting/ambient_color", "lighting/bright", "weather/off", "terrain/flat")
.map(path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path)).toList();
long seed = LongArgumentType.getLong(context, "seed");
var colors = PortAgeColors.compile(pages, seed);
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
var trailing = PortAgeColors.compile(java.util.List.of(symbol.apply("number/24"), symbol.apply("color/red")), seed);
var zero = PortAgeColors.compile(java.util.List.of(symbol.apply("number/0"), symbol.apply("color/blue")), seed);
var clamped = PortAgeColors.compile(java.util.List.of(symbol.apply("number/24"), symbol.apply("color/white"),
symbol.apply("number/24"), symbol.apply("color/red")), seed);
var unnumbered = java.util.List.of(symbol.apply("color/blue"));
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages)
|| colors.sky() != 0x643296 || colors.ambient() != 0x6496c8
|| !colors.equals(PortAgeColors.compile(pages, seed))
|| trailing.sky() != 0xf00000 || zero.sky() != 0 || clamped.sky() != 0xfff0f0
|| !PortAgeColors.compile(unnumbered, seed).equals(PortAgeColors.compile(unnumbered, seed))
|| PortAgeColors.validNumbers(java.util.List.of(symbol.apply("number/5")))
|| !PortAgeColors.compile(java.util.List.of(), seed).equals(PortAgeColors.NATIVE))
throw new IllegalStateException("Authored color fixture failed");
var server = context.getSource().getServer();
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, PortWeatherMode.OFF, PortLighting.BRIGHT, style, colors);
return probeColors(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeColors(CommandContext<CommandSourceStack> context) {
var server = context.getSource().getServer();
var level = server.getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded"));
return 0;
}
var colors = PortAgeColors.forLevel(level);
verifyCatalogRoundTrip(server);
var pos = new net.minecraft.core.BlockPos(0, 80, 0);
var sky = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.SKY_COLOR, pos);
var fog = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.FOG_COLOR, pos);
var ambient = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.AMBIENT_LIGHT_COLOR, pos);
if (colors.sky() >= 0 && (!sky.equals(PortAgeColors.vector(colors.sky())) || !fog.equals(sky))
|| colors.ambient() >= 0 && !ambient.equals(PortAgeColors.vector(colors.ambient()))) {
context.getSource().sendFailure(Component.literal("Color attribute mismatch"));
return 0;
}
context.getSource().sendSuccess(() -> Component.literal("Color probe " + ageId(context)
+ " saved=" + colors + " sky=" + sky + " fog=" + fog + " ambient=" + ambient), false);
return 1;
}
private static void verifyCatalogRoundTrip(net.minecraft.server.MinecraftServer server) {
var payload = new AgeCatalogPayload(1, server.levelKeys().stream().map(key -> key.identifier()).toList(),
RuntimeAgeBackend.savedColors(server), RuntimeAgeBackend.savedDayCycles(server));
var buffer = new net.minecraft.network.RegistryFriendlyByteBuf(io.netty.buffer.Unpooled.buffer(),
server.registryAccess(), net.neoforged.neoforge.network.connection.ConnectionType.NEOFORGE);
try {
AgeCatalogPayload.STREAM_CODEC.encode(buffer, payload);
if (!payload.equals(AgeCatalogPayload.STREAM_CODEC.decode(buffer)))
throw new IllegalStateException("Age catalog round-trip failed");
} finally { buffer.release(); }
}
private static int createSkyAge(CommandContext<CommandSourceStack> context) {
try {
PortSkyStyle sky = PortSkyStyle.valueOf(StringArgumentType.getString(context, "sky").toUpperCase(Locale.ROOT));
PortLighting lighting = PortLighting.valueOf(StringArgumentType.getString(context, "lighting").toUpperCase(Locale.ROOT));
PortAgeTerrain terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var pages = java.util.List.of(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "sky/" + sky.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/" + lighting.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "weather/off"));
var server = context.getSource().getServer();
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
RuntimeAgeBackend.create(server, ageId(context), LongArgumentType.getLong(context, "seed"),
biomes, terrain, null, null, 0, null, null, PortWeatherMode.OFF, lighting, sky);
return probeSky(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeSky(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
var server = context.getSource().getServer();
var level = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
return 0;
}
var type = level.dimensionType();
context.getSource().sendSuccess(() -> Component.literal("Sky probe " + id
+ " type=" + level.dimensionTypeRegistration().unwrapKey().orElseThrow().identifier()
+ " skybox=" + type.skybox() + " skylight=" + type.hasSkyLight() + " ambient=" + type.ambientLight()
+ " fog=" + level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.FOG_COLOR,
new net.minecraft.core.BlockPos(0, 80, 0))
+ " ceiling=" + type.hasCeiling() + " scale=" + type.coordinateScale()
+ " minY=" + type.minY() + " height=" + type.height() + " dragon=" + type.hasEnderDragonFight()), false);
return 1;
}
private static int probeEnvironment(CommandContext<CommandSourceStack> context) {
try {
long seed = LongArgumentType.getLong(context, "seed");
PortAgeTerrain terrain = PortAgeTerrain.valueOf(
StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var selected = PortEnvironmentSelection.resolve(java.util.List.of(), seed, terrain);
var explicitPages = java.util.List.of(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "weather/storm"),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/bright"));
var explicit = PortEnvironmentSelection.resolve(explicitPages, seed, terrain);
var weatherOnly = PortEnvironmentSelection.resolve(java.util.List.of(explicitPages.get(0)), seed, terrain);
var lightingOnly = PortEnvironmentSelection.resolve(java.util.List.of(explicitPages.get(1)), seed, terrain);
if (!selected.equals(PortEnvironmentSelection.resolve(java.util.List.of(), seed, terrain))
|| explicit.lighting() != PortLighting.BRIGHT
|| explicit.weather() != (terrain == PortAgeTerrain.CAVE ? PortWeatherMode.OFF : PortWeatherMode.STORM)
|| weatherOnly.lighting() != selected.lighting() || lightingOnly.weather() != selected.weather()
|| explicit.sky() != selected.sky() || weatherOnly.sky() != selected.sky()
|| lightingOnly.sky() != selected.sky())
throw new IllegalStateException("Environment selection invariant failed");
for (PortSkyStyle sky : PortSkyStyle.values()) {
var page = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
"sky/" + sky.name().toLowerCase(Locale.ROOT));
var skyOnly = PortEnvironmentSelection.resolve(java.util.List.of(page), seed, terrain);
var allExplicit = PortEnvironmentSelection.resolve(java.util.List.of(
page, explicitPages.get(0), explicitPages.get(1)), seed, terrain);
if (skyOnly.sky() != sky || allExplicit.sky() != sky
|| skyOnly.weather() != selected.weather() || skyOnly.lighting() != selected.lighting()
|| allExplicit.weather() != explicit.weather() || allExplicit.lighting() != explicit.lighting())
throw new IllegalStateException("Sky override/independence invariant failed: " + sky);
}
for (String celestial : java.util.List.of("normal_time", "sun", "moon")) {
var page = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "celestial/" + celestial);
var automatic = PortEnvironmentSelection.resolve(java.util.List.of(page), seed, terrain);
if (automatic.sky() != PortSkyStyle.OVERWORLD || automatic.weather() != selected.weather()
|| automatic.lighting() != selected.lighting())
throw new IllegalStateException("Celestial-compatible sky fallback failed: " + celestial);
for (PortSkyStyle sky : PortSkyStyle.values()) {
var skyPage = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
"sky/" + sky.name().toLowerCase(Locale.ROOT));
if (PortSkyStyle.resolve(java.util.List.of(page, skyPage), seed) != sky
|| PortSkyStyle.resolve(java.util.List.of(skyPage, page), seed) != sky)
throw new IllegalStateException("Explicit sky lost precedence for " + celestial);
}
}
context.getSource().sendSuccess(() -> Component.literal("Environment probe seed=" + seed
+ " terrain=" + terrain + " selected=" + selected + " explicit=" + explicit
+ " deterministic=true skyOverrides=true celestialFallback=true"), false);
return 1;
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int createLightingAge(CommandContext<CommandSourceStack> context) {
try {
PortLighting mode = PortLighting.valueOf(
StringArgumentType.getString(context, "mode").toUpperCase(Locale.ROOT));
PortAgeTerrain terrain = PortAgeTerrain.valueOf(
StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var pages = java.util.List.of(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/" + mode.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
var server = context.getSource().getServer();
var biomePlan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
RuntimeAgeBackend.create(server, ageId(context), LongArgumentType.getLong(context, "seed"),
biomePlan, terrain, null, null, 0, null, null, PortWeatherMode.NORMAL, mode);
return probeLighting(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeLighting(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
var server = context.getSource().getServer();
ServerLevel level = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
return 0;
}
context.getSource().sendSuccess(() -> Component.literal("Lighting probe " + id
+ " type=" + level.dimensionTypeRegistration().unwrapKey().orElseThrow().identifier()
+ " skylight=" + level.dimensionType().hasSkyLight()
+ " ambient=" + level.dimensionType().ambientLight()
+ " undergroundBrightness=" + level.getMaxLocalRawBrightness(
new net.minecraft.core.BlockPos(0, level.getMinY() + 2, 0))
+ " vanillaDarkEnough=" + net.minecraft.world.entity.monster.Monster.isDarkEnoughToSpawn(
level, new net.minecraft.core.BlockPos(0, level.getMinY() + 2, 0),
net.minecraft.util.RandomSource.create(0))
+ " overworldSkylight=" + server.overworld().dimensionType().hasSkyLight()
+ " overworldAmbient=" + server.overworld().dimensionType().ambientLight()), false);
return 1;
}
private static int probeSurface(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
@@ -602,13 +1083,23 @@ public final class PortAgeCommands {
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures) {
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
new PortEnvironmentSelection(PortWeatherMode.NORMAL, PortLighting.NORMAL, PortSkyStyle.OVERWORLD));
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures,
PortEnvironmentSelection environment) {
String name = StringArgumentType.getString(context, "name");
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
long seed = LongArgumentType.getLong(context, "seed");
ServerLevel level;
try {
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures);
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
environment.weather(), environment.lighting(), environment.sky());
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
@@ -14,6 +14,21 @@ final class PortAgeLevel extends ServerLevel {
private final WeatherData ageWeather;
private final PortWeatherSettings weatherSettings;
private final boolean cave;
private PortAgeColors colors = PortAgeColors.NATIVE;
private PortDayCycle dayCycle = PortDayCycle.NATIVE;
PortDayCycle dayCycle() { return dayCycle == null ? PortDayCycle.NATIVE : dayCycle; }
void configureDayCycle(PortDayCycle cycle) {
this.dayCycle = cycle;
environmentAttributes().invalidateTickCache();
updateSkyBrightness();
}
PortAgeColors colors() { return colors == null ? PortAgeColors.NATIVE : colors; }
void configureColors(PortAgeColors colors) {
this.colors = colors;
environmentAttributes().invalidateTickCache();
}
PortAgeLevel(MinecraftServer server, ResourceKey<Level> key, LevelStem stem,
long biomeZoomSeed, PortAgeTerrain terrain) {
@@ -0,0 +1,212 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import net.minecraft.core.Holder;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.Identifier;
import net.minecraft.world.clock.ClockInstance;
import net.minecraft.world.clock.ClockManager;
import net.minecraft.world.clock.WorldClock;
import net.minecraft.world.clock.WorldClocks;
import net.minecraft.world.level.Level;
/** Authored native celestial controls. Never mutates the shared clock. */
public record PortDayCycle(boolean authored, double period, int stoppedTime,
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize) {
public static final PortDayCycle NATIVE = new PortDayCycle(false, 1, 0);
public static final Identifier NORMAL_TIME = Identifier.fromNamespaceAndPath("rivencraft", "celestial/normal_time");
public static final Codec<PortDayCycle> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.BOOL.fieldOf("authored").forGetter(PortDayCycle::authored),
Codec.doubleRange(0, 2).fieldOf("period").forGetter(PortDayCycle::period),
Codec.intRange(0, 23999).fieldOf("stopped_time").forGetter(PortDayCycle::stoppedTime),
Codec.LONG.optionalFieldOf("origin_tick", 0L).forGetter(PortDayCycle::originTick),
Codec.intRange(0, 23999).optionalFieldOf("phase_tick", 0).forGetter(PortDayCycle::phaseTick),
Codec.BOOL.optionalFieldOf("clockwise", true).forGetter(PortDayCycle::clockwise),
Codec.intRange(-1, 359).optionalFieldOf("bearing", -1).forGetter(PortDayCycle::bearing),
Codec.floatRange(0.25F, 3F).optionalFieldOf("sun_size", 1F).forGetter(PortDayCycle::sunSize)
).apply(instance, PortDayCycle::new));
public static final StreamCodec<RegistryFriendlyByteBuf, PortDayCycle> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.BOOL, PortDayCycle::authored, ByteBufCodecs.DOUBLE, PortDayCycle::period,
ByteBufCodecs.VAR_INT, PortDayCycle::stoppedTime, ByteBufCodecs.VAR_LONG, PortDayCycle::originTick,
ByteBufCodecs.VAR_INT, PortDayCycle::phaseTick, ByteBufCodecs.BOOL, PortDayCycle::clockwise,
ByteBufCodecs.INT, PortDayCycle::bearing, ByteBufCodecs.FLOAT, PortDayCycle::sunSize, PortDayCycle::new);
private static volatile Map<Identifier, PortDayCycle> clientCycles = Map.of();
public PortDayCycle {
if (!Double.isFinite(period) || period < 0 || period > 2 || stoppedTime < 0 || stoppedTime >= 24000
|| phaseTick < 0 || phaseTick >= 24000 || bearing < -1 || bearing >= 360
|| !Float.isFinite(sunSize) || sunSize < 0.25F || sunSize > 3F)
throw new IllegalArgumentException("Invalid day cycle");
}
public PortDayCycle(boolean authored, double period, int stoppedTime) {
this(authored, period, stoppedTime, 0, 0, true);
}
public PortDayCycle(boolean authored, double period, int stoppedTime,
long originTick, int phaseTick, boolean clockwise) {
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, -1);
}
public PortDayCycle(boolean authored, double period, int stoppedTime,
long originTick, int phaseTick, boolean clockwise, int bearing) {
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, bearing, 1F);
}
public static boolean isPeriod(Identifier page) {
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
case "period/full", "period/half", "period/double", "period/zero" -> true;
default -> false;
};
}
public static boolean isPhase(Identifier page) {
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
case "phase/rising", "phase/zenith", "phase/setting", "phase/nadir" -> true;
default -> false;
};
}
public static boolean isDirection(Identifier page) {
return page.getNamespace().equals("rivencraft") && (page.getPath().equals("direction/clockwise")
|| page.getPath().equals("direction/counterclockwise"));
}
public static boolean isAngle(Identifier page) {
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
case "angle/north", "angle/east", "angle/south", "angle/west" -> true;
default -> false;
};
}
public static boolean supports(Identifier page) {
return page.equals(NORMAL_TIME) || isPeriod(page) || isPhase(page) || isDirection(page) || isAngle(page) || isSize(page);
}
public static boolean isSize(Identifier page) {
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
case "size/small", "size/normal", "size/large" -> true;
default -> false;
};
}
public static boolean validWriting(List<Identifier> pages) {
int target = pages.indexOf(NORMAL_TIME), periods = 0, phases = 0, directions = 0, angles = 0, sizes = 0;
if (target != pages.lastIndexOf(NORMAL_TIME)) return false;
for (int i = 0; i < pages.size(); i++) {
if (isPeriod(pages.get(i)) && (++periods > 1 || i > target)) return false;
if (isPhase(pages.get(i)) && (++phases > 1 || i > target)) return false;
if (isDirection(pages.get(i)) && (++directions > 1 || i > target)) return false;
if (isAngle(pages.get(i)) && (++angles > 1 || i > target)) return false;
if (isSize(pages.get(i)) && (++sizes > 1 || i > target)) return false;
}
return true;
}
public static PortDayCycle compile(List<Identifier> pages, long seed) {
return compile(pages, seed, 0);
}
public static PortDayCycle compile(List<Identifier> pages, long seed, long nativeTicks) {
if (!validWriting(pages)) throw new IllegalArgumentException("Place at most one Length, Phase, Direction, Angle, and Size before Normal Time");
if (!pages.contains(NORMAL_TIME)) return NATIVE;
double period = 1;
Integer phase = null;
boolean clockwise = true;
int bearing = -1;
float size = 1F;
for (int i = 0; i < pages.size(); i++) {
Identifier page = pages.get(i);
int number = i > 0 ? PortAgeColors.number(pages.get(i - 1)) : -1;
if (isPhase(page)) {
int degrees = number >= 0 ? number * 15 % 360 : switch (page.getPath()) {
case "phase/rising" -> 90;
case "phase/zenith" -> 180;
case "phase/setting" -> 270;
default -> 0;
};
// Same conversion as 1.21.1 AgeDimensionService.applyInitialCelestialPhase:
// the written angle starts at Nadir, while native time zero is sunrise.
phase = (degrees * 24000 / 360 + 18000) % 24000;
} else if (isAngle(page)) {
bearing = number >= 0 ? number * 15 % 360 : switch (page.getPath()) {
case "angle/east" -> 90;
case "angle/south" -> 180;
case "angle/west" -> 270;
default -> 0;
};
} else if (isSize(page)) {
size = number >= 0 ? (float) (0.25D + number * (2.75D / 24D)) : switch (page.getPath()) {
case "size/small" -> 0.5F;
case "size/large" -> 2F;
default -> 1F;
};
} else if (isDirection(page)) {
clockwise = page.getPath().equals("direction/clockwise");
} else if (isPeriod(page)) period = number >= 0 ? number * 0.08 : switch (page.getPath()) {
case "period/half" -> 0.5;
case "period/double" -> 2;
case "period/zero" -> 0;
default -> 1;
};
}
int stopped = period == 0 ? phase != null ? phase
: new Random(seed ^ 0x4441594359434C45L).nextInt(24000) : 0;
return new PortDayCycle(true, period, stopped, phase != null ? nativeTicks : 0,
phase == null ? 0 : phase, clockwise, bearing, size);
}
public long ticks(long nativeTicks) {
if (!authored) return nativeTicks;
return (long) Math.floor(time(nativeTicks));
}
private double time(long nativeTicks) {
return !authored ? nativeTicks : period == 0 ? stoppedTime
: (clockwise ? 1 : -1) * ((double) nativeTicks - originTick) / period + phaseTick;
}
public static long nativeTicks(Level level) {
return level.clockManager().getInstance(level.registryAccess().lookupOrThrow(
net.minecraft.core.registries.Registries.WORLD_CLOCK).getOrThrow(WorldClocks.OVERWORLD)).totalTicks();
}
public static void updateClient(Map<Identifier, PortDayCycle> cycles) { clientCycles = Map.copyOf(cycles); }
public static PortDayCycle forLevel(Level level) {
if (level instanceof PortAgeLevel age) return age.dayCycle();
return level.isClientSide() ? clientCycles.getOrDefault(level.dimension().identifier(), NATIVE) : NATIVE;
}
/** Timelines keep this manager; profiles are read lazily after level construction/catalog sync. */
public static ClockManager timelines(Level level, ClockManager nativeManager) {
Map<Holder<WorldClock>, ClockInstance> wrappers = new HashMap<>();
return definition -> {
if (!definition.is(WorldClocks.OVERWORLD)) return nativeManager.getInstance(definition);
return wrappers.computeIfAbsent(definition, ignored -> new ClockInstance() {
private ClockInstance source() { return nativeManager.getInstance(definition); }
public long totalTicks() { return forLevel(level).ticks(source().totalTicks()); }
public float partialTick() {
PortDayCycle cycle = forLevel(level);
if (!cycle.authored) return source().partialTick();
if (cycle.period == 0) return 0;
double time = cycle.time(source().totalTicks());
return (float) (time - Math.floor(time)
+ (cycle.clockwise ? 1 : -1) * source().partialTick() / cycle.period);
}
public float rate() {
PortDayCycle cycle = forLevel(level);
return !cycle.authored ? source().rate() : cycle.period == 0 ? 0
: (float) ((cycle.clockwise ? 1 : -1) * source().rate() / cycle.period);
}
public boolean isPaused() { return source().isPaused() || forLevel(level).authored && forLevel(level).period == 0; }
});
};
}
}
@@ -0,0 +1,28 @@
package net.dnorberg.rivencraft.age;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import net.minecraft.resources.Identifier;
/** Seed-completes missing environment controllers once, before an Age is saved. */
public record PortEnvironmentSelection(PortWeatherMode weather, PortLighting lighting, PortSkyStyle sky) {
public static PortEnvironmentSelection resolve(List<Identifier> pages, long seed, PortAgeTerrain terrain) {
PortLighting lighting = pages.stream().map(PortLighting::forPage).filter(java.util.Objects::nonNull)
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x4C49474854494E47L), PortLighting.values()));
PortWeatherMode weather = terrain == PortAgeTerrain.CAVE ? PortWeatherMode.OFF
: pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x574541544845525FL), PortWeatherMode.values()));
return new PortEnvironmentSelection(weather, lighting, PortSkyStyle.resolve(pages, seed));
}
private static <T> T rankedDefault(Random random, T[] choices) {
// LegacyRankChoice: the first (Normal) option is rank 1, the rest rank 2.
// Its rank-1 weight is the complete rare group plus one. Derive this
// from the supported pool, rather than selecting unported weather.
int[] weights = new int[choices.length];
Arrays.fill(weights, 1);
weights[0] = choices.length;
return PortRandomAgeSelection.weighted(random, List.of(choices), weights);
}
}
@@ -0,0 +1,52 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import java.util.Locale;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.Level;
/** Authored lighting choices using statically registered, client-synchronized types. */
public enum PortLighting {
NORMAL, DARK, BRIGHT;
public static final Codec<PortLighting> CODEC = Codec.STRING.xmap(
name -> valueOf(name.toUpperCase(Locale.ROOT)), mode -> mode.name().toLowerCase(Locale.ROOT));
public static PortLighting forPage(Identifier page) {
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
return switch (page.getPath()) {
case "lighting/normal" -> NORMAL;
case "lighting/dark" -> DARK;
case "lighting/bright" -> BRIGHT;
default -> null;
};
}
public static boolean isBright(Level level) {
return level.dimensionTypeRegistration().unwrapKey().map(key -> {
Identifier id = key.identifier();
return id.getNamespace().equals(Rivencraft.MOD_ID)
&& (id.getPath().equals("age_bright") || id.getPath().equals("age_cave_bright")
|| id.getPath().startsWith("age_bright_sky_")
|| id.getPath().startsWith("age_cave_bright_sky_"));
}).orElse(false);
}
Holder<DimensionType> dimensionType(MinecraftServer server, PortAgeTerrain terrain,
Holder<DimensionType> overworldType, PortSkyStyle sky) {
boolean cave = terrain == PortAgeTerrain.CAVE;
if (this == NORMAL && !cave && sky == PortSkyStyle.OVERWORLD) return overworldType;
String variant = this == BRIGHT ? (cave ? "age_cave_bright" : "age_bright")
: this == DARK || cave ? "age_dark" : "age_normal";
if (sky != PortSkyStyle.OVERWORLD) variant += "_sky_" + sky.name().toLowerCase(Locale.ROOT);
return server.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(
ResourceKey.create(Registries.DIMENSION_TYPE,
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, variant)));
}
}
@@ -68,7 +68,7 @@ public final class PortRandomAgeSelection {
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
}
private static <T> T weighted(Random random, List<T> choices, int[] weights) {
static <T> T weighted(Random random, List<T> choices, int[] weights) {
int total = 0;
for (int weight : weights) total += weight;
int ticket = random.nextInt(total);
@@ -0,0 +1,57 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
/** Persisted sky styles, including the client-rendered Legacy static background. */
public enum PortSkyStyle {
OVERWORLD, NETHER, END, STATIC;
public static final Codec<PortSkyStyle> CODEC = Codec.STRING.xmap(
name -> valueOf(name.toUpperCase(Locale.ROOT)), sky -> sky.name().toLowerCase(Locale.ROOT));
public static PortSkyStyle forPage(Identifier page) {
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
return switch (page.getPath()) {
case "sky/overworld" -> OVERWORLD;
case "sky/nether" -> NETHER;
case "sky/end" -> END;
case "sky/static" -> STATIC;
default -> null;
};
}
public static PortSkyStyle resolve(List<Identifier> pages, long seed) {
return pages.stream().map(PortSkyStyle::forPage).filter(java.util.Objects::nonNull)
.findFirst().orElseGet(() -> pages.stream().anyMatch(PortSkyStyle::isCelestial)
? OVERWORLD : random(seed));
}
private static boolean isCelestial(Identifier page) {
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
return switch (page.getPath()) {
// Only Overworld currently draws bodies in this port. Recognize the
// Sun/Moon intentions too, without marking those pages implemented.
case "celestial/normal_time", "celestial/sun", "celestial/moon" -> true;
default -> false;
};
}
private static PortSkyStyle random(long seed) {
// Match the first draw in 1.21.1 AgeSkyProfile.random: its equally
// likely INHERITED and OVERWORLD slots currently share the native
// Overworld presentation here. Do not add unported colors/bodies.
int choice = new Random(seed ^ 0x534B5950524F464CL).nextInt(5);
return switch (choice) {
case 0, 1 -> OVERWORLD;
case 2 -> NETHER;
case 3 -> END;
case 4 -> STATIC;
default -> throw new IllegalStateException("Invalid random sky choice");
};
}
}
@@ -1,7 +1,6 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
@@ -26,8 +25,4 @@ public enum PortWeatherMode {
};
}
public static PortWeatherMode fromPages(List<Identifier> pages) {
return pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
.findFirst().orElse(NORMAL);
}
}
@@ -113,13 +113,50 @@ public final class RuntimeAgeBackend {
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, weather, PortLighting.NORMAL);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
PortLighting lighting) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, weather, lighting, PortSkyStyle.OVERWORLD);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
PortLighting lighting, PortSkyStyle sky) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, weather, lighting, sky, PortAgeColors.NATIVE);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
PortLighting lighting, PortSkyStyle sky, PortAgeColors colors) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, weather, lighting, sky, colors, PortDayCycle.NATIVE);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
PortLighting lighting, PortSkyStyle sky, PortAgeColors colors, PortDayCycle dayCycle) {
checkServerThread(server);
ageName(id);
AgeLevelIndex index = index(server);
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, lighting, sky, colors, dayCycle);
((PortAgeLevel) level).configureWeather(weather);
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures, lighting, sky, colors, dayCycle);
// A newly bound book may be used immediately. Persist its Age identity
// before publishing the catalog so a later crash cannot leave that
// book pointing at a world omitted from the on-disk index.
@@ -194,6 +231,10 @@ public final class RuntimeAgeBackend {
PortTerrainFeaturePlan terrainFeatures = index.terrainFeatures(id);
boolean loaded = server.getLevel(dimensionKey(id)) != null;
ArchivePaths paths = archivePaths(server, name);
PortLighting lighting = index.lighting(id);
PortSkyStyle sky = index.sky(id);
PortAgeColors colors = index.colors(id);
PortDayCycle dayCycle = index.dayCycle(id);
if (loaded) unloadLoaded(server, id);
// Persist the tombstone before moving files: a crash mid-archive must
// not let an old book silently regenerate this identity.
@@ -211,7 +252,7 @@ public final class RuntimeAgeBackend {
} catch (IOException | RuntimeException exception) {
try {
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures);
terrainFeatures, lighting, sky, colors, dayCycle);
server.overworld().getDataStorage().saveAndJoin();
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
@@ -234,6 +275,16 @@ public final class RuntimeAgeBackend {
return index(server).entries();
}
public static Map<Identifier, PortAgeColors> savedColors(MinecraftServer server) {
checkServerThread(server);
return index(server).colors();
}
public static Map<Identifier, PortDayCycle> savedDayCycles(MinecraftServer server) {
checkServerThread(server);
return index(server).dayCycles();
}
/** Includes retired IDs, which must never be silently reused by a new book. */
public static boolean isNameTaken(MinecraftServer server, Identifier id) {
checkServerThread(server);
@@ -337,6 +388,15 @@ public final class RuntimeAgeBackend {
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures) {
return load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, index(server).lighting(id), index(server).sky(id), index(server).colors(id), index(server).dayCycle(id));
}
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortLighting lighting, PortSkyStyle sky,
PortAgeColors colors, PortDayCycle dayCycle) {
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
@@ -409,9 +469,12 @@ public final class RuntimeAgeBackend {
}
if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
LevelStem stem = new LevelStem(lighting.dimensionType(server, terrain, overworldStem.type(), sky),
generator, OptionalLong.of(seed));
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
ServerLevel level = new PortAgeLevel(server, key, stem, biomeZoomSeed, terrain);
((PortAgeLevel) level).configureColors(colors);
((PortAgeLevel) level).configureDayCycle(dayCycle);
// Publish only after the constructor succeeds. If the load event fails,
// roll back the map and close the newly opened chunk/entity stores.
@@ -18,10 +18,22 @@ public final class AgeCatalogClient {
private AgeCatalogClient() {
}
@SubscribeEvent
public static void logout(net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent.LoggingOut event) {
net.dnorberg.rivencraft.age.PortAgeColors.updateClient(java.util.Map.of());
net.dnorberg.rivencraft.age.PortDayCycle.updateClient(java.util.Map.of());
}
@SubscribeEvent
public static void register(RegisterClientPayloadHandlersEvent event) {
event.register(AgeCatalogPayload.TYPE, (payload, context) -> {
if (!(context.listener() instanceof ClientPacketListener listener)) return;
net.dnorberg.rivencraft.age.PortAgeColors.updateClient(payload.colors());
net.dnorberg.rivencraft.age.PortDayCycle.updateClient(payload.dayCycles());
if (listener.getLevel() != null) {
listener.getLevel().environmentAttributes().invalidateTickCache();
listener.getLevel().updateSkyBrightness();
}
listener.levels = payload.dimensions().stream()
.map(id -> ResourceKey.<Level>create(Registries.DIMENSION, id))
.collect(Collectors.toUnmodifiableSet());
@@ -0,0 +1,93 @@
package net.dnorberg.rivencraft.client;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.BufferBuilder;
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.MeshData;
import com.mojang.renderpearl.api.buffers.GpuBuffer;
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
import java.util.Optional;
import java.util.OptionalDouble;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.util.RandomSource;
import org.joml.Matrix4f;
/** Legacy's six 48x48 noise faces, uploaded once per animation frame rather than per cell. */
public final class PortStaticSky implements AutoCloseable {
private static final int GRID = 48;
private final DynamicTexture texture = new DynamicTexture("Rivencraft static sky", GRID * 6, GRID, false);
private final GpuBuffer cube = buildCube();
private long uploadedFrame = Long.MIN_VALUE;
private int uploadedTint = -1;
public void render(RenderTarget target, GpuBufferSlice fog, long frame, int tint) {
if (tint < 0) tint = 0x929292;
if (uploadedFrame != frame || uploadedTint != tint) {
RandomSource random = RandomSource.create(frame);
var image = texture.getPixels();
for (int face = 0; face < 6; face++) {
for (int y = 0; y < GRID; y++) {
for (int x = 0; x < GRID; x++) {
int noise = 65 + random.nextInt(160);
int red = (tint >> 16 & 255) * noise / 255;
int green = (tint >> 8 & 255) * noise / 255;
int blue = (tint & 255) * noise / 255;
image.setPixel(face * GRID + x, y, 0xff000000 | red << 16 | green << 8 | blue);
}
}
}
texture.upload();
uploadedFrame = frame;
uploadedTint = tint;
}
RenderSystem.setShaderFog(fog);
var indices = RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
var indexBuffer = indices.getBuffer(36);
var transform = RenderSystem.getDynamicUniforms().writeTransform(RenderSystem.getModelViewMatrixCopy());
try (var pass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(
() -> "Rivencraft static sky", target.getColorTextureView(), Optional.empty(),
target.getDepthTextureView(), OptionalDouble.empty())) {
pass.setPipeline(RenderSystem.getCompiledPipeline(RenderPipelines.END_SKY));
RenderSystem.bindDefaultUniforms(pass);
pass.setUniform("DynamicTransforms", transform);
pass.setUniform("Sampler0", texture.getTextureView(), texture.getSampler());
pass.setVertexBuffer(0, cube.slice());
pass.setIndexBuffer(indexBuffer, indices.type());
pass.drawIndexed(36, 1, 0, 0, 0);
}
}
private static GpuBuffer buildCube() {
var format = DefaultVertexFormat.POSITION_TEX_COLOR;
try (var bytes = ByteBufferBuilder.exactlySized(24 * format.getVertexSize())) {
var builder = new BufferBuilder(bytes, PrimitiveTopology.QUADS, format);
for (int face = 0; face < 6; face++) {
var pose = new Matrix4f();
switch (face) {
case 1 -> pose.rotationX((float) Math.PI / 2);
case 2 -> pose.rotationX((float) -Math.PI / 2);
case 3 -> pose.rotationX((float) Math.PI);
case 4 -> pose.rotationZ((float) Math.PI / 2);
case 5 -> pose.rotationZ((float) -Math.PI / 2);
}
float u0 = face / 6f, u1 = (face + 1) / 6f;
builder.addVertex(pose, -100, -100, -100).setUv(u0, 0).setColor(-1);
builder.addVertex(pose, -100, -100, 100).setUv(u0, 1).setColor(-1);
builder.addVertex(pose, 100, -100, 100).setUv(u1, 1).setColor(-1);
builder.addVertex(pose, 100, -100, -100).setUv(u1, 0).setColor(-1);
}
try (MeshData mesh = builder.buildOrThrow()) {
return RenderSystem.getDevice().createBuffer(() -> "Rivencraft static sky cube", 40, mesh.vertexBuffer());
}
}
}
@Override public void close() {
cube.close();
texture.close();
}
}
@@ -0,0 +1,13 @@
package net.dnorberg.rivencraft.client;
/** Carries extracted sky presentation values without live-world reads during drawing. */
public interface StaticSkyState {
long rivencraft$staticFrame();
void rivencraft$staticFrame(long frame);
int rivencraft$skyTint();
void rivencraft$skyTint(int color);
int rivencraft$orbitBearing();
void rivencraft$orbitBearing(int bearing);
float rivencraft$sunSize();
void rivencraft$sunSize(float size);
}
@@ -0,0 +1,23 @@
package net.dnorberg.rivencraft.mixin;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import net.dnorberg.rivencraft.age.PortLighting;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.lighting.LevelLightEngine;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
/** Ambient light contributes to effective brightness, not fake block/sky propagation. */
@Mixin(LevelLightEngine.class)
public abstract class AgeAmbientLightMixin {
@Shadow @Final protected LevelHeightAccessor levelHeightAccessor;
@ModifyReturnValue(method = "getRawBrightness", at = @At("RETURN"))
private int rivencraft$ambientBrightness(int brightness) {
return levelHeightAccessor instanceof Level level && PortLighting.isBright(level)
? Math.max(brightness, 15) : brightness;
}
}
@@ -0,0 +1,32 @@
package net.dnorberg.rivencraft.mixin;
import net.dnorberg.rivencraft.age.PortAgeColors;
import net.minecraft.world.attribute.EnvironmentAttributeSystem;
import net.minecraft.world.attribute.EnvironmentAttributes;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(EnvironmentAttributeSystem.Builder.class)
public class AgeColorAttributesMixin {
@Inject(method = "addDefaultLayers", at = @At("RETURN"))
private void rivencraft$colors(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.SKY_COLOR, (base, tick) -> {
int rgb = PortAgeColors.forLevel(level).sky();
return rgb < 0 ? base : PortAgeColors.vector(rgb);
});
builder.addTimeBasedLayer(EnvironmentAttributes.FOG_COLOR, (base, tick) -> {
int rgb = PortAgeColors.forLevel(level).sky();
return rgb < 0 ? base : PortAgeColors.vector(rgb);
});
builder.addTimeBasedLayer(EnvironmentAttributes.AMBIENT_LIGHT_COLOR, (base, tick) -> {
int rgb = PortAgeColors.forLevel(level).ambient();
return rgb < 0 ? base : PortAgeColors.vector(rgb);
});
}
}
@@ -0,0 +1,24 @@
package net.dnorberg.rivencraft.mixin;
import net.dnorberg.rivencraft.age.PortDayCycle;
import net.minecraft.world.attribute.EnvironmentAttributeSystem;
import net.minecraft.world.attribute.WeatherAttributes;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(EnvironmentAttributeSystem.Builder.class)
public class AgeDayTimelineMixin {
@Inject(method = "addDynamicLayers", at = @At("HEAD"), cancellable = true)
private void rivencraft$dayCycle(Level level, CallbackInfoReturnable<EnvironmentAttributeSystem.Builder> cir) {
var id = level.dimension().identifier();
if (!id.getNamespace().equals("rivencraft") || !id.getPath().startsWith("age/")) return;
var builder = (EnvironmentAttributeSystem.Builder) (Object) this;
var clocks = PortDayCycle.timelines(level, level.clockManager());
level.dimensionType().timelines().forEach(timeline -> builder.addTimelineLayer(timeline, clocks));
if (level.canHaveWeather()) WeatherAttributes.addBuiltinLayers(builder, WeatherAttributes.WeatherAccess.from(level));
cir.setReturnValue(builder);
}
}
@@ -0,0 +1,102 @@
package net.dnorberg.rivencraft.mixin;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
import net.dnorberg.rivencraft.client.PortStaticSky;
import net.dnorberg.rivencraft.client.StaticSkyState;
import net.minecraft.client.Camera;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.SkyRenderer;
import net.minecraft.client.renderer.state.level.SkyRenderState;
import net.minecraft.world.level.dimension.DimensionType;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(SkyRenderer.class)
public class StaticSkyRendererMixin {
@Shadow @Final private RenderTarget renderTarget;
@Unique private PortStaticSky rivencraft$staticSky;
@Unique private int rivencraft$endTint = -1;
@Unique private int rivencraft$orbitBearing = -1;
@Unique private float rivencraft$sunSize = 1F;
@Inject(method = "extractRenderState", at = @At("TAIL"))
private void rivencraft$extract(ClientLevel level, float partialTicks, Camera camera,
SkyRenderState state, CallbackInfo ci) {
boolean enabled = level.dimensionTypeRegistration().unwrapKey().map(key ->
key.identifier().getNamespace().equals("rivencraft")
&& key.identifier().getPath().endsWith("_sky_static")).orElse(false);
((StaticSkyState) state).rivencraft$staticFrame(enabled ? level.getGameTime() / 2 : Long.MIN_VALUE);
((StaticSkyState) state).rivencraft$skyTint(net.dnorberg.rivencraft.age.PortAgeColors.forLevel(level).sky());
var cycle = net.dnorberg.rivencraft.age.PortDayCycle.forLevel(level);
((StaticSkyState) state).rivencraft$orbitBearing(cycle.authored() ? cycle.bearing() : -1);
((StaticSkyState) state).rivencraft$sunSize(cycle.authored() ? cycle.sunSize() : 1F);
// Only the extracted presentation uses END to schedule the sky pass. The actual
// dimension has NONE, so it never acquires End flashes or End gameplay behavior.
if (enabled) state.skybox = DimensionType.Skybox.END;
}
@Inject(method = "render", at = @At("HEAD"), cancellable = true)
private void rivencraft$render(GpuBufferSlice fog, SkyRenderState state, CallbackInfo ci) {
long frame = ((StaticSkyState) state).rivencraft$staticFrame();
rivencraft$endTint = ((StaticSkyState) state).rivencraft$skyTint();
rivencraft$orbitBearing = ((StaticSkyState) state).rivencraft$orbitBearing();
rivencraft$sunSize = ((StaticSkyState) state).rivencraft$sunSize();
if (frame == Long.MIN_VALUE) return;
if (rivencraft$staticSky == null) rivencraft$staticSky = new PortStaticSky();
rivencraft$staticSky.render(renderTarget, fog, frame, rivencraft$endTint);
ci.cancel();
}
@ModifyArg(method = "renderSunMoonAndStars", at = @At(value = "INVOKE", target =
"Lcom/mojang/blaze3d/vertex/PoseStack;rotateDegrees(Lcom/mojang/math/Axis;F)V", ordinal = 0), index = 1)
private float rivencraft$orbitAngle(float nativeBearing) {
return rivencraft$orbitBearing < 0 ? nativeBearing : rivencraft$orbitBearing;
}
@ModifyArg(method = "renderSun", 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$scaleSun(float nativeSize) {
// 1.21.1 Normal Time applies Size to its sun; its paired moon remains 1x.
return nativeSize * rivencraft$sunSize;
}
@Inject(method = "renderSunriseAndSunset", at = @At("HEAD"))
private void rivencraft$rotateHorizon(com.mojang.renderpearl.api.commands.RenderPass pass,
com.mojang.blaze3d.vertex.PoseStack pose, float angle,
org.joml.Vector4fc color, CallbackInfo ci) {
if (rivencraft$orbitBearing < 0) return;
pose.pushPose();
pose.rotateDegrees(com.mojang.math.Axis.YP, rivencraft$orbitBearing + 90);
}
@Inject(method = "renderSunriseAndSunset", at = @At("TAIL"))
private void rivencraft$restoreHorizon(com.mojang.renderpearl.api.commands.RenderPass pass,
com.mojang.blaze3d.vertex.PoseStack pose, float angle,
org.joml.Vector4fc color, CallbackInfo ci) {
if (rivencraft$orbitBearing >= 0) pose.popPose();
}
@Redirect(method = "renderEndSky", at = @At(value = "INVOKE", target =
"Lnet/minecraft/client/renderer/DynamicGpuData;writeTransform(Lorg/joml/Matrix4f;)Lcom/mojang/renderpearl/api/buffers/GpuBufferSlice;"))
private GpuBufferSlice rivencraft$endColor(net.minecraft.client.renderer.DynamicGpuData uniforms,
org.joml.Matrix4f matrix) {
return rivencraft$endTint < 0 ? uniforms.writeTransform(matrix) : uniforms.writeTransform(matrix,
new org.joml.Vector4f(net.dnorberg.rivencraft.age.PortAgeColors.vector(rivencraft$endTint), 1));
}
@Inject(method = "close", at = @At("HEAD"))
private void rivencraft$close(CallbackInfo ci) {
if (rivencraft$staticSky != null) {
rivencraft$staticSky.close();
rivencraft$staticSky = null;
}
}
}
@@ -0,0 +1,33 @@
package net.dnorberg.rivencraft.mixin;
import net.dnorberg.rivencraft.client.StaticSkyState;
import net.minecraft.client.renderer.state.level.SkyRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(SkyRenderState.class)
public class StaticSkyStateMixin implements StaticSkyState {
@Unique private long rivencraft$frame = Long.MIN_VALUE;
@Unique private int rivencraft$tint = -1;
@Unique private int rivencraft$bearing = -1;
@Unique private float rivencraft$sunSize = 1F;
public long rivencraft$staticFrame() { return rivencraft$frame; }
public void rivencraft$staticFrame(long frame) { rivencraft$frame = frame; }
public int rivencraft$skyTint() { return rivencraft$tint; }
public void rivencraft$skyTint(int color) { rivencraft$tint = color; }
public int rivencraft$orbitBearing() { return rivencraft$bearing; }
public void rivencraft$orbitBearing(int bearing) { rivencraft$bearing = bearing; }
public float rivencraft$sunSize() { return rivencraft$sunSize; }
public void rivencraft$sunSize(float size) { rivencraft$sunSize = size; }
@Inject(method = "reset", at = @At("TAIL"))
private void rivencraft$reset(CallbackInfo ci) {
rivencraft$frame = Long.MIN_VALUE;
rivencraft$tint = -1;
rivencraft$bearing = -1;
rivencraft$sunSize = 1F;
}
}
@@ -134,13 +134,17 @@ public final class PortBookNetworking {
terrain = random.terrain();
biomePlan = random.biomes();
}
var environment = net.dnorberg.rivencraft.age.PortEnvironmentSelection.resolve(pages, seed, terrain);
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
PortAuthoredWriting.rockPlan(pages, seed),
PortAuthoredWriting.vegetationPlan(pages),
PortAuthoredWriting.dungeonCount(pages),
PortAuthoredWriting.lakePlan(pages, seed),
PortAuthoredWriting.terrainFeaturePlan(pages, seed),
net.dnorberg.rivencraft.age.PortWeatherMode.fromPages(pages));
environment.weather(), environment.lighting(), environment.sky(),
net.dnorberg.rivencraft.age.PortAgeColors.compile(pages, seed),
net.dnorberg.rivencraft.age.PortDayCycle.compile(pages, seed,
net.dnorberg.rivencraft.age.PortDayCycle.nativeTicks(server.overworld())));
} catch (IllegalArgumentException | IllegalStateException exception) {
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
return null;
@@ -15,6 +15,8 @@ import net.dnorberg.rivencraft.age.PortTerrainFeaturePlan;
import net.dnorberg.rivencraft.age.PortStructurePages;
import net.dnorberg.rivencraft.age.PortCarverPages;
import net.dnorberg.rivencraft.age.PortWeatherMode;
import net.dnorberg.rivencraft.age.PortLighting;
import net.dnorberg.rivencraft.age.PortSkyStyle;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.Registries;
@@ -53,7 +55,10 @@ public final class PortAuthoredWriting {
if (page == null || !page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
var entry = PortSymbolPages.find(page);
if (entry == null) return false;
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null) return true;
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) return true;
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null
|| PortLighting.forPage(page) != null || PortSkyStyle.forPage(page) != null) return true;
if (entry.category().equals("rivencraft:biome") && PortRegistryPages.biomeId(page) != null) return true;
if (isBlockPage(page) && (entry.category().equals("rivencraft:block")
|| entry.category().equals("rivencraft:fluid"))) return true;
@@ -67,6 +72,8 @@ public final class PortAuthoredWriting {
/** A page never disappears into an Age unless this port can apply its effect. */
public static boolean supports(List<Identifier> pages) {
if (!net.dnorberg.rivencraft.age.PortAgeColors.validNumbers(pages)) return false;
if (!net.dnorberg.rivencraft.age.PortDayCycle.validWriting(pages)) return false;
boolean terrain = false;
int biomes = 0;
int controllers = 0;
@@ -79,6 +86,8 @@ public final class PortAuthoredWriting {
int denseOres = 0;
int starFissures = 0;
int weather = 0;
int lighting = 0;
int sky = 0;
Map<Identifier, Integer> terrainFeatures = new HashMap<>();
Map<Identifier, Integer> structures = new HashMap<>();
Map<Identifier, Integer> carvers = new HashMap<>();
@@ -86,8 +95,15 @@ public final class PortAuthoredWriting {
if (pendingMaterial != null && !(pendingMaterial.equals("rivencraft:block")
&& (page.equals(ROCKS) || PortTerrainFeaturePlan.acceptsMaterial(page))
|| pendingMaterial.equals("rivencraft:fluid") && PortLakePlan.isLake(page))) return false;
if (isTerrain(page) && !terrain) {
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) {
// Shared color groups can precede a sky/ambient target or tint the sky at the end.
} else if (isTerrain(page) && !terrain) {
terrain = true;
} else if (PortSkyStyle.forPage(page) != null && sky++ == 0) {
// Sky presentation, independent of terrain and illumination.
} else if (PortLighting.forPage(page) != null && lighting++ == 0) {
// A single lighting controller; omitted choices resolve from the Age seed.
} else if (PortWeatherMode.forPage(page) != null && weather++ == 0) {
// A single independent weather policy for this Age.
} else if (controller(page) != null && controllers++ == 0) {
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.lighting.ambient_color","description":"symbol.rivencraft.lighting.ambient_color.description","category":"rivencraft:lighting","poem":"Ethereal Red Sustain Balance"}
@@ -3,6 +3,7 @@
"minVersion": "0.8",
"package": "net.dnorberg.rivencraft.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin"],
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin", "AgeAmbientLightMixin", "AgeColorAttributesMixin", "AgeDayTimelineMixin"],
"client": ["StaticSkyStateMixin", "StaticSkyRendererMixin"],
"injectors": {"defaultRequire": 1}
}