Port Age lighting and animated sky styles to 26.3 beta
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-11-02 15:49:58 -05:00
1 parent 48fbae9be9
commit a7a73d3c17
23 files changed
+967 -24

No files matched your search

+3 -2
View File
@@ -14,10 +14,11 @@ The gameplay guide below describes the **Minecraft 1.21.1 release**. A separate,
incomplete **Minecraft 26.3 beta** is available: Java 25, NeoForge 26.3.0.99,
and a first-party Age backend (no Dynamic Dimensions or Bad Packets). It now
supports authored terrain/biomes, structure and vegetation pages, independent
Age weather, and a growing set of generation features,
Age weather/lighting, Overworld/Nether/End and animated Static skies,
and a growing set of generation features,
but does not yet provide full 1.21.1 gameplay or optional-integration parity.
See the [26.3 beta guide](ports/mc263/README.md) and
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.65-beta.1.md).
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.71-beta.1.md).
Keep the builds and their saves separate, and back up worlds before updating.
## Getting started
+165
View File
@@ -900,6 +900,171 @@ 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.
## Verification of this first slice
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
+141 -1
View File
@@ -5,7 +5,7 @@ with the Minecraft 1.21.1 release. The 26.3 backend persists separate Ages and
their seeds. Books, the Book Binder, and the Ink Mixer work, and the Binder can
author Standard, Amplified, Island, Flat, Void, or Cave terrain with supported
biome-distribution pages, selected material/vegetation/structure features, and
independent Age weather. Celestial/lighting systems, instability, crystal
independent Age weather and lighting. Celestial systems, instability, crystal
portals, and much of the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not
substitute this JAR for the 1.21.1 mod or import 1.21.1 saves.
@@ -558,6 +558,146 @@ 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.
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).
+65
View File
@@ -121,6 +121,71 @@ def generatePortLanguage = tasks.register('generatePortLanguage', GeneratePortLa
}
sourceSets.main.resources.srcDir generatePortLanguage
// Derive registered presentation variants without duplicating vanilla gameplay settings.
abstract class GeneratePortDimensionTypes extends DefaultTask {
@InputFile abstract RegularFileProperty getMinecraftJar()
@OutputDirectory abstract DirectoryProperty getOutputDir()
@TaskAction void generate() {
new java.util.zip.ZipFile(minecraftJar.get().asFile).withCloseable { archive ->
def entry = archive.getEntry('data/minecraft/dimension_type/overworld.json')
if (entry == null) throw new GradleException('Missing vanilla Overworld dimension type')
String template = archive.getInputStream(entry).withCloseable { it.getText('UTF-8') }
def dayEntry = archive.getEntry('data/minecraft/timeline/day.json')
if (dayEntry == null) throw new GradleException('Missing vanilla day timeline')
def day = archive.getInputStream(dayEntry).withCloseable {
new groovy.json.JsonSlurper().parseText(it.getText('UTF-8'))
}
// Keep gameplay schedules and illumination, but do not tint these
// native non-Overworld sky backgrounds blue/black with the day cycle.
['fog_color', 'sky_color', 'cloud_color'].each { day.tracks.remove("minecraft:visual/${it}".toString()) }
// Markers are global per clock and already owned by minecraft:day.
day.remove('time_markers')
File dayFile = new File(outputDir.get().asFile, 'data/rivencraft/timeline/age_sky_day.json')
dayFile.parentFile.mkdirs()
dayFile.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(day)) + '\n'
File timelineTag = new File(outputDir.get().asFile, 'data/rivencraft/tags/timeline/age_sky.json')
timelineTag.parentFile.mkdirs()
timelineTag.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson([
values: ['#minecraft:universal', 'rivencraft:age_sky_day',
'minecraft:moon', 'minecraft:early_game']])) + '\n'
['overworld', 'nether', 'end', 'static'].each { sky ->
['age_normal', 'age_dark', 'age_bright', 'age_cave_bright'].each { lighting ->
if (sky == 'overworld' && lighting == 'age_normal') return
String name = lighting + (sky == 'overworld' ? '' : "_sky_${sky}")
def definition = new groovy.json.JsonSlurper().parseText(template)
definition.has_skylight = lighting in ['age_normal', 'age_bright']
definition.ambient_light = lighting in ['age_bright', 'age_cave_bright'] ? 1.0 : 0.0
// 26.3's lightmap reads this attribute, not ambient_light.
if (definition.ambient_light == 1.0)
definition.attributes['minecraft:visual/ambient_light_color'] = '#ffffff'
if (sky != 'overworld') {
definition.skybox = sky == 'end' ? 'end' : 'none'
definition.attributes['minecraft:visual/fog_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#181318' : '#6b2725'
definition.attributes['minecraft:visual/sky_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#000000' : '#6b2725'
definition.attributes['minecraft:visual/cloud_color'] = '#00000000'
if (sky == 'nether') {
definition.attributes['minecraft:visual/fog_start_distance'] = 10.0
definition.attributes['minecraft:visual/fog_end_distance'] = 96.0
}
definition.timelines = '#rivencraft:age_sky'
}
File result = new File(outputDir.get().asFile, "data/rivencraft/dimension_type/${name}.json")
result.parentFile.mkdirs()
result.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(definition)) + '\n'
}
}
}
}
}
def generatePortDimensionTypes = tasks.register('generatePortDimensionTypes', GeneratePortDimensionTypes) {
dependsOn 'createMinecraftArtifacts'
minecraftJar.set(layout.buildDirectory.file("moddev/artifacts/minecraft-patched-${neo_version}.jar"))
outputDir.set(layout.buildDirectory.dir('generated/resources/dimensionTypes'))
}
sourceSets.main.resources.srcDir generatePortDimensionTypes
tasks.named('processResources', ProcessResources).configure {
// Reuse the existing sprite and base model without copying binary assets.
from('../../src/main/resources') {
+1 -1
View File
@@ -7,4 +7,4 @@ neo_version=26.3.0.99
mod_id=rivencraft
mod_name=Rivencraft
mod_license=LGPL-3.0-or-later
mod_version=1.0.1.65-beta.1
mod_version=1.0.1.71-beta.1
+7
View File
@@ -8,6 +8,13 @@
"item.rivencraft.page.unresolved": "Unknown Symbol Page",
"item.rivencraft.page.symbol.implemented": "Implemented in 26.3; book and terrain requirements still apply.",
"symbol.rivencraft.weather.normal.description": "Use an independent normal weather cycle. Cave terrain keeps weather off.",
"symbol.rivencraft.sky.overworld.description": "Use the native Overworld sky. Terrain and lighting remain independently authored.",
"symbol.rivencraft.sky.nether.description": "Use a Nether-style sky and reddish fog, without Nether terrain or gameplay rules.",
"symbol.rivencraft.sky.end.description": "Use the native End sky backdrop, without End terrain or a dragon fight.",
"symbol.rivencraft.sky.static.description": "An animated gray static sky surrounds the Age.",
"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.",
@@ -34,7 +34,11 @@ 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)
).apply(instance, AgeLevelIndex::new));
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
@@ -53,6 +57,8 @@ 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 AgeLevelIndex() {
this.ages = new LinkedHashMap<>();
@@ -65,6 +71,8 @@ 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<>();
}
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
@@ -75,7 +83,9 @@ 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) {
this.ages = new LinkedHashMap<>(ages);
this.retired = new LinkedHashSet<>(retired);
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
@@ -86,6 +96,8 @@ 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);
}
Map<Identifier, Long> entries() {
@@ -116,10 +128,12 @@ 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); }
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) {
if (contains(id)) {
throw new IllegalArgumentException("Age already exists: " + id);
}
@@ -131,6 +145,8 @@ 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);
setDirty();
}
@@ -144,6 +160,8 @@ final class AgeLevelIndex extends SavedData {
dungeons.remove(id);
lakes.remove(id);
terrainFeatures.remove(id);
lighting.remove(id);
sky.remove(id);
retired.add(id);
setDirty();
}
@@ -151,7 +169,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) {
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 +180,8 @@ 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);
setDirty();
}
@@ -68,6 +68,32 @@ public final class PortAgeCommands {
return 1;
})))
.then(Commands.literal("age")
.then(Commands.literal("create_sky")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("sky", StringArgumentType.word())
.then(Commands.argument("lighting", StringArgumentType.word())
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::createSkyAge)))))))
.then(Commands.literal("sky_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeSky)))
.then(Commands.literal("environment_probe")
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::probeEnvironment))))
.then(Commands.literal("create_lighting")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("mode", StringArgumentType.word())
.suggests((context, builder) -> net.minecraft.commands.SharedSuggestionProvider.suggest(
java.util.Arrays.stream(PortLighting.values())
.map(mode -> mode.name().toLowerCase(Locale.ROOT)), builder))
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::createLightingAge))))))
.then(Commands.literal("lighting_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeLighting)))
.then(Commands.literal("surface_probe")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("x", IntegerArgumentType.integer())
@@ -137,10 +163,13 @@ public final class PortAgeCommands {
long seed = LongArgumentType.getLong(context, "seed");
PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select(
context.getSource().getServer(), seed);
int created = createAge(context, selected.terrain(), selected.biomes());
var environment = PortEnvironmentSelection.resolve(java.util.List.of(), seed, selected.terrain());
int created = createAge(context, selected.terrain(), selected.biomes(),
null, null, 0, null, null, environment);
if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
"Random Age plan: terrain=" + selected.terrain()
+ " biomes=" + selected.biomes()), false);
+ " biomes=" + selected.biomes()
+ " environment=" + environment), false);
return created;
}))))
.then(Commands.literal("create_rocks")
@@ -318,6 +347,115 @@ public final class PortAgeCommands {
.executes(PortAgeCommands::sampleBiome)))))));
}
private static int createSkyAge(CommandContext<CommandSourceStack> context) {
try {
PortSkyStyle sky = PortSkyStyle.valueOf(StringArgumentType.getString(context, "sky").toUpperCase(Locale.ROOT));
PortLighting lighting = PortLighting.valueOf(StringArgumentType.getString(context, "lighting").toUpperCase(Locale.ROOT));
PortAgeTerrain terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var pages = java.util.List.of(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "sky/" + sky.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/" + lighting.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "weather/off"));
var server = context.getSource().getServer();
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
RuntimeAgeBackend.create(server, ageId(context), LongArgumentType.getLong(context, "seed"),
biomes, terrain, null, null, 0, null, null, PortWeatherMode.OFF, lighting, sky);
return probeSky(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeSky(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
var server = context.getSource().getServer();
var level = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
return 0;
}
var type = level.dimensionType();
context.getSource().sendSuccess(() -> Component.literal("Sky probe " + id
+ " type=" + level.dimensionTypeRegistration().unwrapKey().orElseThrow().identifier()
+ " skybox=" + type.skybox() + " skylight=" + type.hasSkyLight() + " ambient=" + type.ambientLight()
+ " fog=" + level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.FOG_COLOR,
new net.minecraft.core.BlockPos(0, 80, 0))
+ " ceiling=" + type.hasCeiling() + " scale=" + type.coordinateScale()
+ " minY=" + type.minY() + " height=" + type.height() + " dragon=" + type.hasEnderDragonFight()), false);
return 1;
}
private static int probeEnvironment(CommandContext<CommandSourceStack> context) {
try {
long seed = LongArgumentType.getLong(context, "seed");
PortAgeTerrain terrain = PortAgeTerrain.valueOf(
StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var selected = PortEnvironmentSelection.resolve(java.util.List.of(), seed, terrain);
var explicitPages = java.util.List.of(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "weather/storm"),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/bright"));
var explicit = PortEnvironmentSelection.resolve(explicitPages, seed, terrain);
var weatherOnly = PortEnvironmentSelection.resolve(java.util.List.of(explicitPages.get(0)), seed, terrain);
var lightingOnly = PortEnvironmentSelection.resolve(java.util.List.of(explicitPages.get(1)), seed, terrain);
if (!selected.equals(PortEnvironmentSelection.resolve(java.util.List.of(), seed, terrain))
|| explicit.lighting() != PortLighting.BRIGHT
|| explicit.weather() != (terrain == PortAgeTerrain.CAVE ? PortWeatherMode.OFF : PortWeatherMode.STORM)
|| weatherOnly.lighting() != selected.lighting() || lightingOnly.weather() != selected.weather())
throw new IllegalStateException("Environment selection invariant failed");
context.getSource().sendSuccess(() -> Component.literal("Environment probe seed=" + seed
+ " terrain=" + terrain + " selected=" + selected + " explicit=" + explicit
+ " deterministic=true"), false);
return 1;
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int createLightingAge(CommandContext<CommandSourceStack> context) {
try {
PortLighting mode = PortLighting.valueOf(
StringArgumentType.getString(context, "mode").toUpperCase(Locale.ROOT));
PortAgeTerrain terrain = PortAgeTerrain.valueOf(
StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var pages = java.util.List.of(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/" + mode.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
var server = context.getSource().getServer();
var biomePlan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
RuntimeAgeBackend.create(server, ageId(context), LongArgumentType.getLong(context, "seed"),
biomePlan, terrain, null, null, 0, null, null, PortWeatherMode.NORMAL, mode);
return probeLighting(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeLighting(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
var server = context.getSource().getServer();
ServerLevel level = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
return 0;
}
context.getSource().sendSuccess(() -> Component.literal("Lighting probe " + id
+ " type=" + level.dimensionTypeRegistration().unwrapKey().orElseThrow().identifier()
+ " skylight=" + level.dimensionType().hasSkyLight()
+ " ambient=" + level.dimensionType().ambientLight()
+ " undergroundBrightness=" + level.getMaxLocalRawBrightness(
new net.minecraft.core.BlockPos(0, level.getMinY() + 2, 0))
+ " vanillaDarkEnough=" + net.minecraft.world.entity.monster.Monster.isDarkEnoughToSpawn(
level, new net.minecraft.core.BlockPos(0, level.getMinY() + 2, 0),
net.minecraft.util.RandomSource.create(0))
+ " overworldSkylight=" + server.overworld().dimensionType().hasSkyLight()
+ " overworldAmbient=" + server.overworld().dimensionType().ambientLight()), false);
return 1;
}
private static int probeSurface(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
@@ -602,13 +740,23 @@ public final class PortAgeCommands {
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures) {
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
new PortEnvironmentSelection(PortWeatherMode.NORMAL, PortLighting.NORMAL));
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures,
PortEnvironmentSelection environment) {
String name = StringArgumentType.getString(context, "name");
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
long seed = LongArgumentType.getLong(context, "seed");
ServerLevel level;
try {
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures);
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
environment.weather(), environment.lighting());
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 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) {
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);
}
private static <T> T rankedDefault(Random random, T[] choices) {
// LegacyRankChoice: the first (Normal) option is rank 1, the rest rank 2.
// Its rank-1 weight is the complete rare group plus one. Derive this
// from the supported pool, rather than selecting unported weather.
int[] weights = new int[choices.length];
Arrays.fill(weights, 1);
weights[0] = choices.length;
return PortRandomAgeSelection.weighted(random, List.of(choices), weights);
}
}
@@ -0,0 +1,52 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import java.util.Locale;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.Level;
/** Authored lighting choices using statically registered, client-synchronized types. */
public enum PortLighting {
NORMAL, DARK, BRIGHT;
public static final Codec<PortLighting> CODEC = Codec.STRING.xmap(
name -> valueOf(name.toUpperCase(Locale.ROOT)), mode -> mode.name().toLowerCase(Locale.ROOT));
public static PortLighting forPage(Identifier page) {
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
return switch (page.getPath()) {
case "lighting/normal" -> NORMAL;
case "lighting/dark" -> DARK;
case "lighting/bright" -> BRIGHT;
default -> null;
};
}
public static boolean isBright(Level level) {
return level.dimensionTypeRegistration().unwrapKey().map(key -> {
Identifier id = key.identifier();
return id.getNamespace().equals(Rivencraft.MOD_ID)
&& (id.getPath().equals("age_bright") || id.getPath().equals("age_cave_bright")
|| id.getPath().startsWith("age_bright_sky_")
|| id.getPath().startsWith("age_cave_bright_sky_"));
}).orElse(false);
}
Holder<DimensionType> dimensionType(MinecraftServer server, PortAgeTerrain terrain,
Holder<DimensionType> overworldType, PortSkyStyle sky) {
boolean cave = terrain == PortAgeTerrain.CAVE;
if (this == NORMAL && !cave && sky == PortSkyStyle.OVERWORLD) return overworldType;
String variant = this == BRIGHT ? (cave ? "age_cave_bright" : "age_bright")
: this == DARK || cave ? "age_dark" : "age_normal";
if (sky != PortSkyStyle.OVERWORLD) variant += "_sky_" + sky.name().toLowerCase(Locale.ROOT);
return server.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(
ResourceKey.create(Registries.DIMENSION_TYPE,
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, variant)));
}
}
@@ -68,7 +68,7 @@ public final class PortRandomAgeSelection {
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
}
private static <T> T weighted(Random random, List<T> choices, int[] weights) {
static <T> T weighted(Random random, List<T> choices, int[] weights) {
int total = 0;
for (int weight : weights) total += weight;
int ticket = random.nextInt(total);
@@ -0,0 +1,31 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import java.util.List;
import java.util.Locale;
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 fromPages(List<Identifier> pages) {
return pages.stream().map(PortSkyStyle::forPage).filter(java.util.Objects::nonNull)
.findFirst().orElse(OVERWORLD);
}
}
@@ -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,32 @@ 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) {
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);
((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);
// 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 +213,8 @@ 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);
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 +232,7 @@ public final class RuntimeAgeBackend {
} catch (IOException | RuntimeException exception) {
try {
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures);
terrainFeatures, lighting, sky);
server.overworld().getDataStorage().saveAndJoin();
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
@@ -337,6 +358,14 @@ 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));
}
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) {
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
@@ -409,7 +438,8 @@ public final class RuntimeAgeBackend {
}
if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
LevelStem stem = new LevelStem(lighting.dimensionType(server, terrain, overworldStem.type(), sky),
generator, OptionalLong.of(seed));
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
ServerLevel level = new PortAgeLevel(server, key, stem, biomeZoomSeed, terrain);
@@ -0,0 +1,87 @@
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;
public void render(RenderTarget target, GpuBufferSlice fog, long frame) {
if (uploadedFrame != frame) {
RandomSource random = RandomSource.create(frame);
var image = texture.getPixels();
for (int face = 0; face < 6; face++) {
for (int y = 0; y < GRID; y++) {
for (int x = 0; x < GRID; x++) {
int gray = 146 * (65 + random.nextInt(160)) / 255;
image.setPixel(face * GRID + x, y, 0xff000000 | gray << 16 | gray << 8 | gray);
}
}
}
texture.upload();
uploadedFrame = frame;
}
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,7 @@
package net.dnorberg.rivencraft.client;
/** Carries the extracted animation frame without consulting the live world during drawing. */
public interface StaticSkyState {
long rivencraft$staticFrame();
void rivencraft$staticFrame(long frame);
}
@@ -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,53 @@
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.callback.CallbackInfo;
@Mixin(SkyRenderer.class)
public class StaticSkyRendererMixin {
@Shadow @Final private RenderTarget renderTarget;
@Unique private PortStaticSky rivencraft$staticSky;
@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);
// Only the extracted presentation uses END to schedule the sky pass. The actual
// dimension has NONE, so it never acquires End flashes or End gameplay behavior.
if (enabled) state.skybox = DimensionType.Skybox.END;
}
@Inject(method = "render", at = @At("HEAD"), cancellable = true)
private void rivencraft$render(GpuBufferSlice fog, SkyRenderState state, CallbackInfo ci) {
long frame = ((StaticSkyState) state).rivencraft$staticFrame();
if (frame == Long.MIN_VALUE) return;
if (rivencraft$staticSky == null) rivencraft$staticSky = new PortStaticSky();
rivencraft$staticSky.render(renderTarget, fog, frame);
ci.cancel();
}
@Inject(method = "close", at = @At("HEAD"))
private void rivencraft$close(CallbackInfo ci) {
if (rivencraft$staticSky != null) {
rivencraft$staticSky.close();
rivencraft$staticSky = null;
}
}
}
@@ -0,0 +1,19 @@
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;
public long rivencraft$staticFrame() { return rivencraft$frame; }
public void rivencraft$staticFrame(long frame) { rivencraft$frame = frame; }
@Inject(method = "reset", at = @At("TAIL"))
private void rivencraft$reset(CallbackInfo ci) { rivencraft$frame = Long.MIN_VALUE; }
}
@@ -134,13 +134,15 @@ public final class PortBookNetworking {
terrain = random.terrain();
biomePlan = random.biomes();
}
var environment = net.dnorberg.rivencraft.age.PortEnvironmentSelection.resolve(pages, seed, terrain);
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
PortAuthoredWriting.rockPlan(pages, seed),
PortAuthoredWriting.vegetationPlan(pages),
PortAuthoredWriting.dungeonCount(pages),
PortAuthoredWriting.lakePlan(pages, seed),
PortAuthoredWriting.terrainFeaturePlan(pages, seed),
net.dnorberg.rivencraft.age.PortWeatherMode.fromPages(pages));
environment.weather(), environment.lighting(),
net.dnorberg.rivencraft.age.PortSkyStyle.fromPages(pages));
} catch (IllegalArgumentException | IllegalStateException exception) {
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
return null;
@@ -15,6 +15,8 @@ import net.dnorberg.rivencraft.age.PortTerrainFeaturePlan;
import net.dnorberg.rivencraft.age.PortStructurePages;
import net.dnorberg.rivencraft.age.PortCarverPages;
import net.dnorberg.rivencraft.age.PortWeatherMode;
import net.dnorberg.rivencraft.age.PortLighting;
import net.dnorberg.rivencraft.age.PortSkyStyle;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.Registries;
@@ -53,7 +55,8 @@ public final class PortAuthoredWriting {
if (page == null || !page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
var entry = PortSymbolPages.find(page);
if (entry == null) return false;
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null) 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;
@@ -79,6 +82,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<>();
@@ -88,6 +93,10 @@ public final class PortAuthoredWriting {
|| pendingMaterial.equals("rivencraft:fluid") && PortLakePlan.isLake(page))) return false;
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) {
@@ -3,6 +3,7 @@
"minVersion": "0.8",
"package": "net.dnorberg.rivencraft.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin"],
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin", "AgeAmbientLightMixin"],
"client": ["StaticSkyStateMixin", "StaticSkyRendererMixin"],
"injectors": {"defaultRequire": 1}
}