From 38e40ddba3e957c83f731f39facbd5b8f79a1729 Mon Sep 17 00:00:00 2001 From: Dustin Norberg Date: Tue, 3 Nov 2026 16:56:07 -0500 Subject: [PATCH] Port seeded sky colors and Normal Time celestial controls to 26.3 --- README.md | 5 +- docs/MINECRAFT_26_3_PORT.md | 238 ++++++++++++ ports/mc263/README.md | 249 +++++++++++++ ports/mc263/RELEASE_NOTES_1.0.1.78-beta.1.md | 53 +++ ports/mc263/gradle.properties | 2 +- ports/mc263/port_en_us.json | 28 +- .../rivencraft/age/AgeCatalogPayload.java | 10 +- .../rivencraft/age/AgeCatalogSync.java | 5 +- .../rivencraft/age/AgeLevelIndex.java | 30 +- .../rivencraft/age/PortAgeColors.java | 101 +++++ .../rivencraft/age/PortAgeCommands.java | 351 +++++++++++++++++- .../dnorberg/rivencraft/age/PortAgeLevel.java | 15 + .../dnorberg/rivencraft/age/PortDayCycle.java | 212 +++++++++++ .../age/PortEnvironmentSelection.java | 4 +- .../dnorberg/rivencraft/age/PortSkyStyle.java | 30 +- .../rivencraft/age/RuntimeAgeBackend.java | 43 ++- .../rivencraft/client/AgeCatalogClient.java | 12 + .../rivencraft/client/PortStaticSky.java | 14 +- .../rivencraft/client/StaticSkyState.java | 8 +- .../mixin/AgeColorAttributesMixin.java | 32 ++ .../rivencraft/mixin/AgeDayTimelineMixin.java | 24 ++ .../mixin/StaticSkyRendererMixin.java | 51 ++- .../rivencraft/mixin/StaticSkyStateMixin.java | 16 +- .../network/PortBookNetworking.java | 6 +- .../writing/PortAuthoredWriting.java | 9 +- .../symbols/lighting/ambient_color.json | 1 + .../src/main/resources/rivencraft.mixins.json | 2 +- 27 files changed, 1516 insertions(+), 35 deletions(-) create mode 100644 ports/mc263/RELEASE_NOTES_1.0.1.78-beta.1.md create mode 100644 ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeColors.java create mode 100644 ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortDayCycle.java create mode 100644 ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeColorAttributesMixin.java create mode 100644 ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeDayTimelineMixin.java create mode 100644 ports/mc263/src/main/resources/data/rivencraft/rivencraft/symbols/lighting/ambient_color.json diff --git a/README.md b/README.md index 807003c..de67953 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/MINECRAFT_26_3_PORT.md b/docs/MINECRAFT_26_3_PORT.md index a15ceb8..ca0c116 100644 --- a/docs/MINECRAFT_26_3_PORT.md +++ b/docs/MINECRAFT_26_3_PORT.md @@ -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 diff --git a/ports/mc263/README.md b/ports/mc263/README.md index 1baaf38..26a02ab 100644 --- a/ports/mc263/README.md +++ b/ports/mc263/README.md @@ -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 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 ` creates a repeatable Flat, +Bright fixture with sky `#643296` and ambient `#6496c8`; `color_probe ` +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 [number]` and +`cycle_probe ` 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 + [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 + [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 + [number]` creates a frozen-noon fixture without explicit +Sky; `cycle_probe ` 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 diff --git a/ports/mc263/RELEASE_NOTES_1.0.1.78-beta.1.md b/ports/mc263/RELEASE_NOTES_1.0.1.78-beta.1.md new file mode 100644 index 0000000..7bc8ac3 --- /dev/null +++ b/ports/mc263/RELEASE_NOTES_1.0.1.78-beta.1.md @@ -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). diff --git a/ports/mc263/gradle.properties b/ports/mc263/gradle.properties index abeb29f..63c42fb 100644 --- a/ports/mc263/gradle.properties +++ b/ports/mc263/gradle.properties @@ -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 diff --git a/ports/mc263/port_en_us.json b/ports/mc263/port_en_us.json index a3f5cba..cf9c570 100644 --- a/ports/mc263/port_en_us.json +++ b/ports/mc263/port_en_us.json @@ -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.", diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeCatalogPayload.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeCatalogPayload.java index 18cc06f..a228739 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeCatalogPayload.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeCatalogPayload.java @@ -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 dimensions) implements CustomPacketPayload { +public record AgeCatalogPayload(int version, List dimensions, + Map colors, + Map dayCycles) implements CustomPacketPayload { public static final Type TYPE = new Type<>( Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age_catalog")); public static final StreamCodec 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 diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeCatalogSync.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeCatalogSync.java index 0496456..012c3a5 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeCatalogSync.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeCatalogSync.java @@ -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) { diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeLevelIndex.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeLevelIndex.java index 6da72ca..a3744a2 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeLevelIndex.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeLevelIndex.java @@ -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 TYPE = new SavedDataType<>( @@ -59,6 +63,8 @@ final class AgeLevelIndex extends SavedData { private final Map terrainFeatures; private final Map lighting; private final Map sky; + private final Map colors; + private final Map 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 ages, List retired, @@ -85,7 +93,8 @@ final class AgeLevelIndex extends SavedData { Map lakes, Map terrainFeatures, Map lighting, - Map sky) { + Map sky, Map colors, + Map 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 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 colors() { return Map.copyOf(colors); } + PortDayCycle dayCycle(Identifier id) { return dayCycles.getOrDefault(id, PortDayCycle.NATIVE); } + Map 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(); } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeColors.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeColors.java new file mode 100644 index 0000000..8447e55 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeColors.java @@ -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 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 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 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 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 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 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); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeCommands.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeCommands.java index 6a41929..0321b3d 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeCommands.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeCommands.java @@ -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 context, int number) { + try { + java.util.function.Function symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path); + var pages = new java.util.ArrayList(); + 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 context, int number) { + try { + java.util.function.Function symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path); + var pages = new java.util.ArrayList(); + 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 context, int number) { + try { + java.util.function.Function symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path); + var pages = new java.util.ArrayList(); + 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 context, int number) { + try { + java.util.function.Function symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path); + var pages = new java.util.ArrayList(); + 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 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 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 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 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 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 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; diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeLevel.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeLevel.java index 1e755ac..49c743c 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeLevel.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeLevel.java @@ -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 key, LevelStem stem, long biomeZoomSeed, PortAgeTerrain terrain) { diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortDayCycle.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortDayCycle.java new file mode 100644 index 0000000..2379727 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortDayCycle.java @@ -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 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 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 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 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 pages, long seed) { + return compile(pages, seed, 0); + } + + public static PortDayCycle compile(List 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 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, 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; } + }); + }; + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortEnvironmentSelection.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortEnvironmentSelection.java index 7266020..b64e446 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortEnvironmentSelection.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortEnvironmentSelection.java @@ -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 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 rankedDefault(Random random, T[] choices) { diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortSkyStyle.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortSkyStyle.java index a737af4..38f0492 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortSkyStyle.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortSkyStyle.java @@ -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 pages) { + public static PortSkyStyle resolve(List 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"); + }; } } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/RuntimeAgeBackend.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/RuntimeAgeBackend.java index 624d22f..ef07ab3 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/RuntimeAgeBackend.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/RuntimeAgeBackend.java @@ -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 savedColors(MinecraftServer server) { + checkServerThread(server); + return index(server).colors(); + } + + public static Map 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 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. diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/AgeCatalogClient.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/AgeCatalogClient.java index 2035825..b2e4d11 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/AgeCatalogClient.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/AgeCatalogClient.java @@ -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.create(Registries.DIMENSION, id)) .collect(Collectors.toUnmodifiableSet()); diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortStaticSky.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortStaticSky.java index a562297..81c881e 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortStaticSky.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortStaticSky.java @@ -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); diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/StaticSkyState.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/StaticSkyState.java index 8852d3f..a4c1116 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/StaticSkyState.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/StaticSkyState.java @@ -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); } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeColorAttributesMixin.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeColorAttributesMixin.java new file mode 100644 index 0000000..a845287 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeColorAttributesMixin.java @@ -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 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); + }); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeDayTimelineMixin.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeDayTimelineMixin.java new file mode 100644 index 0000000..43b6081 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeDayTimelineMixin.java @@ -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 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); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyRendererMixin.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyRendererMixin.java index ee3da38..b8710bd 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyRendererMixin.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyRendererMixin.java @@ -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) { diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyStateMixin.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyStateMixin.java index a732875..0bec028 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyStateMixin.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyStateMixin.java @@ -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; + } } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java index b4b5d28..0cdd178 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java @@ -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; diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/PortAuthoredWriting.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/PortAuthoredWriting.java index 26576a4..ff6d4f3 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/PortAuthoredWriting.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/PortAuthoredWriting.java @@ -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 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. diff --git a/ports/mc263/src/main/resources/data/rivencraft/rivencraft/symbols/lighting/ambient_color.json b/ports/mc263/src/main/resources/data/rivencraft/rivencraft/symbols/lighting/ambient_color.json new file mode 100644 index 0000000..390c491 --- /dev/null +++ b/ports/mc263/src/main/resources/data/rivencraft/rivencraft/symbols/lighting/ambient_color.json @@ -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"} diff --git a/ports/mc263/src/main/resources/rivencraft.mixins.json b/ports/mc263/src/main/resources/rivencraft.mixins.json index f0b8dac..55d5576 100644 --- a/ports/mc263/src/main/resources/rivencraft.mixins.json +++ b/ports/mc263/src/main/resources/rivencraft.mixins.json @@ -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} }