diff --git a/README.md b/README.md index b3aa48f..807003c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/MINECRAFT_26_3_PORT.md b/docs/MINECRAFT_26_3_PORT.md index 38a615b..a15ceb8 100644 --- a/docs/MINECRAFT_26_3_PORT.md +++ b/docs/MINECRAFT_26_3_PORT.md @@ -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 diff --git a/ports/mc263/README.md b/ports/mc263/README.md index 90a7b44..1baaf38 100644 --- a/ports/mc263/README.md +++ b/ports/mc263/README.md @@ -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 ` 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 ` and +`/rivencraft_port age lighting_probe ` 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 ` uses the same environment +resolver and logs its frozen plan. `/rivencraft_port age environment_probe + ` 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 ` and +`/rivencraft_port age sky_probe `. 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 diff --git a/ports/mc263/RELEASE_NOTES_1.0.1.71-beta.1.md b/ports/mc263/RELEASE_NOTES_1.0.1.71-beta.1.md new file mode 100644 index 0000000..f4ba8bf --- /dev/null +++ b/ports/mc263/RELEASE_NOTES_1.0.1.71-beta.1.md @@ -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). diff --git a/ports/mc263/build.gradle b/ports/mc263/build.gradle index 934371a..1fa9860 100644 --- a/ports/mc263/build.gradle +++ b/ports/mc263/build.gradle @@ -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') { diff --git a/ports/mc263/gradle.properties b/ports/mc263/gradle.properties index a65f96f..abeb29f 100644 --- a/ports/mc263/gradle.properties +++ b/ports/mc263/gradle.properties @@ -7,4 +7,4 @@ neo_version=26.3.0.99 mod_id=rivencraft mod_name=Rivencraft mod_license=LGPL-3.0-or-later -mod_version=1.0.1.65-beta.1 +mod_version=1.0.1.71-beta.1 diff --git a/ports/mc263/port_en_us.json b/ports/mc263/port_en_us.json index 5d1b430..a3f5cba 100644 --- a/ports/mc263/port_en_us.json +++ b/ports/mc263/port_en_us.json @@ -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.", diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeLevelIndex.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeLevelIndex.java index aa47b0f..6da72ca 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeLevelIndex.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/AgeLevelIndex.java @@ -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 TYPE = new SavedDataType<>( @@ -53,6 +57,8 @@ final class AgeLevelIndex extends SavedData { private final Map dungeons; private final Map lakes; private final Map terrainFeatures; + private final Map lighting; + private final Map 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 ages, List retired, @@ -75,7 +83,9 @@ final class AgeLevelIndex extends SavedData { Map vegetation, Map dungeons, Map lakes, - Map terrainFeatures) { + Map terrainFeatures, + Map lighting, + Map 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 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(); } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeCommands.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeCommands.java index 5adbc75..6a41929 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeCommands.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeCommands.java @@ -68,6 +68,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 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 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 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 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 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 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 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; diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortEnvironmentSelection.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortEnvironmentSelection.java new file mode 100644 index 0000000..7266020 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortEnvironmentSelection.java @@ -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 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 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); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortLighting.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortLighting.java new file mode 100644 index 0000000..5a1d48a --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortLighting.java @@ -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 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(MinecraftServer server, PortAgeTerrain terrain, + Holder 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))); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRandomAgeSelection.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRandomAgeSelection.java index 1614bd4..246e763 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRandomAgeSelection.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortRandomAgeSelection.java @@ -68,7 +68,7 @@ public final class PortRandomAgeSelection { return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface; } - private static T weighted(Random random, List choices, int[] weights) { + static T weighted(Random random, List choices, int[] weights) { int total = 0; for (int weight : weights) total += weight; int ticket = random.nextInt(total); diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortSkyStyle.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortSkyStyle.java new file mode 100644 index 0000000..a737af4 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortSkyStyle.java @@ -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 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 pages) { + return pages.stream().map(PortSkyStyle::forPage).filter(java.util.Objects::nonNull) + .findFirst().orElse(OVERWORLD); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortWeatherMode.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortWeatherMode.java index b35f086..0ebd432 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortWeatherMode.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortWeatherMode.java @@ -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 pages) { - return pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull) - .findFirst().orElse(NORMAL); - } } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/RuntimeAgeBackend.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/RuntimeAgeBackend.java index 1de5755..624d22f 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/RuntimeAgeBackend.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/RuntimeAgeBackend.java @@ -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 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); diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortStaticSky.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortStaticSky.java new file mode 100644 index 0000000..a562297 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortStaticSky.java @@ -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(); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/StaticSkyState.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/StaticSkyState.java new file mode 100644 index 0000000..8852d3f --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/StaticSkyState.java @@ -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); +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeAmbientLightMixin.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeAmbientLightMixin.java new file mode 100644 index 0000000..16e2da7 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeAmbientLightMixin.java @@ -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; + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyRendererMixin.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyRendererMixin.java new file mode 100644 index 0000000..ee3da38 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyRendererMixin.java @@ -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; + } + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyStateMixin.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyStateMixin.java new file mode 100644 index 0000000..a732875 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StaticSkyStateMixin.java @@ -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; } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java index 9bb7b11..b4b5d28 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/network/PortBookNetworking.java @@ -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; diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/PortAuthoredWriting.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/PortAuthoredWriting.java index 9470a1a..26576a4 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/PortAuthoredWriting.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/writing/PortAuthoredWriting.java @@ -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 terrainFeatures = new HashMap<>(); Map structures = new HashMap<>(); Map 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) { diff --git a/ports/mc263/src/main/resources/rivencraft.mixins.json b/ports/mc263/src/main/resources/rivencraft.mixins.json index 3e36dc3..f0b8dac 100644 --- a/ports/mc263/src/main/resources/rivencraft.mixins.json +++ b/ports/mc263/src/main/resources/rivencraft.mixins.json @@ -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} }