Public Access
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40770841ee | ||
|
|
38e40ddba3 | ||
|
|
a7a73d3c17 |
No files matched your search
@@ -14,10 +14,13 @@ The gameplay guide below describes the **Minecraft 1.21.1 release**. A separate,
|
||||
incomplete **Minecraft 26.3 beta** is available: Java 25, NeoForge 26.3.0.99,
|
||||
and a first-party Age backend (no Dynamic Dimensions or Bad Packets). It now
|
||||
supports authored terrain/biomes, structure and vegetation pages, independent
|
||||
Age weather, and a growing set of generation features,
|
||||
but does not yet provide full 1.21.1 gameplay or optional-integration parity.
|
||||
Age weather/lighting, seeded and colorable Overworld/Nether/End/Static skies,
|
||||
independent Sun/Moon controls, multiple bodies and eclipses, seeded biome and
|
||||
feature completion, and random structure selection. Nether/End noise-biome
|
||||
materials now follow their native rules. The beta does not yet provide full
|
||||
1.21.1 gameplay or optional-integration parity.
|
||||
See the [26.3 beta guide](ports/mc263/README.md) and
|
||||
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.65-beta.1.md).
|
||||
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.93-beta.1.md).
|
||||
Keep the builds and their saves separate, and back up worlds before updating.
|
||||
|
||||
## Getting started
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 238 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
@@ -900,6 +900,907 @@ loaded; that fixture is not claimed as tested. The server shut down cleanly,
|
||||
and its temporary world was deleted. Actual player arrival in the affected
|
||||
Age and the unchanged Cave/Linking Book paths remain manual checks.
|
||||
|
||||
The local `1.0.1.66-beta.1` slice ports the three 1.21.1 lighting controls.
|
||||
Normal surface types keep the host Overworld holder; Dark disables skylight,
|
||||
Bright sets ambient light to 0.25. Cave Bright is explicitly supported with
|
||||
0.25 ambient and no skylight; Cave Normal/Dark have 0.0 ambient and no skylight.
|
||||
The native vanilla registry-sync path supplies types to clients before travel.
|
||||
Generated data derives three variants from the exact 26.3 vanilla Overworld
|
||||
resource, preserving every key except has_skylight and ambient_light. The
|
||||
generator has declared inputs/outputs and depends on Minecraft artifacts.
|
||||
|
||||
The Age index has an optional lighting map, defaulting old entries to Normal.
|
||||
Creation supplies the choice before constructing the level and saving the
|
||||
index. Restore/reload/reseed read it from the index; retirement removes it and
|
||||
failed retirement restores it before reloading. Pages share grammar/tint and
|
||||
the positive biome-page filter, avoiding the weather-only compiler regression.
|
||||
Missing lighting stays Normal until the random-trait port; Bright instability
|
||||
is pending with the instability system. No sky/celestial renderer is added.
|
||||
The static variants derive vanilla data, not arbitrary data-pack overrides
|
||||
to the host's Overworld type; broader custom type inheritance is not claimed.
|
||||
|
||||
Build/diff checks and generated-data equality checks pass (only the two
|
||||
lighting fields differ from vanilla). A disposable server with a normal
|
||||
Overworld compiled all six surface/Cave lighting-page combinations and
|
||||
reported the expected type, skylight and ambient values. Cave Bright survived
|
||||
unload/reload and reseeding, and all six choices survived a full restart.
|
||||
Overworld skylight/ambient stayed unchanged. An earlier Superflat-host fixture
|
||||
could test surface variants but could not create Cave terrain because the
|
||||
existing backend requires a noise-based host generator; it was discarded and
|
||||
the Cave checks ran on a fresh normal-host fixture. Test servers stopped
|
||||
cleanly and both temporary worlds were deleted. Client-side appearance,
|
||||
authoring, travel, placed lights and multiplayer registry behavior are manual
|
||||
checks, not inferred from the server probes. No commit/push/publication.
|
||||
|
||||
The local `1.0.1.67-beta.1` corrects the visual failure found in slice 66:
|
||||
LightmapRenderStateExtractor and lightmap.fsh use AMBIENT_LIGHT_COLOR, while
|
||||
dimension ambient_light only affects other brightness queries. Merely porting
|
||||
1.21.1's 0.25 field was insufficient. Bright types now provide white native
|
||||
ambient color and ambient_light 1.0; Cave Bright retains no skylight. A scoped
|
||||
NeoForge spawn-placement handler denies natural/chunk-generation MONSTER
|
||||
spawns in Bright runtime Ages, leaving passive/existing mobs, spawners,
|
||||
commands/eggs and event spawns untouched. It does not manufacture propagated
|
||||
block light or alter crop light checks. This supersedes slice 66's Bright
|
||||
appearance and natural-spawn limitation; existing Bright types update on boot.
|
||||
|
||||
Requested follow-up for the color-page port: add ambient-light color as a
|
||||
separate authorable target using minecraft:visual/ambient_light_color. Keep
|
||||
the existing number/color mixing semantics and white Bright default; do not
|
||||
confuse ambient tint with sky/celestial color or its server spawn policy.
|
||||
Color pages themselves remain unported in this slice.
|
||||
Build/diff and generated attribute checks pass. A disposable dedicated server
|
||||
verified the registered event handler denies a successful-default natural
|
||||
zombie placement in surface/Cave Bright, leaves spawner zombies and natural
|
||||
cows unchanged, and leaves Normal unaffected. Those are isolated event-policy
|
||||
probes, not a long-running natural-spawn census. The server shut down and its
|
||||
temporary world was deleted. Actual client appearance awaits player validation.
|
||||
|
||||
The local `1.0.1.68-beta.1` removes the requested-against MobSpawnEvent veto.
|
||||
An Age-scoped LevelLightEngine.getRawBrightness return modifier contributes
|
||||
Bright's full ambient minimum of 15 to combined brightness on both sides.
|
||||
Vanilla Monster.isDarkEnoughToSpawn now sees that light, rather than being
|
||||
overridden or filtered by mob category/spawn reason. Light-ignoring mobs and
|
||||
special spawns retain vanilla rules; no spawner exemptions are introduced.
|
||||
Block/sky storage and propagation remain physical, while combined-light
|
||||
consumers (including plant checks) see ambient light. Cave Bright retains
|
||||
no skylight, and the native white render attribute remains. This supersedes
|
||||
slice 67's separate spawn policy and unchanged-crop-light behavior.
|
||||
|
||||
The lighting probe now calls vanilla's darkness predicate and combined
|
||||
brightness instead of constructing synthetic spawn events. Ambient-color
|
||||
authoring remains a planned part of the color-page port.
|
||||
Build/diff checks pass. A disposable dedicated server loaded the required
|
||||
mixin and reported effective brightness 15 / vanilla darkness false for
|
||||
surface/Cave Bright, versus 0 / true for Dark. These are direct brightness/
|
||||
vanilla-predicate probes, not a natural-spawn census or a fully generated
|
||||
underground-room test; an unloaded Normal column returned native sky light 15.
|
||||
The server stopped cleanly and its temporary world was deleted. Client visual
|
||||
validation remains with the player; no publication is performed.
|
||||
|
||||
The local `1.0.1.69-beta.1` slice completes missing weather/lighting choices
|
||||
at book binding. PortEnvironmentSelection resolves explicit supported pages
|
||||
first and uses independent seed-salted Legacy-rank rolls for omissions.
|
||||
Normal is rank 1, remaining supported modes rank 2: 3:1:1 lighting and
|
||||
6:1:1:1:1:1 weather. These weights derive from the active enum pool and reuse
|
||||
the existing weighted ticket routine, rather than duplicating random-choice
|
||||
logic or enabling unported Cloudy/Rain/Snow modes. It is rank-policy parity,
|
||||
not exact seed-stream parity with 1.21.1's broader trait pool.
|
||||
|
||||
The selected weather/lighting are passed through the existing save-before-
|
||||
catalog creation path. Restoring Ages never calls the resolver, so old Ages
|
||||
retain their saved values. Cave weather is always Off; Cave lighting remains
|
||||
independent and supports Bright. Explicit developer fixtures remain controlled,
|
||||
but create_random now uses the same resolver as pageless books. A read-only
|
||||
environment probe checks seed repeatability and explicit overrides. Superseded
|
||||
Normal-only fromPages helpers are removed. Random feature, sky/celestial,
|
||||
blocklist policy and instability work remain pending.
|
||||
Build/diff checks pass. Disposable dedicated-server probes repeated seeds
|
||||
0/4/7, verified explicit overrides and independent one-field completion, and
|
||||
confirmed Cave weather Off with Bright lighting. Three random Ages saved
|
||||
Always/Normal, Normal/Dark and Fast/Bright respectively; weather/lighting
|
||||
choices and live weather timers persisted through a full server restart.
|
||||
Overworld weather/type stayed unchanged. The server stopped cleanly and its
|
||||
temporary world was deleted. Player book-authoring/visual checks remain
|
||||
manual. No commit, push or publication is performed.
|
||||
|
||||
The local `1.0.1.70-beta.1` slice ports native Overworld/Nether/End sky styles.
|
||||
The shared grammar accepts one style and the biome compiler ignores it by
|
||||
category. Sky is saved in an optional Age-index map with Overworld fallback;
|
||||
restore/reseed/retirement rollback use the saved choice. It does not alter
|
||||
the missing-weather/lighting seed-completion rules, add random sky selection,
|
||||
or enable Static Sky/color/individual-celestial pages prematurely.
|
||||
|
||||
The dimension-data generator now produces 11 sky/lighting variants from the
|
||||
vanilla Overworld baseline. It changes visual skybox/fog/cloud settings only
|
||||
in addition to lighting. Geometry, scale, ceiling, monster settings, default
|
||||
clock and gameplay attributes are retained; no Nether gameplay or End dragon
|
||||
fight is inherited. A derived native day timeline omits only sky/fog/cloud
|
||||
color tracks for non-Overworld styles, preserving schedules and illumination.
|
||||
Native End/no-sky rendering needs no optional renderer or new artwork. The
|
||||
Legacy animated Static Sky renderer cannot be replaced by native flat gray
|
||||
without losing its behavior, so its page remains red/unimplemented.
|
||||
|
||||
Bright detection includes all registered sky variants, preserving both white
|
||||
ambient rendering and the effective brightness floor for surface/Cave Bright.
|
||||
Data equality checks confirm all 11 variants preserve geometry/gameplay and
|
||||
the day timeline keeps native tracks except the three excluded visual colors.
|
||||
Its global time markers remain defined only by minecraft:day, not duplicated
|
||||
in the derived timeline. The grouped timeline list is registered as a tag.
|
||||
Client appearance and multiplayer travel validation remain manual.
|
||||
Registry validation initially caught an invalid inline tag reference and
|
||||
duplicate global clock markers in the derived day timeline; both were fixed
|
||||
before handoff. The sky diagnostic also now supplies a position when sampling
|
||||
the spatial fog attribute. A disposable dedicated server then restored all
|
||||
sampled types after restart, including Nether normal/Bright and End normal/
|
||||
Cave Bright, with the expected reddish/purple fog at the sample position,
|
||||
unchanged geometry/gameplay flags, and effective light 15 on Bright variants.
|
||||
Cave Bright End retained its type after unload/reload; a reseeded Nether Age
|
||||
retained its style after restart. Servers stopped cleanly and temporary test
|
||||
directories/worlds were deleted. These are server/data checks, not a rendered
|
||||
client pass. No commit/push/publication is performed.
|
||||
|
||||
## Static Sky renderer slice
|
||||
|
||||
`1.0.1.71-beta.1` ports the 1.21.1 Legacy-style animated Static Sky, superseding
|
||||
the previous unsupported/red status. The six independent 48×48 noise faces
|
||||
use the existing default gray tint (146 per channel), noise range 65–224,
|
||||
and a two-world-tick refresh. A cached 24-vertex cube and a single 288×48
|
||||
dynamic texture use the native textured sky pipeline instead of rebuilding
|
||||
thousands of colored cells every frame. No new asset or dependency is needed.
|
||||
|
||||
Client-only mixins carry the animation frame in extracted sky state, reset
|
||||
it with the state, and own/close GPU resources with SkyRenderer. Static types
|
||||
have native skybox NONE; the extracted state alone requests a sky pass and
|
||||
the custom renderer replaces it. End flashes and End gameplay are not enabled.
|
||||
Normal/Dark/Bright/Cave Bright variants preserve the existing lighting policy,
|
||||
geometry and gameplay. Existing persistence handles STATIC as a sky enum value.
|
||||
Random sky selection, color pages, and individual celestial bodies remain
|
||||
separate unfinished work; this slice does not claim complete sky parity.
|
||||
|
||||
Verification: port build passed (no automated unit tests configured). A
|
||||
disposable dedicated server created Static Standard Normal and Static Cave
|
||||
Bright Ages, confirmed their gray fog, NONE native skybox and correct skylight/
|
||||
ambient values, then restored both choices after restart. It stopped cleanly;
|
||||
the temporary server/world was deleted. Client animation, travel between sky
|
||||
styles, and restart appearance still require manual validation. No user world
|
||||
was modified, and nothing was committed, pushed or published.
|
||||
|
||||
## Seeded sky completion slice
|
||||
|
||||
`1.0.1.72-beta.1` completes missing sky controllers using the first seeded
|
||||
selection from 1.21.1 `AgeSkyProfile.random` (`0x534B5950524F464C` salt).
|
||||
Inherited and Overworld currently map to the same native Overworld style;
|
||||
the effective Overworld/Nether/End/Static weights are 2:1:1:1. This deliberately
|
||||
ports only style selection, not random colors or celestial-body lists.
|
||||
|
||||
The player book path and `create_random` fixture now share one environment
|
||||
record containing weather, lighting and sky. Explicit pages win. Separate
|
||||
seed streams keep controllers independent, including when one is supplied
|
||||
explicitly, and Cave weather remains Off. Existing Age index values/defaults
|
||||
remain authoritative; this does not reroll old Ages or reseeded Age skies.
|
||||
The one-purpose controlled fixtures keep their explicit Overworld defaults.
|
||||
|
||||
The environment probe checks repeatability and every explicit sky override
|
||||
with and without explicit lighting/weather. Manual seeds 0–11 and -1 covered
|
||||
all four sky choices, Cave and Void, without changing established weather/
|
||||
lighting results; signed-long boundary seeds also passed. Disposable-server
|
||||
fixtures created all four random styles plus Bright Static and an explicit
|
||||
Overworld override. Static survived unload/reload; Nether retained its saved
|
||||
style after reseeding to an End-selecting seed. All six choices survived a
|
||||
server restart. The server stopped cleanly and its temporary world was deleted.
|
||||
The port build passed (no unit-test sources are configured). Player rendering
|
||||
still needs validation of new book creation and travel. No user save is changed
|
||||
and nothing is committed or published.
|
||||
|
||||
## Shared colors and ambient-light target slice
|
||||
|
||||
`1.0.1.73-beta.1` adapts the 1.21.1 `AgeSkyProfile.ColorAccumulator` rules:
|
||||
Number 0–24 followed by Red/Green/Blue adds ten times that amount to one
|
||||
channel, White adds to all, Black subtracts from all, and final channels are
|
||||
clamped. Unnumbered colors use the same seeded index-salt approach. Color
|
||||
groups preceding Sky pages, or trailing the book, tint the sky. No colors
|
||||
means native rendering; omitted channels within an authored group are zero.
|
||||
The new port-only `lighting/ambient_color` target consumes the preceding group
|
||||
as ambient color instead. It appears in the existing Lighting creative tab
|
||||
and uses the shared procedural glyph/item compositor, not generated artwork.
|
||||
|
||||
Profiles are frozen in the Age index with native defaults, retained on reload/
|
||||
reseed/retirement rollback, and included in the full catalog. The catalog
|
||||
protocol is now 2; both peers need the matching beta. The client replaces its
|
||||
profile map before catalog acknowledgment, invalidates the current attribute
|
||||
cache and clears the map on disconnect. Age-only default-layer hooks apply
|
||||
the native sky/fog/ambient RGB attributes dynamically; vanilla dimensions are
|
||||
not changed. No runtime dimension registry mutation or RGB variant explosion
|
||||
is involved. Client sky state carries tint separately from the live world:
|
||||
Static cells use that tint, and the native End texture uses a color modulator.
|
||||
Native celestial rendering is otherwise unchanged; authorable bodies remain
|
||||
pending. Explicit ambient tints do not alter the effective Bright floor or
|
||||
fabricate block/sky light; native lightmap tinting controls their appearance.
|
||||
|
||||
Numbers are supported only before shared colors in this slice. Celestial
|
||||
numeric modifiers and variable Static resolution (#109) remain unsupported.
|
||||
Random sky color completion and deprecated legacy fixed-color aliases remain
|
||||
pending; this is not a claim of full celestial/color parity. Rendering needs
|
||||
manual checks of authored groups, all sky types, ambient color, no leakage on
|
||||
travel/reconnect, and persistence. Dedicated-server fixtures exercise known
|
||||
RGB results, zero/upper amounts, clamping, seeded repeatability, rejection of
|
||||
dangling numbers, and a catalog encode/decode round trip. All four colored
|
||||
styles and an uncolored native fixture restored correctly after restart with
|
||||
the expected sampled RGB attributes. The server stopped cleanly and its
|
||||
temporary world was deleted. Port build/static checks passed; there are no
|
||||
unit-test sources configured. Client rendering and live multiplayer sync
|
||||
remain manual checks. No commit, push, or publication was requested.
|
||||
|
||||
## Authored native day-cycle slice
|
||||
|
||||
`1.0.1.74-beta.1` ports Normal Time with Full/Half/Double/Zero Length modifiers,
|
||||
using the fixed values from the 1.21.1 symbol definitions. Number pages before
|
||||
Length apply the existing `CelestialPageValues` rule (`number × 0.08` days).
|
||||
Zero chooses a seeded time once and saves it. Missing Normal Time retains the
|
||||
native cycle. The strict interim grammar requires at most one Length preceding
|
||||
one Normal Time; broader ordered recovery and multiple celestial bodies remain
|
||||
separate work. Number targets other than colors and lengths are still unported.
|
||||
|
||||
The Age index persists a native-default day-cycle profile. Lifecycle load,
|
||||
reseed, retirement/rollback and catalog sync include it. Catalog protocol 3
|
||||
carries the new profile map; clients replace it before acknowledgment, clear it
|
||||
on logout and invalidate current attributes/sky brightness. Both peers need
|
||||
the same beta build. A small Age-only Builder mixin preserves native dynamic
|
||||
timeline ordering and weather layers, but supplies a read-only wrapped clock
|
||||
manager for this Age's timeline samples. Its clock instances are cached per
|
||||
definition, read profiles lazily (including during level construction), and
|
||||
delegate non-Overworld clocks untouched. No global clock mutation occurs.
|
||||
|
||||
Native day shading, sun/moon tracks, moon phase, and gameplay attributes in
|
||||
those timelines share the scaled sample. Zero has a constant integer sample,
|
||||
rate zero, and partial tick zero, preventing the prior stopped-time bounce.
|
||||
This does not accelerate blocks/entities, change the native `/time query`
|
||||
result, make a Nether/End/Static sky show bodies, or port custom bodies. Global
|
||||
time commands still affect moving samples. Authored RGB overrides and existing
|
||||
Bright/Dark behavior remain independent. Separate Sun/Moon, angle/phase/size/
|
||||
direction, eclipses, and randomized celestial completion are still pending.
|
||||
|
||||
Fixtures validate parser eligibility, known half/double/zero clock values,
|
||||
Number overrides, deterministic stopped time, and the shared catalog codec.
|
||||
At shared native time 18022 the sampled Full time was 18022 (sky light 4),
|
||||
Half 36044 (sky light ~13.92), Double 9011 (sky light 15), and Zero stayed at
|
||||
11611 with its unchanged sun angle. An unmodified Age matched Full exactly.
|
||||
Zero survived unload/reload and reseeding without rerolling; the color fixture
|
||||
retained its RGB values. All authored profiles, numeric endpoints 1/24, zero
|
||||
override, native fallback, and the color fixture restored after server restart.
|
||||
The final port build passed (no unit-test sources configured). Test servers
|
||||
stopped cleanly; their temporary world was deleted. Visual/client reconnect
|
||||
checks remain manual. No commit/push/publication is performed without an
|
||||
explicit request.
|
||||
|
||||
## Native cycle phase and direction slice
|
||||
|
||||
`1.0.1.75-beta.1` extends the existing day-cycle profile with a saved native
|
||||
clock origin, phase offset and direction. Phase conversion is adapted directly
|
||||
from 1.21.1 `AgeDimensionService.applyInitialCelestialPhase`: the symbolic
|
||||
angle is measured from Nadir, then shifted 18000 native ticks. Rising/Zenith/
|
||||
Setting/Nadir give 0/6000/12000/18000, and preceding Number pages override a
|
||||
Phase using the `CelestialPageValues` 15-degree rule (24 wraps to 0). Length
|
||||
and phase remain independent. Explicit phase starts at Age creation, not at
|
||||
the current Overworld position divided by Length.
|
||||
|
||||
Moving samples are `direction * (nativeTicks - origin) / period + phase`;
|
||||
Counterclockwise reverses the native day timelines, including moon-phase and
|
||||
gameplay tracks. Stopped samples remain constant and prefer written phase,
|
||||
otherwise the previous saved seeded stopping time. Existing profiles without
|
||||
the new optional fields retain their former behavior. Saved/profile lifecycle
|
||||
and catalog synchronization already carry the extended codec. Catalog protocol
|
||||
4 requires matching client/server beta versions. No registry types, textures,
|
||||
or global clock mutations are added. Angle/size/per-body color and multiple
|
||||
celestial rendering remain pending, as do eclipses and random celestial
|
||||
completion. #110's Time/Normal Length naming changes remain separate work.
|
||||
|
||||
The interim grammar accepts at most one Length, Phase and Direction preceding
|
||||
one Normal Time; numbers currently target Color/Length/Phase only. Fixtures
|
||||
check all phase mappings at a shared origin, forward/reverse deltas, stopped
|
||||
behavior, parsing of the earlier three-field profile, and full catalog codec
|
||||
round trips. At native tick 1518, profiles sharing origin 496 and noon offset
|
||||
sampled 7022 forward and 4978 backward, with sun angles ~10.756° and ~349.244°.
|
||||
Zero Zenith stayed at 6000 / 0° / sky light 15; Zero Nadir stayed at 18000 /
|
||||
180° / sky light 4. Number 6 yielded sunrise; Number 24 yielded midnight.
|
||||
The reverse profile survived unload/reload; reseeding retained frozen midnight.
|
||||
All sampled profiles restored after a server restart, retaining their origins,
|
||||
direction and fixed phases; the old-style seeded Zero fixture also retained
|
||||
its saved stopping time. Build checks passed (no unit-test sources configured).
|
||||
The test server stopped cleanly and its temporary world was deleted. Rendered
|
||||
motion and reconnect behavior remain manual. No commit/push/publication is
|
||||
performed without explicit direction.
|
||||
|
||||
## Native orbital bearing slice
|
||||
|
||||
`1.0.1.76-beta.1` ports the four Angle pages and the existing 1.21.1
|
||||
`CelestialPageValues` number rule (15° per number, modulo 360). North/East/
|
||||
South/West map to 0/90/180/270. The interim grammar accepts one Angle before
|
||||
Normal Time, independent of Length, Phase and Direction. Missing Angle keeps
|
||||
the native orientation rather than enabling random-bearing completion.
|
||||
|
||||
The cycle codec adds an optional bearing defaulting to -1 (native). Existing
|
||||
origin/phase/direction defaults and lifecycle persistence remain unchanged.
|
||||
Catalog protocol 5 carries the extended profile; older peers must update.
|
||||
Extracted sky state carries/resets bearing. Native SkyRenderer's Y rotation
|
||||
for the shared sun/moon/star path uses the explicit value; the sunrise/sunset
|
||||
pose receives the corresponding delta from the native -90° body bearing, with
|
||||
balanced pose-stack restoration. No live-world lookup occurs during drawing.
|
||||
Colors, native geometry/textures, phase timing, illumination, and direction
|
||||
are retained. Angle changes only horizontal orientation, not altitude. Body
|
||||
counts/sizes/colors and eclipses remain pending; no extra rendering backend,
|
||||
asset or registry variant is introduced. #110 is deliberately not implemented
|
||||
as part of this port slice.
|
||||
|
||||
The port build passes (no unit-test sources configured), and the targeted
|
||||
PoseStack invocation signature was checked against the patched native class.
|
||||
Disposable-server fixtures exercise cardinal values, all 0–24 Number values,
|
||||
six-field profile fallback, and the complete catalog stream codec. All four
|
||||
frozen-sunrise fixtures sampled the same time 0 and sky light ~14.19; only
|
||||
their saved bearings differ. Number 3 gave 45°, Number 24 gave 0°. Unwritten
|
||||
bearing retained -1. East survived unload/reload; the 45° profile retained its
|
||||
bearing after reseed. Client rendering/glow alignment and travel isolation
|
||||
still require manual validation; server probes cannot prove the drawn result.
|
||||
All six explicit bearings and the native fallback restored correctly after
|
||||
restart. The test server stopped cleanly and its temporary world was deleted.
|
||||
No commit/push/publication is performed without an explicit request.
|
||||
|
||||
## Celestial-compatible missing-Sky fallback
|
||||
|
||||
`1.0.1.77-beta.1` restricts the missing-Sky fallback for celestial writing to
|
||||
the current renderer's compatible style, Overworld. Normal Time no longer
|
||||
inherits an unsuitable seeded Nether/End/Static background. The resolver
|
||||
also recognizes Sun/Moon intentions for future support without enabling
|
||||
those pages prematurely. Explicit Sky pages always win; no-celestial random
|
||||
selection, terrain, and independent weather/lighting streams are unchanged.
|
||||
No packet or saved-profile format changes are needed. Existing Ages retain
|
||||
their saved style; tests of the fix must create new Ages.
|
||||
|
||||
1.21.1 comparison: `AgeSkyProfile.compile` starts with inherited sky style;
|
||||
the bundled Age profiles all use the Overworld dimension type. Authored Sun/
|
||||
Moon selects `AgeEnvironmentCompiler.CUSTOM_SKY_EFFECTS`, whose renderer draws
|
||||
the body list after its background without a sky-style exclusion. Thus the
|
||||
bundled 1.21.1 paths already avoid this visibility regression, and no follow-up
|
||||
issue or root-code change is needed for them. This is not a claim about every
|
||||
possible third-party datapack profile.
|
||||
|
||||
The environment diagnostic checks missing/explicit Sky for Normal Time, Sun,
|
||||
and Moon, including both explicit-page orders, and verifies other environment
|
||||
choices are unchanged. The day-cycle fixture now supplies no Sky page and
|
||||
uses the same environment resolver as player books. Client creation/travel
|
||||
checks remain manual. Build and disposable-server creation checks passed:
|
||||
Normal Time without Sky selected native Overworld for seeds 0, 6 and 8,
|
||||
while the same seeds without celestials retained Nether, Static and End.
|
||||
Explicit-Sky precedence and independent environment choices passed the
|
||||
diagnostic checks, including Cave terrain. The server stopped cleanly and
|
||||
its temporary world was deleted. No commit/push/publication is performed
|
||||
without an explicit request.
|
||||
|
||||
## Normal Time sun size
|
||||
|
||||
`1.0.1.78-beta.1` ports Small/Normal/Large Celestial Size modifiers to the
|
||||
native Normal Time sun. Presets are 0.5×/1×/2×. An immediately preceding Number
|
||||
uses the root compiler's `0.25 + number * (2.75 / 24)` rule, covering 0.25×
|
||||
through 3×. Root Normal Time creates a size-modified sun and a 1× moon; this
|
||||
slice preserves that distinction rather than resizing both. Standalone Sun/
|
||||
Moon controllers, multiple bodies, per-body colors and eclipses remain pending.
|
||||
No random size completion is added yet.
|
||||
|
||||
`PortDayCycle` persists optional `sun_size`, defaults old profiles to 1×,
|
||||
validates the supported range and transmits it in catalog protocol 6. Size
|
||||
is extracted into the sky render state and multiplies only the native sun
|
||||
transform. Native texture/glow and rendering pipeline remain; no new assets
|
||||
or buffers are allocated. State resets/defaults prevent size leaking to other
|
||||
dimensions. Clock sampling, illumination and orbit bearing are unaffected.
|
||||
|
||||
The `create_size` op fixture freezes the sun at Zenith with Zero Length and
|
||||
omits Sky to exercise compatible completion. Diagnostic checks cover all
|
||||
25 numeric sizes, presets, older profile defaults, unchanged timing and
|
||||
catalog round trips. Disposable dedicated-server checks produced sizes
|
||||
0.5/1/2/0.25/1.625/3 with the same noon angle and sky light; reload, reseed
|
||||
and full restart retained them. Build passed. Visual size/glow alignment,
|
||||
unchanged moon, and travel isolation still require the user's client check.
|
||||
The server stopped cleanly and its temporary world was deleted. Nothing is
|
||||
committed, pushed or published without an explicit request.
|
||||
|
||||
## Normal Time shared celestial color
|
||||
|
||||
`1.0.1.79-beta.1` ports the root `AgeSkyProfile` behavior that consumes a
|
||||
pending shared color group at Normal Time and applies it to both paired
|
||||
bodies. `PortAgeColors` now saves an optional `celestial` RGB value, default
|
||||
-1 (native/untinted), with catalog protocol 7 for synchronization. Existing
|
||||
sky/ambient fields and old profiles retain their values. Color accumulation
|
||||
passes through non-color celestial modifiers, clamps at the target, resets
|
||||
after consumption, and leaves trailing colors assigned to sky as before.
|
||||
|
||||
Extraction copies the saved tint into `SkyRenderState`. A narrowly scoped
|
||||
native sun/moon uniform modification multiplies RGB while retaining alpha;
|
||||
there are no live world reads during drawing, new textures, pipelines or GPU
|
||||
buffers. Native weather fading, transparent halos, moon phases, stars and
|
||||
sunrise glow remain unchanged. Time and effective illumination are not tied
|
||||
to RGB. State reset/defaults prevent cross-dimension tint leakage. Independent
|
||||
Sun/Moon pages, multiple bodies and per-body colors remain unported.
|
||||
|
||||
The `create_celestial_color` fixture checks three separate target groups,
|
||||
completed-group clamping, modifier passthrough, consumed-group reset, seeded
|
||||
unnumbered colors, old codec defaults and catalog round trips. Client checks
|
||||
must cover a frozen Zenith sun, Nadir moon, mixed colors, transparent halos,
|
||||
restart and return to an untinted dimension. Server probes validate saved
|
||||
profiles and attributes, not rendered output. Build passed; disposable-server
|
||||
fixtures retained separate sky/ambient/body colors through reload, reseed and
|
||||
restart, including zero/black colors and untouched native defaults. Sampled
|
||||
time/light remained unchanged. The server stopped cleanly and its temporary
|
||||
world was deleted. No commit/push/publication is performed without an explicit
|
||||
request.
|
||||
|
||||
## Standalone Sun and Moon controllers
|
||||
|
||||
`1.0.1.80-beta.1` extends the existing cycle profile with optional `body_mode`
|
||||
(paired=0, Sun=1, Moon=2); old profiles default to the paired renderer.
|
||||
Catalog protocol 8 includes this field. One celestial controller is accepted
|
||||
per book, with existing Length/Phase/Direction/Angle/Size and shared Color
|
||||
groups. Multiple controllers and dangling/duplicate modifiers retain the
|
||||
interim strict rejection rather than losing written pages.
|
||||
|
||||
Standalone bodies use a linear angular orbit as in root `AgeSkyProfile.Body`.
|
||||
Both native body-angle attributes point at that orbit; render-state mode
|
||||
suppresses the absent counterpart. The selected Moon consumes Size itself;
|
||||
Normal Time retains its original sun-only sizing. Moon-only sunrise glow is
|
||||
suppressed before any horizon pose is pushed. Shared RGB/alpha handling and
|
||||
native buffers/pipelines are reused, with extraction/reset defaults protecting
|
||||
other dimensions. Missing Sky completion uses Overworld, as before. Explicit
|
||||
Nether/End/Static still does not draw bodies in this native-renderer slice.
|
||||
|
||||
The gameplay skylight attribute follows root `lightAt` and
|
||||
`LevelSkyBrightnessMixin`: positive cosine altitude, 0.18 for a moon, weather
|
||||
attenuation, then the 11-level sky-darkening range. It does not infer light
|
||||
from color or size, and does not bypass Bright/Dark or no-skylight terrain.
|
||||
Native background/star/moon-phase schedules remain, rather than claiming a
|
||||
full custom sky port. Seeded default phase/bearing completion, multiple
|
||||
independent bodies and eclipses remain future work.
|
||||
|
||||
The op `create_body` fixture checks all named phases for both bodies, light
|
||||
weights, numeric size, reverse motion, order rejection, old-profile defaults,
|
||||
compatible missing Sky and catalog synchronization. Actual body visibility,
|
||||
moon size, glow transparency and travel isolation require manual client
|
||||
validation. Build and disposable-server probes passed: Zenith sun/moon
|
||||
skylight was 15/6, Nadir and horizon were 4; named phase angles were correct.
|
||||
The paired fixture retained its opposite moon and native lighting. Reload,
|
||||
reseed and full restart preserved body mode, color, size, bearing and phase.
|
||||
The test server stopped and its temporary world was deleted. No commit/push/
|
||||
publication is performed without an explicit request.
|
||||
|
||||
## Multiple independent authored celestials
|
||||
|
||||
`1.0.1.81-beta.1` adds an optional immutable `PortCelestialBody` list to the
|
||||
cycle profile and catalog protocol 9. Existing profiles default to an empty
|
||||
list and use their validated native paired/single path. Books with multiple
|
||||
controllers compile ordered modifier groups into independent body orbits;
|
||||
Normal Time expands into opposite-phase sun/moon entries. Colors are consumed
|
||||
at each controller, with original page indices retained for seeded amounts.
|
||||
Uncolored bodies reset to native white rather than inheriting the previous
|
||||
body's tint. Size/phase/length/bearing/direction also reset per group.
|
||||
|
||||
The profile constructor and authoring gate enforce the root ten-sun/ten-moon
|
||||
limit, with Normal Time counting toward both. Duplicate modifiers within a
|
||||
group and dangling modifiers remain strict errors until grammar recovery is
|
||||
ported. The first controller continues to govern the shared day timeline and
|
||||
native background/star/moon-phase schedules. Positive-altitude sunlight sums
|
||||
to at most 1; only if no sunlight contributes is summed 0.18-weight moonlight
|
||||
used. Weather and Bright/Dark remain separate. No eclipse attenuation is
|
||||
included yet.
|
||||
|
||||
Extraction resolves body angles into immutable presentation snapshots. The
|
||||
native body pass is replaced only for a nonempty multi-body list: draw suns
|
||||
first, then moons with their own color, size and bearing, then native stars
|
||||
once. Native buffers, pipelines and alpha are reused. Temporary renderer
|
||||
parameters and pose transforms restore in finally blocks; no world lookup is
|
||||
performed during drawing. Explicit Nether/End/Static rendering remains a
|
||||
separate limitation. Eclipse disc occlusion, corona and sky/light attenuation
|
||||
will be the following slice, not a claim of this implementation.
|
||||
|
||||
`create_celestials` checks independent motion/color/size, modifier reset, Normal
|
||||
Time expansion, ten-pair acceptance/overflow rejection, profile codecs and
|
||||
catalog round trips. Manual checks cover body placement/alpha, multiple
|
||||
controllers, restart and ordinary-dimension isolation. Build passed, as did
|
||||
disposable-server profile/catalog checks, combined-light invariants, reload,
|
||||
reseed and full restart. Single-body, paired and tinted fixtures retained
|
||||
their earlier behavior. The test server stopped and its temporary world was
|
||||
deleted. No commit/push/release is performed without explicit instruction.
|
||||
|
||||
## Eclipse effects
|
||||
|
||||
`1.0.1.82-beta.1` adapts root `AgeSkyProfile.eclipseFor`, `eclipseAt` and
|
||||
`lightAt`, plus `RivencraftSkyEffects.drawBody`. The dot-product separation
|
||||
of normalized orbit directions is compared with the sum of quarter-disc
|
||||
angular radii; each body takes the strongest opposite-kind contact. This is
|
||||
root's radial proximity formula, not a new physical disc-area model. Bearing,
|
||||
phase, size, motion and direction all affect contact. Same-kind and isolated
|
||||
bodies have no eclipse; only an above-horizon sun contributes global shading.
|
||||
|
||||
Sunlight is multiplied by `1 - 0.85 * overlap` before the existing light
|
||||
aggregation and weather handling. Extraction shades sky RGB by
|
||||
`1 - 0.65 * strongestVisibleSolarOverlap`. Moon RGB scales by `1 - overlap`;
|
||||
sun RGB moves toward white by `pow(overlap, 8) * 0.8`. Values are derived from
|
||||
saved profiles, requiring neither a new saved schema nor packet fields.
|
||||
Existing multi-body Ages gain the effects on update; single/paired native
|
||||
paths stay unchanged. Overlap calculations are bounded by the 20-body limit,
|
||||
and extracted presentation values are reused without live-world drawing reads.
|
||||
|
||||
Native transparent glows draw before each body's central ordinary-alpha disc.
|
||||
A registered pipeline cloned from native Celestial uses conventional alpha
|
||||
blending; cached center-cropped atlas meshes retain native UV orientation.
|
||||
An eclipsed moon uses the full-moon core to avoid transparent phase pixels
|
||||
exposing the sun (root always drew that full-moon center). Weather alpha is
|
||||
retained. The helper drops meshes on atlas-generation change and closes them
|
||||
with the renderer. There is no generated art or per-frame mesh allocation.
|
||||
Explicit Nether/End/Static body rendering remains outside this slice.
|
||||
|
||||
`create_eclipse` supplies aligned, partial, clear, below-horizon and moving
|
||||
fixtures; it checks overlap/color/light constants, same-kind exclusion and
|
||||
motion out of contact. The multi-body op fixture is renamed
|
||||
`create_celestials` to avoid merging with the unrelated existing terrain
|
||||
`create_multi` command. Manual checks must confirm disc opacity, halo,
|
||||
corona/sky darkening, resource reload and travel isolation. Build passed.
|
||||
Disposable-server fixtures passed with overlap 1/0.293/0 for aligned/partial/
|
||||
clear cases and no global shading for below-horizon alignment. Aligned
|
||||
sunlight was 0.15, clear sunlight 1; sampled skylight was 6/12/15 for aligned/
|
||||
partial/clear. Moving contact ended as expected. Reload, reseed, catalog
|
||||
round trips and full restart retained profiles and derived light behavior;
|
||||
earlier standalone/paired fixtures remained unchanged. The test server stopped
|
||||
and its temporary world was deleted. No commit/push/publication is performed
|
||||
without an explicit request.
|
||||
|
||||
## Authored moon sprite correction
|
||||
|
||||
`1.0.1.83-beta.1` removes the mixed native-phase/full-core adaptation introduced
|
||||
with the opaque eclipse pass. Standalone and multi-body authored moons now
|
||||
draw full-moon glow and center consistently, matching root's custom renderer,
|
||||
which always samples the first (full-moon) atlas cell. Under ordinary alpha,
|
||||
native dark phase pixels can produce black opaque centers; the old overlay
|
||||
glow had made those dark pixels effectively invisible. The ordinary native
|
||||
Normal Time pair retains its calendar phases. No eclipse math, saved profile,
|
||||
catalog protocol or world data changes are needed.
|
||||
|
||||
The user's `celest` Age was read without modification. Its body colors are
|
||||
all -1 (native white), not corrupted black values. Its first Moon and second
|
||||
Sun both have period 1, phase 0, origin 0, clockwise direction and native
|
||||
bearing -1; they remain exactly coincident. That moon's full eclipse and its
|
||||
occlusion of the sun are expected under the newly added root overlap rules,
|
||||
independent of the phase-sprite correction. Do not silently alter player-authored
|
||||
orbits to conceal the intentional alignment. Manual checks cover nonaligned
|
||||
moons, the known permanent eclipse, Native Normal Time, resource reload and
|
||||
cross-dimension isolation. No user save was modified, and nothing is committed,
|
||||
pushed or published without explicit instruction.
|
||||
|
||||
## Celestials over explicit non-Overworld skies
|
||||
|
||||
`1.0.1.84-beta.1` removes the presentation exclusion that hid authored bodies
|
||||
over Nether/End/Static. Root `RivencraftSkyEffects` draws its body list after
|
||||
any chosen background. The native 26.3 paths instead returned after End or
|
||||
skipped a NONE sky entirely. An extracted overlay-mode flag now schedules an
|
||||
authored body layer without changing the actual dimension type or gameplay.
|
||||
Single Sun/Moon and Normal Time profiles expand into temporary presentation
|
||||
bodies on these backgrounds; saved profiles are not rewritten. Existing
|
||||
Overworld single/paired/multi-body paths stay unchanged.
|
||||
|
||||
End uses a RETURN hook that includes the native early return, after its pass
|
||||
is closed. Static closes its own background pass before drawing the overlay.
|
||||
Fog-only Nether presentation schedules an extracted END sky pass, suppresses
|
||||
its End background/flash drawing, and retains the fog clear color. Overlay
|
||||
passes load existing targets without clearing, reuse native body resources
|
||||
and suppress additional stars/horizon glow on non-Overworld backgrounds.
|
||||
Weather alpha is extracted even when the native NONE/END branch omits it.
|
||||
All presentation flags reset when the render state is reused.
|
||||
|
||||
Eclipse shading modulates End/Static background tint; Nether-style fog color
|
||||
uses the same root shade factor through an environment attribute layer.
|
||||
Native untinted fog and authored sky colors are both supported; other
|
||||
backgrounds retain their existing fog behavior. Body lighting and eclipse
|
||||
geometry do not depend on style. Missing Sky selection still chooses
|
||||
Overworld for authored celestials. No schema/protocol changes or new assets
|
||||
are required. The eclipse fixture accepts an optional explicit Sky; server
|
||||
checks cover saved choices, fog/light behavior and persistence, while visual
|
||||
layering, no duplication, resource reload and travel isolation require manual
|
||||
client checks. Build and disposable-server checks passed for all four sky
|
||||
styles: aligned eclipse skylight stayed 6, clear stayed 15, and Nether fog
|
||||
scaled by 0.35 only during overlap. No-celestial and tinted-Nether fixtures
|
||||
retained their expected colors. Saved profiles, style, light and fog survived
|
||||
reload/reseed and full restart. The test server stopped and its temporary
|
||||
world was deleted. No commit/push/publication is performed without explicit
|
||||
request.
|
||||
|
||||
## Bare celestial seeded randomization
|
||||
|
||||
`1.0.1.85-beta.1` adds per-controller randomization specifically for bare
|
||||
Sun/Moon groups, as requested for easier testing. This deliberately uses the
|
||||
root **random-Age** ranges rather than its authored compile defaults (which
|
||||
reuse shared seeded phase/bearing and default size/color). A dedicated stream
|
||||
mixes Age seed with controller ordinal, independent of unrelated page indices,
|
||||
terrain/weather/lighting RNG and sky selection. Palette/mixing and 0.75–1.75
|
||||
size come from `AgeSkyProfile.random`; 0.2–2 periods with 1/7 stopped chance
|
||||
come from `AgeTraits.select`. Bearing and phase use the port's existing degree/
|
||||
tick granularity. Direction is random per body. Counts and body-kind limits
|
||||
still follow explicitly written controller pages.
|
||||
|
||||
The compiler recognizes whether a controller has any written celestial
|
||||
modifier. Colors already consumed by Sky/Ambient do not suppress randomization.
|
||||
Any written celestial modifier retains the previous authored path as a whole,
|
||||
not just individual field overrides; partially authored groups do not gain
|
||||
new random missing fields in this slice. Normal Time and absent controllers
|
||||
are unchanged. Bare controllers materialize immutable full body profiles,
|
||||
including a one-body list for standalone cases. Multi-body compilation carries
|
||||
their colors/orbits without replacing them with native defaults. No new
|
||||
saved schema, protocol or client renderer code is needed.
|
||||
|
||||
Only newly created Ages compile these defaults. Older saved profiles load
|
||||
unchanged; GUI opening, restart and terrain reseeding do not reroll them.
|
||||
`create_random_celestials` checks reproducibility, ordinal independence,
|
||||
unrelated-page stability, 128-seed bounds/pool coverage and unchanged authored/
|
||||
Normal Time behavior. The regular fixtures still exercise explicit groups.
|
||||
Manual checks cover visible variation, moving/stopped bodies, mixed authored
|
||||
groups and restart. Build and disposable-server checks passed, including
|
||||
single bare bodies, multi-body/128-seed coverage, catalog round trips,
|
||||
unrelated-page stability, explicit/eclipsed fixtures, reload, terrain reseed
|
||||
and full restart. Saved random parameters remained unchanged while moving
|
||||
orbits continued with the clock. The server stopped and its temporary world
|
||||
was deleted. No commit/push/publication occurs without explicit request.
|
||||
|
||||
## Missing celestial branch completion
|
||||
|
||||
`1.0.1.86-beta.1` adds creation-only `PortDayCycle.resolve`. Written controllers
|
||||
delegate to the validated compiler unchanged. If none exist, the root
|
||||
`AgeSkyProfile.random` draw positions select 0–3 suns and 0–3 moons: consume
|
||||
the sky-style draw and mixed sky-color draws, then the two counts. Body
|
||||
settings reuse .85's independent per-controller streams. This matches root's
|
||||
unrestricted count model, not bit-identical per-body values; the .85 per-body
|
||||
length/direction independence is retained. Missing sky tint completion and
|
||||
random-page policy parity are not claimed by this slice.
|
||||
|
||||
The shared resolver is used by player book creation and the `create_random`
|
||||
operator command. Other exact diagnostic fixtures continue using `compile`
|
||||
or native profiles to isolate their subject. A saved NONE body mode (3)
|
||||
represents a resolved zero-body sky. Its sun/moon/horizon drawing is suppressed
|
||||
without switching to Dark lighting or changing the native background/star
|
||||
timeline. Old missing profiles still decode to NATIVE rather than being
|
||||
retroactively completed. The mode-domain extension uses catalog protocol 10.
|
||||
No generation runs or rerolls happen during GUI rendering, load or reseed.
|
||||
|
||||
`create_completed_sky` tests 256-seed count bounds and coverage of empty,
|
||||
sun-only, moon-only and mixed results; reproducibility and unrelated-page
|
||||
stability; explicit-controller precedence; old defaults; and catalog round
|
||||
trips. Manual checks cover varied new books, empty skies, explicit-controller
|
||||
counts, all backgrounds, restart and Overworld isolation. No commit/push/
|
||||
publication is performed without explicit instruction.
|
||||
|
||||
The .86 build and disposable dedicated-server checks passed: four background
|
||||
fixtures, the seed-14 empty sky, random Age creation, explicit controllers,
|
||||
catalog round trips, unload/reload, terrain reseeding and full restart. Saved
|
||||
body parameters survived unchanged. The test server was stopped and its world
|
||||
deleted; client visual confirmation remains a manual check.
|
||||
|
||||
## Random Age sky-color completion
|
||||
|
||||
`1.0.1.87-beta.1` uses creation-only `PortAgeColors.resolve` for player books
|
||||
and `age create_random`. Empty symbol lists choose a sky tint from root
|
||||
`AgeSkyProfile.random`'s palette and draw order, including its rounded channel
|
||||
averages. Nonempty symbol lists delegate to the existing compiler unchanged.
|
||||
This intentionally distinguishes wholly random Ages from authored books with
|
||||
an omitted sky-color branch, as 1.21.1 does. The existing .85 per-body random
|
||||
color truncation is preserved by a shared palette helper; celestial count and
|
||||
body random streams do not change.
|
||||
|
||||
Colors are stored in the existing profile and catalog fields. Old native
|
||||
colors are not retroactively randomized; reload, restart and terrain reseed
|
||||
do not rerun completion. No protocol update is necessary. The completed-sky
|
||||
diagnostic checks six root seed/color vectors, codec persistence, authored
|
||||
precedence (including explicit black) and old-profile defaults. Random-page
|
||||
blocklist parity remains separate pending work.
|
||||
|
||||
Build and disposable dedicated-server checks passed: root vectors and authored
|
||||
precedence, color attributes/catalog round trips for all four background
|
||||
styles, explicit sky/ambient colors, unload/reload, reseed and full restart.
|
||||
Saved random colors stayed unchanged after reseeding and restart. The test
|
||||
server was stopped and its disposable world deleted. Client rendering is
|
||||
still a manual check. No commit, push or publication was performed.
|
||||
|
||||
## Authored biome branch completion
|
||||
|
||||
`1.0.1.88-beta.1` ports the explicit-controller missing-pool behavior from
|
||||
1.21.1 `AgeBiomeSelection.compile`: Single requires one biome; other nonnative
|
||||
controllers require two distinct biomes. An entirely omitted pool uses the
|
||||
seeded 2:1 continuation/stop draw up to the eligible pool size. Authored pools
|
||||
fill only to the minimum, preserve order and duplicate weights, and never add
|
||||
an unrequested third biome once the minimum is met. Native still disallows
|
||||
biome pages. The binder's availability validation accepts incomplete pools;
|
||||
the server resolves actual registry eligibility before creating the Age.
|
||||
|
||||
Player creation now passes the actual Age seed to biome compilation. Exact
|
||||
older diagnostics retain the registry-only compiler; the new completion
|
||||
fixture exercises the production seeded path. Cave uses recognized Cave and
|
||||
Nether biomes. Void does not inject The Void alongside authored biomes and
|
||||
reports a clear distinct-biome error when completion is impossible. Pools
|
||||
are stored in existing `PortBiomePlan` fields and never rerolled on restart
|
||||
or terrain reseed. No protocol change is needed. Broader writing recovery,
|
||||
structure-driven missing-biome selection and random-page policies remain
|
||||
separate parity work.
|
||||
|
||||
The build and disposable dedicated-server checks passed. The completion
|
||||
fixture checked every distribution over 32 seeds, partial/full/weighted pools
|
||||
and codec round trips. Created Standard Single, Flat Tiny, Cave Tiny and Void
|
||||
Single plans; Cave included the new 26.3 Sulfur Caves biome. An impossible
|
||||
controller-only Void Tiny pool was refused with the distinct-biome message.
|
||||
Generator pools survived unload/reload, reseed and full server restart.
|
||||
The server was stopped and the temporary world deleted. Binder interaction
|
||||
and in-game biome layouts still require manual validation. No commit, push
|
||||
or publication was performed.
|
||||
|
||||
## Unpaired material completion
|
||||
|
||||
`1.0.1.89-beta.1` adapts 1.21.1 `AgeFeatureSelection.implied`. A shared pure
|
||||
normalizer expands an unpaired material into an internal material/feature
|
||||
pair, keyed by Age seed, material ID and its original book index. Rock, lake
|
||||
and terrain-feature compilers all use the same completed view. Original book
|
||||
pages stay unchanged and no random selection happens at render/load time.
|
||||
Explicit feature targets suppress completion, including incompatible targets
|
||||
that must still be rejected by the current strict grammar.
|
||||
|
||||
Candidate ordering and terrain restrictions follow root's material-feature
|
||||
list: Rocks/Floating Islands/Tendrils/Spheres/Spikes/Obelisks for solids and
|
||||
the three lake types for fluids, filtered by supported terrain. Cave only
|
||||
permits the two terrain-independent solid features; Void also permits floating
|
||||
islands; Flat excludes Rocks. No fallback fluid feature exists on Flat, Cave
|
||||
or Void. This does not add page-free random features, nor remove the existing
|
||||
per-feature caps or introduce instability recovery. Resulting plans use their
|
||||
existing save codecs, so no catalog/protocol change is needed.
|
||||
|
||||
The operator fixture checks 64 seeds on each terrain: independent consecutive
|
||||
materials, deterministic/idempotent completion, explicit precedence, candidate
|
||||
coverage, fluid retention and incompatible-fluid rejection. Persistence and
|
||||
actual generator plans are inspected with `feature_plan` without generating
|
||||
chunks. Client binder interaction and feature visibility remain manual checks.
|
||||
|
||||
The .89 build and disposable server checks passed: seeded fixture assertions,
|
||||
Standard Glowstone Spikes, Cave Diamond Spheres, Void Stone Tendrils, Flat
|
||||
Glowstone Obelisks and Standard Water Deep Lakes. Cave Water was refused.
|
||||
Feature plans and materials survived unload/reload, terrain reseed and full
|
||||
restart. The server was stopped and its temporary world deleted. No commit,
|
||||
push or publication was performed.
|
||||
|
||||
## Page-free random material features
|
||||
|
||||
`1.0.1.90-beta.1` adapts the material and rare-formation portion of 1.21.1
|
||||
`AgeFeatureSelection.randomChoices`. A new creation-only selector shares the
|
||||
production authored plan compilers rather than duplicating generator logic.
|
||||
It rolls 0–2 unique material feature types, uses root's candidate ordering and
|
||||
Legacy rank chooser, removes chosen types, and selects full-collision solids
|
||||
or source fluids from the registered sorted pools. Terrain eligibility uses
|
||||
the current port validator. The separate seeded Skylands and Star Fissure
|
||||
rolls and the 1-in-10 Crystal Formations roll follow root's rules.
|
||||
|
||||
Player Link-Panel-only creation and `age create_random` share this selector.
|
||||
Authored books still use their own feature compilers, including implied
|
||||
materials from .89. Plans are frozen in existing save fields, without extra
|
||||
book pages or a protocol change. Old Ages keep their original plans.
|
||||
Random structures, random-page policies and instability are not claimed by
|
||||
this slice. Material values can be valuable/hazardous, matching the root pool;
|
||||
the instability port must account for that before stable release.
|
||||
|
||||
`create_random_features` checks 64 seeds across all terrain types: seeded
|
||||
determinism, counts 0/1/2, unique feature types, all nine candidate families,
|
||||
terrain compatibility, rare-feature coverage, and plan codec round trips.
|
||||
`feature_plan` treats an absent feature generator as a valid empty result.
|
||||
Client feature visibility remains a manual check.
|
||||
|
||||
The .90 build and disposable server checks passed. The seeded matrix covered
|
||||
all six terrains and all nine material feature families. Created surface,
|
||||
Cave, Flat and Void fixtures, including lakes, rare Skylands and a Star
|
||||
Fissure, and verified the production `create_random` plan. Seed 0 Standard
|
||||
correctly produced no features; seed 1 produced a single lake. Saved plans
|
||||
survived unload/reload, reseed and full restart. The server was stopped and
|
||||
its temporary world deleted. Nothing was committed, pushed or published.
|
||||
|
||||
## Page-free random structures
|
||||
|
||||
`1.0.1.91-beta.1` ports `AgeStructureSelection.randomChoices`: an independent
|
||||
seed stream rolls 0–2 unique candidates using Legacy ranks. Terrain and
|
||||
registry availability are checked, then the selected biome pool must intersect
|
||||
the structure set's normal biome eligibility. This is deliberately stricter
|
||||
than authored pages, which retain their existing biome overrides. Native
|
||||
Flat uses Plains, Native Cave uses the backend's Lush Caves fallback, and
|
||||
other native noise terrain uses the Overworld LevelStem biome source. A
|
||||
non-Native plan uses its saved biome IDs. Abandoned Camps are added after
|
||||
the root candidate list at rank 3. Cave Dungeons remain excluded by current
|
||||
port support, whereas root permits that combination; it needs a separate
|
||||
placement validation before enabling it.
|
||||
|
||||
The combined random feature resolver preserves .90 material/rare choices,
|
||||
adds Dungeons separately, and merges structure pages into the saved terrain
|
||||
feature plan. A new optional `structure_control` field defaults false for
|
||||
old profiles and is true for new random Ages, even for an empty selection.
|
||||
The generator applies its existing controlled-set filter when that flag is
|
||||
true. This prevents zero choices from accidentally retaining all vanilla
|
||||
controlled sets. Other native structure families still use their normal
|
||||
biome gate. Existing authored/old plans are not rewritten, and no catalog
|
||||
protocol change is needed.
|
||||
|
||||
The operator fixture checks seeded counts/uniqueness, all terrains, fixed
|
||||
Plains/End/Nether biome pools, native defaults, biome eligibility, the empty
|
||||
controlled-set gate, explicit Village precedence, merge/codec persistence,
|
||||
and old-profile defaults. Actual placement remains an in-game check.
|
||||
|
||||
The .91 build and disposable server checks passed. Fixtures exercised Plains,
|
||||
Native Flat, Nether-biome Cave, End-biome surface and empty Void selection,
|
||||
plus the production random-Age command. Village, Fortress and Dungeon plans,
|
||||
and an intentionally empty controlled plan, survived unload/reload, reseed
|
||||
and full restart. Namespaced biome arguments were checked. The server was
|
||||
stopped and its temporary world deleted. No commit, push or publication was
|
||||
performed; generated structure placement remains a manual client check.
|
||||
|
||||
## Cave Dungeon parity
|
||||
|
||||
`1.0.1.92-beta.1` closes the Cave Dungeon terrain exclusion documented in .91.
|
||||
Root `AgeStructureSelection.error` permits Dungeons on noise terrain, which
|
||||
includes Cave. The 26.3 feature and placement implementation were already in
|
||||
place; the port's shared lake/dungeon Cave rejection blocked authoring and,
|
||||
through the availability check, random selection. Splitting that validation
|
||||
permits Cave Dungeons without allowing Cave lakes or Flat/Void Dungeons.
|
||||
|
||||
The two targets' `worldgen/placed_feature/dungeons.json` files compare exactly:
|
||||
vanilla monster_room, count 8, in-square and full-height uniform placement
|
||||
using above-bottom/below-top anchors. No legacy Y=0 assumption is introduced.
|
||||
Vanilla's solid floor/ceiling and opening-count requirements still determine
|
||||
successful placement. The 32-page cap and per-occurrence deterministic seed
|
||||
remain unchanged. New random Cave Ages now consider Dungeons automatically;
|
||||
old frozen plans are not changed.
|
||||
|
||||
The extended `create_dungeons` fixture checks Cave stacking/cap and the
|
||||
remaining lake/Flat/Void restrictions. The random-structure matrix now requires
|
||||
Cave Dungeon coverage. `dungeons_probe` accepts an optional 1–4 chunk-side
|
||||
bound for manual tests; it is not run automatically, avoiding a large sync
|
||||
chunk-generation scan. Actual dungeon placement is handed to the client check.
|
||||
|
||||
The .92 build and disposable server checks passed: Cave and surface authored
|
||||
fixtures, 32-page stacking/overflow, retained lake and Flat/Void rejection,
|
||||
and random-structure coverage including a Nether Cave Dungeon selection.
|
||||
Dungeon counts survived unload/reload and full restart; frozen random plans
|
||||
survived reseed. Placement JSON matched the 1.21.1 file byte-for-byte. The
|
||||
server was stopped and its temporary world deleted. No commit, push or
|
||||
publication was performed. No dungeon chunk scan was run automatically.
|
||||
|
||||
## Biome-specific Nether and End materials
|
||||
|
||||
`1.0.1.93-beta.1` ports root `AgeDimensionService.withBiomeMaterialRules`.
|
||||
The backend was using `caves.materialRule()` unchanged, which selects
|
||||
`minecraft:overworld_caves` and can apply Overworld grass to Nether biomes.
|
||||
The new noise-settings wrapper sequences the five vanilla End biomes' registered
|
||||
End rule, then the five vanilla Nether biomes' registered Nether rule, then
|
||||
the original terrain rule. This fixes Cave Nether coverings and the equivalent
|
||||
End/Nether mismatch in authored Standard, Amplified and Island noise Ages.
|
||||
It uses the active rule registry rather than copying vanilla material tables.
|
||||
|
||||
All non-material settings are preserved. Dry Cave's AIR default fluid, empty
|
||||
aquifer config and minimum-relative sea level remain intact. Native Nether
|
||||
surface rules can still place their own lava patches; this does not restore
|
||||
flooded Cave aquifers. Flat/Void layer materials and custom feature materials
|
||||
are not rewritten. Old saved Ages pick up the rule when reloaded, but only
|
||||
newly generated chunks change; no generated blocks are retroactively edited.
|
||||
No profile field or network protocol change is required.
|
||||
|
||||
The creation fixture verifies unchanged geometry/router/default blocks and
|
||||
fluids, aquifers and sea level for four noise presets, and round-trips the
|
||||
composed material rule with registry-aware codecs. `material_probe` limits
|
||||
generation to one terrain-stage chunk and reports actual block counts.
|
||||
|
||||
Build, registry-aware rule codec tests and disposable server terrain checks
|
||||
passed. All five Nether Cave fixtures generated their native material families
|
||||
without grass/dirt: Nylium, Soul Sand/Soil, Basalt/Blackstone or Netherrack.
|
||||
Standard The End and Island End Highlands generated End Stone; Lush Cave's
|
||||
original Overworld materials remained present. Reload retained the rules, and
|
||||
fresh chunks after full restart again used Nylium/End Stone. No full feature
|
||||
or entity chunk scans were run. The server was stopped and temporary world
|
||||
deleted. Nothing was committed, pushed or published.
|
||||
|
||||
## Verification of this first slice
|
||||
|
||||
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
|
||||
|
||||
+735
-2
@@ -5,8 +5,12 @@ with the Minecraft 1.21.1 release. The 26.3 backend persists separate Ages and
|
||||
their seeds. Books, the Book Binder, and the Ink Mixer work, and the Binder can
|
||||
author Standard, Amplified, Island, Flat, Void, or Cave terrain with supported
|
||||
biome-distribution pages, selected material/vegetation/structure features, and
|
||||
independent Age weather. Celestial/lighting systems, instability, crystal
|
||||
portals, and much of the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not
|
||||
independent Age weather and lighting, multiple authored or seeded celestial
|
||||
bodies and eclipses, biome/material completion, and random features/structures.
|
||||
Instability, crystal portals, discovery/stations, writing recovery and optional
|
||||
integration parity remain unfinished. The latest published beta is
|
||||
[1.0.1.93-beta.1](RELEASE_NOTES_1.0.1.93-beta.1.md).
|
||||
Use a test world and keep a backup; do not
|
||||
substitute this JAR for the 1.21.1 mod or import 1.21.1 saves.
|
||||
|
||||
The entries below record how the port was built, including earlier alpha-only
|
||||
@@ -558,6 +562,735 @@ Operator-only `/rivencraft_port age surface_probe <name> <x> <z>` reports the
|
||||
actual highest obstruction and a safe nearby surface without creating a
|
||||
platform or teleporting. It can generate chunks while inspecting the region.
|
||||
|
||||
The local `1.0.1.66-beta.1` slice ports Normal, Dark, and Bright Lighting.
|
||||
One lighting page is accepted per book and its choice is saved in the Age
|
||||
index before catalog publication. Missing lighting currently defaults to
|
||||
Normal; random lighting selection and Bright's instability cost await those
|
||||
systems' later ports. Lighting pages should no longer be tinted red.
|
||||
|
||||
Normal surface Ages retain the Overworld type. Dark removes skylight with
|
||||
ambient light 0.0; Bright uses ambient light 0.25 and retains surface skylight,
|
||||
matching 1.21.1. **Cave + Bright is supported:** it has ambient light 0.25
|
||||
without skylight. Cave Normal/Dark use no skylight and ambient light 0.0.
|
||||
Placed light sources continue working; ambient light is not a placed light
|
||||
level or a guarantee that monsters cannot spawn.
|
||||
|
||||
The build derives three registered dimension-type variants from the current
|
||||
vanilla Overworld JSON, changing only has_skylight and ambient_light. This
|
||||
preserves native attributes/timelines and synchronizes types through the
|
||||
normal login registry exchange; no dynamic client registry mutation is used.
|
||||
These variants currently use the vanilla type, not custom runtime changes to
|
||||
the host Overworld dimension type made by another data pack.
|
||||
Saved lighting survives reload, reseed, restart and retirement rollback.
|
||||
Existing non-Cave Ages without a choice remain Normal; old Cave Ages now use
|
||||
the no-skylight baseline. Use new Ages for clear visual comparisons and avoid
|
||||
confusing previously lit chunks with the current dimension settings.
|
||||
|
||||
Manual check: create Normal/Dark/Bright surface books and Cave + Bright;
|
||||
compare an unlit enclosed room, check placed torches, revisit/restart and
|
||||
confirm the Overworld stays unchanged. Operator diagnostics:
|
||||
`/rivencraft_port age create_lighting <name> <seed> <mode> <terrain>` and
|
||||
`/rivencraft_port age lighting_probe <name>` report the registered type,
|
||||
skylight and ambient light. Test JAR: `1.0.1.66-beta.1`.
|
||||
|
||||
The local `1.0.1.67-beta.1` correction supersedes slice 66's Bright rendering:
|
||||
26.3's lightmap uses `minecraft:visual/ambient_light_color`, not the old
|
||||
dimension `ambient_light` field. Bright surface/Cave types now set that native
|
||||
attribute to white and ambient_light to 1.0. Cave still has no skylight.
|
||||
Bright Ages reject natural/chunk-generation hostile spawns independently of
|
||||
stored light levels. Existing mobs, passive mobs, spawners, eggs, commands
|
||||
and special-event spawns are not deleted or globally disabled. Actual block
|
||||
light propagation and plant light requirements are unchanged. Existing Bright
|
||||
Ages receive the corrected registered types after restart; no new Age needed.
|
||||
|
||||
Planned color-page target: **ambient light color**, alongside sky/celestial
|
||||
colors. Reuse the native 26.3 attribute for authored tints; Bright defaults to
|
||||
neutral white until the color-page port. This does not enable color pages yet.
|
||||
|
||||
The local `1.0.1.68-beta.1` replaces slice 67's special spawn veto with actual
|
||||
effective ambient brightness. The spawn event handler is removed. Bright's
|
||||
combined light-engine reading now has a minimum of 15 on server and client,
|
||||
including after sky darkening. Its white rendering attribute is retained.
|
||||
Vanilla darkness checks therefore reject light-sensitive monsters naturally;
|
||||
light-ignoring mobs, spawners and other spawn sources use their native rules,
|
||||
not Rivencraft exemptions or bans. This does not remove existing mobs.
|
||||
|
||||
Ambient light is not stored as emitted block light or fabricated skylight.
|
||||
Consumers of combined brightness, including plant light checks, now see the
|
||||
ambient contribution. Normal/Dark and non-Rivencraft dimensions are unchanged.
|
||||
Cave Bright remains without skylight. Existing Bright Ages update on restart.
|
||||
This supersedes slice 67's statement that crop light checks are unchanged.
|
||||
|
||||
The local `1.0.1.69-beta.1` slice seed-completes missing weather and lighting
|
||||
pages for newly bound books, including Link-Panel-only random Ages. Explicit
|
||||
pages take precedence. Cave weather remains Off, while Cave lighting can
|
||||
still choose Bright. Old Ages retain their saved choices; they are not rerolled.
|
||||
|
||||
The supported pool uses the 1.21.1 Legacy-rank rule: Normal is rank 1, other
|
||||
choices rank 2. Lighting weights are 3:1:1 for Normal/Dark/Bright; weather
|
||||
weights are 6:1:1:1:1:1 for Normal/Fast/Slow/Off/Always/Storm. The common weight
|
||||
is derived from the supported pool size. Unported Cloudy/Rain/Snow modes are
|
||||
not selected. Independent seed salts keep a missing lighting roll unchanged
|
||||
when weather is explicitly written, and vice versa. This matches the rank
|
||||
policy, not the exact 1.21.1 seed stream/pool. No random feature list, sky or
|
||||
celestial choices are added by this slice.
|
||||
|
||||
`/rivencraft_port age create_random <name> <seed>` uses the same environment
|
||||
resolver and logs its frozen plan. `/rivencraft_port age environment_probe
|
||||
<seed> <terrain>` inspects deterministic defaults and explicit Storm/Bright
|
||||
precedence without creating an Age. Other explicit operator fixtures keep
|
||||
their Normal defaults to isolate the feature under test.
|
||||
|
||||
For manual checking, create several Link-Panel-only books and verify weather/
|
||||
lighting varies across Ages and persists on revisit/restart. Then author an
|
||||
explicit Normal Weather + Normal Lighting book, and a Cave + Bright book.
|
||||
Include explicit Normal pages when comparing terrain without random lighting
|
||||
or weather changes. Random blocklist policy and instability remain pending.
|
||||
|
||||
The local `1.0.1.70-beta.1` slice ports the Overworld, Nether, and End Sky
|
||||
pages. One sky style is accepted per book and persisted independently of
|
||||
terrain, weather and lighting. Missing sky pages still use Overworld; random
|
||||
sky selection is not part of this slice. Static Sky remains unsupported/red
|
||||
until its Legacy animated renderer is ported, rather than being replaced by
|
||||
a flat gray backdrop. Color and individual celestial pages are also pending.
|
||||
|
||||
Nether Sky uses the native no-sky backdrop and reddish fog; End Sky uses the
|
||||
native End backdrop. Neither changes dimension height, coordinate scale,
|
||||
terrain, Nether water/bed rules, or enables an End dragon fight. All lighting
|
||||
choices work with these styles, including Cave + Bright. Bright's render
|
||||
attribute and effective ambient level 15 are retained on each variant.
|
||||
|
||||
The registered variants derive the vanilla Overworld type and preserve its
|
||||
gameplay attributes. Non-Overworld skies use a derived day timeline without
|
||||
the three sky/fog/cloud tint tracks, retaining native gameplay schedules and
|
||||
illumination while avoiding Overworld day tint on those backdrops. Their
|
||||
cloud color is transparent. Biome/weather fog modifiers still apply. Types
|
||||
sync through normal registry login; no new rendering dependency is required.
|
||||
Sky choices survive reload/reseed/restart and retirement rollback. Old Ages
|
||||
without a sky choice retain Overworld style.
|
||||
|
||||
For visual checks, author Plains + Normal Lighting + No Weather with each
|
||||
sky page; compare the sky/fog and verify the same terrain and normal bed/water
|
||||
behavior. Then try Cave + Bright + End Sky and revisit after restarting.
|
||||
Static Sky should remain red. Operator fixtures:
|
||||
`/rivencraft_port age create_sky <name> <seed> <sky> <lighting> <terrain>` and
|
||||
`/rivencraft_port age sky_probe <name>`. Use new books to author a sky choice;
|
||||
this does not rewrite the sky choice of already-bound books.
|
||||
|
||||
The local `1.0.1.71-beta.1` slice adds the previously pending Static Sky.
|
||||
Its client renderer adapts the 1.21.1 Legacy-style background: six independent
|
||||
48×48 noise faces, default gray tint `#929292`, and noise intensity 65–224.
|
||||
The noise updates every two world ticks. A single small dynamic texture and
|
||||
cached cube replace thousands of per-cell draw vertices; resources are closed
|
||||
with the sky renderer. No artwork or rendering dependency is added.
|
||||
|
||||
Static Sky is now supported rather than red, and persists through the same
|
||||
Age index as other sky styles. Four registered lighting variants retain the
|
||||
existing terrain/gameplay rules. Their native skybox is NONE (no End flashes);
|
||||
only the extracted client state schedules the custom background sky pass.
|
||||
Animation state resets between frames and does not consult a different live
|
||||
world while rendering. Color pages, celestial pages, and random sky selection
|
||||
remain pending.
|
||||
|
||||
Build and disposable dedicated-server creation/restart checks passed for
|
||||
Static + Normal + Standard and Static + Bright + Cave. These do not validate
|
||||
client rendering. For manual testing, bind a new Static Sky + Normal Lighting
|
||||
+ No Weather book, look around/above/below for animated gray static, then
|
||||
visit an Overworld/End Sky Age and return to check there is no sky-state leak.
|
||||
Revisit after restarting. Also try Static Sky + Cave + Bright (look outside
|
||||
terrain or in spectator mode). The fixture accepts `static` as its sky value.
|
||||
The dedicated test server was stopped and its temporary world deleted.
|
||||
Changes remain local; no commit/push/publication was requested.
|
||||
|
||||
The local `1.0.1.72-beta.1` slice completes seeded default sky selection for
|
||||
new Ages. Missing sky pages now choose Overworld, Nether, End, or Static
|
||||
instead of always Overworld. This applies both to Link-Panel-only books and
|
||||
authored books without a sky controller. An explicit sky page always wins.
|
||||
|
||||
The selection uses the first random draw and seed salt from 1.21.1's
|
||||
`AgeSkyProfile.random`. Its equally likely Inherited and Overworld slots both
|
||||
use the native Overworld presentation in this port, giving effective weights
|
||||
2:1:1:1. Sky uses an independent random stream, so adding sky/weather/lighting
|
||||
controllers does not reroll the other choices, terrain, or biomes. Colors,
|
||||
individual celestial bodies, and random feature completion remain pending.
|
||||
|
||||
The chosen sky is saved at Age creation and is not recalculated on travel,
|
||||
reload, restart, or reseed. Existing Ages (including ones with no saved sky
|
||||
entry) keep their previous presentation. Explicit diagnostic fixtures retain
|
||||
their controlled defaults; `create_random` uses the same complete environment
|
||||
resolver as player books. `environment_probe` now also checks every explicit
|
||||
sky override and cross-controller independence.
|
||||
|
||||
For manual checking, create several new Link-Panel-only books and compare
|
||||
their sky styles. Also author a terrain/biome book without a sky page, and one
|
||||
with an explicit Overworld Sky page. Revisit/restart to check persistence.
|
||||
For repeatable operator fixtures, `create_random <name> 0` selects Nether,
|
||||
seed 2 selects Overworld, seed 6 Static, and seed 8 End. These seeds only test
|
||||
the current port selection, not complete 1.21.1 world-generation equivalence.
|
||||
No commit/push/publication is performed until explicitly requested.
|
||||
|
||||
The local `1.0.1.73-beta.1` slice ports shared Red/Green/Blue/White/Black
|
||||
colors and their Number modifiers using the 1.21.1 additive mixing rules.
|
||||
Numbers 0–24 directly before a color contribute `number × 10`; White adds to
|
||||
all three channels and Black subtracts from them. Missing channels begin at
|
||||
zero, groups can be written in any channel order, and the completed result is
|
||||
clamped to 0–255 per channel. A color without a number gets a deterministic
|
||||
seeded amount. Numbers currently require a following color: celestial numeric
|
||||
targets and Static Sky resolution (#109) are not implemented by this slice.
|
||||
|
||||
Color groups preceding a Sky page tint that sky; a trailing color group also
|
||||
tints the sky, matching the existing 1.21.1 behavior. The new **Ambient Light
|
||||
Color** page (Lighting creative category) consumes the preceding color group
|
||||
for ambient illumination instead. Author Bright separately when full effective
|
||||
light level 15 is wanted; this color target does not change stored block light
|
||||
or the selected lighting controller. A target without a color group leaves
|
||||
that color native. For example:
|
||||
|
||||
`Number 24 → Blue → Overworld Sky → Number 24 → White → Ambient Light Color → Bright`
|
||||
|
||||
This gives a blue sky/fog with white ambient illumination. Do not use an
|
||||
unmodified Blue page when an exact amount is desired: its amount is seeded.
|
||||
Explicit colors are saved once; they survive reload/reseed/restart. Existing
|
||||
Ages without a color profile remain native. There are no random color groups
|
||||
for books that do not contain color pages yet.
|
||||
|
||||
Sky colors override the native sky/fog color attributes, tint Static Sky's
|
||||
animated cells, and multiply the native End background texture. End texture
|
||||
detail/brightness remains native; a tint does not make it a flat solid color.
|
||||
Ambient color uses 26.3's native ambient-light attribute. Both profiles sync in
|
||||
the Age catalog before travel, not through unbounded RGB dimension types.
|
||||
Client state is replaced with each catalog and cleared on disconnect. Use the
|
||||
same beta JAR on server and client: the catalog protocol has changed.
|
||||
|
||||
Build and dedicated-server checks cover compiled color groups, native fallback,
|
||||
all four sky types, catalog encoding/decoding, reload and reseed. Client color
|
||||
rendering and page availability still need manual checking. Test the example
|
||||
above, a tinted Static/End Sky, return to an uncolored Age/Overworld, and revisit
|
||||
after restarting. `create_colors <name> <seed> <sky>` creates a repeatable Flat,
|
||||
Bright fixture with sky `#643296` and ambient `#6496c8`; `color_probe <name>`
|
||||
checks saved colors against sampled attributes and round-trips the catalog.
|
||||
No commit/push/publication is performed until explicitly requested.
|
||||
|
||||
The local `1.0.1.74-beta.1` slice adds authored day-cycle lengths through the
|
||||
existing **Normal Time** page. Put one **Half Length**, **Full Length**,
|
||||
**Double Length**, or **Zero Length** before Normal Time. These use the
|
||||
1.21.1 period values 0.5, 1, 2, and 0 normal days respectively. Half runs twice
|
||||
as fast; Double runs half as fast. A Number immediately before a Length page
|
||||
overrides it with `number × 0.08` days (0–24), including Number 0 for stopped
|
||||
time. Normal Time without a Length uses one normal day.
|
||||
|
||||
Zero Length freezes the native day timelines at a deterministic, saved random
|
||||
time. It does not snap back to noon on reload/reseed, and contributes no
|
||||
fractional clock interpolation. A book with no Normal Time page retains the
|
||||
existing native cycle; missing periods are not randomly completed yet.
|
||||
Until the full grammar port, a Length requires one following Normal Time page;
|
||||
repeated Length/Normal Time pages are unsupported.
|
||||
|
||||
The implementation wraps only this Age's native Overworld-clock timeline
|
||||
sampling, on server and client. It does not change the global clock or tick
|
||||
rate. Native sun/moon angles, sky/fog/day shading, moon phase, and gameplay
|
||||
attributes supplied by those timelines use the sampled time. Weather layers
|
||||
and authored colors remain intact. Explicit RGB sky/fog overrides remain
|
||||
constant rather than acquiring day tint. Global `/time` changes still shift
|
||||
moving Ages' underlying clock; Zero remains fixed. `/time query` reports the
|
||||
global clock, not the scaled sample; use `cycle_probe` to inspect the latter.
|
||||
|
||||
This controls the existing native pair only. Separate Sun/Moon pages, custom
|
||||
angles/phases/directions/sizes, per-body colors, eclipses, and random celestial
|
||||
completion remain pending. Normal Time does not add visible bodies to End,
|
||||
Nether or Static skies; include Overworld Sky for the visual cycle test.
|
||||
Client/server must use the same beta: the catalog protocol is now 3.
|
||||
|
||||
For manual testing, bind **Link Panel → Half Length → Normal Time → Overworld
|
||||
Sky** (optionally add Normal Lighting and No Weather), compare with Full and
|
||||
Double, then try Zero Length and check that the sky stays still without
|
||||
bouncing. Revisit after restarting. For numeric authoring, put Number 12 before
|
||||
Full Length for a 0.96-day cycle. Existing Ages are not rewritten. Operator
|
||||
fixtures: `create_cycle <name> <seed> <half|full|double|zero> [number]` and
|
||||
`cycle_probe <name>` under `/rivencraft_port age`.
|
||||
|
||||
Port build and disposable-server probes passed for all four lengths and
|
||||
numeric overrides, native fallback, real sun-angle/light differences, catalog
|
||||
round trips, unload/reload, and reseed preservation. Rendering/travel/reconnect
|
||||
checks still require manual validation. Changes remain local until explicitly
|
||||
requested for commit/push/publication.
|
||||
|
||||
The local `1.0.1.75-beta.1` slice adds **Rising**, **Zenith**, **Setting**, and
|
||||
**Nadir** Phase pages and **Clockwise/Counterclockwise** Direction pages to the
|
||||
native Normal Time cycle. Put these before Normal Time, alongside an optional
|
||||
Length. This does not implement separate Sun/Moon bodies or rename Normal
|
||||
Time/Full Length; the naming cleanup is tracked separately in #110.
|
||||
|
||||
The phase conversion matches 1.21.1's `applyInitialCelestialPhase`: written
|
||||
angles start at midnight, while native day time zero is sunrise. Rising,
|
||||
Zenith, Setting, and Nadir select native times 0, 6000, 12000, and 18000.
|
||||
A Number immediately before a Phase overrides it with `number × 15°` from
|
||||
midnight; Number 6 means sunrise, 12 noon, 18 sunset, and 0 or 24 midnight.
|
||||
These are native day-time positions, not uneased geometric sun angles.
|
||||
|
||||
Written phase is anchored when the Age is created, independently of the
|
||||
current Overworld time and Length. Moving cycles then progress from that
|
||||
saved point. Counterclockwise runs the cycle backward; Clockwise is the
|
||||
default. This reverses the Age's day timelines, including their moon-phase
|
||||
and gameplay attributes, not just a visual disc. The global clock is untouched.
|
||||
As before, global `/time` changes can shift moving samples.
|
||||
|
||||
With Zero Length, written Phase replaces the seeded random stopping time:
|
||||
Zero + Zenith stays at noon, Zero + Nadir at midnight. Direction has no motion
|
||||
to reverse in a stopped cycle. Without a Phase, old behavior and saved Ages
|
||||
are preserved. Origins, phase offsets, and direction survive reload, restart,
|
||||
and reseed. Old profile fields default to origin/offset zero and forward
|
||||
motion. The catalog protocol is now 4; server and client need the same beta.
|
||||
|
||||
Interim grammar permits at most one each of Length, Phase and Direction before
|
||||
one Normal Time target. Angle/bearing, size, multiple bodies and eclipses remain
|
||||
unsupported. No random phase/direction completion was added. Example visual
|
||||
checks (include Link Panel, Overworld Sky, Normal Lighting and No Weather):
|
||||
|
||||
- `Zero Length → Zenith → Normal Time`: frozen noon, including after restart.
|
||||
- `Zero Length → Nadir → Normal Time`: frozen midnight.
|
||||
- `Half Length → Zenith → Counterclockwise → Normal Time`: starts at noon and
|
||||
travels backward, twice as fast. Compare with Clockwise.
|
||||
- `Zero Length → Number 6 → Nadir → Normal Time`: Number overrides Nadir to sunrise.
|
||||
|
||||
Operator fixture: `/rivencraft_port age create_phase <name> <seed> <length>
|
||||
<rising|zenith|setting|nadir> <clockwise|counterclockwise> [phaseNumber]`.
|
||||
`cycle_probe` reports both the saved origin and current native/scaled samples.
|
||||
Build and disposable-server checks cover named/numeric phases, opposite motion,
|
||||
fixed day/night light levels, older profile defaults, catalog round trips,
|
||||
reload and reseed. Client rendering/reconnect checks remain manual. Changes
|
||||
remain local until explicitly requested for commit/push/publication.
|
||||
|
||||
The local `1.0.1.76-beta.1` slice ports **North/East/South/West Angle** pages
|
||||
for the native Normal Time cycle. Their bearings match the 1.21.1 symbol
|
||||
values: 0°, 90°, 180°, and 270°. Put at most one Angle before Normal Time.
|
||||
A Number immediately before it overrides the bearing with `number × 15°`,
|
||||
wrapping at 360°: Number 3 is 45°, Number 12 is 180°, and Number 24 returns
|
||||
to 0°. Angle is the orbit's horizontal bearing, distinct from Phase (position
|
||||
along that orbit) and Direction (which way time progresses).
|
||||
|
||||
The client uses the extracted saved bearing to rotate the native sun/moon
|
||||
path, star field, and sunrise/sunset glow together. The native buffers,
|
||||
textures, phase, and lighting tracks are retained. Bearing does not change
|
||||
altitude, day length, light levels, or clock direction. Without an Angle page,
|
||||
the existing native orientation is preserved; old profiles use the same
|
||||
fallback. No seeded random bearing completion was added. This does not add
|
||||
bodies to Nether/End/Static skies or port custom Sun/Moon counts, sizes,
|
||||
per-body colors, or eclipses. #110 naming work remains separate.
|
||||
|
||||
Bearing persists in the existing cycle profile and catalog; the catalog
|
||||
protocol is now 5, requiring the same beta on server and client. Extracted
|
||||
bearing resets with sky state and is replaced at each render, preventing an
|
||||
Age's orientation from leaking into the next world. Sunrise/sunset glow is
|
||||
rotated relative to the native -90° body-path orientation, not around a
|
||||
different axis. No new textures or dimension variants are required.
|
||||
|
||||
Visual check: bind **Link Panel → Zero Length → Rising Phase → North Angle →
|
||||
Normal Time → Overworld Sky** (Normal Lighting/No Weather help comparison).
|
||||
Compare with East/South/West Angle, then put Number 3 before North Angle for
|
||||
a 45° intermediate bearing. Use Rising/Setting rather than Zenith when
|
||||
checking bearing: a body directly overhead stays overhead under a horizontal
|
||||
rotation. Check the glow stays aligned, then travel to an unmodified Age or
|
||||
Overworld and revisit after restart.
|
||||
|
||||
Operator fixture: `/rivencraft_port age create_angle <name> <seed>
|
||||
<north|east|south|west> [number]` creates a frozen sunrise for comparison.
|
||||
The fixture validates all 25 numeric overrides, cardinal values, timing
|
||||
independence, older profile defaults and catalog codec round trips. Dedicated
|
||||
checks cover persistence and unchanged sampled day lighting; actual rendered
|
||||
orientation remains a manual check. No commit/push/publication is performed
|
||||
until explicitly requested.
|
||||
|
||||
The local `1.0.1.77-beta.1` slice fixes automatic sky selection for authored
|
||||
celestials. **Normal Time without a Sky page now selects Overworld Sky**,
|
||||
the current port's celestial-capable style, instead of sometimes selecting
|
||||
Nether/End/Static and hiding the native bodies. Explicit Sky pages retain
|
||||
precedence. Books without celestial pages retain the existing seeded sky
|
||||
distribution, and lighting/weather choices are unchanged. The resolver also
|
||||
recognizes Sun/Moon page intentions for their eventual port; this does not
|
||||
make those still-unported body pages bindable.
|
||||
|
||||
This matches the visible outcome of the current bundled 1.21.1 Age path:
|
||||
its native fallback is Overworld, and its custom sky renderer draws authored
|
||||
suns/moons over its backgrounds. No 1.21.1 change or follow-up issue is needed
|
||||
for those bundled paths. The fix applies at creation of new 26.3 Ages; it
|
||||
does not rewrite existing saved sky choices. Recreate an older affected book
|
||||
to test the corrected automatic choice. Weather and terrain can still obscure
|
||||
celestials naturally; this is a compatible-sky rule, not a guarantee of a clear
|
||||
view from underground or during storms.
|
||||
|
||||
Test **Link Panel → Normal Time**, optionally with No Weather and Normal
|
||||
Lighting, but without any Sky page. The celestial view should be present.
|
||||
Also test Length/Phase/Angle/Direction + Normal Time without a Sky page and
|
||||
confirm that an explicit Sky page still wins. `create_cycle` now exercises
|
||||
this missing-Sky path rather than forcing an explicit sky in its fixture.
|
||||
`environment_probe` verifies all three celestial intentions, explicit Sky
|
||||
precedence in either order, and unchanged lighting/weather selection.
|
||||
No commit/push/publication is performed until explicitly requested.
|
||||
|
||||
The local `1.0.1.78-beta.1` slice ports **Small/Normal/Large Celestial**
|
||||
Size modifiers for Normal Time: 0.5×, 1× and 2× respectively. A Number page
|
||||
immediately before Size overrides that preset with the existing 1.21.1
|
||||
0.25×–3× range (Number 0 = 0.25×, 12 = 1.625×, 24 = 3×).
|
||||
As in 1.21.1, Size before Normal Time sizes its sun; the paired moon stays
|
||||
normal-sized. Standalone Sun/Moon and independent body sizing remain unported.
|
||||
Size changes neither day length nor illumination, phase or bearing. Older
|
||||
profiles default to 1×; new profiles retain size through reload/restart/reseed.
|
||||
The catalog protocol is now 6, so clients and servers must use matching builds.
|
||||
Rendering retains the native sun texture/pipeline without new GPU buffers.
|
||||
|
||||
Test **Link Panel → Zero Length → Zenith Phase → Large Celestial → Normal
|
||||
Time**, optionally with No Weather and Normal Lighting. Compare Small and
|
||||
Normal, then Number 0/12/24 immediately before Size. No Sky page is needed.
|
||||
The sun should change size, while the moon and Overworld remain unchanged.
|
||||
Reopen the world to check persistence. `create_size <name> <seed>
|
||||
<small|normal|large> [number]` creates a frozen-noon fixture without explicit
|
||||
Sky; `cycle_probe <name>` reports its saved size and catalog round trip.
|
||||
Build and disposable-server checks passed, including all numeric sizes,
|
||||
reload, reseed, restart and unchanged sampled lighting/time. Actual rendered
|
||||
size remains a manual check. The test server stopped and its world was deleted.
|
||||
No commit/push/publication is performed until explicitly requested.
|
||||
|
||||
The local `1.0.1.79-beta.1` slice adds shared color groups before **Normal
|
||||
Time**, tinting both its sun and moon as in 1.21.1. Use Number + Red/Green/
|
||||
Blue/White/Black pages before that controller; Length/Phase/Angle/Size can
|
||||
appear between the colors and Normal Time without losing the pending group.
|
||||
Normal Time consumes the group, so it does not tint a later Sky or Ambient
|
||||
Light Color target. Trailing colors still target the sky. Mixing, seeded
|
||||
unnumbered amounts and completed-group clamping use the existing color rules.
|
||||
|
||||
The tint changes native sun/moon RGB only, retaining sprite transparency,
|
||||
weather fading, moon phases and existing size/orbit/time settings. It does
|
||||
not recolor stars, sunrise glow or change gameplay illumination. Older saved
|
||||
color profiles keep untinted bodies; new profiles save and sync their tint.
|
||||
Catalog protocol is now 7: use matching client/server builds. Standalone
|
||||
Sun/Moon controllers and independent per-body colors remain pending.
|
||||
|
||||
Test **Link Panel → Zero Length → Zenith Phase → Number 24 → Red → Normal
|
||||
Time**, optionally with No Weather and Normal Lighting, for a red sun. Use
|
||||
Nadir Phase to view the same tint on the moon. Mix several colors, add a
|
||||
separate color group before Sky, and confirm that the Overworld stays normal
|
||||
after travel. Check glow transparency and persistence after reopening the
|
||||
world. The op fixture `create_celestial_color <name> <seed>
|
||||
<rising|zenith|setting|nadir> <red|green|blue|white|black> <0..24>` provides
|
||||
independently colored sky/ambient plus a frozen body for diagnostics.
|
||||
Client rendering remains a manual check; nothing is committed or published
|
||||
without an explicit request.
|
||||
|
||||
The local `1.0.1.80-beta.1` slice supports one standalone **Sun** or **Moon**
|
||||
controller in place of Normal Time. Both accept the existing Color, Length,
|
||||
Phase, Direction, Angle and Size modifiers (including Number overrides).
|
||||
Sun omits the paired moon; Moon omits the paired sun and sunrise glow.
|
||||
Size applies to the selected standalone body, while Normal Time still sizes
|
||||
only its sun. A missing Sky selects Overworld; explicit Sky remains decisive,
|
||||
and non-Overworld body rendering is still pending.
|
||||
|
||||
Standalone bodies use the root-style linear orbit rather than the native
|
||||
paired cycle's eased orbit. Zenith puts either body overhead, Nadir puts it
|
||||
below the world, and Rising/Setting place it at the horizon. Zero Length
|
||||
freezes that position. Gameplay skylight combines altitude with precipitation
|
||||
and the root 0.18 moonlight multiplier (overhead sun = 15, overhead moon = 6,
|
||||
below-horizon body = 4 under clear weather). Bright/Dark rules remain separate.
|
||||
Native background, stars and moon-phase schedules remain for this slice;
|
||||
multiple independent bodies, eclipse handling, and full seeded celestial
|
||||
completion are not yet supported. Catalog protocol is now 8.
|
||||
|
||||
Test **Link Panel → Zero Length → Zenith Phase → Large Celestial → Sun**,
|
||||
then replace Sun with Moon in a new book. Each should show only that body
|
||||
overhead. Try Number + Color and Size, a moving/reversed cycle, and Nadir;
|
||||
confirm the paired Normal Time and Overworld are unaffected after travel.
|
||||
Restart and revisit. The diagnostic `create_body <name> <seed> <sun|moon>
|
||||
<rising|zenith|setting|nadir>` creates a red, large, east-bearing frozen body.
|
||||
More than one celestial controller is still rejected, not silently discarded.
|
||||
Nothing is committed or published without an explicit request.
|
||||
|
||||
The local `1.0.1.81-beta.1` slice accepts multiple celestial controllers.
|
||||
Each Sun/Moon consumes its own preceding Color, Length, Phase, Direction,
|
||||
Angle and Size group; modifiers reset after that controller. Normal Time
|
||||
contributes a sun/moon pair with opposite phases and its usual sun-only Size.
|
||||
The 1.21.1 limit of **ten suns and ten moons** applies, counting pairs toward
|
||||
both limits. Unsupported duplicate modifiers within one group and dangling
|
||||
modifiers are still rejected; the full recovery/instability grammar is pending.
|
||||
|
||||
Multiple bodies retain independent saved orbits/colors and draw using the
|
||||
native body resources, with suns before moons. Each has its own linear orbit;
|
||||
the first controller supplies the shared day timeline and native background/
|
||||
star/moon-phase schedules. Skylight combines positive-altitude suns (capped at
|
||||
1), or sums weaker moonlight when no sun contributes, matching the root
|
||||
aggregation rule. Explicit non-Overworld backgrounds still hide bodies.
|
||||
Catalog protocol is now 9. Older single-body/paired profiles remain unchanged.
|
||||
|
||||
**Eclipse effects are not in this slice:** sky darkening, disc occlusion and
|
||||
corona effects will be ported next. Visual overlap alone is not an eclipse.
|
||||
|
||||
Test two groups in one book: **Zero Length → Zenith Phase → Large Celestial
|
||||
→ Number 24 → Red → Sun**, then **Zero Length → Rising Phase → Small Celestial
|
||||
→ Number 24 → Blue → Moon** (with Link Panel first). Each should retain its
|
||||
own size, color and position. Try a moving/reversed third body, a Normal Time
|
||||
pair alongside a standalone body, restart, and travel back to an ordinary
|
||||
Age/Overworld. `/rivencraft_port age create_celestials <name> <seed>` creates a
|
||||
diagnostic three-body Age. Rendering remains a manual client check. Nothing
|
||||
is committed or published without an explicit request.
|
||||
|
||||
The local `1.0.1.82-beta.1` slice adds eclipse effects to saved multi-body
|
||||
profiles. Contact is based on the central quarter-size discs, not their
|
||||
halos. Opposite-kind overlap progressively dims a moon and whitens a sun's
|
||||
corona. A visible eclipsed sun darkens the sky by up to 65% and reduces its
|
||||
sunlight contribution by up to 85%, using the 1.21.1 formulas. Other suns can
|
||||
still supply light; same-kind overlaps do not produce eclipses. Below-horizon
|
||||
overlap does not darken the visible sky.
|
||||
|
||||
Each body's native transparent glow is followed by an ordinary-alpha central
|
||||
disc, so an eclipsing moon obscures the sun instead of showing it through the
|
||||
glow. During an eclipse the moon core uses the solid full-moon sprite, matching
|
||||
root's occluding disc; native phases are retained outside eclipse contact.
|
||||
Weather fading remains. Meshes are cached, rebuilt after atlas changes and
|
||||
closed with the sky renderer. No new image assets are needed. Existing Ages
|
||||
with multiple bodies get these derived effects without rebinding their books.
|
||||
|
||||
Test **Link Panel → Zero Length → Zenith Phase → Sun → Zero Length → Zenith
|
||||
Phase → Large Celestial → Moon** (No Weather and Normal Lighting recommended).
|
||||
This holds an eclipse overhead. Change the Moon to Half Length in a new book
|
||||
to watch it pass across the sun. Verify opaque center/transparent glow, sky
|
||||
and ground darkening, restart and travel back to an ordinary dimension.
|
||||
The op fixture `/rivencraft_port age create_eclipse <name> <seed>
|
||||
<aligned|partial|clear|below|moving>` creates controlled comparisons.
|
||||
The multi-body fixture is now named `create_celestials`, avoiding a collision
|
||||
with the existing terrain-generation `create_multi` command. Catalog schema
|
||||
remains protocol 9. Explicit non-Overworld body rendering and broader seeded
|
||||
completion are still pending. Nothing is committed or published without an
|
||||
explicit request.
|
||||
|
||||
The local `1.0.1.83-beta.1` correction makes standalone and multi-body authored
|
||||
moons use the **full-moon sprite in both glow and opaque-center passes**, as
|
||||
the 1.21.1 custom renderer does. Native dark lunar-phase pixels could become
|
||||
black patches once the new ordinary-alpha center pass made them opaque.
|
||||
The native Normal Time pair keeps its calendar phases. Eclipse geometry,
|
||||
darkening and corona formulas are unchanged; genuinely eclipsed moons still
|
||||
turn dark and obscure their overlapping sun.
|
||||
|
||||
Read-only inspection of the reported `celest` save found all four body colors
|
||||
intact (`-1`, untinted). Its first moon and second sun share period, direction,
|
||||
phase, origin and bearing, creating a permanent eclipse after eclipse support
|
||||
was added. This is separate from the phase-sprite rendering correction. No
|
||||
saved orbits/colors are rewritten, and existing Ages need not be recreated.
|
||||
Please revisit `celest`, compare a nonaligned authored moon, check an eclipse
|
||||
and resource reload, then return to Overworld/Normal Time to confirm native
|
||||
phases are unaffected. This rendering correction still needs a client check.
|
||||
Nothing is committed or published without an explicit request.
|
||||
|
||||
The local `1.0.1.84-beta.1` slice renders authored **Normal Time, Sun, Moon
|
||||
and multiple bodies over all four Sky styles**, including explicit Nether,
|
||||
End and animated Static. Backgrounds draw first; the existing authored body
|
||||
pass follows with its colors, sizes, orbits, opaque centers and eclipse effects.
|
||||
Nether retains its fog backdrop, End retains its native sky texture, and
|
||||
Static keeps animating. These are presentation changes only: no Nether/End
|
||||
terrain, ceiling, coordinate-scale or dragon behavior is introduced.
|
||||
|
||||
Eclipses shade End/Static background tint and the Nether-style fog backdrop;
|
||||
gameplay light still uses the saved body profile. Native Overworld rendering
|
||||
and no-celestial Ages are unchanged. No new assets, saved fields or catalog
|
||||
protocol changes are needed. Already-created Ages with explicit backgrounds
|
||||
and authored celestials gain the rendering on update without rebinding.
|
||||
|
||||
Test the same frozen overhead Sun/Moon book with **Nether Sky**, **End Sky**
|
||||
and **Static Sky**, then test a moving body and eclipse in each. Check body
|
||||
colors/size, no duplicate bodies, unchanged background style, Static animation,
|
||||
F3+T, restart and travel to Overworld. The existing diagnostic
|
||||
`age create_eclipse <name> <seed> <mode> [overworld|nether|end|static]` now
|
||||
accepts an optional explicit Sky for comparisons. Client rendering remains a
|
||||
manual check. Nothing is committed or published without an explicit request.
|
||||
|
||||
The local `1.0.1.85-beta.1` slice randomizes a **bare Sun or Moon page** when
|
||||
a new Age is created. Each such controller gets its own deterministic color,
|
||||
size, bearing, phase, direction and length. Defaults use the root random-Age
|
||||
palette (mixing 1–3 colors), size 0.75×–1.75×, bearing 0–359°, full-cycle phase,
|
||||
either direction, and length 0.2–2 days with Legacy's 1-in-7 stopped-cycle
|
||||
chance. Sun/Moon counts still come from your pages; nothing adds extra bodies.
|
||||
|
||||
This applies only when that controller has **no written celestial modifiers**.
|
||||
If its group includes Color, Size, Angle, Phase, Direction or Length, the
|
||||
existing explicitly authored behavior is retained, including defaults for
|
||||
its other fields. Sky/ambient colors consumed by their own target and unrelated
|
||||
terrain/weather/lighting pages do not make a body authored or reshuffle its
|
||||
random settings. Normal Time is unchanged. Groups use Age seed plus controller
|
||||
ordinal; repeated bare pages get independent settings. Choices are saved,
|
||||
not rerolled on GUI opening, reload/restart or terrain reseeding.
|
||||
|
||||
Test a **new** book with **Link Panel → Sun → Moon → Sun**, optionally adding
|
||||
No Weather and Normal Lighting for visibility. Expect varied colors/sizes/
|
||||
orbits; some bodies can be below the horizon or stopped. Test an explicitly
|
||||
modified body in the same book, then restart and revisit. Existing Ages keep
|
||||
their original saved settings and are not randomized retroactively. The op
|
||||
fixture `age create_random_celestials <name> <seed>` tests independent seeded
|
||||
defaults and bounds. Nothing is committed or published without explicit request.
|
||||
|
||||
The local `1.0.1.86-beta.1` slice completes the celestial branch for **new
|
||||
books with no celestial controller pages**, including Link-Panel-only books.
|
||||
The Age seed chooses **0–3 suns and 0–3 moons**, following the root random
|
||||
count draws. Each body receives the independent saved settings introduced
|
||||
in .85. An explicit Sun, Moon or Normal Time suppresses this extra-body
|
||||
completion entirely; its written controllers remain authoritative.
|
||||
|
||||
A valid zero-sun/zero-moon result draws neither body nor sunrise glow, rather
|
||||
than accidentally falling back to a vanilla pair. This does not force Dark
|
||||
Lighting: background/star schedules and independent lighting retain their
|
||||
normal behavior. Random background selection remains independent of body
|
||||
counts, and all supported Sky styles can render the chosen bodies. Default
|
||||
sky colors retain their current behavior; this slice completes bodies only.
|
||||
|
||||
Choices persist through restart and terrain reseeding. Existing Ages are not
|
||||
rerolled. Catalog protocol is now **10**, requiring matching client/server
|
||||
builds. Test several new Link-Panel-only books, plus books that contain only
|
||||
terrain/weather/lighting/Sky pages. Expect varied counts, including sun-only,
|
||||
moon-only and empty skies; some bodies may be below the horizon. Compare an
|
||||
explicit one-Sun book and a Normal Time book, then restart and revisit.
|
||||
`age create_completed_sky <name> <seed> [overworld|nether|end|static]` provides
|
||||
clear-weather Flat fixtures and reports a sample zero-body seed. The existing
|
||||
`age create_random` now also uses the completed celestial plan. Nothing is
|
||||
committed or published without explicit request.
|
||||
|
||||
The local `1.0.1.87-beta.1` slice gives **new Link-Panel-only random Ages**
|
||||
a saved random sky/fog tint, matching 1.21.1's seeded palette and rounded
|
||||
mix of 1–3 colors. This completes the sky-color draw already reserved by the
|
||||
random celestial count selection. Body settings/counts are unchanged.
|
||||
|
||||
Books containing symbol pages keep their authored/default color rules; a
|
||||
black authored sky remains black. Ambient lighting is not randomized by this
|
||||
change. Existing Ages retain their saved colors. No new saved fields or
|
||||
catalog protocol change is needed. Create several new Link-Panel-only books,
|
||||
check sky/fog colors and restart persistence, then compare an explicitly
|
||||
colored Sky book and an uncolored terrain-only book. Nothing is committed or
|
||||
published without explicit request.
|
||||
|
||||
The local `1.0.1.88-beta.1` slice seed-completes **explicit biome distribution
|
||||
pages with missing biome pages**. Single Biome alone selects one eligible
|
||||
biome. Tiled/Grid/region controllers alone choose at least two distinct
|
||||
biomes, using 1.21.1's seeded continuation rule and the available pool as the
|
||||
maximum. With one authored biome they fill just the missing second biome;
|
||||
with two distinct authored biomes they add nothing. Repeated biome pages keep
|
||||
their weight but do not satisfy the distinct-biome minimum by themselves.
|
||||
|
||||
Cave completion draws only recognized Cave/Nether biomes. Void supplies The
|
||||
Void only when no biome is authored; a multi-biome Void book needs sufficient
|
||||
explicit biome pages rather than acquiring an unwanted Void biome. Native
|
||||
still rejects biome pages. No-controller biome behavior is unchanged, and
|
||||
the broader recovery/instability grammar is not part of this slice.
|
||||
|
||||
Test a new Link Panel + Tiny Biome Regions book, Single Biome alone, and Tiny
|
||||
with one/two specific biome pages. Also try Cave + Tiny and restart/revisit.
|
||||
The operator fixture is `age create_completed_biomes <name> <seed> <terrain>
|
||||
<distribution>`; `age biome_pool <name>` inspects the generator without loading
|
||||
chunks. Existing Age pools do not change. Nothing is committed or published
|
||||
without explicit request.
|
||||
|
||||
The local `1.0.1.89-beta.1` slice supports **unpaired Block/Fluid pages**.
|
||||
When no feature immediately follows a material, the Age seed and original
|
||||
page position choose a compatible implied feature, following 1.21.1's model.
|
||||
Explicit material → feature pairs remain unchanged. Consecutive material
|
||||
pages each receive their own selection; the resulting choices may repeat.
|
||||
Only the saved feature plans gain these implied choices, not extra physical
|
||||
pages in the book.
|
||||
|
||||
Solid candidates are Rocks, Floating Islands, Tendrils, Spheres, Spikes and
|
||||
Obelisks, restricted by terrain. Cave permits Tendrils/Spheres; Void also
|
||||
permits Floating Islands. Flat excludes Rocks. Fluid candidates are the three
|
||||
lake types on supported surface noise terrain. An unpaired fluid on Flat,
|
||||
Void or Cave is still refused because no compatible lake implementation is
|
||||
available there. Explicit incompatible pairings and existing 32-feature
|
||||
limits are unchanged; the full recovery/instability grammar remains pending.
|
||||
|
||||
Test Link Panel + Glowstone, consecutive Glowstone/Stone pages, and an unpaired
|
||||
Water page on Standard terrain. Compare an explicit Glowstone → Tendrils
|
||||
pair, try a solid on Cave/Void, and restart/revisit. Existing Ages retain
|
||||
their saved features. Operator diagnostics: `age create_implied_material
|
||||
<name> <seed> <terrain> <material-id>` and `age feature_plan <name>` (no chunk
|
||||
generation). Nothing is committed or published without explicit request.
|
||||
|
||||
The local `1.0.1.90-beta.1` slice adds **random material features to new
|
||||
Link-Panel-only Ages**. The Age seed rolls 0–2 distinct feature types using
|
||||
1.21.1's Legacy rank weighting, then chooses a registered compatible solid
|
||||
or source fluid for each. Candidates are Rocks, Floating Islands, Tendrils,
|
||||
Spheres, Spikes, Obelisks and the three lake types, filtered by terrain.
|
||||
|
||||
The same creation pass rolls rare Skylands (1-in-8 on noise terrain), Crystal
|
||||
Formations (1-in-10 on exposed surface terrain), and Star Fissures using the
|
||||
root small-feature grammar. These are additional to the 0–2 material features.
|
||||
Random structure selection and page-eligibility config remain future port
|
||||
work. Authored books do not gain unrequested random features, and existing
|
||||
Age plans are not rerolled. Choices persist through restart and reseed.
|
||||
|
||||
Test several new Link-Panel-only books for varied material features, including
|
||||
Ages with none; compare an explicitly authored book and revisit after restart.
|
||||
For controlled checks, `age create_random_features <name> <seed> <terrain>`
|
||||
creates a feature-only fixture and `age feature_plan <name>` reports its
|
||||
saved generator choices without generating chunks. `age create_random` uses
|
||||
the same production selector. Nothing is committed or published without
|
||||
explicit request.
|
||||
|
||||
The local `1.0.1.91-beta.1` slice ports **random structure selection** from
|
||||
1.21.1. New Link-Panel-only Ages roll 0–2 distinct structure families in
|
||||
addition to .90's material features. Legacy ranks prefer Villages, Strongholds
|
||||
and Mineshafts, followed by Dungeons/Fortresses; newer structures are rarer.
|
||||
The 26.3 Abandoned Camps page joins that rare pool. Candidates must support
|
||||
the terrain and at least one biome in the actual selected biome pool.
|
||||
|
||||
A saved structure-control flag distinguishes an intentionally empty selection
|
||||
from older Ages' native behavior. Unselected page-controlled structure sets
|
||||
are disabled even when no structures were chosen; other native structure
|
||||
families remain governed by vanilla biome rules. Authored pages retain their
|
||||
biome override behavior, and old saved Ages are not changed. Cave Dungeons
|
||||
remain excluded by the port's existing terrain restriction; this is a known
|
||||
remaining difference from 1.21.1, not a new root behavior.
|
||||
|
||||
Test several new random Ages, including a restart/revisit, and compare an
|
||||
authored Village book. For focused checks, `age create_random_structures
|
||||
<name> <seed> <terrain> <biome-id|native>` creates a controlled fixture;
|
||||
`age feature_plan <name>` reports the frozen selection, including Dungeons.
|
||||
No catalog protocol change is needed. Nothing is committed or published
|
||||
without explicit request.
|
||||
|
||||
The local `1.0.1.92-beta.1` slice enables **Dungeons on Cave terrain**, including
|
||||
Nether cave biomes, matching 1.21.1's noise-terrain support. The already-ported
|
||||
placement JSON is byte-identical to root: eight vanilla monster-room attempts
|
||||
per page, uniformly from one block above the minimum to one below the maximum
|
||||
build height. Vanilla still requires a suitable enclosed chamber; an attempt
|
||||
is not a guaranteed dungeon. Stacking up to 32 pages is unchanged.
|
||||
|
||||
Only the combined Cave dungeon/lake rejection was split: lakes remain rejected
|
||||
there, and Flat/Void still reject Dungeons. New random Cave Ages can now select
|
||||
Dungeons through .91's normal eligibility/rank rules. Existing saved plans
|
||||
are unchanged. `age create_dungeons <name> <seed> cave` creates an eight-page
|
||||
fixture. `age dungeons_probe <name> [chunks]` optionally limits the search to
|
||||
1–4 chunks per side; omitting it retains the previous 4×4 search. That probe
|
||||
generates chunks and is reserved for manual placement checks.
|
||||
|
||||
Test Cave + Dungeons, optionally stacking pages for visibility; use spectator
|
||||
mode to find monster rooms and compare a Nether-biome Cave Age. This slice's
|
||||
server validation checks authoring and persistence without generating a large
|
||||
region. No saved-field/protocol change is needed. Nothing is committed or
|
||||
published without explicit request.
|
||||
|
||||
The local `1.0.1.93-beta.1` slice fixes **Nether and End biome materials on
|
||||
noise terrain**, including Nether biomes in Cave Ages. The port had retained
|
||||
the vanilla terrain's Overworld material rules, which could coat Nether
|
||||
biomes in grass. It now ports 1.21.1's biome rule layer: the five vanilla End
|
||||
biomes use the registered End rule, the five vanilla Nether biomes use the
|
||||
registered Nether rule, and all other biomes retain the terrain's original
|
||||
rules. Nether coverings include Nylium, Soul Sand/Soil, Basalt and Blackstone
|
||||
where their native rules select them; End areas use End Stone.
|
||||
|
||||
Noise geometry, biome selection, lighting, default fluids, sea level and dry
|
||||
Cave aquifer settings remain unchanged. This is not a Flat/Void layer rewrite.
|
||||
The correction applies after reload to **new chunks only** in existing Ages;
|
||||
already generated grass is not replaced. Test a new Nether-biome Cave Age
|
||||
and End-biome Standard/Island Age, or explore new chunks after restarting.
|
||||
|
||||
Operator fixtures: `age create_biome_surface <name> <seed> <terrain> <biome-id>`
|
||||
and `age material_probe <name> [chunk_x chunk_z]`. The latter examines one
|
||||
terrain-stage chunk (default 0,0) without feature, lighting or entity generation. Nothing is committed
|
||||
or published without explicit request.
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.71-beta.1
|
||||
|
||||
This beta collects the player-tested lighting and sky port slices since
|
||||
1.0.1.65-beta.1. It is not yet feature-complete with Minecraft 1.21.1.
|
||||
|
||||
## Lighting and seeded environments
|
||||
|
||||
- Added Normal, Dark, and Bright lighting pages with saved per-Age settings.
|
||||
- Bright works in Cave terrain too, without adding skylight or weather.
|
||||
- Bright supplies effective ambient light level 15 to native combined-light
|
||||
checks and the white ambient rendering attribute. Vanilla darkness-based
|
||||
monster checks respond to that lighting; there is no separate mob-spawn
|
||||
veto. Mobs that ignore light retain their normal rules.
|
||||
- Missing weather and lighting controllers now resolve deterministically from
|
||||
the Age seed, using independent choices and Legacy-style Normal weighting.
|
||||
Explicit pages win; Cave terrain still forces weather Off. Existing Ages
|
||||
keep their saved choices rather than rerolling on load.
|
||||
|
||||
## Sky styles
|
||||
|
||||
- Added Overworld, Nether, End, and animated Static Sky pages, independently
|
||||
of terrain, weather, and lighting. Choices persist on reload and restart.
|
||||
- Nether Sky uses a reddish fog backdrop; End Sky uses the native End sky.
|
||||
These are visual styles, not Nether/End terrain or gameplay conversions:
|
||||
dimension geometry, coordinate scale, bed/water rules, and dragon behavior
|
||||
are not changed.
|
||||
- Static Sky adapts the existing Legacy-style renderer: six independent
|
||||
48×48 animated gray noise faces. A cached cube and small dynamic texture
|
||||
avoid rebuilding thousands of per-cell vertices each frame. Client-only
|
||||
rendering resources are released with the sky renderer.
|
||||
- Non-Overworld backgrounds have transparent clouds and a derived day
|
||||
timeline that avoids Overworld sky/fog/cloud color tracks while retaining
|
||||
gameplay and illumination schedules.
|
||||
- Updated supported-page feedback and added operator sky/environment probes.
|
||||
|
||||
## Current limits and testing
|
||||
|
||||
- Missing Sky pages still default to Overworld; random sky selection, color
|
||||
pages and individual celestial bodies remain pending. Number-controlled
|
||||
Static Sky resolution is tracked separately in issue #109 for both versions.
|
||||
- The writing/discovery loop, instability, portals, and optional-integration
|
||||
parity are still incomplete. Creative availability is not a support claim;
|
||||
check page tooltips and the beta guide for supported combinations.
|
||||
- Previously generated chunks are not regenerated. Save compatibility during
|
||||
the beta is not guaranteed; keep backups and keep both version tracks apart.
|
||||
|
||||
The player reported in-game passes for the included slices, including Static
|
||||
Sky. Build checks passed for both version tracks. Targeted disposable-server
|
||||
checks covered environment defaults, explicit choices, registered sky/lighting
|
||||
variants, persistence and restart, including Cave + Bright and Static Sky.
|
||||
This is not a claim of complete dedicated-server or optional-mod coverage.
|
||||
Test servers were stopped and temporary worlds removed.
|
||||
|
||||
Requires Minecraft **26.3**, NeoForge **26.3.0.99** and **Java 25**. Dynamic
|
||||
Dimensions and Bad Packets are not required by this build. The 1.21.1 release
|
||||
and its dependencies are unchanged.
|
||||
|
||||
Report issues and help improve the wiki at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -0,0 +1,53 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.78-beta.1
|
||||
|
||||
Collects the player-validated port slices since 1.0.1.71-beta.1. This beta is
|
||||
not yet feature-complete with the Minecraft 1.21.1 version.
|
||||
|
||||
## Sky and color writing
|
||||
|
||||
- Missing Sky pages now select a deterministic seeded style. Books with
|
||||
Normal Time and no explicit Sky select celestial-compatible Overworld Sky.
|
||||
Explicit Sky pages always win; weather and lighting choices stay independent.
|
||||
- Ported Red, Green, Blue, Black and White color pages. Preceding Number pages
|
||||
control component strength; unnumbered colors use seeded values. Colors mix
|
||||
before clamping the completed group. Colors before Sky tint that sky; a
|
||||
trailing color group also targets the sky.
|
||||
- Added an Ambient Color controller for an independent ambient-light tint.
|
||||
Color alone does not change effective illumination or monster-spawn rules.
|
||||
- Native, End and animated Static backgrounds use saved Age color settings.
|
||||
|
||||
## Normal Time celestial controls
|
||||
|
||||
- Length pages control the native day cycle; a preceding Number fine-tunes
|
||||
length. Zero Length freezes the cycle at a seeded time or an authored Phase,
|
||||
without interpolation bounce or changing the shared Overworld clock.
|
||||
- Rising, Zenith, Setting and Nadir Phase pages choose the starting phase.
|
||||
Clockwise and Counterclockwise control direction. North, East, South and
|
||||
West Angle pages choose orbit bearing. Preceding Number pages fine-tune
|
||||
Phase and Angle in 15-degree increments.
|
||||
- Small, Normal and Large Celestial size Normal Time's sun to 0.5×, 1× and 2×.
|
||||
A preceding Number overrides size from 0.25× to 3×. As in 1.21.1, the paired
|
||||
moon remains normal-sized. Size does not alter illumination or day length.
|
||||
- Settings persist through reload/restart and remain stable when reseeding
|
||||
future terrain. Older profiles retain native/default values.
|
||||
- Added operator diagnostics for environment, color and celestial profiles.
|
||||
|
||||
## Compatibility and testing
|
||||
|
||||
- Catalog protocol is now 6: use matching client/server builds.
|
||||
- Player in-game checks passed for the included slices. Build checks and
|
||||
targeted disposable-server diagnostics covered numeric modifiers, catalog
|
||||
round trips, unchanged sampled lighting, reload, reseed and restart.
|
||||
- Standalone Sun/Moon controllers, multiple bodies, per-body colors, eclipses,
|
||||
full writing/discovery, instability, portals and optional-integration parity
|
||||
remain incomplete. Creative availability does not imply full page support.
|
||||
- Number-controlled Static Sky resolution remains tracked in issue #109;
|
||||
time-page naming improvements remain tracked in issue #110.
|
||||
- Existing chunks are not regenerated. Save compatibility is not guaranteed
|
||||
during beta; keep backups and keep the two Minecraft version tracks apart.
|
||||
|
||||
Requires Minecraft **26.3**, NeoForge **26.3.0.99**, and **Java 25**. Dynamic
|
||||
Dimensions and Bad Packets are not required. Minecraft 1.21.1 is unchanged.
|
||||
|
||||
Report issues and help improve the wiki at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -0,0 +1,83 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.93-beta.1
|
||||
|
||||
Collects the player-validated port slices .79–.93 since 1.0.1.78-beta.1.
|
||||
This beta is not yet feature-complete with Minecraft 1.21.1.
|
||||
|
||||
## Celestial writing and rendering
|
||||
|
||||
- Ported standalone Sun and Moon pages and multiple independently authored
|
||||
bodies, up to ten of each. Each controller consumes its own ordered Color,
|
||||
Length, Phase, Direction, Angle and Size modifiers; Number pages fine-tune
|
||||
supported values. Normal Time retains its paired sun/moon behavior.
|
||||
- Color groups before Normal Time tint both bodies; independent Sun/Moon
|
||||
groups can use different colors. Authored moons use full-moon sprites as in
|
||||
1.21.1; native Normal Time retains calendar moon phases.
|
||||
- Authored bodies render over Overworld, Nether, End and animated Static
|
||||
backgrounds. Opaque centers retain transparent glows. Eclipse alignment
|
||||
affects body color, background shading and gameplay sky light.
|
||||
- Bare Sun/Moon pages receive independent seeded colors, sizes, bearings,
|
||||
phases, directions and lengths, including stopped cycles. Explicit modifier
|
||||
groups retain their authored behavior.
|
||||
- Books without celestial controllers seed-complete 0–3 suns and 0–3 moons.
|
||||
Empty skies do not accidentally draw a vanilla pair. Explicit controllers
|
||||
suppress extra-body completion. Link-Panel-only random Ages also receive
|
||||
seeded sky/fog colors using the 1.21.1 palette.
|
||||
- All resolved settings are saved, not rerolled by viewing a book, restarting,
|
||||
or reseeding future terrain. Existing Age profiles remain unchanged.
|
||||
|
||||
## Biome and feature completion
|
||||
|
||||
- Explicit biome-distribution pages can now seed-complete missing biome
|
||||
pools. Single selects one; other nonnative controllers require two distinct
|
||||
biomes. Fully omitted pools use the seeded continuation rule, bounded by the
|
||||
available biome pool. Authored pools fill only the missing minimum.
|
||||
- Supplied biome order and duplicate weights are retained. Two distinct
|
||||
supplied biomes do not acquire an unwanted third biome. Cave pools use
|
||||
recognized Cave/Nether biomes; Void does not inject The Void alongside
|
||||
supplied biome pages.
|
||||
- Unpaired Block/Fluid pages receive seeded terrain-compatible implied
|
||||
features. Consecutive materials resolve independently; explicit pairs remain
|
||||
authoritative. Unsupported pairings and the current feature caps remain.
|
||||
- Link-Panel-only Ages roll 0–2 distinct material feature types with Legacy
|
||||
rank weighting and compatible registered solids/source fluids. Additional
|
||||
rare rolls cover Skylands, Crystal Formations and Star Fissures.
|
||||
- Random Ages also roll 0–2 distinct structure families, filtered by terrain,
|
||||
registry availability and the chosen biome pool. Newer structures, including
|
||||
26.3 Abandoned Camps, are rare candidates. An intentionally empty selection
|
||||
disables page-controlled structure sets rather than enabling all of them.
|
||||
- Cave terrain now supports authored and randomly selected Dungeons, matching
|
||||
1.21.1. Dungeon placement retains eight monster-room attempts per page and
|
||||
modern build-height anchors. Flat/Void Dungeons and Cave lakes remain
|
||||
unsupported.
|
||||
|
||||
## Nether and End material correction
|
||||
|
||||
- Ported 1.21.1's biome-specific material-rule layer for noise terrain. Nether
|
||||
biomes in Cave Ages now use their native Netherrack, Nylium, Soul Sand/Soil,
|
||||
Basalt and Blackstone rules instead of Overworld grass. End biomes use End
|
||||
Stone. Other biomes retain the original terrain rules.
|
||||
- Geometry, biome selection, dry Cave aquifers and other environment settings
|
||||
are preserved. Flat/Void layers and authored feature materials are unchanged.
|
||||
- Existing Ages pick up the correction after reload, but only new chunks
|
||||
change. Already generated grass is not replaced.
|
||||
|
||||
## Compatibility and validation
|
||||
|
||||
- Requires Minecraft **26.3**, NeoForge **26.3.0.99**, and **Java 25**. Dynamic
|
||||
Dimensions and Bad Packets are not required. Minecraft 1.21.1 is unchanged.
|
||||
- Catalog protocol is **10**: use matching client/server builds.
|
||||
- Player in-game checks passed for all included slices. Builds and targeted
|
||||
disposable-server checks covered writing, seeded completion, codecs/catalog
|
||||
round trips, terrain materials, reload, reseed and restart. Temporary servers
|
||||
and test worlds were removed. This is not a full automated GameTest run.
|
||||
- Full writing recovery, discovery/stations, instability, portals and optional
|
||||
integration parity remain unfinished. Random-page eligibility config also
|
||||
remains pending; valuable/hazardous random materials currently lack the
|
||||
finished version's instability scoring. Creative availability does not imply
|
||||
full page support. Number-controlled Static resolution (#109) and time-page
|
||||
naming (#110) remain separate work.
|
||||
- Beta save compatibility is not guaranteed. Back up worlds and keep the two
|
||||
Minecraft version tracks and saves separate.
|
||||
|
||||
Report issues and help improve the wiki at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -121,6 +121,71 @@ def generatePortLanguage = tasks.register('generatePortLanguage', GeneratePortLa
|
||||
}
|
||||
sourceSets.main.resources.srcDir generatePortLanguage
|
||||
|
||||
// Derive registered presentation variants without duplicating vanilla gameplay settings.
|
||||
abstract class GeneratePortDimensionTypes extends DefaultTask {
|
||||
@InputFile abstract RegularFileProperty getMinecraftJar()
|
||||
@OutputDirectory abstract DirectoryProperty getOutputDir()
|
||||
|
||||
@TaskAction void generate() {
|
||||
new java.util.zip.ZipFile(minecraftJar.get().asFile).withCloseable { archive ->
|
||||
def entry = archive.getEntry('data/minecraft/dimension_type/overworld.json')
|
||||
if (entry == null) throw new GradleException('Missing vanilla Overworld dimension type')
|
||||
String template = archive.getInputStream(entry).withCloseable { it.getText('UTF-8') }
|
||||
def dayEntry = archive.getEntry('data/minecraft/timeline/day.json')
|
||||
if (dayEntry == null) throw new GradleException('Missing vanilla day timeline')
|
||||
def day = archive.getInputStream(dayEntry).withCloseable {
|
||||
new groovy.json.JsonSlurper().parseText(it.getText('UTF-8'))
|
||||
}
|
||||
// Keep gameplay schedules and illumination, but do not tint these
|
||||
// native non-Overworld sky backgrounds blue/black with the day cycle.
|
||||
['fog_color', 'sky_color', 'cloud_color'].each { day.tracks.remove("minecraft:visual/${it}".toString()) }
|
||||
// Markers are global per clock and already owned by minecraft:day.
|
||||
day.remove('time_markers')
|
||||
File dayFile = new File(outputDir.get().asFile, 'data/rivencraft/timeline/age_sky_day.json')
|
||||
dayFile.parentFile.mkdirs()
|
||||
dayFile.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(day)) + '\n'
|
||||
File timelineTag = new File(outputDir.get().asFile, 'data/rivencraft/tags/timeline/age_sky.json')
|
||||
timelineTag.parentFile.mkdirs()
|
||||
timelineTag.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson([
|
||||
values: ['#minecraft:universal', 'rivencraft:age_sky_day',
|
||||
'minecraft:moon', 'minecraft:early_game']])) + '\n'
|
||||
['overworld', 'nether', 'end', 'static'].each { sky ->
|
||||
['age_normal', 'age_dark', 'age_bright', 'age_cave_bright'].each { lighting ->
|
||||
if (sky == 'overworld' && lighting == 'age_normal') return
|
||||
String name = lighting + (sky == 'overworld' ? '' : "_sky_${sky}")
|
||||
def definition = new groovy.json.JsonSlurper().parseText(template)
|
||||
definition.has_skylight = lighting in ['age_normal', 'age_bright']
|
||||
definition.ambient_light = lighting in ['age_bright', 'age_cave_bright'] ? 1.0 : 0.0
|
||||
// 26.3's lightmap reads this attribute, not ambient_light.
|
||||
if (definition.ambient_light == 1.0)
|
||||
definition.attributes['minecraft:visual/ambient_light_color'] = '#ffffff'
|
||||
if (sky != 'overworld') {
|
||||
definition.skybox = sky == 'end' ? 'end' : 'none'
|
||||
definition.attributes['minecraft:visual/fog_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#181318' : '#6b2725'
|
||||
definition.attributes['minecraft:visual/sky_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#000000' : '#6b2725'
|
||||
definition.attributes['minecraft:visual/cloud_color'] = '#00000000'
|
||||
if (sky == 'nether') {
|
||||
definition.attributes['minecraft:visual/fog_start_distance'] = 10.0
|
||||
definition.attributes['minecraft:visual/fog_end_distance'] = 96.0
|
||||
}
|
||||
definition.timelines = '#rivencraft:age_sky'
|
||||
}
|
||||
File result = new File(outputDir.get().asFile, "data/rivencraft/dimension_type/${name}.json")
|
||||
result.parentFile.mkdirs()
|
||||
result.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(definition)) + '\n'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def generatePortDimensionTypes = tasks.register('generatePortDimensionTypes', GeneratePortDimensionTypes) {
|
||||
dependsOn 'createMinecraftArtifacts'
|
||||
minecraftJar.set(layout.buildDirectory.file("moddev/artifacts/minecraft-patched-${neo_version}.jar"))
|
||||
outputDir.set(layout.buildDirectory.dir('generated/resources/dimensionTypes'))
|
||||
}
|
||||
sourceSets.main.resources.srcDir generatePortDimensionTypes
|
||||
|
||||
tasks.named('processResources', ProcessResources).configure {
|
||||
// Reuse the existing sprite and base model without copying binary assets.
|
||||
from('../../src/main/resources') {
|
||||
|
||||
@@ -7,4 +7,4 @@ neo_version=26.3.0.99
|
||||
mod_id=rivencraft
|
||||
mod_name=Rivencraft
|
||||
mod_license=LGPL-3.0-or-later
|
||||
mod_version=1.0.1.65-beta.1
|
||||
mod_version=1.0.1.93-beta.1
|
||||
@@ -8,6 +8,41 @@
|
||||
"item.rivencraft.page.unresolved": "Unknown Symbol Page",
|
||||
"item.rivencraft.page.symbol.implemented": "Implemented in 26.3; book and terrain requirements still apply.",
|
||||
"symbol.rivencraft.weather.normal.description": "Use an independent normal weather cycle. Cave terrain keeps weather off.",
|
||||
"symbol.rivencraft.sky.overworld.description": "Use the native Overworld sky. Terrain and lighting remain independently authored.",
|
||||
"symbol.rivencraft.sky.nether.description": "Use a Nether-style reddish fog backdrop. Authored celestials render over it; terrain and gameplay rules remain independent.",
|
||||
"symbol.rivencraft.sky.end.description": "Use the native End sky backdrop with authored celestials over it, without End terrain or a dragon fight.",
|
||||
"symbol.rivencraft.sky.static.description": "Animated static surrounds the Age, behind authored celestials. Preceding colors tint it; otherwise it is gray.",
|
||||
"symbol.rivencraft.lighting.ambient_color": "Ambient Light Color",
|
||||
"symbol.rivencraft.lighting.ambient_color.description": "Apply preceding shared colors to ambient light. Use Bright separately for full effective ambient lighting.",
|
||||
"symbol.rivencraft.number.description": "Before Color: value × 10 per channel. Before Length: value × 0.08 days. Before Phase/Angle: value × 15 degrees. Before Size: 0.25× to 3× (0-24).",
|
||||
"symbol.rivencraft.size.small.description": "Before Sun/Moon: 0.5× body size. Before Normal Time: size the sun only. A preceding Number overrides the size.",
|
||||
"symbol.rivencraft.size.normal.description": "Before Sun/Moon: 1× body size. Before Normal Time: size the sun only. A preceding Number selects 0.25× through 3× (0-24).",
|
||||
"symbol.rivencraft.size.large.description": "Before Sun/Moon: 2× body size. Before Normal Time: size the sun only. A preceding Number selects 0.25× through 3× (0-24).",
|
||||
"symbol.rivencraft.angle.north.description": "Before Normal Time, Sun or Moon: set orbit bearing to 0 degrees. A preceding Number overrides Angle in 15-degree steps.",
|
||||
"symbol.rivencraft.angle.east.description": "Before Normal Time, Sun or Moon: set orbit bearing to 90 degrees. A preceding Number overrides Angle in 15-degree steps.",
|
||||
"symbol.rivencraft.angle.south.description": "Before Normal Time, Sun or Moon: set orbit bearing to 180 degrees. A preceding Number overrides Angle in 15-degree steps.",
|
||||
"symbol.rivencraft.angle.west.description": "Before Normal Time, Sun or Moon: set orbit bearing to 270 degrees. A preceding Number overrides Angle in 15-degree steps.",
|
||||
"symbol.rivencraft.phase.rising.description": "Before a celestial controller: start the sun or standalone moon at rising. A preceding Number overrides Phase in 15-degree steps from Nadir.",
|
||||
"symbol.rivencraft.phase.zenith.description": "Before a celestial controller: start the sun or standalone moon overhead. Zero Length holds this position.",
|
||||
"symbol.rivencraft.phase.setting.description": "Before a celestial controller: start the sun or standalone moon at setting. A preceding Number overrides Phase in 15-degree steps from Nadir.",
|
||||
"symbol.rivencraft.phase.nadir.description": "Before a celestial controller: start the sun or standalone moon below the world. Zero Length holds this position.",
|
||||
"symbol.rivencraft.direction.clockwise.description": "Before Normal Time, Sun or Moon: advance this Age's celestial cycle in its usual direction.",
|
||||
"symbol.rivencraft.direction.counterclockwise.description": "Before Normal Time, Sun or Moon: reverse this Age's celestial cycle. Does not change the Overworld clock.",
|
||||
"symbol.rivencraft.celestial.normal_time.description": "Add a sun/moon pair. Preceding modifiers apply to this pair; Size affects its sun only. Colors tint both. Limit ten suns and ten moons per Age.",
|
||||
"symbol.rivencraft.celestial.sun.description": "Add a sun. A bare page gets saved seeded color, size and orbit controls. Written modifiers preserve authored behavior and reset per controller. Moon alignment causes eclipses; limit ten suns.",
|
||||
"symbol.rivencraft.celestial.moon.description": "Add a moon with weaker light. A bare page gets saved seeded color, size and orbit controls. Written modifiers preserve authored behavior and reset per controller. Sun alignment causes eclipses; limit ten moons.",
|
||||
"symbol.rivencraft.period.zero.description": "Before Normal Time, Sun or Moon: freeze at the written Phase, or a saved seeded time if omitted. A preceding Number overrides the Length.",
|
||||
"symbol.rivencraft.period.half.description": "Before Normal Time, Sun or Moon: a half-length cycle (twice as fast). A preceding Number sets Length to value × 0.08 days.",
|
||||
"symbol.rivencraft.period.full.description": "Before Normal Time, Sun or Moon: a normal-length cycle. A preceding Number sets Length to value × 0.08 days.",
|
||||
"symbol.rivencraft.period.double.description": "Before Normal Time, Sun or Moon: a double-length cycle (half as fast). A preceding Number sets Length to value × 0.08 days.",
|
||||
"symbol.rivencraft.color.red.description": "Add red to the next Sky, Ambient Light Color or celestial controller; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.color.green.description": "Add green to the next Sky, Ambient Light Color or celestial controller; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.color.blue.description": "Add blue to the next Sky, Ambient Light Color or celestial controller; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.color.white.description": "Add equally to all RGB channels. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.color.black.description": "Subtract equally from all RGB channels. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.lighting.normal.description": "Use normal skylight and ambient brightness. Cave terrain remains without skylight.",
|
||||
"symbol.rivencraft.lighting.dark.description": "Remove skylight from this Age; placed light sources still work.",
|
||||
"symbol.rivencraft.lighting.bright.description": "Provide full ambient brightness and effective light level 15. Vanilla light-based spawn rules apply, including in Cave terrain without skylight.",
|
||||
"symbol.rivencraft.weather.fast.description": "Use the faster Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
|
||||
"symbol.rivencraft.weather.slow.description": "Use the slower Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
|
||||
"symbol.rivencraft.weather.off.description": "Keep this Age clear without changing Overworld weather.",
|
||||
@@ -49,8 +84,10 @@
|
||||
"symbol.rivencraft.feature.red_shrubs.description": "Add red shrub patches to surface terrain with suitable ground.",
|
||||
"symbol.rivencraft.feature.abandoned_camps": "Abandoned Camps",
|
||||
"symbol.rivencraft.feature.abandoned_camps.description": "Allow abandoned camps on surface noise terrain, including outside their usual biomes.",
|
||||
"symbol.rivencraft.block.generated.description": "Place immediately before a compatible feature; unpaired materials are not yet supported.",
|
||||
"symbol.rivencraft.block.generated.description": "Modify the next compatible feature. Without a following feature, choose a compatible feature from the Age seed.",
|
||||
"symbol.rivencraft.biome.native.description": "Use the terrain's native biome source; do not include Biome pages.",
|
||||
"symbol.rivencraft.feature.dungeons.description": "Add monster-room dungeon attempts throughout surface noise or Cave terrain, including Nether cave biomes. Not Flat or Void.",
|
||||
"symbol.rivencraft.biome.single.description": "Use one biome across the Age. If omitted, choose an eligible biome from the Age seed.",
|
||||
"symbol.rivencraft.terrain.flat.description": "A level, biome-decorated landscape with its foundation at Y=0.",
|
||||
"symbol.rivencraft.feature.floating_islands.description": "Add high floating islands alongside noise, Flat, or Void terrain; not Cave terrain.",
|
||||
"symbol.rivencraft.feature.caves.description": "Add extra cave carving to noise terrain, including Cave; not Flat or Void.",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
@@ -9,17 +11,23 @@ import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Full server level catalog, sent on login and after a runtime Age is added. */
|
||||
public record AgeCatalogPayload(int version, List<Identifier> dimensions) implements CustomPacketPayload {
|
||||
public record AgeCatalogPayload(int version, List<Identifier> dimensions,
|
||||
Map<Identifier, PortAgeColors> colors,
|
||||
Map<Identifier, PortDayCycle> dayCycles) implements CustomPacketPayload {
|
||||
public static final Type<AgeCatalogPayload> TYPE = new Type<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age_catalog"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AgeCatalogPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.VAR_INT, AgeCatalogPayload::version,
|
||||
Identifier.STREAM_CODEC.apply(ByteBufCodecs.list()), AgeCatalogPayload::dimensions,
|
||||
ByteBufCodecs.map(HashMap::new, Identifier.STREAM_CODEC, PortAgeColors.STREAM_CODEC), AgeCatalogPayload::colors,
|
||||
ByteBufCodecs.map(HashMap::new, Identifier.STREAM_CODEC, PortDayCycle.STREAM_CODEC), AgeCatalogPayload::dayCycles,
|
||||
AgeCatalogPayload::new
|
||||
);
|
||||
|
||||
public AgeCatalogPayload {
|
||||
dimensions = List.copyOf(dimensions);
|
||||
colors = Map.copyOf(colors);
|
||||
dayCycles = Map.copyOf(dayCycles);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,7 +21,7 @@ public final class AgeCatalogSync {
|
||||
}
|
||||
|
||||
public static void registerPayloads(RegisterPayloadHandlersEvent event) {
|
||||
var registrar = event.registrar("1");
|
||||
var registrar = event.registrar("10");
|
||||
registrar.playToClient(AgeCatalogPayload.TYPE, AgeCatalogPayload.STREAM_CODEC);
|
||||
registrar.playToServer(AgeCatalogAckPayload.TYPE, AgeCatalogAckPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
@@ -77,7 +77,8 @@ public final class AgeCatalogSync {
|
||||
State state = state(server);
|
||||
state.acknowledged.remove(player.getUUID());
|
||||
var dimensions = server.levelKeys().stream().map(key -> key.identifier()).sorted().toList();
|
||||
PacketDistributor.sendToPlayer(player, new AgeCatalogPayload(state.version, dimensions));
|
||||
PacketDistributor.sendToPlayer(player, new AgeCatalogPayload(state.version, dimensions,
|
||||
RuntimeAgeBackend.savedColors(server), RuntimeAgeBackend.savedDayCycles(server)));
|
||||
}
|
||||
|
||||
private static void acknowledge(ServerPlayer player, int version) {
|
||||
|
||||
@@ -34,7 +34,15 @@ final class AgeLevelIndex extends SavedData {
|
||||
Codec.unboundedMap(Identifier.CODEC, PortLakePlan.CODEC)
|
||||
.optionalFieldOf("lakes", Map.of()).forGetter(index -> index.lakes),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortTerrainFeaturePlan.CODEC)
|
||||
.optionalFieldOf("terrain_features", Map.of()).forGetter(index -> index.terrainFeatures)
|
||||
.optionalFieldOf("terrain_features", Map.of()).forGetter(index -> index.terrainFeatures),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortLighting.CODEC)
|
||||
.optionalFieldOf("lighting", Map.of()).forGetter(index -> index.lighting),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortSkyStyle.CODEC)
|
||||
.optionalFieldOf("sky", Map.of()).forGetter(index -> index.sky),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortAgeColors.CODEC)
|
||||
.optionalFieldOf("colors", Map.of()).forGetter(index -> index.colors),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortDayCycle.CODEC)
|
||||
.optionalFieldOf("day_cycles", Map.of()).forGetter(index -> index.dayCycles)
|
||||
).apply(instance, AgeLevelIndex::new));
|
||||
|
||||
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
||||
@@ -53,6 +61,10 @@ final class AgeLevelIndex extends SavedData {
|
||||
private final Map<Identifier, Integer> dungeons;
|
||||
private final Map<Identifier, PortLakePlan> lakes;
|
||||
private final Map<Identifier, PortTerrainFeaturePlan> terrainFeatures;
|
||||
private final Map<Identifier, PortLighting> lighting;
|
||||
private final Map<Identifier, PortSkyStyle> sky;
|
||||
private final Map<Identifier, PortAgeColors> colors;
|
||||
private final Map<Identifier, PortDayCycle> dayCycles;
|
||||
|
||||
private AgeLevelIndex() {
|
||||
this.ages = new LinkedHashMap<>();
|
||||
@@ -65,6 +77,10 @@ final class AgeLevelIndex extends SavedData {
|
||||
this.dungeons = new LinkedHashMap<>();
|
||||
this.lakes = new LinkedHashMap<>();
|
||||
this.terrainFeatures = new LinkedHashMap<>();
|
||||
this.lighting = new LinkedHashMap<>();
|
||||
this.sky = new LinkedHashMap<>();
|
||||
this.colors = new LinkedHashMap<>();
|
||||
this.dayCycles = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
|
||||
@@ -75,7 +91,10 @@ final class AgeLevelIndex extends SavedData {
|
||||
Map<Identifier, PortVegetationPlan> vegetation,
|
||||
Map<Identifier, Integer> dungeons,
|
||||
Map<Identifier, PortLakePlan> lakes,
|
||||
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures) {
|
||||
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures,
|
||||
Map<Identifier, PortLighting> lighting,
|
||||
Map<Identifier, PortSkyStyle> sky, Map<Identifier, PortAgeColors> colors,
|
||||
Map<Identifier, PortDayCycle> dayCycles) {
|
||||
this.ages = new LinkedHashMap<>(ages);
|
||||
this.retired = new LinkedHashSet<>(retired);
|
||||
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
||||
@@ -86,6 +105,10 @@ final class AgeLevelIndex extends SavedData {
|
||||
this.dungeons = new LinkedHashMap<>(dungeons);
|
||||
this.lakes = new LinkedHashMap<>(lakes);
|
||||
this.terrainFeatures = new LinkedHashMap<>(terrainFeatures);
|
||||
this.lighting = new LinkedHashMap<>(lighting);
|
||||
this.sky = new LinkedHashMap<>(sky);
|
||||
this.colors = new LinkedHashMap<>(colors);
|
||||
this.dayCycles = new LinkedHashMap<>(dayCycles);
|
||||
}
|
||||
|
||||
Map<Identifier, Long> entries() {
|
||||
@@ -116,10 +139,17 @@ final class AgeLevelIndex extends SavedData {
|
||||
int dungeons(Identifier id) { return dungeons.getOrDefault(id, 0); }
|
||||
PortLakePlan lakes(Identifier id) { return lakes.get(id); }
|
||||
PortTerrainFeaturePlan terrainFeatures(Identifier id) { return terrainFeatures.get(id); }
|
||||
PortLighting lighting(Identifier id) { return lighting.getOrDefault(id, PortLighting.NORMAL); }
|
||||
PortSkyStyle sky(Identifier id) { return sky.getOrDefault(id, PortSkyStyle.OVERWORLD); }
|
||||
PortAgeColors colors(Identifier id) { return colors.getOrDefault(id, PortAgeColors.NATIVE); }
|
||||
Map<Identifier, PortAgeColors> colors() { return Map.copyOf(colors); }
|
||||
PortDayCycle dayCycle(Identifier id) { return dayCycles.getOrDefault(id, PortDayCycle.NATIVE); }
|
||||
Map<Identifier, PortDayCycle> dayCycles() { return Map.copyOf(dayCycles); }
|
||||
|
||||
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light, PortSkyStyle skyStyle,
|
||||
PortAgeColors colorProfile, PortDayCycle dayCycle) {
|
||||
if (contains(id)) {
|
||||
throw new IllegalArgumentException("Age already exists: " + id);
|
||||
}
|
||||
@@ -131,6 +161,10 @@ final class AgeLevelIndex extends SavedData {
|
||||
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
|
||||
if (lakePlan != null) lakes.put(id, lakePlan);
|
||||
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
|
||||
if (light != PortLighting.NORMAL) lighting.put(id, light);
|
||||
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
|
||||
if (!colorProfile.equals(PortAgeColors.NATIVE)) colors.put(id, colorProfile);
|
||||
if (!dayCycle.equals(PortDayCycle.NATIVE)) dayCycles.put(id, dayCycle);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
@@ -144,6 +178,10 @@ final class AgeLevelIndex extends SavedData {
|
||||
dungeons.remove(id);
|
||||
lakes.remove(id);
|
||||
terrainFeatures.remove(id);
|
||||
lighting.remove(id);
|
||||
sky.remove(id);
|
||||
colors.remove(id);
|
||||
dayCycles.remove(id);
|
||||
retired.add(id);
|
||||
setDirty();
|
||||
}
|
||||
@@ -151,7 +189,8 @@ final class AgeLevelIndex extends SavedData {
|
||||
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
|
||||
PortAgeTerrain ageTerrain, PortRockPlan rockPlan,
|
||||
PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light,
|
||||
PortSkyStyle skyStyle, PortAgeColors colorProfile, PortDayCycle dayCycle) {
|
||||
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
|
||||
ages.put(id, seed);
|
||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||
@@ -161,6 +200,10 @@ final class AgeLevelIndex extends SavedData {
|
||||
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
|
||||
if (lakePlan != null) lakes.put(id, lakePlan);
|
||||
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
|
||||
if (light != PortLighting.NORMAL) lighting.put(id, light);
|
||||
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
|
||||
if (!colorProfile.equals(PortAgeColors.NATIVE)) colors.put(id, colorProfile);
|
||||
if (!dayCycle.equals(PortDayCycle.NATIVE)) dayCycles.put(id, dayCycle);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
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, int celestial) {
|
||||
public static final PortAgeColors NATIVE = new PortAgeColors(-1, -1);
|
||||
public static final Codec<PortAgeColors> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.intRange(-1, 0xffffff).fieldOf("sky").forGetter(PortAgeColors::sky),
|
||||
Codec.intRange(-1, 0xffffff).fieldOf("ambient").forGetter(PortAgeColors::ambient),
|
||||
Codec.intRange(-1, 0xffffff).optionalFieldOf("celestial", -1).forGetter(PortAgeColors::celestial)
|
||||
).apply(instance, PortAgeColors::new));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, PortAgeColors> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.INT, PortAgeColors::sky, ByteBufCodecs.INT, PortAgeColors::ambient,
|
||||
ByteBufCodecs.INT, PortAgeColors::celestial, PortAgeColors::new);
|
||||
public static final Identifier AMBIENT = Identifier.fromNamespaceAndPath("rivencraft", "lighting/ambient_color");
|
||||
private static volatile Map<Identifier, PortAgeColors> clientColors = Map.of();
|
||||
|
||||
public PortAgeColors {
|
||||
if (sky < -1 || sky > 0xffffff || ambient < -1 || ambient > 0xffffff
|
||||
|| celestial < -1 || celestial > 0xffffff)
|
||||
throw new IllegalArgumentException("Invalid Age color");
|
||||
}
|
||||
|
||||
public PortAgeColors(int sky, int ambient) { this(sky, ambient, -1); }
|
||||
|
||||
public static void updateClient(Map<Identifier, PortAgeColors> colors) { clientColors = Map.copyOf(colors); }
|
||||
|
||||
public static PortAgeColors forLevel(Level level) {
|
||||
if (level instanceof PortAgeLevel age) return age.colors();
|
||||
return level.isClientSide() ? clientColors.getOrDefault(level.dimension().identifier(), NATIVE) : NATIVE;
|
||||
}
|
||||
|
||||
public static boolean isColor(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "color/red", "color/green", "color/blue", "color/white", "color/black" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static int number(Identifier page) {
|
||||
if (!page.getNamespace().equals("rivencraft") || !page.getPath().startsWith("number/")) return -1;
|
||||
try {
|
||||
int value = Integer.parseInt(page.getPath().substring(7));
|
||||
return value >= 0 && value <= 24 ? value : -1;
|
||||
} catch (NumberFormatException exception) { return -1; }
|
||||
}
|
||||
|
||||
public static boolean isModifier(Identifier page) { return isColor(page) || number(page) >= 0 || page.equals(AMBIENT); }
|
||||
|
||||
public static boolean validNumbers(List<Identifier> pages) {
|
||||
for (int i = 0; i < pages.size(); i++)
|
||||
if (number(pages.get(i)) >= 0 && (i + 1 == pages.size()
|
||||
|| !isColor(pages.get(i + 1)) && !PortDayCycle.isPeriod(pages.get(i + 1))
|
||||
&& !PortDayCycle.isPhase(pages.get(i + 1)) && !PortDayCycle.isAngle(pages.get(i + 1))
|
||||
&& !PortDayCycle.isSize(pages.get(i + 1)))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static PortAgeColors compile(List<Identifier> pages, long seed) {
|
||||
if (!validNumbers(pages)) throw new IllegalArgumentException("Number pages must precede a supported Color, Length, Phase, Angle, or Size modifier");
|
||||
int red = 0, green = 0, blue = 0, sky = -1, ambient = -1, celestial = -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)
|
||||
|| PortDayCycle.isController(page)) {
|
||||
if (PortDayCycle.isController(page)) celestial = -1;
|
||||
if (written) {
|
||||
int rgb = clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
|
||||
if (page.equals(AMBIENT)) ambient = rgb;
|
||||
else if (PortDayCycle.isController(page)) celestial = rgb;
|
||||
else sky = rgb;
|
||||
}
|
||||
red = green = blue = 0;
|
||||
written = false;
|
||||
}
|
||||
}
|
||||
if (written) sky = clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
|
||||
return new PortAgeColors(sky, ambient, celestial);
|
||||
}
|
||||
|
||||
/** Only pageless random Ages receive a random sky tint, as in 1.21.1. */
|
||||
public static PortAgeColors resolve(List<Identifier> pages, long seed) {
|
||||
if (!pages.isEmpty()) return compile(pages, seed);
|
||||
var random = new Random(seed ^ 0x534B5950524F464CL);
|
||||
random.nextInt(5); // Root sky-style draw precedes the sky-color draw.
|
||||
return new PortAgeColors(randomColor(random, true), -1);
|
||||
}
|
||||
|
||||
/** Shared palette; retain .85's truncation for already-defined body defaults. */
|
||||
static int randomColor(Random random, boolean rounded) {
|
||||
int[] palette = {0xB74A50, 0xD9834B, 0xDBC574, 0x5AAB80, 0x6FA8DF, 0x9870B9, 0xD5DBE1, 0x252531};
|
||||
int count = 1 + random.nextInt(3), red = 0, green = 0, blue = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int color = palette[random.nextInt(palette.length)];
|
||||
red += color >> 16 & 255;
|
||||
green += color >> 8 & 255;
|
||||
blue += color & 255;
|
||||
}
|
||||
int offset = rounded ? count / 2 : 0;
|
||||
return (red + offset) / count << 16 | (green + offset) / count << 8 | (blue + offset) / count;
|
||||
}
|
||||
|
||||
private static int clamp(int value) { return Math.max(0, Math.min(255, value)); }
|
||||
public static Vector3fc vector(int rgb) {
|
||||
return new Vector3f((rgb >> 16 & 255) / 255f, (rgb >> 8 & 255) / 255f, (rgb & 255) / 255f);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -14,6 +14,21 @@ final class PortAgeLevel extends ServerLevel {
|
||||
private final WeatherData ageWeather;
|
||||
private final PortWeatherSettings weatherSettings;
|
||||
private final boolean cave;
|
||||
private PortAgeColors colors = PortAgeColors.NATIVE;
|
||||
private PortDayCycle dayCycle = PortDayCycle.NATIVE;
|
||||
|
||||
PortDayCycle dayCycle() { return dayCycle == null ? PortDayCycle.NATIVE : dayCycle; }
|
||||
void configureDayCycle(PortDayCycle cycle) {
|
||||
this.dayCycle = cycle;
|
||||
environmentAttributes().invalidateTickCache();
|
||||
updateSkyBrightness();
|
||||
}
|
||||
|
||||
PortAgeColors colors() { return colors == null ? PortAgeColors.NATIVE : colors; }
|
||||
void configureColors(PortAgeColors colors) {
|
||||
this.colors = colors;
|
||||
environmentAttributes().invalidateTickCache();
|
||||
}
|
||||
|
||||
PortAgeLevel(MinecraftServer server, ResourceKey<Level> key, LevelStem stem,
|
||||
long biomeZoomSeed, PortAgeTerrain terrain) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.data.worldgen.material.EndMaterialRules;
|
||||
import net.minecraft.data.worldgen.material.NetherMaterialRules;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.material.MaterialRules;
|
||||
|
||||
/** Root AgeDimensionService.withBiomeMaterialRules adapted to 26.3's rule registry. */
|
||||
final class PortBiomeMaterialRules {
|
||||
private PortBiomeMaterialRules() {}
|
||||
|
||||
static Holder<NoiseGeneratorSettings> apply(HolderLookup.Provider registries, NoiseGeneratorSettings base) {
|
||||
var biomes = registries.lookupOrThrow(Registries.BIOME);
|
||||
var rules = registries.lookupOrThrow(Registries.MATERIAL_RULE);
|
||||
var materials = MaterialRules.sequence(
|
||||
MaterialRules.ifTrue(MaterialRules.isBiome(biomes, Biomes.THE_END, Biomes.END_HIGHLANDS,
|
||||
Biomes.END_MIDLANDS, Biomes.SMALL_END_ISLANDS, Biomes.END_BARRENS),
|
||||
MaterialRules.getRule(rules, EndMaterialRules.END)),
|
||||
MaterialRules.ifTrue(MaterialRules.isBiome(biomes, Biomes.NETHER_WASTES, Biomes.SOUL_SAND_VALLEY,
|
||||
Biomes.CRIMSON_FOREST, Biomes.WARPED_FOREST, Biomes.BASALT_DELTAS),
|
||||
MaterialRules.getRule(rules, NetherMaterialRules.NETHER)),
|
||||
base.materialRule().value());
|
||||
return Holder.direct(new NoiseGeneratorSettings(base.noiseSettings(), base.defaultBlock(), base.defaultFluid(),
|
||||
base.noiseRouter(), Holder.direct(materials), base.spawnTarget(), base.seaLevel(), base.disableMobGeneration(),
|
||||
base.aquifers(), base.useLegacyRandomSource(), base.debugFunctions()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
|
||||
/** One frozen authored orbit, using the same angular motion as 1.21.1 Body. */
|
||||
public record PortCelestialBody(int kind, double period, int stoppedTime, long originTick,
|
||||
int phaseTick, boolean clockwise, int bearing, float size, int color) {
|
||||
public static final Codec<PortCelestialBody> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.intRange(1, 2).fieldOf("kind").forGetter(PortCelestialBody::kind),
|
||||
Codec.doubleRange(0, 2).fieldOf("period").forGetter(PortCelestialBody::period),
|
||||
Codec.intRange(0, 23999).fieldOf("stopped_time").forGetter(PortCelestialBody::stoppedTime),
|
||||
Codec.LONG.fieldOf("origin_tick").forGetter(PortCelestialBody::originTick),
|
||||
Codec.intRange(0, 23999).fieldOf("phase_tick").forGetter(PortCelestialBody::phaseTick),
|
||||
Codec.BOOL.fieldOf("clockwise").forGetter(PortCelestialBody::clockwise),
|
||||
Codec.intRange(-1, 359).fieldOf("bearing").forGetter(PortCelestialBody::bearing),
|
||||
Codec.floatRange(0.25F, 3F).fieldOf("size").forGetter(PortCelestialBody::size),
|
||||
Codec.intRange(-1, 0xffffff).fieldOf("color").forGetter(PortCelestialBody::color)
|
||||
).apply(instance, PortCelestialBody::new));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, PortCelestialBody> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.VAR_INT, PortCelestialBody::kind, ByteBufCodecs.DOUBLE, PortCelestialBody::period,
|
||||
ByteBufCodecs.VAR_INT, PortCelestialBody::stoppedTime, ByteBufCodecs.VAR_LONG, PortCelestialBody::originTick,
|
||||
ByteBufCodecs.VAR_INT, PortCelestialBody::phaseTick, ByteBufCodecs.BOOL, PortCelestialBody::clockwise,
|
||||
ByteBufCodecs.INT, PortCelestialBody::bearing, ByteBufCodecs.FLOAT, PortCelestialBody::size,
|
||||
ByteBufCodecs.INT, PortCelestialBody::color, PortCelestialBody::new);
|
||||
|
||||
public PortCelestialBody {
|
||||
if (kind != PortDayCycle.SUN && kind != PortDayCycle.MOON || !Double.isFinite(period)
|
||||
|| period < 0 || period > 2 || stoppedTime < 0 || stoppedTime >= 24000
|
||||
|| phaseTick < 0 || phaseTick >= 24000 || bearing < -1 || bearing > 359
|
||||
|| !Float.isFinite(size) || size < 0.25F || size > 3 || color < -1 || color > 0xffffff)
|
||||
throw new IllegalArgumentException("Invalid celestial body");
|
||||
}
|
||||
|
||||
public static PortCelestialBody of(PortDayCycle cycle, int kind, int color, boolean pairedMoon) {
|
||||
int offset = pairedMoon ? 12000 : 0;
|
||||
return new PortCelestialBody(kind, cycle.period(), (cycle.stoppedTime() + offset) % 24000,
|
||||
cycle.originTick(), (cycle.phaseTick() + offset) % 24000, cycle.clockwise(),
|
||||
cycle.bearing(), pairedMoon ? 1F : cycle.sunSize(), color);
|
||||
}
|
||||
|
||||
public float angle(double nativeTicks) {
|
||||
double time = period == 0 ? stoppedTime
|
||||
: (clockwise ? 1 : -1) * (nativeTicks - originTick) / period + phaseTick;
|
||||
return (float) (((time - 6000D) % 24000D) * (360D / 24000D));
|
||||
}
|
||||
|
||||
public float light(long nativeTicks) {
|
||||
return Math.max(0F, (float) Math.cos(Math.toRadians(angle(nativeTicks))))
|
||||
* (kind == PortDayCycle.MOON ? 0.18F : 1F);
|
||||
}
|
||||
|
||||
public net.minecraft.world.phys.Vec3 direction(double ticks) {
|
||||
double orbit = Math.toRadians(angle(ticks)), axis = Math.toRadians(bearing < 0 ? -90 : bearing);
|
||||
double lateral = Math.sin(orbit);
|
||||
return new net.minecraft.world.phys.Vec3(Math.sin(axis) * lateral, Math.cos(orbit), Math.cos(axis) * lateral);
|
||||
}
|
||||
|
||||
/** Root AgeSkyProfile.eclipseFor: contact uses the central quarter-size disc, not its halo. */
|
||||
public float eclipse(java.util.List<PortCelestialBody> bodies, double ticks) {
|
||||
float strongest = 0;
|
||||
var direction = direction(ticks);
|
||||
double radius = Math.atan((kind == PortDayCycle.SUN ? 30D : 20D) * size * 0.25D / 100D);
|
||||
for (var other : bodies) {
|
||||
if (other.kind == kind) continue;
|
||||
double separation = Math.acos(Math.max(-1D, Math.min(1D, direction.dot(other.direction(ticks)))));
|
||||
double contact = radius + Math.atan((other.kind == PortDayCycle.SUN ? 30D : 20D) * other.size * 0.25D / 100D);
|
||||
strongest = Math.max(strongest, (float) Math.max(0D, 1D - separation / contact));
|
||||
}
|
||||
return strongest;
|
||||
}
|
||||
|
||||
public static float eclipseAt(java.util.List<PortCelestialBody> bodies, double ticks) {
|
||||
float strongest = 0;
|
||||
for (var body : bodies) if (body.kind == PortDayCycle.SUN && body.direction(ticks).y > 0D)
|
||||
strongest = Math.max(strongest, body.eclipse(bodies, ticks));
|
||||
return strongest;
|
||||
}
|
||||
|
||||
/** Shared client/server-checkable RGB rule; alpha is handled by the renderer. */
|
||||
public int eclipseColor(float eclipse) {
|
||||
int rgb = color < 0 ? 0xffffff : color;
|
||||
float corona = kind == PortDayCycle.SUN ? (float) Math.pow(eclipse, 8D) * 0.8F : 0F;
|
||||
int red = channel(rgb >> 16 & 255, eclipse, corona);
|
||||
int green = channel(rgb >> 8 & 255, eclipse, corona);
|
||||
int blue = channel(rgb & 255, eclipse, corona);
|
||||
return red << 16 | green << 8 | blue;
|
||||
}
|
||||
|
||||
private int channel(int value, float eclipse, float corona) {
|
||||
return Math.round(kind == PortDayCycle.MOON ? value * (1F - eclipse) : value + (255 - value) * corona);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
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, int bodyMode,
|
||||
List<PortCelestialBody> bodies) {
|
||||
public static final int PAIRED = 0, SUN = 1, MOON = 2, NONE = 3;
|
||||
public static final PortDayCycle NATIVE = new PortDayCycle(false, 1, 0);
|
||||
public static final Identifier NORMAL_TIME = Identifier.fromNamespaceAndPath("rivencraft", "celestial/normal_time");
|
||||
public static final Codec<PortDayCycle> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
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),
|
||||
Codec.intRange(0, 3).optionalFieldOf("body_mode", PAIRED).forGetter(PortDayCycle::bodyMode),
|
||||
PortCelestialBody.CODEC.listOf().optionalFieldOf("bodies", List.of()).forGetter(PortDayCycle::bodies)
|
||||
).apply(instance, PortDayCycle::new));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, PortDayCycle> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.BOOL, PortDayCycle::authored, ByteBufCodecs.DOUBLE, PortDayCycle::period,
|
||||
ByteBufCodecs.VAR_INT, PortDayCycle::stoppedTime, ByteBufCodecs.VAR_LONG, PortDayCycle::originTick,
|
||||
ByteBufCodecs.VAR_INT, PortDayCycle::phaseTick, ByteBufCodecs.BOOL, PortDayCycle::clockwise,
|
||||
ByteBufCodecs.INT, PortDayCycle::bearing, ByteBufCodecs.FLOAT, PortDayCycle::sunSize,
|
||||
ByteBufCodecs.VAR_INT, PortDayCycle::bodyMode,
|
||||
PortCelestialBody.STREAM_CODEC.apply(ByteBufCodecs.list()), PortDayCycle::bodies, PortDayCycle::new);
|
||||
private static volatile Map<Identifier, PortDayCycle> clientCycles = Map.of();
|
||||
|
||||
public PortDayCycle {
|
||||
bodies = List.copyOf(bodies);
|
||||
if (!Double.isFinite(period) || period < 0 || period > 2 || stoppedTime < 0 || stoppedTime >= 24000
|
||||
|| phaseTick < 0 || phaseTick >= 24000 || bearing < -1 || bearing >= 360
|
||||
|| !Float.isFinite(sunSize) || sunSize < 0.25F || sunSize > 3F
|
||||
|| bodyMode < PAIRED || bodyMode > NONE
|
||||
|| bodyMode == NONE && (!authored || !bodies.isEmpty()))
|
||||
throw new IllegalArgumentException("Invalid day cycle");
|
||||
if (bodies.stream().filter(body -> body.kind() == SUN).count() > 10
|
||||
|| bodies.stream().filter(body -> body.kind() == MOON).count() > 10)
|
||||
throw new IllegalArgumentException("An Age may have at most ten suns and ten moons");
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize, int bodyMode) {
|
||||
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, bearing, sunSize, bodyMode, List.of());
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize) {
|
||||
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, bearing, sunSize, PAIRED);
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime) {
|
||||
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 isController(page) || isPeriod(page) || isPhase(page) || isDirection(page) || isAngle(page) || isSize(page);
|
||||
}
|
||||
|
||||
public static boolean isController(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "celestial/normal_time", "celestial/sun", "celestial/moon" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isSize(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "size/small", "size/normal", "size/large" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean validWriting(List<Identifier> pages) {
|
||||
int periods = 0, phases = 0, directions = 0, angles = 0, sizes = 0, suns = 0, moons = 0;
|
||||
for (Identifier page : pages) {
|
||||
if (isPeriod(page) && ++periods > 1 || isPhase(page) && ++phases > 1
|
||||
|| isDirection(page) && ++directions > 1 || isAngle(page) && ++angles > 1
|
||||
|| isSize(page) && ++sizes > 1) return false;
|
||||
if (isController(page)) {
|
||||
if (!page.getPath().equals("celestial/moon")) suns++;
|
||||
if (!page.getPath().equals("celestial/sun")) moons++;
|
||||
if (suns > 10 || moons > 10) return false;
|
||||
periods = phases = directions = angles = sizes = 0;
|
||||
}
|
||||
}
|
||||
return periods + phases + directions + angles + sizes == 0;
|
||||
}
|
||||
|
||||
public static PortDayCycle compile(List<Identifier> pages, long seed) {
|
||||
return compile(pages, seed, 0);
|
||||
}
|
||||
|
||||
/** Creation-only completion. Exact diagnostic fixtures can still use compile. */
|
||||
public static PortDayCycle resolve(List<Identifier> pages, long seed, long nativeTicks) {
|
||||
if (!validWriting(pages) || !PortAgeColors.validNumbers(pages))
|
||||
throw new IllegalArgumentException("Invalid celestial modifier order");
|
||||
if (pages.stream().anyMatch(PortDayCycle::isController)) return compile(pages, seed, nativeTicks);
|
||||
// Match root AgeSkyProfile.random's count draw positions: sky style,
|
||||
// mixed sky color, then independent 0..3 sun and moon counts.
|
||||
var random = new Random(seed ^ 0x534B5950524F464CL);
|
||||
random.nextInt(5);
|
||||
randomColor(random);
|
||||
int suns = random.nextInt(4), moons = random.nextInt(4);
|
||||
var bodies = new java.util.ArrayList<PortCelestialBody>();
|
||||
for (int i = 0; i < suns + moons; i++)
|
||||
bodies.addAll(randomUnmodified(seed, i, nativeTicks, i < suns ? SUN : MOON).bodies);
|
||||
if (bodies.isEmpty()) return new PortDayCycle(true, 1, 0, 0, 0, true, -1, 1F, NONE);
|
||||
var first = bodies.getFirst();
|
||||
return new PortDayCycle(true, first.period(), first.stoppedTime(), first.originTick(), first.phaseTick(),
|
||||
first.clockwise(), first.bearing(), first.size(), first.kind(), bodies);
|
||||
}
|
||||
|
||||
public static PortDayCycle compile(List<Identifier> pages, long seed, long nativeTicks) {
|
||||
if (!validWriting(pages)) throw new IllegalArgumentException("Place at most one of each celestial modifier before its controller; limit ten suns and ten moons");
|
||||
if (pages.stream().filter(PortDayCycle::isController).count() <= 1)
|
||||
return compileOne(pages, seed, nativeTicks, 0);
|
||||
var bodies = new java.util.ArrayList<PortCelestialBody>();
|
||||
PortDayCycle first = null;
|
||||
int start = 0, ordinal = 0;
|
||||
for (int i = 0; i < pages.size(); i++) if (isController(pages.get(i))) {
|
||||
var cycle = compileOne(pages.subList(start, i + 1), seed, nativeTicks, ordinal++);
|
||||
if (first == null) first = cycle;
|
||||
// Prefix retains original page indices for seeded unnumbered colors.
|
||||
int color = PortAgeColors.compile(pages.subList(0, i + 1), seed).celestial();
|
||||
if (!cycle.bodies.isEmpty()) bodies.addAll(cycle.bodies);
|
||||
else {
|
||||
if (cycle.bodyMode != MOON) bodies.add(PortCelestialBody.of(cycle, SUN, color, false));
|
||||
if (cycle.bodyMode != SUN) bodies.add(PortCelestialBody.of(cycle, MOON, color, cycle.bodyMode == PAIRED));
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
return new PortDayCycle(first.authored, first.period, first.stoppedTime, first.originTick,
|
||||
first.phaseTick, first.clockwise, first.bearing, first.sunSize, first.bodyMode, bodies);
|
||||
}
|
||||
|
||||
private static PortDayCycle compileOne(List<Identifier> pages, long seed, long nativeTicks, int ordinal) {
|
||||
var controller = pages.stream().filter(PortDayCycle::isController).findFirst().orElse(null);
|
||||
if (controller == null) return NATIVE;
|
||||
int mode = controller.getPath().equals("celestial/sun") ? SUN
|
||||
: controller.getPath().equals("celestial/moon") ? MOON : PAIRED;
|
||||
if (mode != PAIRED && !hasBodyModifiers(pages)) return randomUnmodified(seed, ordinal, nativeTicks, mode);
|
||||
double period = 1;
|
||||
Integer phase = null;
|
||||
boolean clockwise = true;
|
||||
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, mode);
|
||||
}
|
||||
|
||||
private static boolean hasBodyModifiers(List<Identifier> pages) {
|
||||
boolean color = false, controls = false;
|
||||
for (var page : pages) {
|
||||
if (isController(page)) return controls || color;
|
||||
if (PortAgeColors.isColor(page)) color = true;
|
||||
else if (PortSkyStyle.forPage(page) != null || page.equals(PortAgeColors.AMBIENT)) color = false;
|
||||
if (isPeriod(page) || isPhase(page) || isDirection(page) || isAngle(page) || isSize(page)) controls = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static PortDayCycle randomUnmodified(long seed, int ordinal, long nativeTicks, int mode) {
|
||||
// Independent per-controller stream: unrelated terrain/sky/weather pages
|
||||
// cannot reshuffle the bodies. Use root random-Age bounds, not native defaults.
|
||||
var random = new Random(seed ^ 0x43454C4553544941L ^ (ordinal + 1L) * 0x9E3779B97F4A7C15L);
|
||||
double period = random.nextInt(7) == 0 ? 0D : 0.2D + random.nextDouble() * 1.8D;
|
||||
int phase = random.nextInt(24000), bearing = random.nextInt(360);
|
||||
float size = 0.75F + random.nextFloat();
|
||||
boolean clockwise = random.nextBoolean();
|
||||
int color = randomColor(random);
|
||||
var cycle = new PortDayCycle(true, period, period == 0 ? phase : 0, nativeTicks,
|
||||
phase, clockwise, bearing, size, mode);
|
||||
return new PortDayCycle(true, period, cycle.stoppedTime, nativeTicks, phase, clockwise, bearing, size,
|
||||
mode, List.of(PortCelestialBody.of(cycle, mode, color, false)));
|
||||
}
|
||||
|
||||
private static int randomColor(Random random) {
|
||||
return PortAgeColors.randomColor(random, false);
|
||||
}
|
||||
|
||||
public boolean singleBody() { return authored && (bodyMode == SUN || bodyMode == MOON); }
|
||||
public boolean customLight() { return singleBody() || !bodies.isEmpty(); }
|
||||
|
||||
/** Linear authored orbit, as in 1.21.1 Body; zero degrees is Zenith. */
|
||||
public float bodyAngle(long nativeTicks) {
|
||||
return (float) (((time(nativeTicks) - 6000D) % 24000D) * (360D / 24000D));
|
||||
}
|
||||
|
||||
public float bodyLight(long nativeTicks) {
|
||||
if (bodyMode == NONE) return 0;
|
||||
if (!bodies.isEmpty()) {
|
||||
float sun = 0, moon = 0;
|
||||
for (var body : bodies) {
|
||||
if (body.kind() == SUN) sun += body.light(nativeTicks) * (1F - 0.85F * body.eclipse(bodies, nativeTicks));
|
||||
else moon += body.light(nativeTicks);
|
||||
}
|
||||
return Math.min(1F, sun > 0 ? sun : moon);
|
||||
}
|
||||
float altitude = Math.max(0F, (float) Math.cos(Math.toRadians(bodyAngle(nativeTicks))));
|
||||
return altitude * (bodyMode == MOON ? 0.18F : 1F);
|
||||
}
|
||||
|
||||
public long ticks(long nativeTicks) {
|
||||
if (!authored) return nativeTicks;
|
||||
return (long) Math.floor(time(nativeTicks));
|
||||
}
|
||||
|
||||
private double time(long nativeTicks) {
|
||||
return !authored ? nativeTicks : period == 0 ? stoppedTime
|
||||
: (clockwise ? 1 : -1) * ((double) nativeTicks - originTick) / period + phaseTick;
|
||||
}
|
||||
|
||||
public static long nativeTicks(Level level) {
|
||||
return level.clockManager().getInstance(level.registryAccess().lookupOrThrow(
|
||||
net.minecraft.core.registries.Registries.WORLD_CLOCK).getOrThrow(WorldClocks.OVERWORLD)).totalTicks();
|
||||
}
|
||||
|
||||
public static void updateClient(Map<Identifier, PortDayCycle> cycles) { clientCycles = Map.copyOf(cycles); }
|
||||
public static PortDayCycle forLevel(Level level) {
|
||||
if (level instanceof PortAgeLevel age) return age.dayCycle();
|
||||
return level.isClientSide() ? clientCycles.getOrDefault(level.dimension().identifier(), NATIVE) : NATIVE;
|
||||
}
|
||||
|
||||
/** Timelines keep this manager; profiles are read lazily after level construction/catalog sync. */
|
||||
public static ClockManager timelines(Level level, ClockManager nativeManager) {
|
||||
Map<Holder<WorldClock>, ClockInstance> wrappers = new HashMap<>();
|
||||
return definition -> {
|
||||
if (!definition.is(WorldClocks.OVERWORLD)) return nativeManager.getInstance(definition);
|
||||
return wrappers.computeIfAbsent(definition, ignored -> new ClockInstance() {
|
||||
private ClockInstance source() { return nativeManager.getInstance(definition); }
|
||||
public long totalTicks() { return forLevel(level).ticks(source().totalTicks()); }
|
||||
public float partialTick() {
|
||||
PortDayCycle cycle = forLevel(level);
|
||||
if (!cycle.authored) return source().partialTick();
|
||||
if (cycle.period == 0) return 0;
|
||||
double time = cycle.time(source().totalTicks());
|
||||
return (float) (time - Math.floor(time)
|
||||
+ (cycle.clockwise ? 1 : -1) * source().partialTick() / cycle.period);
|
||||
}
|
||||
public float rate() {
|
||||
PortDayCycle cycle = forLevel(level);
|
||||
return !cycle.authored ? source().rate() : cycle.period == 0 ? 0
|
||||
: (float) ((cycle.clockwise ? 1 : -1) * source().rate() / cycle.period);
|
||||
}
|
||||
public boolean isPaused() { return source().isPaused() || forLevel(level).authored && forLevel(level).period == 0; }
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Seed-completes missing environment controllers once, before an Age is saved. */
|
||||
public record PortEnvironmentSelection(PortWeatherMode weather, PortLighting lighting, PortSkyStyle sky) {
|
||||
public static PortEnvironmentSelection resolve(List<Identifier> pages, long seed, PortAgeTerrain terrain) {
|
||||
PortLighting lighting = pages.stream().map(PortLighting::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x4C49474854494E47L), PortLighting.values()));
|
||||
PortWeatherMode weather = terrain == PortAgeTerrain.CAVE ? PortWeatherMode.OFF
|
||||
: pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x574541544845525FL), PortWeatherMode.values()));
|
||||
return new PortEnvironmentSelection(weather, lighting, PortSkyStyle.resolve(pages, seed));
|
||||
}
|
||||
|
||||
private static <T> T rankedDefault(Random random, T[] choices) {
|
||||
// LegacyRankChoice: the first (Normal) option is rank 1, the rest rank 2.
|
||||
// Its rank-1 weight is the complete rare group plus one. Derive this
|
||||
// from the supported pool, rather than selecting unported weather.
|
||||
int[] weights = new int[choices.length];
|
||||
Arrays.fill(weights, 1);
|
||||
weights[0] = choices.length;
|
||||
return PortRandomAgeSelection.weighted(random, List.of(choices), weights);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -113,7 +113,7 @@ public final class PortFeatureChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
|
||||
RandomState random, long seed) {
|
||||
if (hasStructurePages()) {
|
||||
if (controlsStructures()) {
|
||||
var lookup = PortStructurePages.filtered(sets, terrainFeatures.structures(), getBiomeSource());
|
||||
var planning = PortStructurePages.planningBiomes(getBiomeSource(), sets, terrainFeatures.structures());
|
||||
if (base instanceof PortFlatChunkGenerator || base instanceof net.minecraft.world.level.levelgen.FlatLevelSource)
|
||||
@@ -124,13 +124,13 @@ public final class PortFeatureChunkGenerator extends ChunkGenerator {
|
||||
return base.createState(sets, random, seed);
|
||||
}
|
||||
|
||||
private boolean hasStructurePages() {
|
||||
return terrainFeatures != null && !terrainFeatures.structures().isEmpty();
|
||||
private boolean controlsStructures() {
|
||||
return terrainFeatures != null && (terrainFeatures.structureControl() || !terrainFeatures.structures().isEmpty());
|
||||
}
|
||||
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
|
||||
StructureManager structures, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||
if (hasStructurePages()) super.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||
if (controlsStructures()) super.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||
else base.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||
}
|
||||
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.Locale;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Authored lighting choices using statically registered, client-synchronized types. */
|
||||
public enum PortLighting {
|
||||
NORMAL, DARK, BRIGHT;
|
||||
|
||||
public static final Codec<PortLighting> CODEC = Codec.STRING.xmap(
|
||||
name -> valueOf(name.toUpperCase(Locale.ROOT)), mode -> mode.name().toLowerCase(Locale.ROOT));
|
||||
|
||||
public static PortLighting forPage(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
case "lighting/normal" -> NORMAL;
|
||||
case "lighting/dark" -> DARK;
|
||||
case "lighting/bright" -> BRIGHT;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isBright(Level level) {
|
||||
return level.dimensionTypeRegistration().unwrapKey().map(key -> {
|
||||
Identifier id = key.identifier();
|
||||
return id.getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& (id.getPath().equals("age_bright") || id.getPath().equals("age_cave_bright")
|
||||
|| id.getPath().startsWith("age_bright_sky_")
|
||||
|| id.getPath().startsWith("age_cave_bright_sky_"));
|
||||
}).orElse(false);
|
||||
}
|
||||
|
||||
Holder<DimensionType> dimensionType(MinecraftServer server, PortAgeTerrain terrain,
|
||||
Holder<DimensionType> overworldType, PortSkyStyle sky) {
|
||||
boolean cave = terrain == PortAgeTerrain.CAVE;
|
||||
if (this == NORMAL && !cave && sky == PortSkyStyle.OVERWORLD) return overworldType;
|
||||
String variant = this == BRIGHT ? (cave ? "age_cave_bright" : "age_bright")
|
||||
: this == DARK || cave ? "age_dark" : "age_normal";
|
||||
if (sky != PortSkyStyle.OVERWORLD) variant += "_sky_" + sky.name().toLowerCase(Locale.ROOT);
|
||||
return server.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(
|
||||
ResourceKey.create(Registries.DIMENSION_TYPE,
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, variant)));
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,8 @@ public final class PortRandomAgeSelection {
|
||||
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
|
||||
}
|
||||
|
||||
private static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
|
||||
public static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
|
||||
if (terrain == PortAgeTerrain.VOID) return List.of(Identifier.withDefaultNamespace("the_void"));
|
||||
if (terrain == PortAgeTerrain.CAVE) {
|
||||
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
|
||||
.filter(PortRegistryPages::isCaveBiome)
|
||||
@@ -68,7 +69,7 @@ public final class PortRandomAgeSelection {
|
||||
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
|
||||
}
|
||||
|
||||
private static <T> T weighted(Random random, List<T> choices, int[] weights) {
|
||||
static <T> T weighted(Random random, List<T> choices, int[] weights) {
|
||||
int total = 0;
|
||||
for (int weight : weights) total += weight;
|
||||
int ticket = random.nextInt(total);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Frozen page-free material features and rare formations from 1.21.1. */
|
||||
public final class PortRandomFeatureSelection {
|
||||
private PortRandomFeatureSelection() {}
|
||||
public record Selection(PortRockPlan rocks, PortLakePlan lakes, PortTerrainFeaturePlan terrain, int dungeons) {}
|
||||
|
||||
public static Selection select(net.minecraft.server.MinecraftServer server, long seed,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomes) {
|
||||
var base = select(seed, terrain);
|
||||
var structures = PortRandomStructureSelection.select(server, seed, terrain, biomes);
|
||||
var plan = base.terrain();
|
||||
plan = plan == null ? new PortTerrainFeaturePlan(List.of(), false, 0, false, structures.structures(), List.of(), true)
|
||||
: new PortTerrainFeaturePlan(plan.choices(), plan.skylands(), plan.denseOres(), plan.starFissures(),
|
||||
structures.structures(), plan.carvers(), true);
|
||||
return new Selection(base.rocks(), base.lakes(), plan, structures.dungeons());
|
||||
}
|
||||
|
||||
public static Selection select(long seed, PortAgeTerrain terrain) {
|
||||
var random = new Random(seed ^ 0x4D4154455249414CL);
|
||||
int count = random.nextInt(3);
|
||||
var terrainPage = id("terrain/" + terrain.name().toLowerCase(java.util.Locale.ROOT));
|
||||
var candidates = new ArrayList<PortRankedChoice.Option<Identifier>>();
|
||||
for (String name : List.of("rocks", "floating_islands", "tendrils", "spheres", "spikes", "obelisks",
|
||||
"surface_lakes", "deep_lakes", "lava_lakes")) {
|
||||
var page = id("feature/" + name);
|
||||
if (PortAuthoredWriting.supports(List.of(terrainPage, page)))
|
||||
candidates.add(new PortRankedChoice.Option<>(page, rank(name)));
|
||||
}
|
||||
var pages = new ArrayList<Identifier>();
|
||||
pages.add(terrainPage);
|
||||
List<Identifier> solids = PortRegistryPages.solidMaterialIds(), fluids = PortRegistryPages.fluidMaterialIds();
|
||||
for (int selected = 0; selected < count && !candidates.isEmpty();) {
|
||||
Identifier feature = PortRankedChoice.choose(random, candidates);
|
||||
candidates.removeIf(option -> option.value().equals(feature));
|
||||
var materials = PortLakePlan.isLake(feature) ? fluids : solids;
|
||||
if (materials.isEmpty()) continue;
|
||||
pages.add(PortRegistryPages.blockPageId(materials.get(random.nextInt(materials.size()))));
|
||||
pages.add(feature);
|
||||
selected++;
|
||||
}
|
||||
if (terrain != PortAgeTerrain.FLAT && terrain != PortAgeTerrain.VOID
|
||||
&& Math.floorMod(mix(seed ^ 0x534B594C414E4453L), 8) == 0)
|
||||
pages.add(PortTerrainFeaturePlan.SKYLANDS);
|
||||
if (terrain != PortAgeTerrain.CAVE && terrain != PortAgeTerrain.VOID && random.nextInt(10) == 0)
|
||||
pages.add(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS);
|
||||
if (randomStarFissure(seed)) pages.add(PortTerrainFeaturePlan.STAR_FISSURES);
|
||||
return new Selection(PortAuthoredWriting.rockPlan(pages, seed), PortAuthoredWriting.lakePlan(pages, seed),
|
||||
PortAuthoredWriting.terrainFeaturePlan(pages, seed), 0);
|
||||
}
|
||||
|
||||
private static int rank(String name) {
|
||||
return switch (name) {
|
||||
case "surface_lakes", "deep_lakes" -> 1;
|
||||
case "lava_lakes" -> 2;
|
||||
case "spheres", "spikes", "obelisks" -> 3;
|
||||
default -> 4;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean randomStarFissure(long seed) {
|
||||
var random = new Random(mix(seed ^ 0x535441524752414DL));
|
||||
for (int step = 0; step < 27; step++) {
|
||||
int branch = random.nextInt(5);
|
||||
if (branch == 0) return false;
|
||||
if (random.nextInt(9) == 0) return true;
|
||||
if (branch == 1) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Identifier id(String path) { return Identifier.fromNamespaceAndPath("rivencraft", path); }
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
|
||||
/** Creation-only structure completion, adapted from 1.21.1 AgeStructureSelection. */
|
||||
public final class PortRandomStructureSelection {
|
||||
private static final Identifier DUNGEONS = page("dungeons");
|
||||
private PortRandomStructureSelection() {}
|
||||
public record Selection(int dungeons, List<Identifier> structures) {
|
||||
public Selection { structures = List.copyOf(structures); }
|
||||
}
|
||||
|
||||
public static Selection select(MinecraftServer server, long seed, PortAgeTerrain terrain, PortBiomePlan plan) {
|
||||
Set<Identifier> biomes = actualBiomes(server, terrain, plan);
|
||||
var eligible = new ArrayList<PortRankedChoice.Option<Identifier>>();
|
||||
for (String name : List.of("dungeons", "mineshafts", "nether_fortresses", "strongholds", "villages",
|
||||
"trial_chambers", "bastions", "buried_treasure", "shipwrecks", "ocean_ruins", "pillager_outposts",
|
||||
"ruined_portals", "nether_fossils", "ancient_cities", "trail_ruins", "abandoned_camps")) {
|
||||
Identifier feature = page(name);
|
||||
boolean allowed = feature.equals(DUNGEONS)
|
||||
? net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(List.of(
|
||||
Identifier.fromNamespaceAndPath("rivencraft", "terrain/" + terrain.name().toLowerCase(java.util.Locale.ROOT)), feature))
|
||||
&& server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
.get(ResourceKey.create(Registries.PLACED_FEATURE,
|
||||
Identifier.fromNamespaceAndPath("rivencraft", "dungeons"))).isPresent()
|
||||
: PortStructurePages.randomEligible(feature, terrain, biomes, server.registryAccess());
|
||||
if (allowed) eligible.add(new PortRankedChoice.Option<>(feature, rank(name)));
|
||||
}
|
||||
var random = new Random(seed ^ 0x5354525543545552L);
|
||||
int count = random.nextInt(3), dungeons = 0;
|
||||
var structures = new ArrayList<Identifier>();
|
||||
for (int selected = 0; selected < count && !eligible.isEmpty(); selected++) {
|
||||
var feature = PortRankedChoice.choose(random, eligible);
|
||||
if (feature.equals(DUNGEONS)) dungeons++;
|
||||
else structures.add(feature);
|
||||
eligible.removeIf(option -> option.value().equals(feature));
|
||||
}
|
||||
return new Selection(dungeons, structures);
|
||||
}
|
||||
|
||||
private static Set<Identifier> actualBiomes(MinecraftServer server, PortAgeTerrain terrain, PortBiomePlan plan) {
|
||||
if (plan != null && plan.distribution() != PortBiomePlan.Distribution.NATIVE)
|
||||
return Set.copyOf(plan.biomes());
|
||||
if (terrain == PortAgeTerrain.FLAT) return Set.of(Identifier.withDefaultNamespace("plains"));
|
||||
if (terrain == PortAgeTerrain.VOID) return Set.of(Identifier.withDefaultNamespace("the_void"));
|
||||
if (terrain == PortAgeTerrain.CAVE) return Set.of(Identifier.withDefaultNamespace("lush_caves"));
|
||||
return server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM).getOrThrow(LevelStem.OVERWORLD)
|
||||
.value().generator().getBiomeSource().possibleBiomes().stream()
|
||||
.flatMap(holder -> holder.unwrapKey().stream()).map(key -> key.identifier())
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
private static int rank(String name) {
|
||||
return switch (name) {
|
||||
case "villages", "strongholds", "mineshafts" -> 1;
|
||||
case "dungeons", "nether_fortresses" -> 2;
|
||||
default -> 3;
|
||||
};
|
||||
}
|
||||
private static Identifier page(String name) { return Identifier.fromNamespaceAndPath("rivencraft", "feature/" + name); }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/** The 1.21.1 LegacyRankChoice rule, retained for seeded grammar completion. */
|
||||
final class PortRankedChoice {
|
||||
private PortRankedChoice() {}
|
||||
record Option<T>(T value, int rank) {}
|
||||
|
||||
static <T> T choose(Random random, List<Option<T>> options) {
|
||||
if (options.isEmpty()) throw new IllegalArgumentException("No ranked choices");
|
||||
var counts = new HashMap<Integer, Integer>();
|
||||
int highest = 0;
|
||||
for (var option : options) {
|
||||
counts.merge(option.rank(), 1, Integer::sum);
|
||||
highest = Math.max(highest, option.rank());
|
||||
}
|
||||
var weights = new HashMap<Integer, Integer>();
|
||||
int weight = 1, previousTotal = 0;
|
||||
for (int rank = highest; rank >= 0; rank--) {
|
||||
int count = counts.getOrDefault(rank, 0);
|
||||
if (weight != 1 && count > 0) weight = Math.max(weight, previousTotal / count + 1);
|
||||
weights.put(rank, weight);
|
||||
previousTotal = count * weight;
|
||||
weight++;
|
||||
}
|
||||
int ticket = random.nextInt(options.stream().mapToInt(option -> weights.get(option.rank())).sum());
|
||||
for (var option : options) {
|
||||
ticket -= weights.get(option.rank());
|
||||
if (ticket < 0) return option.value();
|
||||
}
|
||||
throw new IllegalStateException("Ranked choice ticket was not consumed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Persisted sky styles, including the client-rendered Legacy static background. */
|
||||
public enum PortSkyStyle {
|
||||
OVERWORLD, NETHER, END, STATIC;
|
||||
|
||||
public static final Codec<PortSkyStyle> CODEC = Codec.STRING.xmap(
|
||||
name -> valueOf(name.toUpperCase(Locale.ROOT)), sky -> sky.name().toLowerCase(Locale.ROOT));
|
||||
|
||||
public static PortSkyStyle forPage(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
case "sky/overworld" -> OVERWORLD;
|
||||
case "sky/nether" -> NETHER;
|
||||
case "sky/end" -> END;
|
||||
case "sky/static" -> STATIC;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static PortSkyStyle resolve(List<Identifier> pages, long seed) {
|
||||
return pages.stream().map(PortSkyStyle::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElseGet(() -> pages.stream().anyMatch(PortSkyStyle::isCelestial)
|
||||
? OVERWORLD : random(seed));
|
||||
}
|
||||
|
||||
/** Nether-style Ages use their fog clear color as the visible backdrop. */
|
||||
public static float fogShade(net.minecraft.world.level.Level level) {
|
||||
boolean nether = level.dimensionTypeRegistration().unwrapKey().map(key ->
|
||||
key.identifier().getNamespace().equals("rivencraft")
|
||||
&& key.identifier().getPath().endsWith("_sky_nether")).orElse(false);
|
||||
var cycle = PortDayCycle.forLevel(level);
|
||||
return nether && !cycle.bodies().isEmpty()
|
||||
? 1F - 0.65F * PortCelestialBody.eclipseAt(cycle.bodies(), PortDayCycle.nativeTicks(level)) : 1F;
|
||||
}
|
||||
|
||||
private static boolean isCelestial(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
|
||||
return switch (page.getPath()) {
|
||||
// Preserve the established compatible-default rule for written controllers.
|
||||
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");
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,20 @@ public final class PortStructurePages {
|
||||
.orElseThrow(() -> new IllegalArgumentException("Missing structure set for " + page)));
|
||||
}
|
||||
|
||||
/** Random choices need at least one normally eligible biome; authored pages may override it. */
|
||||
public static boolean randomEligible(Identifier page, PortAgeTerrain terrain, Set<Identifier> biomes,
|
||||
HolderLookup.Provider registries) {
|
||||
if (!supportsTerrain(page, terrain)) return false;
|
||||
var set = registries.lookupOrThrow(Registries.STRUCTURE_SET)
|
||||
.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)));
|
||||
if (set.isEmpty()) return false;
|
||||
var compatible = set.get().value().structures().stream()
|
||||
.flatMap(entry -> entry.structure().value().biomes().stream())
|
||||
.flatMap(holder -> holder.unwrapKey().stream()).map(key -> key.identifier())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
return compatible.isEmpty() || biomes.stream().anyMatch(compatible::contains);
|
||||
}
|
||||
|
||||
private static Map<Identifier, Integer> enabled(List<Identifier> pages) {
|
||||
Map<Identifier, Integer> result = new HashMap<>();
|
||||
for (Identifier page : pages) result.merge(SETS.get(page), 1, Integer::sum);
|
||||
|
||||
@@ -9,7 +9,8 @@ import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Frozen terrain filters and ordered solid/surface features for an Age. */
|
||||
public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
|
||||
boolean starFissures, List<Identifier> structures, List<Identifier> carvers) {
|
||||
boolean starFissures, List<Identifier> structures, List<Identifier> carvers,
|
||||
boolean structureControl) {
|
||||
public static final Identifier FLOATING_ISLANDS = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "feature/floating_islands");
|
||||
public static final Identifier TENDRILS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/tendrils");
|
||||
@@ -30,7 +31,8 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
|
||||
.forGetter(PortTerrainFeaturePlan::denseOres),
|
||||
Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("structures", List.of()).forGetter(PortTerrainFeaturePlan::structures),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("carvers", List.of()).forGetter(PortTerrainFeaturePlan::carvers)
|
||||
Identifier.CODEC.listOf().optionalFieldOf("carvers", List.of()).forGetter(PortTerrainFeaturePlan::carvers),
|
||||
Codec.BOOL.optionalFieldOf("structure_control", false).forGetter(PortTerrainFeaturePlan::structureControl)
|
||||
).apply(instance, PortTerrainFeaturePlan::new));
|
||||
|
||||
public record Choice(Identifier page, Identifier material) {
|
||||
@@ -55,7 +57,7 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
|
||||
PortCarverPages.validateCounts(carvers);
|
||||
if (denseOres < 0 || denseOres > MAX_PER_FEATURE)
|
||||
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences");
|
||||
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty())
|
||||
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty() && !structureControl)
|
||||
throw new IllegalArgumentException("Terrain feature plan must not be empty");
|
||||
var counts = new HashMap<Identifier, Integer>();
|
||||
for (Choice choice : choices) {
|
||||
@@ -64,6 +66,11 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
|
||||
}
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
|
||||
boolean starFissures, List<Identifier> structures, List<Identifier> carvers) {
|
||||
this(choices, skylands, denseOres, starFissures, structures, carvers, false);
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
|
||||
boolean starFissures, List<Identifier> structures) {
|
||||
this(choices, skylands, denseOres, starFissures, structures, List.of());
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
@@ -26,8 +25,4 @@ public enum PortWeatherMode {
|
||||
};
|
||||
}
|
||||
|
||||
public static PortWeatherMode fromPages(List<Identifier> pages) {
|
||||
return pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElse(NORMAL);
|
||||
}
|
||||
}
|
||||
@@ -113,13 +113,50 @@ public final class RuntimeAgeBackend {
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, PortLighting.NORMAL);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
|
||||
PortLighting lighting) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, lighting, PortSkyStyle.OVERWORLD);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
|
||||
PortLighting lighting, PortSkyStyle sky) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, lighting, sky, PortAgeColors.NATIVE);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
|
||||
PortLighting lighting, PortSkyStyle sky, PortAgeColors colors) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, lighting, sky, colors, PortDayCycle.NATIVE);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
|
||||
PortLighting lighting, PortSkyStyle sky, PortAgeColors colors, PortDayCycle dayCycle) {
|
||||
checkServerThread(server);
|
||||
ageName(id);
|
||||
AgeLevelIndex index = index(server);
|
||||
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
|
||||
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, lighting, sky, colors, dayCycle);
|
||||
((PortAgeLevel) level).configureWeather(weather);
|
||||
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures, lighting, sky, colors, dayCycle);
|
||||
// A newly bound book may be used immediately. Persist its Age identity
|
||||
// before publishing the catalog so a later crash cannot leave that
|
||||
// book pointing at a world omitted from the on-disk index.
|
||||
@@ -194,6 +231,10 @@ public final class RuntimeAgeBackend {
|
||||
PortTerrainFeaturePlan terrainFeatures = index.terrainFeatures(id);
|
||||
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
||||
ArchivePaths paths = archivePaths(server, name);
|
||||
PortLighting lighting = index.lighting(id);
|
||||
PortSkyStyle sky = index.sky(id);
|
||||
PortAgeColors colors = index.colors(id);
|
||||
PortDayCycle dayCycle = index.dayCycle(id);
|
||||
if (loaded) unloadLoaded(server, id);
|
||||
// Persist the tombstone before moving files: a crash mid-archive must
|
||||
// not let an old book silently regenerate this identity.
|
||||
@@ -211,7 +252,7 @@ public final class RuntimeAgeBackend {
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
try {
|
||||
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures);
|
||||
terrainFeatures, lighting, sky, colors, dayCycle);
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
@@ -234,6 +275,16 @@ public final class RuntimeAgeBackend {
|
||||
return index(server).entries();
|
||||
}
|
||||
|
||||
public static Map<Identifier, PortAgeColors> savedColors(MinecraftServer server) {
|
||||
checkServerThread(server);
|
||||
return index(server).colors();
|
||||
}
|
||||
|
||||
public static Map<Identifier, PortDayCycle> savedDayCycles(MinecraftServer server) {
|
||||
checkServerThread(server);
|
||||
return index(server).dayCycles();
|
||||
}
|
||||
|
||||
/** Includes retired IDs, which must never be silently reused by a new book. */
|
||||
public static boolean isNameTaken(MinecraftServer server, Identifier id) {
|
||||
checkServerThread(server);
|
||||
@@ -337,6 +388,15 @@ public final class RuntimeAgeBackend {
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures) {
|
||||
return load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, index(server).lighting(id), index(server).sky(id), index(server).colors(id), index(server).dayCycle(id));
|
||||
}
|
||||
|
||||
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortLighting lighting, PortSkyStyle sky,
|
||||
PortAgeColors colors, PortDayCycle dayCycle) {
|
||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
||||
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
||||
|
||||
@@ -405,13 +465,16 @@ public final class RuntimeAgeBackend {
|
||||
case CAVE -> dryCaveSettings(server);
|
||||
case FLAT, VOID -> throw new IllegalStateException("Flat terrain should use FlatLevelSource");
|
||||
};
|
||||
generator = new NoiseBasedChunkGenerator(source, settings);
|
||||
generator = new NoiseBasedChunkGenerator(source, PortBiomeMaterialRules.apply(server.registryAccess(), settings.value()));
|
||||
}
|
||||
if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
|
||||
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
|
||||
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
|
||||
LevelStem stem = new LevelStem(lighting.dimensionType(server, terrain, overworldStem.type(), sky),
|
||||
generator, OptionalLong.of(seed));
|
||||
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
|
||||
ServerLevel level = new PortAgeLevel(server, key, stem, biomeZoomSeed, terrain);
|
||||
((PortAgeLevel) level).configureColors(colors);
|
||||
((PortAgeLevel) level).configureDayCycle(dayCycle);
|
||||
|
||||
// Publish only after the constructor succeeds. If the load event fails,
|
||||
// roll back the map and close the newly opened chunk/entity stores.
|
||||
|
||||
@@ -18,10 +18,22 @@ public final class AgeCatalogClient {
|
||||
private AgeCatalogClient() {
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void logout(net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent.LoggingOut event) {
|
||||
net.dnorberg.rivencraft.age.PortAgeColors.updateClient(java.util.Map.of());
|
||||
net.dnorberg.rivencraft.age.PortDayCycle.updateClient(java.util.Map.of());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void register(RegisterClientPayloadHandlersEvent event) {
|
||||
event.register(AgeCatalogPayload.TYPE, (payload, context) -> {
|
||||
if (!(context.listener() instanceof ClientPacketListener listener)) return;
|
||||
net.dnorberg.rivencraft.age.PortAgeColors.updateClient(payload.colors());
|
||||
net.dnorberg.rivencraft.age.PortDayCycle.updateClient(payload.dayCycles());
|
||||
if (listener.getLevel() != null) {
|
||||
listener.getLevel().environmentAttributes().invalidateTickCache();
|
||||
listener.getLevel().updateSkyBrightness();
|
||||
}
|
||||
listener.levels = payload.dimensions().stream()
|
||||
.map(id -> ResourceKey.<Level>create(Registries.DIMENSION, id))
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.commands.RenderPass;
|
||||
import com.mojang.renderpearl.api.pipeline.BlendFunction;
|
||||
import com.mojang.renderpearl.api.pipeline.ColorTargetState;
|
||||
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
|
||||
import com.mojang.renderpearl.api.pipeline.RenderPipeline;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.MoonPhase;
|
||||
import net.neoforged.api.distmarker.Dist;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.client.event.RegisterRenderPipelinesEvent;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/** Legacy's opaque central disc, separate from the native transparent glow. */
|
||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
|
||||
public final class PortCelestialDiscs implements AutoCloseable {
|
||||
private static final Identifier SUN = Identifier.withDefaultNamespace("sun");
|
||||
private static final RenderPipeline CORE = RenderPipelines.CELESTIAL.toBuilder()
|
||||
.withLocation(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "pipeline/celestial_core"))
|
||||
.withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)).build();
|
||||
private final Map<TextureAtlasSprite, GpuBuffer> cores = new IdentityHashMap<>();
|
||||
private TextureAtlasSprite atlasGeneration;
|
||||
|
||||
@SubscribeEvent public static void register(RegisterRenderPipelinesEvent event) { event.registerPipeline(CORE); }
|
||||
|
||||
public void draw(RenderPass pass, TextureAtlas atlas, boolean moon, MoonPhase phase,
|
||||
Matrix4f transform, int color, float alpha) {
|
||||
if (alpha <= 0) return;
|
||||
var generation = atlas.getSprite(SUN);
|
||||
if (generation != atlasGeneration) {
|
||||
close();
|
||||
atlasGeneration = generation;
|
||||
}
|
||||
var sprite = moon ? atlas.getSprite(Identifier.withDefaultNamespace("moon/" + phase.getSerializedName())) : generation;
|
||||
var buffer = cores.computeIfAbsent(sprite, ignored -> build(sprite, moon));
|
||||
var indices = RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
|
||||
var indexBuffer = indices.getBuffer(6);
|
||||
var rgb = net.dnorberg.rivencraft.age.PortAgeColors.vector(color);
|
||||
var uniforms = RenderSystem.getDynamicUniforms().writeTransform(transform, new Vector4f(rgb, alpha));
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(CORE));
|
||||
RenderSystem.bindDefaultUniforms(pass);
|
||||
pass.setUniform("DynamicTransforms", uniforms);
|
||||
pass.setUniform("Sampler0", atlas.getTextureView(), atlas.getSampler());
|
||||
pass.setVertexBuffer(0, buffer.slice());
|
||||
pass.setIndexBuffer(indexBuffer, indices.type());
|
||||
pass.drawIndexed(6, 1, 0, 0, 0);
|
||||
}
|
||||
|
||||
private static GpuBuffer build(TextureAtlasSprite sprite, boolean moon) {
|
||||
var format = DefaultVertexFormat.POSITION_TEX;
|
||||
try (var bytes = ByteBufferBuilder.exactlySized(4 * format.getVertexSize())) {
|
||||
var builder = new BufferBuilder(bytes, PrimitiveTopology.QUADS, format);
|
||||
// Same 8/32 central area as root's ordinary-alpha second pass.
|
||||
float u0 = sprite.getU(0.375F), u1 = sprite.getU(0.625F);
|
||||
float v0 = sprite.getV(0.375F), v1 = sprite.getV(0.625F);
|
||||
builder.addVertex(-0.25F, 0, -0.25F).setUv(moon ? u1 : u0, moon ? v1 : v0);
|
||||
builder.addVertex(0.25F, 0, -0.25F).setUv(moon ? u0 : u1, moon ? v1 : v0);
|
||||
builder.addVertex(0.25F, 0, 0.25F).setUv(moon ? u0 : u1, moon ? v0 : v1);
|
||||
builder.addVertex(-0.25F, 0, 0.25F).setUv(moon ? u1 : u0, moon ? v0 : v1);
|
||||
try (var mesh = builder.buildOrThrow()) {
|
||||
return RenderSystem.getDevice().createBuffer(() -> "Rivencraft celestial core", 40, mesh.vertexBuffer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cores.values().forEach(GpuBuffer::close);
|
||||
cores.clear();
|
||||
atlasGeneration = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.MeshData;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
||||
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalDouble;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/** Legacy's six 48x48 noise faces, uploaded once per animation frame rather than per cell. */
|
||||
public final class PortStaticSky implements AutoCloseable {
|
||||
private static final int GRID = 48;
|
||||
private final DynamicTexture texture = new DynamicTexture("Rivencraft static sky", GRID * 6, GRID, false);
|
||||
private final GpuBuffer cube = buildCube();
|
||||
private long uploadedFrame = Long.MIN_VALUE;
|
||||
private int uploadedTint = -1;
|
||||
|
||||
public void render(RenderTarget target, GpuBufferSlice fog, long frame, int tint) {
|
||||
if (tint < 0) tint = 0x929292;
|
||||
if (uploadedFrame != frame || uploadedTint != tint) {
|
||||
RandomSource random = RandomSource.create(frame);
|
||||
var image = texture.getPixels();
|
||||
for (int face = 0; face < 6; face++) {
|
||||
for (int y = 0; y < GRID; y++) {
|
||||
for (int x = 0; x < GRID; x++) {
|
||||
int noise = 65 + random.nextInt(160);
|
||||
int red = (tint >> 16 & 255) * noise / 255;
|
||||
int green = (tint >> 8 & 255) * noise / 255;
|
||||
int blue = (tint & 255) * noise / 255;
|
||||
image.setPixel(face * GRID + x, y, 0xff000000 | red << 16 | green << 8 | blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
texture.upload();
|
||||
uploadedFrame = frame;
|
||||
uploadedTint = tint;
|
||||
}
|
||||
RenderSystem.setShaderFog(fog);
|
||||
var indices = RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
|
||||
var indexBuffer = indices.getBuffer(36);
|
||||
var transform = RenderSystem.getDynamicUniforms().writeTransform(RenderSystem.getModelViewMatrixCopy());
|
||||
try (var pass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(
|
||||
() -> "Rivencraft static sky", target.getColorTextureView(), Optional.empty(),
|
||||
target.getDepthTextureView(), OptionalDouble.empty())) {
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(RenderPipelines.END_SKY));
|
||||
RenderSystem.bindDefaultUniforms(pass);
|
||||
pass.setUniform("DynamicTransforms", transform);
|
||||
pass.setUniform("Sampler0", texture.getTextureView(), texture.getSampler());
|
||||
pass.setVertexBuffer(0, cube.slice());
|
||||
pass.setIndexBuffer(indexBuffer, indices.type());
|
||||
pass.drawIndexed(36, 1, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static GpuBuffer buildCube() {
|
||||
var format = DefaultVertexFormat.POSITION_TEX_COLOR;
|
||||
try (var bytes = ByteBufferBuilder.exactlySized(24 * format.getVertexSize())) {
|
||||
var builder = new BufferBuilder(bytes, PrimitiveTopology.QUADS, format);
|
||||
for (int face = 0; face < 6; face++) {
|
||||
var pose = new Matrix4f();
|
||||
switch (face) {
|
||||
case 1 -> pose.rotationX((float) Math.PI / 2);
|
||||
case 2 -> pose.rotationX((float) -Math.PI / 2);
|
||||
case 3 -> pose.rotationX((float) Math.PI);
|
||||
case 4 -> pose.rotationZ((float) Math.PI / 2);
|
||||
case 5 -> pose.rotationZ((float) -Math.PI / 2);
|
||||
}
|
||||
float u0 = face / 6f, u1 = (face + 1) / 6f;
|
||||
builder.addVertex(pose, -100, -100, -100).setUv(u0, 0).setColor(-1);
|
||||
builder.addVertex(pose, -100, -100, 100).setUv(u0, 1).setColor(-1);
|
||||
builder.addVertex(pose, 100, -100, 100).setUv(u1, 1).setColor(-1);
|
||||
builder.addVertex(pose, 100, -100, -100).setUv(u1, 0).setColor(-1);
|
||||
}
|
||||
try (MeshData mesh = builder.buildOrThrow()) {
|
||||
return RenderSystem.getDevice().createBuffer(() -> "Rivencraft static sky cube", 40, mesh.vertexBuffer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cube.close();
|
||||
texture.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
/** Carries extracted sky presentation values without live-world reads during drawing. */
|
||||
public interface StaticSkyState {
|
||||
int rivencraft$bodyOverlay();
|
||||
void rivencraft$bodyOverlay(int mode);
|
||||
record Body(int kind, float angle, int bearing, float size, int color, float eclipse) {}
|
||||
java.util.List<Body> rivencraft$bodies();
|
||||
void rivencraft$bodies(java.util.List<Body> bodies);
|
||||
long rivencraft$staticFrame();
|
||||
void rivencraft$staticFrame(long frame);
|
||||
int rivencraft$skyTint();
|
||||
void rivencraft$skyTint(int color);
|
||||
int rivencraft$orbitBearing();
|
||||
void rivencraft$orbitBearing(int bearing);
|
||||
float rivencraft$sunSize();
|
||||
void rivencraft$sunSize(float size);
|
||||
int rivencraft$celestialTint();
|
||||
void rivencraft$celestialTint(int color);
|
||||
int rivencraft$bodyMode();
|
||||
void rivencraft$bodyMode(int mode);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
|
||||
import net.dnorberg.rivencraft.age.PortLighting;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.lighting.LevelLightEngine;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
/** Ambient light contributes to effective brightness, not fake block/sky propagation. */
|
||||
@Mixin(LevelLightEngine.class)
|
||||
public abstract class AgeAmbientLightMixin {
|
||||
@Shadow @Final protected LevelHeightAccessor levelHeightAccessor;
|
||||
|
||||
@ModifyReturnValue(method = "getRawBrightness", at = @At("RETURN"))
|
||||
private int rivencraft$ambientBrightness(int brightness) {
|
||||
return levelHeightAccessor instanceof Level level && PortLighting.isBright(level)
|
||||
? Math.max(brightness, 15) : brightness;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import net.dnorberg.rivencraft.age.PortAgeColors;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributeSystem;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(EnvironmentAttributeSystem.Builder.class)
|
||||
public class AgeColorAttributesMixin {
|
||||
@Inject(method = "addDefaultLayers", at = @At("RETURN"))
|
||||
private void rivencraft$colors(Level level, CallbackInfoReturnable<EnvironmentAttributeSystem.Builder> cir) {
|
||||
var id = level.dimension().identifier();
|
||||
if (!id.getNamespace().equals("rivencraft") || !id.getPath().startsWith("age/")) return;
|
||||
var builder = cir.getReturnValue();
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.SKY_COLOR, (base, tick) -> {
|
||||
int rgb = PortAgeColors.forLevel(level).sky();
|
||||
return rgb < 0 ? base : PortAgeColors.vector(rgb);
|
||||
});
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.FOG_COLOR, (base, tick) -> {
|
||||
int rgb = PortAgeColors.forLevel(level).sky();
|
||||
var color = rgb < 0 ? base : PortAgeColors.vector(rgb);
|
||||
float shade = net.dnorberg.rivencraft.age.PortSkyStyle.fogShade(level);
|
||||
return shade == 1F ? color : new org.joml.Vector3f(color).mul(shade);
|
||||
});
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.AMBIENT_LIGHT_COLOR, (base, tick) -> {
|
||||
int rgb = PortAgeColors.forLevel(level).ambient();
|
||||
return rgb < 0 ? base : PortAgeColors.vector(rgb);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import net.dnorberg.rivencraft.age.PortDayCycle;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributeSystem;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||
import net.minecraft.world.attribute.WeatherAttributes;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
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 = "addDefaultLayers", at = @At("RETURN"))
|
||||
private void rivencraft$singleBody(Level level, CallbackInfoReturnable<EnvironmentAttributeSystem.Builder> cir) {
|
||||
var id = level.dimension().identifier();
|
||||
if (!id.getNamespace().equals("rivencraft") || !id.getPath().startsWith("age/")) return;
|
||||
var builder = cir.getReturnValue();
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.SUN_ANGLE, (base, tick) -> {
|
||||
var cycle = PortDayCycle.forLevel(level);
|
||||
return cycle.singleBody() ? cycle.bodyAngle(PortDayCycle.nativeTicks(level)) : base;
|
||||
});
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.MOON_ANGLE, (base, tick) -> {
|
||||
var cycle = PortDayCycle.forLevel(level);
|
||||
return cycle.singleBody() ? cycle.bodyAngle(PortDayCycle.nativeTicks(level)) : base;
|
||||
});
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.SKY_LIGHT_LEVEL, (base, tick) -> {
|
||||
var cycle = PortDayCycle.forLevel(level);
|
||||
if (!cycle.customLight()) return base;
|
||||
// Match root AgeSkyProfile.lightAt and LevelSkyBrightnessMixin's
|
||||
// single skylight channel. Bright/Dark and no-skylight geometry remain separate.
|
||||
float weather = (1F - level.getRainLevel(1F) * 5F / 16F)
|
||||
* (1F - level.getThunderLevel(1F) * 5F / 16F);
|
||||
return 15F - (int) ((1F - cycle.bodyLight(PortDayCycle.nativeTicks(level)) * weather) * 11F);
|
||||
});
|
||||
}
|
||||
|
||||
@Inject(method = "addDynamicLayers", at = @At("HEAD"), cancellable = true)
|
||||
private void rivencraft$dayCycle(Level level, CallbackInfoReturnable<EnvironmentAttributeSystem.Builder> cir) {
|
||||
var id = level.dimension().identifier();
|
||||
if (!id.getNamespace().equals("rivencraft") || !id.getPath().startsWith("age/")) return;
|
||||
var builder = (EnvironmentAttributeSystem.Builder) (Object) this;
|
||||
var clocks = PortDayCycle.timelines(level, level.clockManager());
|
||||
level.dimensionType().timelines().forEach(timeline -> builder.addTimelineLayer(timeline, clocks));
|
||||
if (level.canHaveWeather()) WeatherAttributes.addBuiltinLayers(builder, WeatherAttributes.WeatherAccess.from(level));
|
||||
cir.setReturnValue(builder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderTarget;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
||||
import net.dnorberg.rivencraft.client.PortStaticSky;
|
||||
import net.dnorberg.rivencraft.client.StaticSkyState;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.SkyRenderer;
|
||||
import net.minecraft.client.renderer.state.level.SkyRenderState;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyArg;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(SkyRenderer.class)
|
||||
public class StaticSkyRendererMixin {
|
||||
@Shadow @Final private RenderTarget renderTarget;
|
||||
@Unique private PortStaticSky rivencraft$staticSky;
|
||||
@Unique private int rivencraft$endTint = -1;
|
||||
@Unique private int rivencraft$orbitBearing = -1;
|
||||
@Unique private float rivencraft$sunSize = 1F;
|
||||
@Unique private int rivencraft$celestialTint = -1;
|
||||
@Unique private int rivencraft$bodyMode;
|
||||
@Unique private java.util.List<StaticSkyState.Body> rivencraft$bodies = java.util.List.of();
|
||||
@Unique private net.dnorberg.rivencraft.client.PortCelestialDiscs rivencraft$discs;
|
||||
@Shadow @Final private net.minecraft.client.renderer.texture.TextureAtlas celestialsAtlas;
|
||||
@Shadow private org.joml.Matrix4f applyCelestialBodyTransform(com.mojang.blaze3d.vertex.PoseStack pose,
|
||||
float height, float size) { throw new AssertionError(); }
|
||||
@Shadow private void renderSun(com.mojang.renderpearl.api.commands.RenderPass pass, float alpha,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose) { throw new AssertionError(); }
|
||||
@Shadow private void renderMoon(com.mojang.renderpearl.api.commands.RenderPass pass,
|
||||
net.minecraft.world.level.MoonPhase phase, float alpha,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose) { throw new AssertionError(); }
|
||||
@Shadow private void renderStars(com.mojang.renderpearl.api.commands.RenderPass pass, float brightness,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose) { throw new AssertionError(); }
|
||||
@Shadow private void renderSunMoonAndStars(com.mojang.renderpearl.api.commands.RenderPass pass,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose, float sunAngle,
|
||||
float moonAngle, float starAngle, net.minecraft.world.level.MoonPhase phase,
|
||||
float alpha, float stars) { throw new AssertionError(); }
|
||||
|
||||
@Inject(method = "extractRenderState", at = @At("TAIL"))
|
||||
private void rivencraft$extract(ClientLevel level, float partialTicks, Camera camera,
|
||||
SkyRenderState state, CallbackInfo ci) {
|
||||
boolean enabled = level.dimensionTypeRegistration().unwrapKey().map(key ->
|
||||
key.identifier().getNamespace().equals("rivencraft")
|
||||
&& key.identifier().getPath().endsWith("_sky_static")).orElse(false);
|
||||
((StaticSkyState) state).rivencraft$staticFrame(enabled ? level.getGameTime() / 2 : Long.MIN_VALUE);
|
||||
((StaticSkyState) state).rivencraft$skyTint(net.dnorberg.rivencraft.age.PortAgeColors.forLevel(level).sky());
|
||||
var cycle = net.dnorberg.rivencraft.age.PortDayCycle.forLevel(level);
|
||||
// 0=native Overworld/no authored overlay, 1=fog-only backdrop,
|
||||
// 2=native End backdrop, 3=custom Static backdrop.
|
||||
int overlay = !cycle.authored() || cycle.bodyMode() == net.dnorberg.rivencraft.age.PortDayCycle.NONE
|
||||
|| state.skybox == DimensionType.Skybox.OVERWORLD ? 0
|
||||
: enabled ? 3 : state.skybox == DimensionType.Skybox.NONE ? 1 : 2;
|
||||
((StaticSkyState) state).rivencraft$bodyOverlay(overlay);
|
||||
((StaticSkyState) state).rivencraft$orbitBearing(cycle.authored() ? cycle.bearing() : -1);
|
||||
((StaticSkyState) state).rivencraft$sunSize(cycle.authored() ? cycle.sunSize() : 1F);
|
||||
((StaticSkyState) state).rivencraft$celestialTint(
|
||||
net.dnorberg.rivencraft.age.PortAgeColors.forLevel(level).celestial());
|
||||
((StaticSkyState) state).rivencraft$bodyMode(cycle.authored() ? cycle.bodyMode() : 0);
|
||||
// Root's authored Moon always samples the full-moon sprite. Preserve
|
||||
// native calendar phases only for the native Normal Time pair.
|
||||
if (cycle.singleBody() && cycle.bodyMode() == net.dnorberg.rivencraft.age.PortDayCycle.MOON)
|
||||
state.moonPhase = net.minecraft.world.level.MoonPhase.FULL_MOON;
|
||||
var authoredBodies = cycle.bodies();
|
||||
if (authoredBodies.isEmpty() && overlay != 0) {
|
||||
var bodies = new java.util.ArrayList<net.dnorberg.rivencraft.age.PortCelestialBody>();
|
||||
int tint = net.dnorberg.rivencraft.age.PortAgeColors.forLevel(level).celestial();
|
||||
if (cycle.bodyMode() != net.dnorberg.rivencraft.age.PortDayCycle.MOON)
|
||||
bodies.add(net.dnorberg.rivencraft.age.PortCelestialBody.of(cycle, 1, tint, false));
|
||||
if (cycle.bodyMode() != net.dnorberg.rivencraft.age.PortDayCycle.SUN)
|
||||
bodies.add(net.dnorberg.rivencraft.age.PortCelestialBody.of(cycle, 2, tint, cycle.bodyMode() == 0));
|
||||
authoredBodies = java.util.List.copyOf(bodies);
|
||||
}
|
||||
if (authoredBodies.isEmpty()) ((StaticSkyState) state).rivencraft$bodies(java.util.List.of());
|
||||
else {
|
||||
var clock = level.clockManager().getInstance(level.registryAccess().lookupOrThrow(
|
||||
net.minecraft.core.registries.Registries.WORLD_CLOCK).getOrThrow(net.minecraft.world.clock.WorldClocks.OVERWORLD));
|
||||
double ticks = clock.totalTicks() + clock.partialTick();
|
||||
var sources = authoredBodies;
|
||||
var bodies = sources.stream().map(body -> {
|
||||
float eclipse = body.eclipse(sources, ticks);
|
||||
return new StaticSkyState.Body(body.kind(), body.angle(ticks), body.bearing(), body.size(),
|
||||
body.eclipseColor(eclipse), eclipse);
|
||||
}).toList();
|
||||
((StaticSkyState) state).rivencraft$bodies(bodies);
|
||||
float eclipse = 0;
|
||||
for (var body : bodies) if (body.kind() == 1 && Math.cos(Math.toRadians(body.angle())) > 0)
|
||||
eclipse = Math.max(eclipse, body.eclipse());
|
||||
if (state.skybox == DimensionType.Skybox.OVERWORLD) {
|
||||
state.skyColor = new org.joml.Vector3f(state.skyColor).mul(1F - 0.65F * eclipse);
|
||||
} else if (eclipse > 0 && overlay >= 2) {
|
||||
int tint = ((StaticSkyState) state).rivencraft$skyTint();
|
||||
if (tint < 0) tint = enabled ? 0x929292 : 0xffffff;
|
||||
((StaticSkyState) state).rivencraft$skyTint(rivencraft$shade(tint, 1F - 0.65F * eclipse));
|
||||
}
|
||||
}
|
||||
// Only the extracted presentation uses END to schedule the sky pass. The actual
|
||||
// dimension has NONE, so it never acquires End flashes or End gameplay behavior.
|
||||
if (enabled || overlay == 1) {
|
||||
state.skybox = DimensionType.Skybox.END;
|
||||
state.endFlashIntensity = 0;
|
||||
}
|
||||
if (overlay != 0) state.rainBrightness = 1F - level.getRainLevel(partialTicks);
|
||||
}
|
||||
|
||||
@Unique private static int rivencraft$shade(int rgb, float shade) {
|
||||
return Math.round((rgb >> 16 & 255) * shade) << 16
|
||||
| Math.round((rgb >> 8 & 255) * shade) << 8 | Math.round((rgb & 255) * shade);
|
||||
}
|
||||
|
||||
@Inject(method = "render", at = @At("HEAD"), cancellable = true)
|
||||
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();
|
||||
rivencraft$celestialTint = ((StaticSkyState) state).rivencraft$celestialTint();
|
||||
rivencraft$bodyMode = ((StaticSkyState) state).rivencraft$bodyMode();
|
||||
rivencraft$bodies = ((StaticSkyState) state).rivencraft$bodies();
|
||||
if (((StaticSkyState) state).rivencraft$bodyOverlay() == 1) {
|
||||
rivencraft$overlay(fog, state);
|
||||
ci.cancel();
|
||||
return;
|
||||
}
|
||||
if (frame == Long.MIN_VALUE) return;
|
||||
if (rivencraft$staticSky == null) rivencraft$staticSky = new PortStaticSky();
|
||||
rivencraft$staticSky.render(renderTarget, fog, frame, rivencraft$endTint);
|
||||
if (((StaticSkyState) state).rivencraft$bodyOverlay() == 3) rivencraft$overlay(fog, state);
|
||||
ci.cancel();
|
||||
}
|
||||
|
||||
@Inject(method = "render", at = @At("RETURN"))
|
||||
private void rivencraft$afterBackground(GpuBufferSlice fog, SkyRenderState state, CallbackInfo ci) {
|
||||
// RETURN includes the native End branch's early return, after its pass closes.
|
||||
if (((StaticSkyState) state).rivencraft$bodyOverlay() == 2) rivencraft$overlay(fog, state);
|
||||
}
|
||||
|
||||
@Unique private void rivencraft$overlay(GpuBufferSlice fog, SkyRenderState state) {
|
||||
if (rivencraft$bodies.isEmpty()) return;
|
||||
com.mojang.blaze3d.systems.RenderSystem.setShaderFog(fog);
|
||||
try (var pass = com.mojang.blaze3d.systems.RenderSystem.getDevice().createCommandEncoder().createRenderPass(
|
||||
() -> "Rivencraft celestial overlay", renderTarget.getColorTextureView(), java.util.Optional.empty(),
|
||||
renderTarget.getDepthTextureView(), java.util.OptionalDouble.empty())) {
|
||||
renderSunMoonAndStars(pass, new com.mojang.blaze3d.vertex.PoseStack(), 0, 0, 0,
|
||||
net.minecraft.world.level.MoonPhase.FULL_MOON, state.rainBrightness, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "renderSunMoonAndStars", at = @At("HEAD"), cancellable = true)
|
||||
private void rivencraft$drawBodies(com.mojang.renderpearl.api.commands.RenderPass pass,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose, float sunAngle,
|
||||
float moonAngle, float starAngle, net.minecraft.world.level.MoonPhase phase,
|
||||
float alpha, float stars, CallbackInfo ci) {
|
||||
if (rivencraft$bodies.isEmpty()) return;
|
||||
int savedMode = rivencraft$bodyMode, savedTint = rivencraft$celestialTint;
|
||||
float savedSize = rivencraft$sunSize;
|
||||
try {
|
||||
// Stars belong behind opaque discs, not on top of a dark eclipsing moon.
|
||||
if (stars > 0) {
|
||||
pose.pushPose();
|
||||
try {
|
||||
pose.rotateDegrees(com.mojang.math.Axis.YP, -90);
|
||||
pose.rotate(com.mojang.math.Axis.XP, starAngle);
|
||||
renderStars(pass, stars, pose);
|
||||
} finally { pose.popPose(); }
|
||||
}
|
||||
// Same ordering as 1.21.1: all suns, then all moons; each opaque
|
||||
// central disc is drawn after its transparent glow.
|
||||
for (int kind = 1; kind <= 2; kind++) for (var body : rivencraft$bodies) {
|
||||
if (body.kind() != kind) continue;
|
||||
rivencraft$bodyMode = kind;
|
||||
rivencraft$celestialTint = body.color();
|
||||
rivencraft$sunSize = body.size();
|
||||
pose.pushPose();
|
||||
try {
|
||||
pose.rotateDegrees(com.mojang.math.Axis.YP, body.bearing() < 0 ? -90 : body.bearing());
|
||||
pose.rotateDegrees(com.mojang.math.Axis.XP, body.angle());
|
||||
float glowAlpha = alpha * (kind == 1 ? 0.84F : 0.88F + 0.12F * body.eclipse());
|
||||
var bodyPhase = net.minecraft.world.level.MoonPhase.FULL_MOON;
|
||||
if (kind == 1) renderSun(pass, glowAlpha, pose); else renderMoon(pass, bodyPhase, glowAlpha, pose);
|
||||
if (rivencraft$discs == null) rivencraft$discs = new net.dnorberg.rivencraft.client.PortCelestialDiscs();
|
||||
// Both passes must use the same sprite. Native dark phase pixels
|
||||
// become black patches under ordinary alpha, unlike the old glow-only pass.
|
||||
var transform = applyCelestialBodyTransform(pose, 100F, (kind == 1 ? 30F : 20F) * body.size());
|
||||
rivencraft$discs.draw(pass, celestialsAtlas, kind == 2, bodyPhase, transform, body.color(), alpha);
|
||||
} finally { pose.popPose(); }
|
||||
}
|
||||
} finally {
|
||||
rivencraft$bodyMode = savedMode;
|
||||
rivencraft$celestialTint = savedTint;
|
||||
rivencraft$sunSize = savedSize;
|
||||
}
|
||||
ci.cancel();
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@ModifyArg(method = "renderMoon", at = @At(value = "INVOKE", target =
|
||||
"Lnet/minecraft/client/renderer/SkyRenderer;applyCelestialBodyTransform(Lcom/mojang/blaze3d/vertex/PoseStack;FF)Lorg/joml/Matrix4f;"), index = 2)
|
||||
private float rivencraft$scaleMoon(float nativeSize) {
|
||||
return rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.MOON
|
||||
? nativeSize * rivencraft$sunSize : nativeSize;
|
||||
}
|
||||
|
||||
@Inject(method = "renderSun", at = @At("HEAD"), cancellable = true)
|
||||
private void rivencraft$hideSun(com.mojang.renderpearl.api.commands.RenderPass pass, float alpha,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose, CallbackInfo ci) {
|
||||
if (rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.MOON
|
||||
|| rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.NONE) ci.cancel();
|
||||
}
|
||||
|
||||
@Inject(method = "renderMoon", at = @At("HEAD"), cancellable = true)
|
||||
private void rivencraft$hideMoon(com.mojang.renderpearl.api.commands.RenderPass pass,
|
||||
net.minecraft.world.level.MoonPhase phase, float alpha,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose, CallbackInfo ci) {
|
||||
if (rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.SUN
|
||||
|| rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.NONE) ci.cancel();
|
||||
}
|
||||
|
||||
@ModifyArg(method = {"renderSun", "renderMoon"}, at = @At(value = "INVOKE", target =
|
||||
"Lnet/minecraft/client/renderer/DynamicGpuData;writeTransform(Lorg/joml/Matrix4f;Lorg/joml/Vector4f;)Lcom/mojang/renderpearl/api/buffers/GpuBufferSlice;"), index = 1)
|
||||
private org.joml.Vector4f rivencraft$tintCelestial(org.joml.Vector4f nativeColor) {
|
||||
// Normal Time consumes one shared color group for its sun and moon in 1.21.1.
|
||||
// Keep native alpha (including weather fading) and the sprite's transparent halo.
|
||||
if (rivencraft$celestialTint < 0) return nativeColor;
|
||||
var tint = net.dnorberg.rivencraft.age.PortAgeColors.vector(rivencraft$celestialTint);
|
||||
return new org.joml.Vector4f(nativeColor.x * tint.x(), nativeColor.y * tint.y(),
|
||||
nativeColor.z * tint.z(), nativeColor.w);
|
||||
}
|
||||
|
||||
@Inject(method = "renderSunriseAndSunset", at = @At("HEAD"), cancellable = true)
|
||||
private void rivencraft$rotateHorizon(com.mojang.renderpearl.api.commands.RenderPass pass,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose, float angle,
|
||||
org.joml.Vector4fc color, CallbackInfo ci) {
|
||||
if (rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.MOON
|
||||
|| rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.NONE) {
|
||||
ci.cancel();
|
||||
return;
|
||||
}
|
||||
if (rivencraft$orbitBearing < 0) return;
|
||||
pose.pushPose();
|
||||
pose.rotateDegrees(com.mojang.math.Axis.YP, rivencraft$orbitBearing + 90);
|
||||
}
|
||||
|
||||
@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$bodyMode != net.dnorberg.rivencraft.age.PortDayCycle.MOON
|
||||
&& rivencraft$bodyMode != net.dnorberg.rivencraft.age.PortDayCycle.NONE
|
||||
&& rivencraft$orbitBearing >= 0) pose.popPose();
|
||||
}
|
||||
|
||||
@Redirect(method = "renderEndSky", at = @At(value = "INVOKE", target =
|
||||
"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$discs != null) {
|
||||
rivencraft$discs.close();
|
||||
rivencraft$discs = null;
|
||||
}
|
||||
if (rivencraft$staticSky != null) {
|
||||
rivencraft$staticSky.close();
|
||||
rivencraft$staticSky = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import net.dnorberg.rivencraft.client.StaticSkyState;
|
||||
import net.minecraft.client.renderer.state.level.SkyRenderState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(SkyRenderState.class)
|
||||
public class StaticSkyStateMixin implements StaticSkyState {
|
||||
@Unique private long rivencraft$frame = Long.MIN_VALUE;
|
||||
@Unique private int rivencraft$tint = -1;
|
||||
@Unique private int rivencraft$bearing = -1;
|
||||
@Unique private float rivencraft$sunSize = 1F;
|
||||
@Unique private int rivencraft$celestialTint = -1;
|
||||
@Unique private int rivencraft$bodyMode;
|
||||
@Unique private int rivencraft$bodyOverlay;
|
||||
public int rivencraft$bodyOverlay() { return rivencraft$bodyOverlay; }
|
||||
public void rivencraft$bodyOverlay(int mode) { rivencraft$bodyOverlay = mode; }
|
||||
@Unique private java.util.List<StaticSkyState.Body> rivencraft$bodies = java.util.List.of();
|
||||
public java.util.List<StaticSkyState.Body> rivencraft$bodies() { return rivencraft$bodies; }
|
||||
public void rivencraft$bodies(java.util.List<StaticSkyState.Body> bodies) { rivencraft$bodies = java.util.List.copyOf(bodies); }
|
||||
public long rivencraft$staticFrame() { return rivencraft$frame; }
|
||||
public void rivencraft$staticFrame(long frame) { rivencraft$frame = frame; }
|
||||
public int rivencraft$skyTint() { return rivencraft$tint; }
|
||||
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; }
|
||||
public int rivencraft$celestialTint() { return rivencraft$celestialTint; }
|
||||
public void rivencraft$celestialTint(int color) { rivencraft$celestialTint = color; }
|
||||
public int rivencraft$bodyMode() { return rivencraft$bodyMode; }
|
||||
public void rivencraft$bodyMode(int mode) { rivencraft$bodyMode = mode; }
|
||||
|
||||
@Inject(method = "reset", at = @At("TAIL"))
|
||||
private void rivencraft$reset(CallbackInfo ci) {
|
||||
rivencraft$frame = Long.MIN_VALUE;
|
||||
rivencraft$tint = -1;
|
||||
rivencraft$bearing = -1;
|
||||
rivencraft$sunSize = 1F;
|
||||
rivencraft$celestialTint = -1;
|
||||
rivencraft$bodyMode = 0;
|
||||
rivencraft$bodyOverlay = 0;
|
||||
rivencraft$bodies = java.util.List.of();
|
||||
}
|
||||
}
|
||||
@@ -108,9 +108,10 @@ public final class PortBookNetworking {
|
||||
|
||||
public static Identifier createAge(ServerPlayer player, String title, List<Identifier> pages) {
|
||||
if (title == null || title.isBlank() || title.length() > MAX_BOOK_TITLE_LENGTH) return null;
|
||||
long seed = player.getRandom().nextLong();
|
||||
net.dnorberg.rivencraft.age.PortBiomePlan biomePlan;
|
||||
try {
|
||||
biomePlan = PortAuthoredWriting.biomePlan(pages, player.registryAccess());
|
||||
biomePlan = PortAuthoredWriting.biomePlan(pages, player.level().getServer(), seed);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age writing unavailable: " + exception.getMessage()));
|
||||
return null;
|
||||
@@ -127,20 +128,25 @@ public final class PortBookNetworking {
|
||||
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||
}
|
||||
try {
|
||||
long seed = player.getRandom().nextLong();
|
||||
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
|
||||
if (pages.isEmpty()) {
|
||||
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
|
||||
terrain = random.terrain();
|
||||
biomePlan = random.biomes();
|
||||
}
|
||||
var environment = net.dnorberg.rivencraft.age.PortEnvironmentSelection.resolve(pages, seed, terrain);
|
||||
var randomFeatures = pages.isEmpty()
|
||||
? net.dnorberg.rivencraft.age.PortRandomFeatureSelection.select(server, seed, terrain, biomePlan) : null;
|
||||
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
||||
PortAuthoredWriting.rockPlan(pages, seed),
|
||||
randomFeatures == null ? PortAuthoredWriting.rockPlan(pages, seed) : randomFeatures.rocks(),
|
||||
PortAuthoredWriting.vegetationPlan(pages),
|
||||
PortAuthoredWriting.dungeonCount(pages),
|
||||
PortAuthoredWriting.lakePlan(pages, seed),
|
||||
PortAuthoredWriting.terrainFeaturePlan(pages, seed),
|
||||
net.dnorberg.rivencraft.age.PortWeatherMode.fromPages(pages));
|
||||
randomFeatures == null ? PortAuthoredWriting.dungeonCount(pages) : randomFeatures.dungeons(),
|
||||
randomFeatures == null ? PortAuthoredWriting.lakePlan(pages, seed) : randomFeatures.lakes(),
|
||||
randomFeatures == null ? PortAuthoredWriting.terrainFeaturePlan(pages, seed) : randomFeatures.terrain(),
|
||||
environment.weather(), environment.lighting(), environment.sky(),
|
||||
net.dnorberg.rivencraft.age.PortAgeColors.resolve(pages, seed),
|
||||
net.dnorberg.rivencraft.age.PortDayCycle.resolve(pages, seed,
|
||||
net.dnorberg.rivencraft.age.PortDayCycle.nativeTicks(server.overworld())));
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
||||
return null;
|
||||
|
||||
+70
-10
@@ -15,6 +15,8 @@ import net.dnorberg.rivencraft.age.PortTerrainFeaturePlan;
|
||||
import net.dnorberg.rivencraft.age.PortStructurePages;
|
||||
import net.dnorberg.rivencraft.age.PortCarverPages;
|
||||
import net.dnorberg.rivencraft.age.PortWeatherMode;
|
||||
import net.dnorberg.rivencraft.age.PortLighting;
|
||||
import net.dnorberg.rivencraft.age.PortSkyStyle;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
@@ -53,7 +55,10 @@ public final class PortAuthoredWriting {
|
||||
if (page == null || !page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
|
||||
var entry = PortSymbolPages.find(page);
|
||||
if (entry == null) return false;
|
||||
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null) return true;
|
||||
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|
||||
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) return true;
|
||||
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null
|
||||
|| PortLighting.forPage(page) != null || PortSkyStyle.forPage(page) != null) return true;
|
||||
if (entry.category().equals("rivencraft:biome") && PortRegistryPages.biomeId(page) != null) return true;
|
||||
if (isBlockPage(page) && (entry.category().equals("rivencraft:block")
|
||||
|| entry.category().equals("rivencraft:fluid"))) return true;
|
||||
@@ -67,6 +72,14 @@ public final class PortAuthoredWriting {
|
||||
|
||||
/** A page never disappears into an Age unless this port can apply its effect. */
|
||||
public static boolean supports(List<Identifier> pages) {
|
||||
try {
|
||||
return supportsExplicit(PortMaterialCompletion.complete(pages, 0));
|
||||
} catch (IllegalArgumentException exception) { return false; }
|
||||
}
|
||||
|
||||
private static boolean supportsExplicit(List<Identifier> pages) {
|
||||
if (!net.dnorberg.rivencraft.age.PortAgeColors.validNumbers(pages)) return false;
|
||||
if (!net.dnorberg.rivencraft.age.PortDayCycle.validWriting(pages)) return false;
|
||||
boolean terrain = false;
|
||||
int biomes = 0;
|
||||
int controllers = 0;
|
||||
@@ -79,6 +92,8 @@ public final class PortAuthoredWriting {
|
||||
int denseOres = 0;
|
||||
int starFissures = 0;
|
||||
int weather = 0;
|
||||
int lighting = 0;
|
||||
int sky = 0;
|
||||
Map<Identifier, Integer> terrainFeatures = new HashMap<>();
|
||||
Map<Identifier, Integer> structures = new HashMap<>();
|
||||
Map<Identifier, Integer> carvers = new HashMap<>();
|
||||
@@ -86,12 +101,19 @@ 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 colors target Sky, Ambient Light Color or Normal Time; trailing colors tint the sky.
|
||||
} else if (isTerrain(page) && !terrain) {
|
||||
terrain = true;
|
||||
} else if (PortSkyStyle.forPage(page) != null && sky++ == 0) {
|
||||
// Sky presentation, independent of terrain and illumination.
|
||||
} else if (PortLighting.forPage(page) != null && lighting++ == 0) {
|
||||
// A single lighting controller; omitted choices resolve from the Age seed.
|
||||
} else if (PortWeatherMode.forPage(page) != null && weather++ == 0) {
|
||||
// A single independent weather policy for this Age.
|
||||
} else if (controller(page) != null && controllers++ == 0) {
|
||||
// An explicit controller needs an authored pool.
|
||||
// Missing biome slots are seed-completed when the Age is created.
|
||||
} else if (isBlockPage(page) && pendingMaterial == null
|
||||
&& PortSymbolPages.find(page) != null
|
||||
&& (PortSymbolPages.find(page).category().equals("rivencraft:block")
|
||||
@@ -136,7 +158,8 @@ public final class PortAuthoredWriting {
|
||||
}
|
||||
if (pendingMaterial != null || (rocks > 0 || pages.stream().anyMatch(PortVegetationPlan::requiresSurface))
|
||||
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||
if ((dungeons > 0 || lakes > 0) && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN)
|
||||
if (dungeons > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||
if (lakes > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN)
|
||||
|| pages.contains(CAVE_TERRAIN))) return false;
|
||||
if (denseOres > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||
if (!carvers.isEmpty() && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||
@@ -153,8 +176,8 @@ public final class PortAuthoredWriting {
|
||||
.filter(value -> value != null).findFirst().orElseThrow();
|
||||
boolean valid = switch (distribution) {
|
||||
case NATIVE -> biomes == 0;
|
||||
case SINGLE -> biomes == 1;
|
||||
default -> biomes >= 2;
|
||||
case SINGLE -> biomes <= 1;
|
||||
default -> true;
|
||||
};
|
||||
return valid;
|
||||
}
|
||||
@@ -198,13 +221,14 @@ public final class PortAuthoredWriting {
|
||||
}
|
||||
|
||||
public static PortRockPlan rockPlan(List<Identifier> pages, long seed) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
pages = completedMaterials(pages, seed);
|
||||
List<Identifier> materials = solidMaterials(pages, seed, ROCKS);
|
||||
return materials.isEmpty() ? null : new PortRockPlan(materials);
|
||||
}
|
||||
|
||||
public static PortTerrainFeaturePlan terrainFeaturePlan(List<Identifier> pages, long seed) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
pages = completedMaterials(pages, seed);
|
||||
final List<Identifier> completed = pages;
|
||||
Map<Identifier, List<Identifier>> materials = new HashMap<>();
|
||||
Map<Identifier, Integer> occurrences = new HashMap<>();
|
||||
List<PortTerrainFeaturePlan.Choice> choices = new ArrayList<>();
|
||||
@@ -214,7 +238,7 @@ public final class PortAuthoredWriting {
|
||||
choices.add(new PortTerrainFeaturePlan.Choice(page, PortTerrainFeaturePlan.CRYSTAL_MATERIAL));
|
||||
continue;
|
||||
}
|
||||
List<Identifier> selected = materials.computeIfAbsent(page, feature -> solidMaterials(pages, seed, feature));
|
||||
List<Identifier> selected = materials.computeIfAbsent(page, feature -> solidMaterials(completed, seed, feature));
|
||||
int occurrence = occurrences.merge(page, 1, Integer::sum) - 1;
|
||||
choices.add(new PortTerrainFeaturePlan.Choice(page, selected.get(occurrence)));
|
||||
}
|
||||
@@ -246,7 +270,7 @@ public final class PortAuthoredWriting {
|
||||
}
|
||||
|
||||
public static PortLakePlan lakePlan(List<Identifier> pages, long seed) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
pages = completedMaterials(pages, seed);
|
||||
if (pages.stream().noneMatch(PortLakePlan::isLake)) return null;
|
||||
List<Identifier> fluidPool = PortRegistryPages.fluidMaterialIds();
|
||||
if (fluidPool.isEmpty()) throw new IllegalStateException("No source-fluid materials are registered");
|
||||
@@ -272,6 +296,12 @@ public final class PortAuthoredWriting {
|
||||
return new PortLakePlan(choices);
|
||||
}
|
||||
|
||||
private static List<Identifier> completedMaterials(List<Identifier> pages, long seed) {
|
||||
var completed = PortMaterialCompletion.complete(pages, seed);
|
||||
if (!supportsExplicit(completed)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
return completed;
|
||||
}
|
||||
|
||||
public static PortVegetationPlan vegetationPlan(List<Identifier> pages) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
List<Identifier> vegetation = pages.stream().filter(PortVegetationPlan::isVegetation).toList();
|
||||
@@ -291,6 +321,17 @@ public final class PortAuthoredWriting {
|
||||
|
||||
/** Resolves all authored biomes from the active registry before creating a level. */
|
||||
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||
return biomePlan(pages, registries, 0, null);
|
||||
}
|
||||
|
||||
/** Creation-only completion; the saved plan never rerolls on load or reseed. */
|
||||
public static PortBiomePlan biomePlan(List<Identifier> pages, net.minecraft.server.MinecraftServer server, long seed) {
|
||||
return biomePlan(pages, server.registryAccess(), seed,
|
||||
net.dnorberg.rivencraft.age.PortRandomAgeSelection.eligibleBiomes(server, terrain(pages)));
|
||||
}
|
||||
|
||||
private static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries,
|
||||
long seed, List<Identifier> available) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException(
|
||||
"Unsupported terrain or biome-page combination in this port slice");
|
||||
List<Identifier> biomes = new ArrayList<>();
|
||||
@@ -325,6 +366,25 @@ public final class PortAuthoredWriting {
|
||||
throw new IllegalArgumentException("Dense Ores needs at least one biome with registered ore placements");
|
||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
return new PortBiomePlan(distribution, List.of());
|
||||
if (distribution != null && available != null) {
|
||||
int minimum = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||
int target = minimum;
|
||||
if (biomes.isEmpty() && distribution != PortBiomePlan.Distribution.SINGLE) {
|
||||
var random = new Random(seed ^ 0x42494F4D45434F55L);
|
||||
while (target < available.size() && random.nextInt(3) != 0) target++;
|
||||
}
|
||||
var distinct = new java.util.HashSet<>(biomes);
|
||||
// Void may retain authored weather/season biomes, but must not gain
|
||||
// an unrequested The Void biome just to fill a multi-biome controller.
|
||||
var filler = new ArrayList<>(pages.contains(VOID_TERRAIN) && !biomes.isEmpty() ? List.<Identifier>of() : available);
|
||||
Collections.shuffle(filler, new Random(seed ^ 0x42494F4D45504F4FL));
|
||||
for (Identifier biome : filler) {
|
||||
if (distinct.size() >= target) break;
|
||||
if (distinct.add(biome)) biomes.add(biome);
|
||||
}
|
||||
if (distinct.size() < minimum) throw new IllegalArgumentException(
|
||||
distribution + " requires " + minimum + " distinct biomes; supply more biome pages for this terrain");
|
||||
}
|
||||
if (biomes.isEmpty()) return null;
|
||||
if (distribution == null) distribution = biomes.size() == 1
|
||||
? PortBiomePlan.Distribution.SINGLE : biomes.size() == 2
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.age.PortCarverPages;
|
||||
import net.dnorberg.rivencraft.age.PortLakePlan;
|
||||
import net.dnorberg.rivencraft.age.PortStructurePages;
|
||||
import net.dnorberg.rivencraft.age.PortTerrainFeaturePlan;
|
||||
import net.dnorberg.rivencraft.age.PortVegetationPlan;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Creation-time implied features, adapted from 1.21.1 AgeFeatureSelection. */
|
||||
public final class PortMaterialCompletion {
|
||||
private PortMaterialCompletion() {}
|
||||
|
||||
public static List<Identifier> complete(List<Identifier> pages, long seed) {
|
||||
var result = new ArrayList<Identifier>();
|
||||
for (int index = 0; index < pages.size(); index++) {
|
||||
Identifier page = pages.get(index);
|
||||
result.add(page);
|
||||
var entry = PortSymbolPages.find(page);
|
||||
if (entry == null || !(entry.category().equals("rivencraft:block")
|
||||
|| entry.category().equals("rivencraft:fluid"))) continue;
|
||||
if (index + 1 < pages.size() && isFeature(pages.get(index + 1))) continue;
|
||||
Identifier material = PortRegistryPages.blockId(page);
|
||||
var candidates = candidates(pages, entry.category().equals("rivencraft:fluid"));
|
||||
if (candidates.isEmpty()) throw new IllegalArgumentException(
|
||||
"No compatible feature for material " + material + " on this terrain");
|
||||
long mixed = mix(seed ^ (long) material.hashCode() * 0x9E3779B97F4A7C15L ^ index);
|
||||
result.add(candidates.get(Math.floorMod(mixed, candidates.size())));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private static List<Identifier> candidates(List<Identifier> pages, boolean fluid) {
|
||||
boolean cave = pages.contains(id("terrain/cave"));
|
||||
boolean empty = pages.contains(id("terrain/void"));
|
||||
boolean flat = pages.contains(id("terrain/flat"));
|
||||
if (fluid) return cave || empty || flat ? List.of()
|
||||
: List.of(id("feature/surface_lakes"), id("feature/deep_lakes"), id("feature/lava_lakes"));
|
||||
var choices = new ArrayList<Identifier>();
|
||||
if (!cave && !empty && !flat) choices.add(id("feature/rocks"));
|
||||
if (!cave) choices.add(PortTerrainFeaturePlan.FLOATING_ISLANDS);
|
||||
choices.add(PortTerrainFeaturePlan.TENDRILS);
|
||||
choices.add(PortTerrainFeaturePlan.SPHERES);
|
||||
if (!cave && !empty) {
|
||||
choices.add(PortTerrainFeaturePlan.SPIKES);
|
||||
choices.add(PortTerrainFeaturePlan.OBELISKS);
|
||||
}
|
||||
return choices;
|
||||
}
|
||||
|
||||
private static boolean isFeature(Identifier page) {
|
||||
return PortTerrainFeaturePlan.isSupported(page) || PortLakePlan.isLake(page)
|
||||
|| PortVegetationPlan.isVegetation(page) || PortStructurePages.supports(page)
|
||||
|| PortCarverPages.supports(page) || page.equals(id("feature/rocks"))
|
||||
|| page.equals(id("feature/dungeons")) || page.equals(PortTerrainFeaturePlan.SKYLANDS)
|
||||
|| page.equals(PortTerrainFeaturePlan.DENSE_ORES) || page.equals(PortTerrainFeaturePlan.STAR_FISSURES);
|
||||
}
|
||||
|
||||
private static Identifier id(String path) { return Identifier.fromNamespaceAndPath("rivencraft", path); }
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"schema_version":2,"name":"symbol.rivencraft.lighting.ambient_color","description":"symbol.rivencraft.lighting.ambient_color.description","category":"rivencraft:lighting","poem":"Ethereal Red Sustain Balance"}
|
||||
@@ -3,6 +3,7 @@
|
||||
"minVersion": "0.8",
|
||||
"package": "net.dnorberg.rivencraft.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin"],
|
||||
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin", "AgeAmbientLightMixin", "AgeColorAttributesMixin", "AgeDayTimelineMixin"],
|
||||
"client": ["StaticSkyStateMixin", "StaticSkyRendererMixin"],
|
||||
"injectors": {"defaultRequire": 1}
|
||||
}
|
||||
Reference in new issue
Block a user