Public Access
Port seeded sky colors and Normal Time celestial controls to 26.3
Build / build (push) Canceled after 0s
Build / build (push) Canceled after 0s
This commit is contained in:
1 parent
a7a73d3c17
commit
38e40ddba3
27 files changed
+1516
-35
No files matched your search
@@ -14,11 +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/lighting, Overworld/Nether/End and animated Static skies,
|
||||
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.71-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
|
||||
|
||||
@@ -1065,6 +1065,244 @@ 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
|
||||
|
||||
@@ -698,6 +698,255 @@ 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,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).
|
||||
@@ -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.71-beta.1
|
||||
mod_version=1.0.1.78-beta.1
|
||||
@@ -11,7 +11,33 @@
|
||||
"symbol.rivencraft.sky.overworld.description": "Use the native Overworld sky. Terrain and lighting remain independently authored.",
|
||||
"symbol.rivencraft.sky.nether.description": "Use a Nether-style sky and reddish fog, without Nether terrain or gameplay rules.",
|
||||
"symbol.rivencraft.sky.end.description": "Use the native End sky backdrop, without End terrain or a dragon fight.",
|
||||
"symbol.rivencraft.sky.static.description": "An animated gray static sky surrounds the Age.",
|
||||
"symbol.rivencraft.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.",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -38,7 +38,11 @@ final class AgeLevelIndex extends SavedData {
|
||||
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)
|
||||
.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<>(
|
||||
@@ -59,6 +63,8 @@ final class AgeLevelIndex extends SavedData {
|
||||
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<>();
|
||||
@@ -73,6 +79,8 @@ final class AgeLevelIndex extends SavedData {
|
||||
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,
|
||||
@@ -85,7 +93,8 @@ final class AgeLevelIndex extends SavedData {
|
||||
Map<Identifier, PortLakePlan> lakes,
|
||||
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures,
|
||||
Map<Identifier, PortLighting> lighting,
|
||||
Map<Identifier, PortSkyStyle> sky) {
|
||||
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);
|
||||
@@ -98,6 +107,8 @@ final class AgeLevelIndex extends SavedData {
|
||||
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() {
|
||||
@@ -130,10 +141,15 @@ final class AgeLevelIndex extends SavedData {
|
||||
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, PortLighting light, PortSkyStyle skyStyle) {
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light, PortSkyStyle skyStyle,
|
||||
PortAgeColors colorProfile, PortDayCycle dayCycle) {
|
||||
if (contains(id)) {
|
||||
throw new IllegalArgumentException("Age already exists: " + id);
|
||||
}
|
||||
@@ -147,6 +163,8 @@ final class AgeLevelIndex extends SavedData {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -162,6 +180,8 @@ final class AgeLevelIndex extends SavedData {
|
||||
terrainFeatures.remove(id);
|
||||
lighting.remove(id);
|
||||
sky.remove(id);
|
||||
colors.remove(id);
|
||||
dayCycles.remove(id);
|
||||
retired.add(id);
|
||||
setDirty();
|
||||
}
|
||||
@@ -170,7 +190,7 @@ final class AgeLevelIndex extends SavedData {
|
||||
PortAgeTerrain ageTerrain, PortRockPlan rockPlan,
|
||||
PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light,
|
||||
PortSkyStyle skyStyle) {
|
||||
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);
|
||||
@@ -182,6 +202,8 @@ final class AgeLevelIndex extends SavedData {
|
||||
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,51 @@ 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())
|
||||
@@ -347,6 +392,277 @@ 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));
|
||||
@@ -402,11 +718,38 @@ public final class PortAgeCommands {
|
||||
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())
|
||||
|| 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"), false);
|
||||
+ " deterministic=true skyOverrides=true celestialFallback=true"), false);
|
||||
return 1;
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
@@ -741,7 +1084,7 @@ public final class PortAgeCommands {
|
||||
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));
|
||||
new PortEnvironmentSelection(PortWeatherMode.NORMAL, PortLighting.NORMAL, PortSkyStyle.OVERWORLD));
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
@@ -756,7 +1099,7 @@ public final class PortAgeCommands {
|
||||
try {
|
||||
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
|
||||
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
|
||||
environment.weather(), environment.lighting());
|
||||
environment.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; }
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,14 @@ import java.util.Random;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Seed-completes missing environment controllers once, before an Age is saved. */
|
||||
public record PortEnvironmentSelection(PortWeatherMode weather, PortLighting lighting) {
|
||||
public 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);
|
||||
return new PortEnvironmentSelection(weather, lighting, PortSkyStyle.resolve(pages, seed));
|
||||
}
|
||||
|
||||
private static <T> T rankedDefault(Random random, T[] choices) {
|
||||
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
|
||||
@@ -24,8 +25,33 @@ public enum PortSkyStyle {
|
||||
};
|
||||
}
|
||||
|
||||
public static PortSkyStyle fromPages(List<Identifier> pages) {
|
||||
public static PortSkyStyle resolve(List<Identifier> pages, long seed) {
|
||||
return pages.stream().map(PortSkyStyle::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElse(OVERWORLD);
|
||||
.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");
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -131,14 +131,32 @@ public final class RuntimeAgeBackend {
|
||||
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, lighting, sky);
|
||||
terrainFeatures, lighting, sky, colors, dayCycle);
|
||||
((PortAgeLevel) level).configureWeather(weather);
|
||||
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures, lighting, sky);
|
||||
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.
|
||||
@@ -215,6 +233,8 @@ public final class RuntimeAgeBackend {
|
||||
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.
|
||||
@@ -232,7 +252,7 @@ public final class RuntimeAgeBackend {
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
try {
|
||||
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, lighting, sky);
|
||||
terrainFeatures, lighting, sky, colors, dayCycle);
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
@@ -255,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);
|
||||
@@ -359,13 +389,14 @@ public final class RuntimeAgeBackend {
|
||||
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));
|
||||
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) {
|
||||
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);
|
||||
|
||||
@@ -442,6 +473,8 @@ public final class RuntimeAgeBackend {
|
||||
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());
|
||||
|
||||
@@ -22,21 +22,27 @@ public final class PortStaticSky implements AutoCloseable {
|
||||
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) {
|
||||
if (uploadedFrame != frame) {
|
||||
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 gray = 146 * (65 + random.nextInt(160)) / 255;
|
||||
image.setPixel(face * GRID + x, y, 0xff000000 | gray << 16 | gray << 8 | gray);
|
||||
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);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
/** Carries the extracted animation frame without consulting the live world during drawing. */
|
||||
/** 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,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);
|
||||
}
|
||||
}
|
||||
+50
-1
@@ -15,12 +15,17 @@ 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,
|
||||
@@ -29,6 +34,10 @@ public class StaticSkyRendererMixin {
|
||||
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;
|
||||
@@ -37,12 +46,52 @@ public class StaticSkyRendererMixin {
|
||||
@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$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) {
|
||||
|
||||
@@ -11,9 +11,23 @@ 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; }
|
||||
private void rivencraft$reset(CallbackInfo ci) {
|
||||
rivencraft$frame = Long.MIN_VALUE;
|
||||
rivencraft$tint = -1;
|
||||
rivencraft$bearing = -1;
|
||||
rivencraft$sunSize = 1F;
|
||||
}
|
||||
}
|
||||
@@ -141,8 +141,10 @@ public final class PortBookNetworking {
|
||||
PortAuthoredWriting.dungeonCount(pages),
|
||||
PortAuthoredWriting.lakePlan(pages, seed),
|
||||
PortAuthoredWriting.terrainFeaturePlan(pages, seed),
|
||||
environment.weather(), environment.lighting(),
|
||||
net.dnorberg.rivencraft.age.PortSkyStyle.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;
|
||||
|
||||
@@ -55,6 +55,8 @@ 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 (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;
|
||||
@@ -70,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;
|
||||
@@ -91,7 +95,10 @@ 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.
|
||||
|
||||
+1
@@ -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,7 +3,7 @@
|
||||
"minVersion": "0.8",
|
||||
"package": "net.dnorberg.rivencraft.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin", "AgeAmbientLightMixin"],
|
||||
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin", "AgeAmbientLightMixin", "AgeColorAttributesMixin", "AgeDayTimelineMixin"],
|
||||
"client": ["StaticSkyStateMixin", "StaticSkyRendererMixin"],
|
||||
"injectors": {"defaultRequire": 1}
|
||||
}
|
||||
Reference in new issue
Block a user