diff --git a/README.md b/README.md index c8481fd..b3aa48f 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,11 @@ the 1.12.2 mod. 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 and a growing set of generation features, +supports authored terrain/biomes, structure and vegetation pages, independent +Age weather, and a growing set of generation features, but does not yet provide full 1.21.1 gameplay or optional-integration parity. See the [26.3 beta guide](ports/mc263/README.md) and -[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.54-beta.1.md). +[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.65-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 2c23610..38a615b 100644 --- a/docs/MINECRAFT_26_3_PORT.md +++ b/docs/MINECRAFT_26_3_PORT.md @@ -681,6 +681,225 @@ resource packaging, and static checks pass for this slice. Generation, visual rendering, actual return travel, and restart checks are left to the player; no development server is started. +The local `1.0.1.55-beta.1` slice begins the authored structure-set port with +Villages, Mineshafts, and Strongholds. Saved page occurrences use the existing +feature plan's new optional structure list. Terrain gates, random-spread +frequency/spacing scaling, and bounded concentric-ring count/spread follow +1.21.1. Modern `AbstractSpreadingStructurePlacement` accessors replace the old +placement-class accessors. Scaled holder copies never mutate global registries. + +Structure-state planning includes authored eligibility biomes while continuing +to sample the actual terrain source. A scoped Structure mixin carries over +1.21.1's authored biome override. The wrapper handles structure starts itself +when a structure plan exists, allowing the hook to recognize the authored Age; +unrelated/unplanned generators remain delegated. Only these three controlled +families are gated in this slice; remaining structure families, random picks, +and biome-mismatch instability remain unported. Existing plans without the +optional list retain their previous structure path. Mod metadata now correctly +calls this target Beta rather than Alpha. + +The player passed Star Fissures. Build/static checks pass for this new slice. +A disposable dedicated-server startup reached `Done` without mixin-loading +errors. Its console `/stop` command hit a null-level CommandSourceStack error; +the exact test JVM was then stopped through SIGTERM and saved/shut down. This +was a startup check, not authored generation or density validation. The +disposable world is removed; client authoring, actual structures, density, +biome overrides, and restart checks are handed to the player. + +The local `1.0.1.56-beta.1` follow-up corrects the structure biome override. +Client logs confirmed the build-55 mixin was applied, but 26.3 JigsawPlacement +checks `GenerationContext.couldStructureExistInColumn` inside generation, +before the final check bypassed by the 1.21.1 hook. The replacement ModifyArg +changes only the biome predicate passed to the GenerationContext constructor +in Structure.generate for explicitly authored Age structures. Both early +and final checks now see the override; all other generation constraints and +unrelated generators retain their existing behavior. + +Build/static checks pass. An isolated `--initSettings` bootstrap initialized +settings without a mixin application error, then FML exited nonzero because +that settings-only mode creates no server thread. This is not a full server +or generation pass. No world was created, and the temporary settings directory +is removed. In-game validation requires a new Age or ungenerated chunks; +previously rejected chunks are not regenerated. No commit/release is made. + +The local `1.0.1.57-beta.1` slice completes the existing 1.21.1 structure-page +mapping (Dungeons remains its separate placed feature) and enables 26.3's +Abandoned Camps page. The shared planner now recognizes 15 structure sets. +Page/terrain restrictions match AgeStructureSelection for the 1.21.1 families; +Abandoned Camps follows surface noise terrain. No material modifiers are added. + +The separate Fortress/Bastion set resources are reused directly from 1.21.1. +The controlled-set gate includes the vanilla combined Nether set, preventing +unrequested cross-family placement. All structure pages share the previously +validated saved list, density logic, and corrected context-level biome override. +Unknown non-spreading placements are left unchanged rather than cast unsafely. +Unmodified/no-authored-structure Ages retain their prior native path; previously +saved authored plans now gate all supported families in new chunks. Instability +and random feature selection remain pending. + +The player passed the build-56 biome fix. Build, diff checks, all 15 registry +and Creative-page mappings, and target structure resource existence checks pass. +Packaged Fortress/Bastion definitions match 1.21.1 byte-for-byte. No server is +started; actual structure placement, terrain-specific limits, repeated density, +and restart validation are handed to the player. Changes remain local. + +The local `1.0.1.58-beta.1` slice ports Caves/Ravines feature pages separately +from Cave terrain. A new optional saved carver-page list participates in the +existing feature-plan lifecycle. Grammar accepts noise terrain including Cave, +rejects Flat/Void and material modifiers, and checks the modern CARVER registry. + +26.3 combines carving with terrain construction and exposes carvers that emit +geometry rather than mutate blocks. A scoped NoiseBasedChunkGenerator mixin +adds authored geometry to its native CarvingMask immediately before that mask +is tested/applied. It only acts for the matching runtime Age wrapper, using +its frozen seed and actual biome resolver. Vanilla applies the combined mask +with its own aquifer/surface rules; no closed NoiseChunk is accessed afterward. +The source range, salts, Nether cave selection, and Nether ravine skip match +1.21.1. Duplicate Caves/Ravines repeat identical seeds in the baseline, so the +port avoids redundant identical mask passes while retaining authored pages. +Added terrain volumes still follow the combined terrain operation, as noted +for the earlier Floating Islands port. + +The player passed structure slice 57. Build/static checks and packaging checks +for the hook, Creative entries, and three required vanilla carvers pass. A +settings-only bootstrap completed initialization without a mixin application +error; FML then returned nonzero because no server thread exists in that mode. +This is not world-generation validation. No world was created; the temporary +settings directory is removed. Actual carving and restart checks are manual, +and no commit/push/publication is performed. + +The local `1.0.1.59-beta.1` slice replaces stale per-page port information +and adds the requested red hue for unsupported pages. A shared +PortAuthoredWriting.isPageImplemented classifier uses the same terrain, +controller, material/biome, and feature recognizers as the grammar, without +misclassifying context-dependent modifiers/controllers by testing them alone. +Page tooltips, the extracted item-render color, and the Binder background use +this single availability check. Tinting does not generate extra textures or +change stored page data. Corrupted-page names/descriptions remain obfuscated. + +Port language overrides remove the old Standard/one-Biome claim, distinguish +implemented/unimplemented pages and invalid combinations, and correct all +15 structure-family descriptions, material adjacency, and selected terrain/ +feature restrictions. The 1.21.1 language assets are unchanged. This is a UI +and information slice, not new generation functionality. + +The player passed Caves/Ravines. Build, diff checks, unique language-key checks, +and packaged-language override checks pass. No server/world is started. +Inventory tint, Binder tint/layering, and tooltip readability await the +player's visual check. Changes remain local and unpublished. + +The local `1.0.1.60-beta.1` slice implements the four requested 26.3-only +vegetation pages: Poplar Trees, Fallen Poplar Trees, Red Shrubs, and Shelf +Mushrooms. They extend PortVegetationPlan recognition, so grammar, saved plans, +Creative status, item tint, and tooltip availability agree without another +page list. The existing ordered vegetation cap and surface-terrain/biome +restrictions are retained. The 1.21.1 code/assets are unchanged. + +Poplar Trees selects only the three vanilla poplar variants, retaining their +relative native weights and six placement attempts. Fallen poplars reuse the +vanilla feature but add authored in-square/surface placement before its sapling +check; the vanilla placed feature alone has no surface-height placement. Red +Shrubs directly reuses its registered patch. No tree/shrub algorithm or artwork +is duplicated. + +Shelf Mushrooms is a tree decorator in 26.3, not a placed feature. The authored +page therefore applies ShelfMushroomDecorator to existing connected log hosts +at its position in the vegetation sequence. Section palettes skip log-free +sections, repeated pages reuse the host scan, and tree placements invalidate +that cache. Host ordering and page seeds are deterministic; callback writes +are chunk-local, fluid-free, supported, and height-bounded. The decorator keeps +its native standing/fallen logic and side/age/adjacency checks. This is an +authored extension using the native decorator, not a new vanilla feature ID. + +Build/static and resource-reference checks pass. The four pages' descriptions +explain their actual effects and requirements. No server/world is started; +the player validates vegetation appearance, support/tint, ordering, and restart. +Changes remain local and unpublished. + +The local `1.0.1.61-beta.1` follow-up separates log-hosted Shelf Mushrooms +from surface plants. A shared requiresSurface classification now governs +terrain/explicit-biome validation; decoration handles Shelf Mushrooms before +the surface-biome skip. Trees, shrubs, and flowers retain their restrictions. +Void/Cave/End-biome log hosts are no longer excluded by the vegetation path. + +The host scan is a stable log-position list rather than connected tree groups. +Native tree decorators assume a meaningful base and only decorate a limited +height band, which is unsuitable for log tendrils. The authored extension +therefore checks exposed log faces at all heights, reusing the native decorator's +replaceability/water predicate and block facing/age/survival rules, with spacing +and chunk bounds. Cached scans still avoid repeated full-volume work. No tree +page or log-producing page is required, but actual log hosts must exist during +chunk generation. This supersedes slice 60's whole-tree height-band behavior. + +Build/static checks pass. Manual validation: new Void + Oak Log + Tendrils + +Shelf Mushrooms Age, without any tree page; inspect exposed log faces and +restart/new-chunk generation. No server is started and nothing is published. + +The player passed the local `1.0.1.62-beta.1` sound slice: the complete 1.21.1 +sound manifest/OGGs are reused, book travel emits modifier-selected audio and +cloud bursts, and Star Fissures play audio after arrival as well as departure. +Portal/meteor events are registered but their gameplay ports remain separate. + +The local `1.0.1.63-beta.1` slice adds authored Normal/Fast/Slow/Off/Always/Storm +weather policies. One policy is accepted per book, with availability/tint and +tooltips following the shared classifier. Cave terrain always suppresses it. +An omitted policy defaults to Normal for now; random-trait selection is pending. + +Unlike 1.21.1, 26.3 ServerLevel.getWeatherData delegates to the server's shared +weather object. A scoped Runtime Age ServerLevel subclass instead loads native +WeatherData from that Age's data storage, alongside its saved policy. Native +weather ticking and client packets remain in charge; only the data owner and +Legacy-style duration policy change. The constructor replaces inherited global +rain/thunder visual levels before publishing the level. Retirement, reseeding, +reload and restart retain the policy through existing per-Age storage lifecycle. +Vanilla /weather remains a global server command; the Age policy is not a +global command override. Native biome precipitation rules still apply. + +Build/diff checks pass. A disposable dedicated server created all six weather +policies: every Age reported independent native data and the expected flags/ +duration ranges. Setting global thunder left the Off Age clear. Unload/reload +retained Storm, and a full stop/restart restored all six policies and their +live timers. Both servers shut down cleanly; the temporary test world is +deleted after completion. Client appearance, cross-Age travel, authoring/tint, +and Cave suppression remain manual checks. +No commit/push/publication is authorized by the player's testing pass alone. + +The local `1.0.1.64-beta.1` follow-up fixes weather-only authoring. Build 63's +server weather checks bypassed the Binder's biome compiler, whose exclusion +list did not yet include weather. As a result weather/storm was incorrectly +resolved as a biome ID despite grammar acceptance. The biome compiler now +selects actual biome-category entries after full-book grammar validation; +controllers retain their separate handling. This avoids repeating the same +missed-exclusion bug as more non-biome page families are ported. The operator +weather fixture now calls that compiler before backend creation. All six +weather-only symbol lists compiled and created their expected weather states +on a fresh dedicated server. Build/diff checks pass. The server shut down +cleanly and its disposable world was removed; player GUI validation remains. + +The local `1.0.1.65-beta.1` spawn correction traces non-Cave arrival columns +from maximum build height, inspecting actual fully generated chunk blocks. +The configured Flat-layer shortcut could select ground below later authored +terrain, and the old heightmap-only path did not explicitly ensure an exposed +surface. The new path stops at the first fluid/leaves/collision, requires dry +footing and headroom, and otherwise tries the next column rather than searching +below an obstruction. Chunk lookup is cached per column and a mutable position +avoids per-air-block allocations. A fallback platform goes above the actual +highest obstruction; empty Void retains floor Y=63. Surface footing accepts +leaves, rejects fluid-bearing floors, and checks build/border bounds. + +Stored non-Cave Star Fissure arrivals also use the surface search rather than +accepting old underground pockets. Cave terrain keeps its Y=64-down search. +Exact bound Linking Books and the general fissure Overworld-return anchor +search are not redirected to the sky. A read-only surface probe command helps +validate the chosen column/nearby surface (chunk generation is possible). +Build/diff checks pass. A disposable dedicated-server probe selected Y=4 on +plain Flat ground, Y=141 above leaves placed at Y=140, and a neighboring dry +column instead of descending below water added above those leaves. A larger +roof-fixture fill was rejected by vanilla because some target chunks were not +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. + ## 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 b2d25a4..90a7b44 100644 --- a/ports/mc263/README.md +++ b/ports/mc263/README.md @@ -4,9 +4,9 @@ This is an **experimental beta port** for issues #97–#104, not gameplay parity 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. Other terrain families, feature/material effects, -portals, and much of -the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not +biome-distribution pages, selected material/vegetation/structure features, and +independent Age weather. Celestial/lighting systems, instability, crystal +portals, and much of the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not substitute this JAR for the 1.21.1 mod or import 1.21.1 saves. The entries below record how the port was built, including earlier alpha-only @@ -351,6 +351,213 @@ the arrival point (generally east/south), descend through the rift, and touch the starfield to return. Re-enter after restarting; check that the same fissure and its portal still work. A Cave Age checks that carving leaves the roof intact. +The local `1.0.1.55-beta.1` slice adds authored Villages, Mineshafts, and +Strongholds. Repeated pages (up to 32 per type) scale the registered placement +using the 1.21.1 frequency/spacing and ring-count rules. Copies are Age-local: +the Overworld's placements and biome source are not modified. Authored +structures can bypass their usual biome eligibility without changing the +terrain biome; their instability cost remains pending with the instability port. + +Villages and Strongholds allow surface noise terrain and Flat, but not Cave +or Void. Mineshafts allow noise terrain including Cave, but not Flat or Void. +These pages do not accept Block/Fluid modifiers. Their ordered counts persist +in the feature plan. In books using these structure pages, unselected sets +from these three families are disabled; other structure families and random +feature selection remain later port work. Earlier books without this new +structure plan retain their existing native structure behavior. + +For manual checks, bind a new Standard/Plains Age with Villages, Mineshafts, +and Strongholds, then use `/locate structure minecraft:village_plains`, +`/locate structure minecraft:mineshaft`, and +`/locate structure minecraft:stronghold` to inspect generated structures. +Compare one page with several copies in a different Age, and restart/revisit. +Flat/Plains with Villages and an otherwise incompatible surface biome with +Villages provide separate terrain and biome-override checks. + +The local `1.0.1.56-beta.1` follow-up fixes authored structures outside their +normal biomes. Build 55 only bypassed the final biome check; 26.3's jigsaw +placement can reject a whole biome column earlier. The override now supplies +the generation context's biome predicate before either check, only for the +explicitly authored structures in that Age. Terrain/height/geometry rules and +unrelated structures keep their normal checks. + +Test with a new Standard or Flat Age using a normally unsupported village +biome, such as The End, plus a Villages page. Multiple copies increase density. +Use `/locate structure #minecraft:village` rather than selecting only the +Plains variant: an override Age can choose other village styles. Old chunks +that already rejected their structure attempt are not regenerated; existing +build-55 Ages can also test the fix in previously unexplored chunks. + +The local `1.0.1.57-beta.1` slice adds the remaining 1.21.1 authored structure +families: Nether Fortresses, Bastions, Trial Chambers, Buried Treasure, +Shipwrecks, Ocean Ruins, Pillager Outposts, Ruined Portals, Nether Fossils, +Ancient Cities, and Trail Ruins. It also enables the existing 26.3-only +Abandoned Camps page using Minecraft's registered camp set and variants. + +All 15 supported structure-set pages share saved counts, biome overrides, +and repeated-page density scaling (up to 32 per type). Fortress and Bastion +sets reuse the separate 1.21.1 placement definitions; the combined vanilla +Nether set is disabled for authored structure plans so the pages stay separate. +Unselected supported families are gated in these plans. Already generated +chunks and Ages without an authored structure plan keep their existing path. + +Only Villages/Strongholds support Flat; none support Void. Fortress, Bastion, +and Nether Fossils require Cave terrain. Villages/Strongholds, Buried Treasure, +Shipwrecks, Ocean Ruins, Outposts, Trail Ruins, and Abandoned Camps require +surface terrain. Mineshafts, Trial Chambers, Ancient Cities, and Ruined Portals +allow surface noise or Cave terrain. Physical terrain/height checks still apply +even when the authored page overrides biome eligibility. Instability penalties +and random selection of these features are not part of this slice. + +For a quick in-game check, author a new Standard Age with Trial Chambers, +Trail Ruins, and Abandoned Camps. Use `/locate structure minecraft:trial_chambers`, +`/locate structure minecraft:trail_ruins`, and +`/locate structure minecraft:abandoned_camp_forest` to inspect actual generation. +Separately, test a Cave/Nether Wastes Age with Fortress and Bastion pages +(`/locate structure minecraft:fortress` and +`/locate structure minecraft:bastion_remnant`). Repeated pages and restart/ +new-chunk generation remain useful checks. Structures can still fail when +their geometry or required terrain is unavailable. + +The local `1.0.1.58-beta.1` slice enables Caves and Ravines feature pages. +They add carving to the chosen noise terrain (Standard, Amplified, Island, +or Cave); they are not aliases for the Cave terrain page. Flat/Void and +Block/Fluid modifiers remain unsupported, matching the 1.21.1 feature rules. + +Authored paths retain 1.21.1's source range and seeded salts, using 26.3's +registered cave/Nether-cave/canyon carvers. Nether-biome sources select the +Nether cave carver and skip Ravines. The paths merge into the native carving +mask before Minecraft applies aquifer, replacement, and surface rules; no +second approximation carves raw air after terrain generation. Biomes are not +replaced. The page list is saved, and old plans default to no extra carving. + +Duplicate pages are accepted up to 32 per type, but repeat the same seeded +pattern in 1.21.1; this port skips those redundant passes rather than multiplying +carving density. Random feature selection and instability remain pending. +New chunks only. For manual testing, author a Standard/Plains Age with one +Caves page and one Ravines page and inspect underground in Spectator mode. +Restart and explore more chunks. A separate Cave/Nether Wastes Age with Caves +checks the Nether variant without altering its biome distribution. + +The local `1.0.1.59-beta.1` slice audits page presentation for the current port. +Each Symbol Page now reports whether its effect is implemented. Unported pages +have red-tinted item artwork and a red Binder page background, plus a red status +line. Supported pages retain their normal artwork. This indicates port +availability only: supported pages in an invalid order, terrain combination, +or excessive quantity are not individually tinted red. Link Panels are unchanged. + +The stale Standard/one-Biome message is removed. Structure descriptions now +describe authored biome overrides rather than falsely requiring vanilla biome +eligibility. Material pages explain the immediate-before-feature requirement; +Flat/Caves/Ravines/Floating Islands descriptions reflect the current rules. +These overrides are specific to the 26.3 language file and do not change 1.21.1. +No new generation effect is introduced in this UI slice. + +For manual validation, compare a working Villages or Caves page with an +unported celestial or lighting page in Creative inventory +and the Binder. Only the latter should be red; supported Block/Fluid modifiers +and biome controllers should stay normal even though they need other pages. + +The local `1.0.1.60-beta.1` slice implements Poplar Trees, Fallen Poplar Trees, +Red Shrubs, and Shelf Mushrooms. They use the existing saved vegetation plan, +ordered attempts, surface-biome rules, and combined 32-vegetation-page limit. +They accept Standard, Amplified, Island, and Flat terrain, but not Cave/Void +or Block/Fluid modifiers. The four pages now show as supported, without a red tint. + +Poplar Trees uses the vanilla red/orange/yellow leaf-litter tree variants with +their relative Dappled Forest weights and six attempts per page per chunk; +it does not also select spruce or fallen trees. Fallen Poplar Trees makes one +surface attempt per page, using the vanilla fallen-tree feature, decorators, +and sapling survival rules. Red Shrubs uses the vanilla patch placement. +All still require suitable ground and space; authoring does not bypass those +requirements, and existing chunks are not redecorated. + +Shelf Mushrooms applies Minecraft's standing/fallen-log decorator to existing +log groups, not a new tree feature. It can decorate natural trees or logs from +earlier authored tree pages; no suitable logs means no mushrooms. The vanilla +side/age/spacing and water-avoidance logic is retained. Writes stay within the +current chunk and require valid support. Consecutive mushroom pages reuse the +host scan; adding trees invalidates the cached host list for subsequent pages. + +For manual testing, bind a new Flat/Plains Age with Poplar Trees, Fallen Poplar +Trees, Red Shrubs, then Shelf Mushrooms. Check leaf colours, fallen trunks, +shrubs, and mushrooms on logs. The four pages should no longer be red. Restart, +revisit existing terrain, and explore new chunks to check persistence. + +The local `1.0.1.61-beta.1` follow-up removes the inappropriate surface-terrain +and surface-biome gates from Shelf Mushrooms only. No tree page is required: +natural logs or generated log Tendrils/Spheres/other features are hosts on +any terrain, including Void and Cave, and in any biome. Other plants keep their +existing terrain/biome restrictions. For example, Void + Oak Log + Tendrils + +Shelf Mushrooms is valid authoring; there must still be exposed log faces. + +The mushroom scan now considers log faces throughout the chunk height instead +of treating a connected tendril as a standing tree with a small base-height +decoration band. Placement retains native replaceability/water checks, valid +support, random age, a per-side chance, and spacing checks. It does not grow +hosts or continuously decorate player-built logs after chunk generation. +Test a new Void Age with the example pages and inspect tendril surfaces. + +The local `1.0.1.62-beta.1` sound slice reuses the complete 1.21.1 sound manifest +and all six OGG files directly in the build. Successful Linking/Descriptive Book +travel now plays the normal, Intra-Linking, or Disarm sound at departure and +arrival, with the same modifier precedence and cloud bursts as 1.21.1. Failed +travel stays silent. Star Fissures also play their return sound after arrival, +instead of relying only on audio sent immediately before a dimension switch. +Portal and meteor sound events are bundled for their later gameplay ports; +this does not implement those systems. Binding still uses vanilla page audio. +Manual checks: normal book travel, Intra-Linking, Disarm, rejected travel, and +a Star Fissure return. Existing books/Ages do not need to be recreated. + +The local `1.0.1.63-beta.1` slice ports the six active weather pages: Normal, +Fast, Slow, Off, Always, and Storm. One weather page is accepted per book and +its policy is saved in the Age's own data directory. Missing weather pages +currently use Normal; random weather selection is still part of the pending +random-trait port. Cave terrain forces Off, matching 1.21.1. Precipitation +still follows biome temperature and precipitation rules; a desert does not +start showing rain merely because Always or Storm is selected. + +26.3 normally shares server WeatherData. Runtime Ages now use an isolated +native WeatherData instance with its own persisted rain/thunder timers. The +weather controller runs before the native cycle, allowing vanilla rendering, +lightning, snow and client dimension-specific packets to handle the result. +Fixed weather is refreshed before its timers expire; Fast and Slow use the +1.21.1 duration ranges and preserve saved timers on reload. Overworld weather +and other Ages are not changed. The vanilla `/weather` command still targets +the server's global weather, not the independent Age policy. + +Operator diagnostics: `/rivencraft_port age create_weather ` +creates a Flat test Age; `/rivencraft_port age weather_probe ` reports +its policy, rain/thunder state and timers, data independence, and global state. +For manual testing, bind separate Storm/Always/Off books with Plains, compare +the Overworld, revisit and restart, and check that Cave + Storm remains dry. +The six weather pages should no longer be tinted red. Existing Ages get a +Normal independent cycle; author a new book to select another policy. + +The local `1.0.1.64-beta.1` fix corrects the Binder's biome compiler: it now +positively selects biome-category pages instead of treating every page not +in an exclusion list as a biome. Weather pages no longer trigger an unknown +biome lookup. The weather fixture command now exercises the same page compiler +before creating an Age, covering the weather-only symbol list used by a Link +Panel + Storm book. Existing Binder contents can be reused after updating. + +The local `1.0.1.65-beta.1` spawn correction removes the Flat-layer shortcut +and heightmap-only choice for non-Cave Descriptive Book arrivals. The search +starts at the actual maximum build height and traces each nearby column down +to its first colliding block, leaves, or fluid. A column without safe dry +footing/headroom is skipped instead of descending through its roof or water +into an underground pocket. Leaves are valid footing; fluids are not. + +This also revalidates a stored Star Fissure arrival from above on non-Cave +terrain. Cave terrain retains its Y=64-downward search, and bound Linking +Books retain their exact coordinates. If no natural surface is safe, the +safety platform is placed above the actual highest obstruction (or floor +Y=63 in an empty Void), never buried below it. Plain Flat terrain still lands +on its ground, not an artificially elevated platform. No new Age is required. +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. + From the repository root: ```sh diff --git a/ports/mc263/RELEASE_NOTES_1.0.1.65-beta.1.md b/ports/mc263/RELEASE_NOTES_1.0.1.65-beta.1.md new file mode 100644 index 0000000..5a3357e --- /dev/null +++ b/ports/mc263/RELEASE_NOTES_1.0.1.65-beta.1.md @@ -0,0 +1,72 @@ +# Rivencraft 26.3 beta 1.0.1.65-beta.1 + +This beta collects the player-tested port slices since 1.0.1.54-beta.1. +It is not yet feature-complete with the Minecraft 1.21.1 release. + +## Age generation + +- Added authored structure pages: Villages, Mineshafts, Strongholds, Trial + Chambers, Buried Treasure, Shipwrecks, Ocean Ruins, Pillager Outposts, + Ruined Portals, Nether Fossils, Ancient Cities, Trail Ruins, Nether + Fortresses, Bastions, and 26.3's Abandoned Camps. Dungeons remain their + separate feature. +- Fixed the early structure-biome check so authored structures can generate + outside their native biome restrictions, within the supported terrain rules. + Repeated structure pages increase density up to the current authoring cap. +- Added Caves and Ravines feature pages through 26.3's native carving stage. + These are distinct from Cave terrain; Flat/Void support remains pending. +- Added Poplar Trees, Fallen Poplar Trees, Red Shrubs, and Shelf Mushrooms. + Shelf Mushrooms find exposed logs at any height, including log Tendrils in + Void terrain. No Trees page is required. +- Added Normal, Fast, Slow, Off, Always, and Storm weather pages. Each Age + has independent saved weather data and timers; its policy does not change + the Overworld or other Ages. Cave terrain keeps weather disabled. + +## Fixes and feedback + +- Non-Cave Descriptive Book arrivals trace actual blocks down from maximum + build height instead of using a Flat-layer shortcut or only a heightmap. + The search stops at roofs/fluids rather than selecting underground pockets. + Leaves count as footing; submerged footing is rejected. Fallback platforms + sit above obstructions, with floor Y=63 in an empty Void. +- Stored surface-Age Star Fissure arrivals are rechecked from above. Cave + terrain still searches down from Y=64, and Linking Books retain exact + bound destinations. +- Restored Legacy linking sounds and successful-link cloud bursts. Disarm + and Intra-Linking use their respective sounds. Star Fissure return audio + also plays after arrival so a dimension switch does not discard it. +- Fixed weather-only authoring, including Link Panel + Storm books: non-biome + pages no longer get interpreted as biome IDs. +- Unsupported pages now have a red tint and availability tooltip. Supported + pages and their descriptions reflect the implemented 26.3 behavior. +- Added operator weather and surface diagnostics for testing. + +## Current limits + +- Only the supported page/terrain combinations can be bound. Most repeated + features are capped at 32; vegetation uses a combined 32-page limit. The + full 1.21.1 grammar recovery and instability systems remain unported. +- Pageless books currently randomize terrain and biome distribution, not the + complete feature/environment list. Omitted weather currently means Normal. +- Base carvers do not carve authored solid volumes added after 26.3's combined + terrain operation. Previously generated chunks are not regenerated. +- The full writing/discovery loop, celestial/lighting systems, crystal portals, + instability, and optional-integration parity remain in progress. Creative + availability alone does not mean a page is supported; check its tooltip. +- Vanilla `/weather` targets global server weather, not the independent Age + policy. Biomes retain their normal precipitation rules. + +The player reported in-game passes for these slices. Build/static checks passed. +Targeted disposable dedicated-server checks covered all six weather policies, +Overworld isolation, unload/reload and restart persistence, weather-only page +compilation, and dry surface/leaf spawn probes. This is not a claim of full +dedicated-server or optional-mod regression coverage. Test servers were stopped +and their temporary worlds removed. + +Requires Minecraft **26.3**, NeoForge **26.3.0.99** or a compatible 26.3 build, +and **Java 25**. This build does not require Dynamic Dimensions or Bad Packets. +Keep it separate from the 1.21.1 build and saves. Back up entire worlds before +updating; beta-save compatibility is not guaranteed. + +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 f8841a9..934371a 100644 --- a/ports/mc263/build.gradle +++ b/ports/mc263/build.gradle @@ -124,9 +124,12 @@ sourceSets.main.resources.srcDir generatePortLanguage tasks.named('processResources', ProcessResources).configure { // Reuse the existing sprite and base model without copying binary assets. from('../../src/main/resources') { + include 'data/rivencraft/worldgen/structure_set/fortresses.json' + include 'data/rivencraft/worldgen/structure_set/bastions.json' include 'assets/rivencraft/models/block/star_fissure.json' include 'assets/rivencraft/blockstates/star_fissure.json' - include 'assets/rivencraft/sounds/linking/link-fissure.ogg' + include 'assets/rivencraft/sounds.json' + include 'assets/rivencraft/sounds/**/*.ogg' include 'assets/rivencraft/textures/item/crystal_shard.png' include 'assets/rivencraft/textures/item/page.png' include 'assets/rivencraft/textures/item/page_link_panel.png' diff --git a/ports/mc263/gradle.properties b/ports/mc263/gradle.properties index 3d5f937..a65f96f 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.54-beta.1 +mod_version=1.0.1.65-beta.1 diff --git a/ports/mc263/port_en_us.json b/ports/mc263/port_en_us.json index 62d40bf..5d1b430 100644 --- a/ports/mc263/port_en_us.json +++ b/ports/mc263/port_en_us.json @@ -6,19 +6,26 @@ "item.rivencraft.page.blank": "Blank Page", "item.rivencraft.page.link_panel": "Link Panel Page", "item.rivencraft.page.unresolved": "Unknown Symbol Page", - "item.rivencraft.page.symbol.port_status": "Binder currently supports Standard terrain and one Biome page; other effects are still being ported.", + "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.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.", + "symbol.rivencraft.weather.always.description": "Keep this Age rainy, or snowy in cold biomes, without thunder. Cave terrain keeps weather off.", + "symbol.rivencraft.weather.storm.description": "Keep this Age rainy and thundering. Precipitation still depends on the biome; Cave terrain keeps weather off.", + "item.rivencraft.page.symbol.unimplemented": "Not yet implemented in 26.3; cannot be bound into an Age.", "item.rivencraft.link_property.disarm": "Disarm", "item.rivencraft.link_property.maintain_momentum": "Maintain Momentum", "item.rivencraft.link_property.intra_linking": "Intra-Linking", "item.rivencraft.ink_vial": "Ink Vial", "item.rivencraft.ink_vial.remaining": "Ink: %s/%s", "item.rivencraft.ink_vial.usage": "Use with paper in the other hand, or place in an Ink Mixer.", - "item.rivencraft.page.blank.port_status": "Base for future symbol writing; not yet usable in this port.", + "item.rivencraft.page.blank.port_status": "Internal symbol-page base; not an authoring ingredient.", "item.rivencraft.page.link_panel.port_status": "Use with leather in a Book Binder to make a Descriptive Book.", "block.rivencraft.book_binder": "Book Binder", "container.rivencraft.book_binder": "Book Binder", "screen.rivencraft.book_binder.missing_panel": "Link Panel required", - "screen.rivencraft.book_binder.authored_pending": "Writing not yet ported", + "screen.rivencraft.book_binder.authored_pending": "Unsupported page or page combination", "block.rivencraft.ink_mixer": "Ink Mixer", "container.rivencraft.ink_mixer": "Ink Mixer", "screen.rivencraft.ink_mixer.ink": "Ink: %s/%s", @@ -33,13 +40,33 @@ "itemGroup.rivencraft": "Rivencraft", "itemGroup.rivencraft.pages.link": "Rivencraft Link Pages", "symbol.rivencraft.feature.poplar_trees": "Poplar Trees", - "symbol.rivencraft.feature.poplar_trees.description": "Poplar tree generation symbol.", + "symbol.rivencraft.feature.poplar_trees.description": "Add red, orange, and yellow poplar trees to surface terrain with suitable soil.", "symbol.rivencraft.feature.fallen_poplar_trees": "Fallen Poplar Trees", - "symbol.rivencraft.feature.fallen_poplar_trees.description": "Fallen poplar tree generation symbol.", + "symbol.rivencraft.feature.fallen_poplar_trees.description": "Add fallen poplar logs and stumps to surface terrain with suitable soil.", "symbol.rivencraft.feature.shelf_mushrooms": "Shelf Mushrooms", - "symbol.rivencraft.feature.shelf_mushrooms.description": "Shelf mushroom generation symbol.", + "symbol.rivencraft.feature.shelf_mushrooms.description": "Add shelf mushrooms to exposed log faces on any terrain, including Void. No tree page is required.", "symbol.rivencraft.feature.red_shrubs": "Red Shrubs", - "symbol.rivencraft.feature.red_shrubs.description": "Red shrub generation symbol.", + "symbol.rivencraft.feature.red_shrubs.description": "Add red shrub patches to surface terrain with suitable ground.", "symbol.rivencraft.feature.abandoned_camps": "Abandoned Camps", - "symbol.rivencraft.feature.abandoned_camps.description": "Abandoned camp structure generation symbol." + "symbol.rivencraft.feature.abandoned_camps.description": "Allow abandoned camps on surface noise terrain, including outside their usual biomes.", + "symbol.rivencraft.block.generated.description": "Place immediately before a compatible feature; unpaired materials are not yet supported.", + "symbol.rivencraft.biome.native.description": "Use the terrain's native biome source; do not include Biome pages.", + "symbol.rivencraft.terrain.flat.description": "A level, biome-decorated landscape with its foundation at Y=0.", + "symbol.rivencraft.feature.floating_islands.description": "Add high floating islands alongside noise, Flat, or Void terrain; not Cave terrain.", + "symbol.rivencraft.feature.caves.description": "Add extra cave carving to noise terrain, including Cave; not Flat or Void.", + "symbol.rivencraft.feature.ravines.description": "Add canyon carving to noise terrain; not Flat or Void. Skips Nether biomes.", + "symbol.rivencraft.feature.mineshafts.description": "Allow mineshafts in noise terrain, including Cave, regardless of normal biome eligibility.", + "symbol.rivencraft.feature.nether_fortresses.description": "Allow Nether fortresses in Cave terrain; the authored page overrides biome eligibility.", + "symbol.rivencraft.feature.strongholds.description": "Allow strongholds in surface noise or Flat terrain, including outside their usual biomes.", + "symbol.rivencraft.feature.villages.description": "Allow villages in surface noise or Flat terrain, including outside their usual biomes.", + "symbol.rivencraft.feature.trial_chambers.description": "Allow underground Trial Chambers in noise terrain, including Cave, regardless of biome eligibility.", + "symbol.rivencraft.feature.bastions.description": "Allow Bastion Remnants in Cave terrain; the authored page overrides biome eligibility.", + "symbol.rivencraft.feature.buried_treasure.description": "Allow buried treasure in surface noise terrain; biome eligibility is overridden, but suitable terrain is required.", + "symbol.rivencraft.feature.shipwrecks.description": "Allow shipwrecks in surface noise terrain, including outside their usual ocean and beach biomes.", + "symbol.rivencraft.feature.ocean_ruins.description": "Allow ocean ruins in surface noise terrain, including outside their usual biomes.", + "symbol.rivencraft.feature.pillager_outposts.description": "Allow pillager outposts in surface noise terrain, including outside their usual biomes.", + "symbol.rivencraft.feature.ruined_portals.description": "Allow ruined portals in noise terrain, including Cave, regardless of normal biome eligibility.", + "symbol.rivencraft.feature.nether_fossils.description": "Allow Nether fossils in Cave terrain; the authored page overrides biome eligibility.", + "symbol.rivencraft.feature.ancient_cities.description": "Allow underground Ancient Cities in noise terrain, including Cave, regardless of biome eligibility.", + "symbol.rivencraft.feature.trail_ruins.description": "Allow Trail Ruins in surface noise terrain, including outside their usual biomes." } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/Rivencraft.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/Rivencraft.java index 5f34d05..a6eebfd 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/Rivencraft.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/Rivencraft.java @@ -59,9 +59,17 @@ public final class Rivencraft { private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MOD_ID); private static final DeferredRegister SOUNDS = DeferredRegister.create(Registries.SOUND_EVENT, MOD_ID); - public static final Supplier LINK_FISSURE = SOUNDS.register( - "linking.link-fissure", () -> SoundEvent.createVariableRangeEvent( - Identifier.fromNamespaceAndPath(MOD_ID, "linking.link-fissure"))); + public static final Supplier LINK = sound("linking.link"); + public static final Supplier LINK_DISARM = sound("linking.link-disarm"); + public static final Supplier LINK_INTRA = sound("linking.link-intra"); + public static final Supplier LINK_FISSURE = sound("linking.link-fissure"); + public static final Supplier LINK_PORTAL = sound("linking.link-portal"); + public static final Supplier METEOR_ROAR = sound("entity.meteor.roar"); + + private static Supplier sound(String name) { + return SOUNDS.register(name, () -> SoundEvent.createVariableRangeEvent( + Identifier.fromNamespaceAndPath(MOD_ID, name))); + } private static final DeferredRegister> BLOCK_ENTITY_TYPES = DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID); private static final DeferredRegister> MENUS = @@ -170,6 +178,7 @@ public final class Rivencraft { PortBiomeSources.register(modEventBus); PortChunkGenerators.register(modEventBus); NeoForge.EVENT_BUS.addListener(RuntimeAgeBackend::onLevelLoad); + NeoForge.EVENT_BUS.addListener(net.dnorberg.rivencraft.age.PortAgeWeather::onLevelTickPre); NeoForge.EVENT_BUS.addListener(PortAgeCommands::register); NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin); NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogout); 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 8800c84..5adbc75 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,22 @@ public final class PortAgeCommands { return 1; }))) .then(Commands.literal("age") + .then(Commands.literal("surface_probe") + .then(Commands.argument("name", StringArgumentType.word()) + .then(Commands.argument("x", IntegerArgumentType.integer()) + .then(Commands.argument("z", IntegerArgumentType.integer()) + .executes(PortAgeCommands::probeSurface))))) + .then(Commands.literal("create_weather") + .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(PortWeatherMode.values()) + .map(mode -> mode.name().toLowerCase(Locale.ROOT)), builder)) + .executes(PortAgeCommands::createWeatherAge))))) + .then(Commands.literal("weather_probe") + .then(Commands.argument("name", StringArgumentType.word()) + .executes(PortAgeCommands::probeWeather))) .then(Commands.literal("create") .then(Commands.argument("name", StringArgumentType.word()) .then(Commands.argument("seed", LongArgumentType.longArg()) @@ -302,6 +318,62 @@ public final class PortAgeCommands { .executes(PortAgeCommands::sampleBiome))))))); } + private static int probeSurface(CommandContext context) { + Identifier id = ageId(context); + ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id)); + if (level == null) { + context.getSource().sendFailure(Component.literal("Age not loaded: " + id)); + return 0; + } + int x = IntegerArgumentType.getInteger(context, "x"); + int z = IntegerArgumentType.getInteger(context, "z"); + var obstruction = PortAgeTravel.highestObstruction(level, x, z); + var landing = PortAgeTravel.findSurfaceLanding(level, x, z); + context.getSource().sendSuccess(() -> Component.literal("Surface probe " + id + + " highest=" + obstruction + " landing=" + landing), false); + return landing == null ? 0 : 1; + } + + private static int createWeatherAge(CommandContext context) { + try { + PortWeatherMode mode = PortWeatherMode.valueOf( + StringArgumentType.getString(context, "mode").toUpperCase(Locale.ROOT)); + Identifier id = ageId(context); + var server = context.getSource().getServer(); + // Exercise the Binder's page compiler too, not just direct backend creation. + var pages = java.util.List.of(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, + "weather/" + mode.name().toLowerCase(Locale.ROOT))); + var biomePlan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan( + pages, server.registryAccess()); + RuntimeAgeBackend.create(server, id, + LongArgumentType.getLong(context, "seed"), biomePlan, PortAgeTerrain.FLAT, + null, null, 0, null, null, mode); + return probeWeather(context); + } catch (IllegalArgumentException | IllegalStateException exception) { + context.getSource().sendFailure(Component.literal(exception.getMessage())); + return 0; + } + } + + private static int probeWeather(CommandContext context) { + Identifier id = ageId(context); + var server = context.getSource().getServer(); + ServerLevel loaded = server.getLevel(ResourceKey.create(Registries.DIMENSION, id)); + if (!(loaded instanceof PortAgeLevel level)) { + context.getSource().sendFailure(Component.literal("Age not loaded: " + id)); + return 0; + } + var weather = level.getWeatherData(); + context.getSource().sendSuccess(() -> Component.literal("Weather probe " + id + + " mode=" + level.weatherMode() + " raining=" + weather.isRaining() + + " thundering=" + weather.isThundering() + " rainTicks=" + weather.getRainTime() + + " thunderTicks=" + weather.getThunderTime() + + " independent=" + (weather != server.getWeatherData()) + + " overworldRaining=" + server.getWeatherData().isRaining() + + " overworldThundering=" + server.getWeatherData().isThundering()), false); + return 1; + } + private static int createAge(CommandContext context, PortAgeTerrain terrain) { return createAge(context, terrain, null); } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeLevel.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeLevel.java new file mode 100644 index 0000000..1e755ac --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeLevel.java @@ -0,0 +1,48 @@ +package net.dnorberg.rivencraft.age; + +import java.util.List; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.saveddata.WeatherData; +import net.minecraft.world.level.storage.DerivedLevelData; + +/** Runtime Age with independent native weather, rather than 26.3's shared server cycle. */ +final class PortAgeLevel extends ServerLevel { + private final WeatherData ageWeather; + private final PortWeatherSettings weatherSettings; + private final boolean cave; + + PortAgeLevel(MinecraftServer server, ResourceKey key, LevelStem stem, + long biomeZoomSeed, PortAgeTerrain terrain) { + super(server, server.executor, server.storageSource, + new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()), + key, stem, server.getWorldData().isDebugWorld(), biomeZoomSeed, List.of(), false); + ageWeather = getDataStorage().computeIfAbsent(WeatherData.TYPE); + weatherSettings = getDataStorage().computeIfAbsent(PortWeatherSettings.TYPE); + cave = terrain == PortAgeTerrain.CAVE; + PortAgeWeather.apply(this); + // The base constructor initializes visual levels from server weather. + // Replace them before publishing this Age or sending its join packets. + setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F); + setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F); + } + + @Override + public WeatherData getWeatherData() { + // The base constructor may query weather before our fields are initialized. + return ageWeather == null ? super.getWeatherData() : ageWeather; + } + + PortWeatherMode weatherMode() { return cave ? PortWeatherMode.OFF : weatherSettings.mode(); } + + void configureWeather(PortWeatherMode mode) { + weatherSettings.setMode(mode); + PortAgeWeather.apply(this); + setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F); + setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F); + getDataStorage().saveAndJoin(); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeTravel.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeTravel.java index 907255c..87a91c5 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeTravel.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeTravel.java @@ -11,8 +11,6 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.levelgen.Heightmap; -import net.minecraft.world.level.levelgen.FlatLevelSource; -import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; /** Shared, server-authoritative landing for the 26.3 book and operator probe. */ public final class PortAgeTravel { @@ -28,7 +26,9 @@ public final class PortAgeTravel { boolean fissure = target.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features && features.terrainFeatures() != null && features.terrainFeatures().starFissures(); BlockPos landing = fissure ? PortStarFissureFeature.savedSpawn(target) : null; - if (landing != null) landing = findFootedDestination(target, landing).orElse(null); + if (landing != null) landing = isCaveTerrain(target) + ? findFootedDestination(target, landing).orElse(null) + : findSurfaceLanding(target, landing.getX(), landing.getZ()); if (landing == null) landing = findLanding(target); if (landing == null) return "No safe test landing available; remaining in source dimension"; if (fissure) PortStarFissureFeature.generateOnce(target, landing, isCaveTerrain(target)); @@ -38,8 +38,6 @@ public final class PortAgeTravel { } private static BlockPos findLanding(ServerLevel target) { - net.minecraft.world.level.chunk.ChunkGenerator generator = target.getChunkSource().getGenerator(); - if (generator instanceof PortFeatureChunkGenerator features) generator = features.base(); if (isCaveTerrain(target)) { // The cave roof is not a valid arrival surface. Search down from // Legacy's Y=64 start, then construct the normal safety platform @@ -57,36 +55,40 @@ public final class PortAgeTravel { } return makePlatform(target, new BlockPos(0, 64, 0)); } - // A Flat Age has a known surface. Use it directly before a heightmap - // fallback can place a platform in the air above a low superflat world. - FlatLevelGeneratorSettings flatSettings = switch (generator) { - case FlatLevelSource flat -> flat.settings(); - case PortFlatChunkGenerator flat -> flat.settings(); - default -> null; - }; - if (flatSettings != null && !flatSettings.getLayersInfo().isEmpty()) { - int surfaceY = target.getMinY() + flatSettings.getLayersInfo().stream() - .mapToInt(layer -> layer.getHeight()).sum(); - BlockPos surface = new BlockPos(0, surfaceY, 0); - if (hasFooting(target, surface)) return surface; - } + BlockPos surface = findSurfaceLanding(target, 0, 0); + if (surface != null) return surface; + // Only build above the actual highest obstruction, never in a pocket + // beneath it. Void Ages retain the usual platform floor at Y=63. + BlockPos obstruction = highestObstruction(target, 0, 0); + int y = obstruction == null ? 64 : Math.max(64, obstruction.getY() + 3); + return makePlatform(target, new BlockPos(0, y, 0)); + } + + static BlockPos findSurfaceLanding(ServerLevel target, int originX, int originZ) { for (int radius = 0; radius <= 16; radius++) { for (int x = -radius; x <= radius; x++) { for (int z = -radius; z <= radius; z++) { if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue; - int y = target.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z); - BlockPos foot = new BlockPos(x, y, z); - if (hasFooting(target, foot)) { - return foot; - } + BlockPos floor = highestObstruction(target, originX + x, originZ + z); + if (floor != null && hasFooting(target, floor.above())) return floor.above(); } } } - // The operator/book probe needs a deterministic landing even when - // origin is ocean or foliage. Final book landing rules are ported later. - int y = Math.max(64, target.getHeight(Heightmap.Types.MOTION_BLOCKING, 0, 0) + 2); - if (y + 2 >= target.getMaxY()) return null; - return makePlatform(target, new BlockPos(0, y, 0)); + return null; + } + + /** First obstructing surface seen from the sky; never continue through a roof or liquid. */ + static BlockPos highestObstruction(ServerLevel level, int x, int z) { + var chunk = level.getChunk(x >> 4, z >> 4); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(x, level.getMaxY() - 1, z); + for (int y = level.getMaxY() - 1; y >= level.getMinY(); y--) { + pos.setY(y); + var state = chunk.getBlockState(pos); + if (!state.isAir() && (!state.getFluidState().isEmpty() + || state.is(net.minecraft.tags.BlockTags.LEAVES) + || !state.getCollisionShape(level, pos).isEmpty())) return pos.immutable(); + } + return null; } private static boolean isCaveTerrain(ServerLevel target) { @@ -119,8 +121,13 @@ public final class PortAgeTravel { } private static boolean hasFooting(ServerLevel target, BlockPos foot) { + if (foot.getY() <= target.getMinY() || foot.getY() + 1 >= target.getMaxY() + || !target.getWorldBorder().isWithinBounds(foot)) return false; BlockPos floor = foot.below(); - return target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP) + var floorState = target.getBlockState(floor); + return (floorState.isFaceSturdy(target, floor, Direction.UP) + || floorState.is(net.minecraft.tags.BlockTags.LEAVES)) + && floorState.getFluidState().isEmpty() && target.getFluidState(foot).isEmpty() && target.getFluidState(foot.above()).isEmpty() && passable(target, foot) && passable(target, foot.above()); diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeWeather.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeWeather.java new file mode 100644 index 0000000..4664048 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortAgeWeather.java @@ -0,0 +1,52 @@ +package net.dnorberg.rivencraft.age; + +import java.util.Map; +import java.util.WeakHashMap; +import net.minecraft.world.level.saveddata.WeatherData; +import net.neoforged.neoforge.event.tick.LevelTickEvent; + +/** 1.21.1 weather policies, applied before the independent native Age cycle advances. */ +public final class PortAgeWeather { + private static final Map STATES = new WeakHashMap<>(); + + private PortAgeWeather() {} + + public static void onLevelTickPre(LevelTickEvent.Pre event) { + if (event.getLevel() instanceof PortAgeLevel level) apply(level); + } + + static void apply(PortAgeLevel level) { + PortWeatherMode mode = level.weatherMode(); + if (mode == PortWeatherMode.NORMAL) { + STATES.remove(level); + return; + } + WeatherData weather = level.getWeatherData(); + if (mode == PortWeatherMode.FAST || mode == PortWeatherMode.SLOW) { + int state = (weather.isRaining() ? 1 : 0) | (weather.isThundering() ? 2 : 0); + Integer previous = STATES.put(level, state); + if (previous != null && previous == state) return; + // Keep persisted timers on reload; rescale only when a cycle changes. + if (previous == null && weather.getRainTime() > 0) return; + int scale = mode == PortWeatherMode.FAST ? 1 : 4; + int duration = 6000 * scale + level.getRandom().nextInt( + (weather.isRaining() ? 6000 : 84000) * scale); + set(weather, duration, weather.isRaining(), weather.isThundering()); + } else { + STATES.remove(level); + boolean rain = mode != PortWeatherMode.OFF; + boolean thunder = mode == PortWeatherMode.STORM; + if (weather.isRaining() != rain || weather.isThundering() != thunder + || weather.getRainTime() < 200 || weather.getThunderTime() < 200 + || weather.getClearWeatherTime() != 0) set(weather, 6000, rain, thunder); + } + } + + private static void set(WeatherData data, int duration, boolean rain, boolean thunder) { + data.setClearWeatherTime(0); + data.setRainTime(duration); + data.setThunderTime(duration); + data.setRaining(rain); + data.setThundering(thunder); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortCarverPages.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortCarverPages.java new file mode 100644 index 0000000..c3c7b10 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortCarverPages.java @@ -0,0 +1,78 @@ +package net.dnorberg.rivencraft.age; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.QuartPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.tags.BiomeTags; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.CarvingMask; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.WorldGenerationContext; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.carver.WorldCarver; + +/** Authored Caves/Ravines, independent of the terrain page, with 1.21.1 source seeds. */ +public final class PortCarverPages { + public static final Identifier CAVES = Identifier.fromNamespaceAndPath("rivencraft", "feature/caves"); + public static final Identifier RAVINES = Identifier.fromNamespaceAndPath("rivencraft", "feature/ravines"); + private static final long CAVE_SALT = 0x4341564553464541L; + private static final long RAVINE_SALT = 0x524156494E45534CL; + + private PortCarverPages() {} + + public static boolean supports(Identifier page) { return CAVES.equals(page) || RAVINES.equals(page); } + + public static void validateCounts(List pages) { + Map counts = new HashMap<>(); + for (Identifier page : pages) if (!supports(page) || counts.merge(page, 1, Integer::sum) > 32) + throw new IllegalArgumentException("Carver plan requires Caves/Ravines pages, at most 32 per type"); + } + + public static void validateRegistry(List pages, HolderLookup.Provider registries) { + if (pages.stream().noneMatch(PortCarverPages::supports)) return; + var carvers = registries.lookupOrThrow(Registries.CARVER); + for (String name : pages.contains(CAVES) ? List.of("cave", "nether_cave", "canyon") : List.of("canyon")) { + if (name.equals("canyon") && !pages.contains(RAVINES)) continue; + carvers.get(ResourceKey.create(Registries.CARVER, Identifier.withDefaultNamespace(name))) + .orElseThrow(() -> new IllegalArgumentException("Missing authored carver minecraft:" + name)); + } + } + + public static void carve(ChunkGenerator generator, ChunkAccess chunk, RandomState randomState, + HolderLookup.Provider registries, List pages, long seed, CarvingMask mask) { + if (pages.isEmpty()) return; + var registry = registries.lookupOrThrow(Registries.CARVER); + var biomes = generator.getBiomeSource().createUncachedResolver(randomState); + var context = new WorldGenerationContext(generator, chunk.getHeightAccessorForGeneration()); + var random = new WorldgenRandom(new LegacyRandomSource(0L)); + ChunkPos target = chunk.getPos(); + // 1.21.1 repeats identical seeds for duplicate Caves/Ravines pages. + // Their union is identical; avoid replaying the same mask up to 32 times. + for (Identifier page : pages.stream().distinct().toList()) { + boolean ravine = page.equals(RAVINES); + WorldCarver overworld = registry.getOrThrow(ResourceKey.create(Registries.CARVER, + Identifier.withDefaultNamespace(ravine ? "canyon" : "cave"))).value(); + WorldCarver nether = ravine ? null : registry.getOrThrow(ResourceKey.create(Registries.CARVER, + Identifier.withDefaultNamespace("nether_cave"))).value(); + for (int dx = -8; dx <= 8; dx++) { + for (int dz = -8; dz <= 8; dz++) { + ChunkPos source = new ChunkPos(target.x() + dx, target.z() + dz); + boolean isNether = biomes.getNoiseBiome(QuartPos.fromBlock(source.getMinBlockX()), 0, + QuartPos.fromBlock(source.getMinBlockZ())).is(BiomeTags.IS_NETHER); + if (ravine && isNether) continue; + WorldCarver carver = isNether ? nether : overworld; + random.setLargeFeatureSeed(seed ^ (ravine ? RAVINE_SALT : CAVE_SALT), source.x(), source.z()); + if (carver.isStartChunk(random)) carver.carve(context, random, target, source, mask); + } + } + } + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortFeatureChunkGenerator.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortFeatureChunkGenerator.java index 412cc08..7d01aa1 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortFeatureChunkGenerator.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortFeatureChunkGenerator.java @@ -113,12 +113,25 @@ public final class PortFeatureChunkGenerator extends ChunkGenerator { } @Override public ChunkGeneratorStructureState createState(HolderLookup sets, RandomState random, long seed) { + if (hasStructurePages()) { + var lookup = PortStructurePages.filtered(sets, terrainFeatures.structures(), getBiomeSource()); + var planning = PortStructurePages.planningBiomes(getBiomeSource(), sets, terrainFeatures.structures()); + if (base instanceof PortFlatChunkGenerator || base instanceof net.minecraft.world.level.levelgen.FlatLevelSource) + return ChunkGeneratorStructureState.createForFlat(random, seed, getOrigin(random), planning, + lookup.listElements().map(holder -> (Holder) holder)); + return ChunkGeneratorStructureState.createForNormal(random, seed, getOrigin(random), planning, lookup); + } return base.createState(sets, random, seed); } + + private boolean hasStructurePages() { + return terrainFeatures != null && !terrainFeatures.structures().isEmpty(); + } @Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state, StructureManager structures, ChunkAccess chunk, StructureTemplateManager templates, ResourceKey dimension) { - base.createStructures(registries, state, structures, chunk, templates, dimension); + if (hasStructurePages()) super.createStructures(registries, state, structures, chunk, templates, dimension); + else base.createStructures(registries, state, structures, chunk, templates, dimension); } @Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) { base.createReferences(level, structures, chunk); @@ -250,19 +263,27 @@ public final class PortFeatureChunkGenerator extends ChunkGenerator { int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 8); Holder biome = level.getBiome(new BlockPos( chunkPos.getMinBlockX() + 8, surface, chunkPos.getMinBlockZ() + 8)); - if (biome.is(BiomeTags.IS_END) || PortRegistryPages.isCaveBiome(biome)) return; + boolean surfaceBiome = !biome.is(BiomeTags.IS_END) && !PortRegistryPages.isCaveBiome(biome); var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE); + List mushroomHosts = null; for (int occurrence = 0; occurrence < vegetation.pages().size(); occurrence++) { Identifier page = vegetation.pages().get(occurrence); + RandomSource random = RandomSource.create(chunkSeed + ^ (long) page.hashCode() * 0x9E3779B97F4A7C15L + ^ (long) occurrence * 0xD1B54A32D192ED03L); + if (page.equals(PortVegetationPlan.SHELF_MUSHROOMS)) { + if (mushroomHosts == null) mushroomHosts = PortShelfMushrooms.findHosts(chunk); + PortShelfMushrooms.decorate(level, chunk, mushroomHosts, random); + continue; + } + if (!surfaceBiome) continue; Identifier placedId = PortVegetationPlan.placedFeature(page); PlacedFeature vanilla = registry.getOrThrow( ResourceKey.create(Registries.PLACED_FEATURE, placedId)).value(); PlacedFeature authored = new PlacedFeature(vanilla.feature(), vanilla.placement().stream() .filter(modifier -> !(modifier instanceof BiomeFilter)).toList()); - RandomSource random = RandomSource.create(chunkSeed - ^ (long) page.hashCode() * 0x9E3779B97F4A7C15L - ^ (long) occurrence * 0xD1B54A32D192ED03L); authored.place(level, this, random, origin); + if (PortVegetationPlan.addsLogs(page)) mushroomHosts = null; } } } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortShelfMushrooms.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortShelfMushrooms.java new file mode 100644 index 0000000..bd2833a --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortShelfMushrooms.java @@ -0,0 +1,70 @@ +package net.dnorberg.rivencraft.age; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.BlockTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.ShelfMushroomBlock; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.levelgen.feature.treedecorators.ShelfMushroomDecorator; +import net.minecraft.world.level.levelgen.feature.treedecorators.TreeDecorator; + +/** Shelf mushrooms on exposed log faces, regardless of how the logs were generated. */ +final class PortShelfMushrooms { + private PortShelfMushrooms() {} + + static List findHosts(ChunkAccess chunk) { + List logs = new ArrayList<>(); + var sections = chunk.getSections(); + for (int index = 0; index < sections.length; index++) { + var section = sections[index]; + if (!section.maybeHas(state -> state.is(BlockTags.LOGS))) continue; + int baseY = chunk.getSectionYFromSectionIndex(index) << 4; + for (int y = 0; y < 16; y++) for (int z = 0; z < 16; z++) for (int x = 0; x < 16; x++) { + if (section.getBlockState(x, y, z).is(BlockTags.LOGS)) + logs.add(new BlockPos(chunk.getPos().getMinBlockX() + x, baseY + y, + chunk.getPos().getMinBlockZ() + z)); + } + } + return logs; + } + + static void decorate(WorldGenLevel level, ChunkAccess chunk, List logs, RandomSource random) { + // Reuse vanilla replaceability/water checks, but not its tree-base height + // band: a log tendril or sphere has no meaningful tree base. + TreeDecorator.Context context = new TreeDecorator.Context(level, + (pos, state) -> level.setBlock(pos, state, 2), random, Set.of(), Set.of(), Set.of()); + for (BlockPos log : logs) { + if (!level.getBlockState(log).is(BlockTags.LOGS)) continue; + Direction first = Direction.Plane.HORIZONTAL.getRandomDirection(random); + for (Direction facing : List.of(first, first.getClockWise())) { + if (random.nextFloat() > 0.25F) continue; + BlockPos pos = log.relative(facing); + if ((pos.getX() >> 4) != chunk.getPos().x() || (pos.getZ() >> 4) != chunk.getPos().z()) continue; + if (pos.getY() < level.getMinY() || pos.getY() >= level.getMaxY()) continue; + if (!level.getFluidState(pos).isEmpty() + || !ShelfMushroomDecorator.isBlockReplaceableWithShelfMushroom(context, pos) + || level.getBlockState(pos.below()).is(Blocks.SHELF_MUSHROOM)) continue; + boolean adjacent = false; + for (Direction side : Direction.Plane.HORIZONTAL) { + if (level.getBlockState(pos.relative(side)).is(Blocks.SHELF_MUSHROOM)) { + adjacent = true; + break; + } + } + if (adjacent) continue; + var mushroom = Blocks.SHELF_MUSHROOM.defaultBlockState() + .setValue(ShelfMushroomBlock.FACING, facing) + .setValue(ShelfMushroomBlock.AGE, random.nextInt(2)); + if (!mushroom.canSurvive(level, pos)) continue; + context.setBlock(pos, mushroom); + break; + } + } + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortStructurePages.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortStructurePages.java new file mode 100644 index 0000000..12a5e2a --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortStructurePages.java @@ -0,0 +1,180 @@ +package net.dnorberg.rivencraft.age; + +import com.mojang.serialization.MapCodec; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; +import net.dnorberg.rivencraft.mixin.StructurePlacementAccessor; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.HolderOwner; +import net.minecraft.core.HolderSet; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.Climate; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement; + +/** 1.21.1 structure planning/density rules adapted to modern placement classes. */ +public final class PortStructurePages { + private static final Map SETS = Map.ofEntries( + vanillaSet("villages", "villages"), + vanillaSet("mineshafts", "mineshafts"), + vanillaSet("strongholds", "strongholds"), + vanillaSet("trial_chambers", "trial_chambers"), + vanillaSet("buried_treasure", "buried_treasures"), + vanillaSet("shipwrecks", "shipwrecks"), + vanillaSet("ocean_ruins", "ocean_ruins"), + vanillaSet("pillager_outposts", "pillager_outposts"), + vanillaSet("ruined_portals", "ruined_portals"), + vanillaSet("nether_fossils", "nether_fossils"), + vanillaSet("ancient_cities", "ancient_cities"), + vanillaSet("trail_ruins", "trail_ruins"), + vanillaSet("abandoned_camps", "abandoned_camp"), + Map.entry(page("nether_fortresses"), Identifier.fromNamespaceAndPath("rivencraft", "fortresses")), + Map.entry(page("bastions"), Identifier.fromNamespaceAndPath("rivencraft", "bastions"))); + // Suppress vanilla's combined Nether set so an authored Fortress page does + // not also introduce Bastions (or vice versa). + private static final Set CONTROLLED_SETS = Stream.concat(SETS.values().stream(), + Stream.of(Identifier.withDefaultNamespace("nether_complexes"))) + .collect(java.util.stream.Collectors.toUnmodifiableSet()); + private static final Set CAVE_ONLY = Set.of( + page("nether_fortresses"), page("bastions"), page("nether_fossils")); + private static final Set SURFACE_ONLY = Set.of( + page("villages"), page("strongholds"), page("buried_treasure"), page("shipwrecks"), + page("ocean_ruins"), page("pillager_outposts"), page("trail_ruins"), page("abandoned_camps")); + private static final Set FLAT_ALLOWED = Set.of(page("villages"), page("strongholds")); + + private PortStructurePages() {} + + private static Identifier page(String name) { + return Identifier.fromNamespaceAndPath("rivencraft", "feature/" + name); + } + + private static Map.Entry vanillaSet(String page, String set) { + return Map.entry(page(page), Identifier.withDefaultNamespace(set)); + } + + public static boolean supports(Identifier page) { return SETS.containsKey(page); } + + public static void validateCounts(List pages) { + Map counts = new HashMap<>(); + for (Identifier page : pages) { + if (!supports(page) || counts.merge(page, 1, Integer::sum) > 32) + throw new IllegalArgumentException("Structure plan requires supported pages, at most 32 per feature"); + } + } + + public static boolean supportsTerrain(Identifier page, PortAgeTerrain terrain) { + if (!supports(page) || terrain == PortAgeTerrain.VOID) return false; + if (terrain == PortAgeTerrain.FLAT && !FLAT_ALLOWED.contains(page)) return false; + if (CAVE_ONLY.contains(page)) return terrain == PortAgeTerrain.CAVE; + return terrain != PortAgeTerrain.CAVE || !SURFACE_ONLY.contains(page); + } + + public static void validateRegistry(List pages, HolderLookup.Provider registries) { + var sets = registries.lookupOrThrow(Registries.STRUCTURE_SET); + pages.stream().filter(PortStructurePages::supports).distinct().forEach(page -> + sets.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page))) + .orElseThrow(() -> new IllegalArgumentException("Missing structure set for " + page))); + } + + private static Map enabled(List pages) { + Map result = new HashMap<>(); + for (Identifier page : pages) result.merge(SETS.get(page), 1, Integer::sum); + return result; + } + + public static boolean authored(Structure structure, HolderLookup.Provider registries, List pages) { + var sets = registries.lookupOrThrow(Registries.STRUCTURE_SET); + return enabled(pages).keySet().stream() + .map(id -> sets.getOrThrow(ResourceKey.create(Registries.STRUCTURE_SET, id)).value()) + .flatMap(set -> set.structures().stream()).anyMatch(entry -> entry.structure().value() == structure); + } + + /** Extra possible biomes affect planning only; terrain still uses its original source. */ + public static BiomeSource planningBiomes(BiomeSource actual, HolderLookup sets, List pages) { + List> extra = enabled(pages).keySet().stream() + .map(id -> sets.getOrThrow(ResourceKey.create(Registries.STRUCTURE_SET, id)).value()) + .flatMap(set -> set.structures().stream()).flatMap(entry -> entry.structure().value().biomes().stream()) + .distinct().toList(); + return new BiomeSource() { + @Override protected MapCodec codec() { + throw new UnsupportedOperationException("Planning biome sources are never serialized"); + } + @Override protected Stream> collectPossibleBiomes() { + return Stream.concat(actual.possibleBiomes().stream(), extra.stream()); + } + @Override public net.minecraft.world.level.biome.BiomeResolver createResolver(Climate.Sampler sampler) { + return actual.createResolver(sampler); + } + }; + } + + public static HolderLookup filtered(HolderLookup source, List pages, + BiomeSource actual) { + Map counts = enabled(pages); + Map, Holder.Reference> copies = new HashMap<>(); + return new HolderLookup<>() { + @Override public Optional> get(ResourceKey key) { + if (CONTROLLED_SETS.contains(key.identifier()) && !counts.containsKey(key.identifier())) return Optional.empty(); + return source.get(key).filter(holder -> counts.containsKey(key.identifier()) || hasBiome(holder.value(), actual)) + .map(holder -> copies.computeIfAbsent(key, ignored -> { + int count = counts.getOrDefault(key.identifier(), 1); + return count == 1 ? holder : new LocalReference(key, scaled(holder.value(), count)); + })); + } + @Override public Optional> get(TagKey key) { return source.get(key); } + @Override public Stream> listElements() { + return source.listElements().flatMap(holder -> get(holder.key()).stream()); + } + @Override public Stream> listTags() { return source.listTags(); } + }; + } + + private static boolean hasBiome(StructureSet set, BiomeSource source) { + Set> possible = source.possibleBiomes(); + return set.structures().stream().flatMap(entry -> entry.structure().value().biomes().stream()).anyMatch(possible::contains); + } + + private static StructureSet scaled(StructureSet set, int count) { + var placement = set.placement(); + if (!(placement instanceof RandomSpreadStructurePlacement) + && !(placement instanceof ConcentricRingsStructurePlacement)) return set; + StructurePlacementAccessor settings = (StructurePlacementAccessor) placement; + if (placement instanceof RandomSpreadStructurePlacement random) { + float original = settings.rivencraft$frequency(); + float frequency = Math.min(1.0F, original * count); + double frequencyFactor = original > 0 ? frequency / original : 1; + int spacing = Math.max(1, (int) Math.round(random.spacing() / Math.sqrt(count / frequencyFactor))); + int separation = spacing == 1 ? 0 : Math.min(spacing - 1, + (int) Math.round((double) random.separation() * spacing / random.spacing())); + return new StructureSet(set.structures(), new RandomSpreadStructurePlacement(placement.locateOffset(), + settings.rivencraft$frequencyReductionMethod(), frequency, settings.rivencraft$salt(), + settings.rivencraft$exclusionZone(), spacing, separation, random.spreadType())); + } + if (placement instanceof ConcentricRingsStructurePlacement rings) { + int total = (int) Math.min(4095, (long) rings.count() * count); + int spread = Math.min(total, (int) Math.min(1023, (long) rings.spread() * count)); + return new StructureSet(set.structures(), new ConcentricRingsStructurePlacement(placement.locateOffset(), + settings.rivencraft$frequencyReductionMethod(), settings.rivencraft$frequency(), + settings.rivencraft$salt(), settings.rivencraft$exclusionZone(), rings.distance(), spread, + total, rings.preferredBiomes())); + } + return set; + } + + private static final class LocalReference extends Holder.Reference { + private static final HolderOwner OWNER = new HolderOwner<>() {}; + private LocalReference(ResourceKey key, StructureSet value) { super(Type.STAND_ALONE, OWNER, key, value); } + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortTerrainFeaturePlan.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortTerrainFeaturePlan.java index 947ff28..410fb13 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortTerrainFeaturePlan.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortTerrainFeaturePlan.java @@ -8,7 +8,8 @@ import net.dnorberg.rivencraft.Rivencraft; import net.minecraft.resources.Identifier; /** Frozen terrain filters and ordered solid/surface features for an Age. */ -public record PortTerrainFeaturePlan(List choices, boolean skylands, int denseOres, boolean starFissures) { +public record PortTerrainFeaturePlan(List choices, boolean skylands, int denseOres, + boolean starFissures, List structures, List carvers) { public static final Identifier FLOATING_ISLANDS = Identifier.fromNamespaceAndPath( Rivencraft.MOD_ID, "feature/floating_islands"); public static final Identifier TENDRILS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/tendrils"); @@ -27,7 +28,9 @@ public record PortTerrainFeaturePlan(List choices, boolean skylands, int Codec.BOOL.optionalFieldOf("skylands", false).forGetter(PortTerrainFeaturePlan::skylands), Codec.intRange(0, MAX_PER_FEATURE).optionalFieldOf("dense_ores", 0) .forGetter(PortTerrainFeaturePlan::denseOres), - Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures) + Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures), + Identifier.CODEC.listOf().optionalFieldOf("structures", List.of()).forGetter(PortTerrainFeaturePlan::structures), + Identifier.CODEC.listOf().optionalFieldOf("carvers", List.of()).forGetter(PortTerrainFeaturePlan::carvers) ).apply(instance, PortTerrainFeaturePlan::new)); public record Choice(Identifier page, Identifier material) { @@ -46,9 +49,13 @@ public record PortTerrainFeaturePlan(List choices, boolean skylands, int public PortTerrainFeaturePlan { choices = List.copyOf(choices); + structures = List.copyOf(structures); + carvers = List.copyOf(carvers); + PortStructurePages.validateCounts(structures); + PortCarverPages.validateCounts(carvers); if (denseOres < 0 || denseOres > MAX_PER_FEATURE) throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences"); - if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures) + if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty()) throw new IllegalArgumentException("Terrain feature plan must not be empty"); var counts = new HashMap(); for (Choice choice : choices) { @@ -57,6 +64,15 @@ public record PortTerrainFeaturePlan(List choices, boolean skylands, int } } + public PortTerrainFeaturePlan(List choices, boolean skylands, int denseOres, + boolean starFissures, List structures) { + this(choices, skylands, denseOres, starFissures, structures, List.of()); + } + + public PortTerrainFeaturePlan(List choices, boolean skylands, int denseOres, boolean starFissures) { + this(choices, skylands, denseOres, starFissures, List.of()); + } + public PortTerrainFeaturePlan(List choices, boolean skylands, int denseOres) { this(choices, skylands, denseOres, false); } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortVegetationPlan.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortVegetationPlan.java index dea2fac..7bfcc51 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortVegetationPlan.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortVegetationPlan.java @@ -6,11 +6,15 @@ import java.util.List; import net.dnorberg.rivencraft.Rivencraft; import net.minecraft.resources.Identifier; -/** Ordered, frozen authored vegetation pages for surface Ages. */ +/** Ordered, frozen plants and log decorations; not all require surface terrain. */ public record PortVegetationPlan(List pages) { public static final Identifier FLOWERS = page("flowers"); public static final Identifier TREES = page("trees"); public static final Identifier HUGE_TREES = page("huge_trees"); + public static final Identifier POPLAR_TREES = page("poplar_trees"); + public static final Identifier FALLEN_POPLAR_TREES = page("fallen_poplar_trees"); + public static final Identifier RED_SHRUBS = page("red_shrubs"); + public static final Identifier SHELF_MUSHROOMS = page("shelf_mushrooms"); public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( Identifier.CODEC.listOf().fieldOf("pages").forGetter(PortVegetationPlan::pages) ).apply(instance, PortVegetationPlan::new)); @@ -22,16 +26,30 @@ public record PortVegetationPlan(List pages) { } public static boolean isVegetation(Identifier page) { - return page.equals(FLOWERS) || page.equals(TREES) || page.equals(HUGE_TREES); + return page.equals(FLOWERS) || page.equals(TREES) || page.equals(HUGE_TREES) + || page.equals(POPLAR_TREES) || page.equals(FALLEN_POPLAR_TREES) + || page.equals(RED_SHRUBS) || page.equals(SHELF_MUSHROOMS); } public static Identifier placedFeature(Identifier page) { if (page.equals(FLOWERS)) return Identifier.withDefaultNamespace("flower_default"); if (page.equals(TREES)) return Identifier.withDefaultNamespace("trees_plains"); if (page.equals(HUGE_TREES)) return Identifier.withDefaultNamespace("trees_old_growth_spruce_taiga"); + if (page.equals(POPLAR_TREES)) return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "poplar_trees"); + if (page.equals(FALLEN_POPLAR_TREES)) return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "fallen_poplar_trees"); + if (page.equals(RED_SHRUBS)) return Identifier.withDefaultNamespace("patch_red_shrub"); throw new IllegalArgumentException("Unsupported vegetation page " + page); } + public static boolean addsLogs(Identifier page) { + return page.equals(TREES) || page.equals(HUGE_TREES) + || page.equals(POPLAR_TREES) || page.equals(FALLEN_POPLAR_TREES); + } + + public static boolean requiresSurface(Identifier page) { + return isVegetation(page) && !page.equals(SHELF_MUSHROOMS); + } + private static Identifier page(String name) { return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + name); } 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 new file mode 100644 index 0000000..b35f086 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortWeatherMode.java @@ -0,0 +1,33 @@ +package net.dnorberg.rivencraft.age; + +import com.mojang.serialization.Codec; +import java.util.List; +import net.dnorberg.rivencraft.Rivencraft; +import net.minecraft.resources.Identifier; + +/** The six active weather pages in the 1.21.1 writing system. */ +public enum PortWeatherMode { + NORMAL, FAST, SLOW, OFF, ALWAYS, STORM; + + public static final Codec CODEC = Codec.STRING.xmap( + name -> valueOf(name.toUpperCase(java.util.Locale.ROOT)), + mode -> mode.name().toLowerCase(java.util.Locale.ROOT)); + + public static PortWeatherMode forPage(Identifier page) { + if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null; + return switch (page.getPath()) { + case "weather/normal" -> NORMAL; + case "weather/fast" -> FAST; + case "weather/slow" -> SLOW; + case "weather/off" -> OFF; + case "weather/always" -> ALWAYS; + case "weather/storm" -> STORM; + default -> null; + }; + } + + 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/PortWeatherSettings.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortWeatherSettings.java new file mode 100644 index 0000000..7905803 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/age/PortWeatherSettings.java @@ -0,0 +1,24 @@ +package net.dnorberg.rivencraft.age; + +import com.mojang.serialization.Codec; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.level.saveddata.SavedDataType; + +/** Age-local policy; native WeatherData separately persists its live cycle. */ +final class PortWeatherSettings extends SavedData { + private static final Codec CODEC = PortWeatherMode.CODEC + .fieldOf("mode").codec().xmap(PortWeatherSettings::new, settings -> settings.mode); + static final SavedDataType TYPE = new SavedDataType<>( + Identifier.fromNamespaceAndPath("rivencraft", "weather_policy"), + () -> new PortWeatherSettings(PortWeatherMode.NORMAL), CODEC); + + private PortWeatherMode mode; + + private PortWeatherSettings(PortWeatherMode mode) { this.mode = mode; } + PortWeatherMode mode() { return mode; } + void setMode(PortWeatherMode mode) { + this.mode = mode; + setDirty(); + } +} 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 cf2f07c..1de5755 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 @@ -34,7 +34,6 @@ import net.minecraft.world.level.levelgen.NoiseGeneratorSettings; import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings; import net.minecraft.world.level.levelgen.flat.FlatLayerInfo; import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.storage.DerivedLevelData; import net.minecraft.world.level.storage.LevelResource; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.common.world.chunk.ForcedChunkManager; @@ -106,11 +105,20 @@ public final class RuntimeAgeBackend { PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks, PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures) { + return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, + terrainFeatures, PortWeatherMode.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) { 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); + ((PortAgeLevel) level).configureWeather(weather); index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures); // A newly bound book may be used immediately. Persist its Age identity // before publishing the catalog so a later crash cannot leave that @@ -403,9 +411,7 @@ public final class RuntimeAgeBackend { generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed); LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed)); long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed()); - ServerLevel level = new ServerLevel(server, server.executor, server.storageSource, - new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()), - key, stem, server.getWorldData().isDebugWorld(), biomeZoomSeed, List.of(), false); + ServerLevel level = new PortAgeLevel(server, key, stem, biomeZoomSeed, terrain); // Publish only after the constructor succeeds. If the load event fails, // roll back the map and close the newly opened chunk/entity stores. diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/StarFissureBlock.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/StarFissureBlock.java index 349e6ea..2b10889 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/StarFissureBlock.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/block/StarFissureBlock.java @@ -56,6 +56,11 @@ public final class StarFissureBlock extends EndPortalBlock { BlockPos landing = net.dnorberg.rivencraft.age.PortAgeTravel.findFootedDestination(overworld, spawn).orElse(spawn); level.playSound(null, pos, Rivencraft.LINK_FISSURE.get(), SoundSource.BLOCKS, 1.0F, 1.0F); return new TeleportTransition(overworld, Vec3.atBottomCenterOf(landing), entity.getDeltaMovement(), - entity.getYRot(), entity.getXRot(), TeleportTransition.PLACE_PORTAL_TICKET); + entity.getYRot(), entity.getXRot(), TeleportTransition.PLACE_PORTAL_TICKET.then(arrived -> { + // Departure audio can be discarded when the client switches levels. + // Play after arrival too so the traveler hears the successful return. + arrived.level().playSound(null, arrived.blockPosition(), Rivencraft.LINK_FISSURE.get(), + SoundSource.BLOCKS, 1.0F, 1.0F); + })); } } diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortBookBinderScreen.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortBookBinderScreen.java index 3c863fa..94549c7 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortBookBinderScreen.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/client/PortBookBinderScreen.java @@ -94,6 +94,8 @@ public final class PortBookBinderScreen extends AbstractContainerScreen { - vertex(vertices, matrix, 0, 0, 0, 1, light, overlay); - vertex(vertices, matrix, 1, 0, 1, 1, light, overlay); - vertex(vertices, matrix, 1, 1, 1, 0, light, overlay); - vertex(vertices, matrix, 0, 1, 0, 0, light, overlay); + vertex(vertices, matrix, 0, 0, 0, 1, light, overlay, art.color()); + vertex(vertices, matrix, 1, 0, 1, 1, light, overlay, art.color()); + vertex(vertices, matrix, 1, 1, 1, 0, light, overlay, art.color()); + vertex(vertices, matrix, 0, 1, 0, 0, light, overlay, art.color()); }); } @@ -44,12 +46,12 @@ public final class PortSymbolPageRenderer implements SpecialModelRenderer { public static final Unbaked INSTANCE = new Unbaked(); diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkVisualEffects.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkVisualEffects.java new file mode 100644 index 0000000..bff392d --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkVisualEffects.java @@ -0,0 +1,35 @@ +package net.dnorberg.rivencraft.item; + +import net.dnorberg.rivencraft.Rivencraft; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.phys.Vec3; + +/** Same successful-link feedback and sound precedence as the 1.21.1 target. */ +public final class LinkVisualEffects { + private LinkVisualEffects() {} + + public static SoundEvent sound(LinkProperties properties, boolean portal) { + if (properties.has(LinkProperties.DISARM)) return Rivencraft.LINK_DISARM.get(); + if (portal) return Rivencraft.LINK_PORTAL.get(); + if (properties.has(LinkProperties.INTRA_LINKING)) return Rivencraft.LINK_INTRA.get(); + return Rivencraft.LINK.get(); + } + + public static void play(ServerLevel source, Vec3 departure, ServerPlayer player, + LinkProperties properties, boolean portal) { + SoundEvent event = sound(properties, portal); + burst(source, departure, event); + burst(player.level(), player.position(), event); + } + + private static void burst(ServerLevel level, Vec3 position, SoundEvent event) { + level.playSound(null, position.x, position.y, position.z, event, + SoundSource.PLAYERS, 0.8F, level.getRandom().nextFloat() * 0.2F + 0.9F); + level.sendParticles(ParticleTypes.CLOUD, position.x, position.y, position.z, + 50, 0.5, 1.0, 0.5, 0.005); + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java index 2c3e3fd..c8d6624 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java @@ -78,6 +78,7 @@ public final class LinkingBookItem extends Item { /** Link Panel effects are shared by Linking and Descriptive Books. */ public static void applySuccessfulLink(ServerPlayer player, LinkProperties properties, ServerLevel source, Vec3 departure, Vec3 incoming, float destinationYaw) { + LinkVisualEffects.play(source, departure, player, properties, false); if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) { double speed = Math.hypot(incoming.x, incoming.z); double yaw = Math.toRadians(destinationYaw); diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/PageItem.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/PageItem.java index 642c4cd..23fb881 100644 --- a/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/PageItem.java +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/item/PageItem.java @@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.item; import java.util.function.Consumer; import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.writing.PortSymbolPages; +import net.dnorberg.rivencraft.writing.PortAuthoredWriting; import net.minecraft.ChatFormatting; import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; @@ -12,7 +13,7 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.item.component.TooltipDisplay; -/** One Page item with a persistent Link Panel variant; symbol writing is not ported yet. */ +/** Shared Page item with persistent Link Panel and authored symbol variants. */ public final class PageItem extends Item { public PageItem(Properties properties) { super(properties); @@ -72,8 +73,10 @@ public final class PageItem extends Item { ? description.copy().withStyle(ChatFormatting.GRAY, ChatFormatting.OBFUSCATED) : description.copy().withStyle(ChatFormatting.GRAY)); } - tooltip.accept(Component.translatable("item.rivencraft.page.symbol.port_status") - .withStyle(ChatFormatting.DARK_GRAY)); + boolean implemented = PortAuthoredWriting.isPageImplemented(symbolId); + tooltip.accept(Component.translatable(implemented + ? "item.rivencraft.page.symbol.implemented" : "item.rivencraft.page.symbol.unimplemented") + .withStyle(implemented ? ChatFormatting.DARK_GRAY : ChatFormatting.RED)); return; } LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE); diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeCarversMixin.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeCarversMixin.java new file mode 100644 index 0000000..49f02a2 --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeCarversMixin.java @@ -0,0 +1,36 @@ +package net.dnorberg.rivencraft.mixin; + +import com.llamalad7.mixinextras.sugar.Local; +import net.dnorberg.rivencraft.age.PortCarverPages; +import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.chunk.CarvingMask; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; +import net.minecraft.world.level.levelgen.NoiseChunk; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.blending.Blender; +import net.minecraft.world.level.levelgen.material.rule.MaterialRule; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** Merge authored geometry before vanilla consumes its mask with aquifer/surface rules. */ +@Mixin(NoiseBasedChunkGenerator.class) +public abstract class AgeCarversMixin { + @Inject(method = "generateCarvers", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/chunk/CarvingMask;isEmpty()Z")) + private void rivencraft$authoredCarvers(ChunkAccess chunk, Blender blender, NoiseChunk noise, + RandomState random, BiomeManager biomes, WorldGenRegion region, MaterialRule material, + CallbackInfo ci, @Local CarvingMask mask) { + if (region == null) return; + var level = region.getLevel(); + if (level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator generator + && generator.base() == (Object) this && generator.terrainFeatures() != null) { + PortCarverPages.carve((NoiseBasedChunkGenerator) (Object) this, chunk, random, + region.registryAccess(), generator.terrainFeatures().carvers(), level.getSeed(), mask); + } + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeStructureBiomeMixin.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeStructureBiomeMixin.java new file mode 100644 index 0000000..1f221dd --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/AgeStructureBiomeMixin.java @@ -0,0 +1,42 @@ +package net.dnorberg.rivencraft.mixin; + +import java.util.function.Predicate; +import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator; +import net.dnorberg.rivencraft.age.PortStructurePages; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.Climate; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +/** Matches 1.21.1's explicit-page biome override, scoped only to authored Age structures. */ +@Mixin(Structure.class) +public abstract class AgeStructureBiomeMixin { + // 26.3 jigsaw placement checks biome columns inside findGenerationPoint, + // before the final isValidBiome test. Set the predicate when constructing + // the context so both checks see the explicit-page override. + @ModifyArg(method = "generate", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/levelgen/structure/Structure$GenerationContext;(Lnet/minecraft/core/RegistryAccess;Lnet/minecraft/world/level/chunk/ChunkGenerator;Lnet/minecraft/world/level/biome/BiomeSource;Lnet/minecraft/world/level/biome/Climate$Sampler;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager;JLnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/LevelHeightAccessor;Ljava/util/function/Predicate;)V"), + index = 9) + private Predicate> rivencraft$authoredBiome( + RegistryAccess registries, ChunkGenerator chunkGenerator, BiomeSource biomes, + Climate.Sampler sampler, RandomState random, StructureTemplateManager templates, + long seed, ChunkPos chunk, LevelHeightAccessor height, Predicate> original) { + if (chunkGenerator instanceof PortFeatureChunkGenerator generator + && generator.terrainFeatures() != null + && PortStructurePages.authored((Structure) (Object) this, registries, + generator.terrainFeatures().structures())) { + return biome -> true; + } + return original; + } +} diff --git a/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StructurePlacementAccessor.java b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StructurePlacementAccessor.java new file mode 100644 index 0000000..e03011f --- /dev/null +++ b/ports/mc263/src/main/java/net/dnorberg/rivencraft/mixin/StructurePlacementAccessor.java @@ -0,0 +1,17 @@ +package net.dnorberg.rivencraft.mixin; + +import java.util.Optional; +import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +/** Read-only placement settings for Age-local copies, never edits global placements. */ +@Mixin(AbstractSpreadingStructurePlacement.class) +public interface StructurePlacementAccessor { + @Accessor("frequencyReductionMethod") + AbstractSpreadingStructurePlacement.FrequencyReductionMethod rivencraft$frequencyReductionMethod(); + @Accessor("frequency") float rivencraft$frequency(); + @Accessor("salt") int rivencraft$salt(); + @Accessor("exclusionZone") + Optional rivencraft$exclusionZone(); +} 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 549ccf3..9bb7b11 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 @@ -139,7 +139,8 @@ public final class PortBookNetworking { PortAuthoredWriting.vegetationPlan(pages), PortAuthoredWriting.dungeonCount(pages), PortAuthoredWriting.lakePlan(pages, seed), - PortAuthoredWriting.terrainFeaturePlan(pages, seed)); + PortAuthoredWriting.terrainFeaturePlan(pages, seed), + net.dnorberg.rivencraft.age.PortWeatherMode.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 410f30d..9470a1a 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 @@ -12,6 +12,9 @@ import net.dnorberg.rivencraft.age.PortRockPlan; import net.dnorberg.rivencraft.age.PortVegetationPlan; import net.dnorberg.rivencraft.age.PortLakePlan; 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.Rivencraft; import net.minecraft.core.HolderLookup; import net.minecraft.core.registries.Registries; @@ -45,6 +48,23 @@ public final class PortAuthoredWriting { private PortAuthoredWriting() {} + /** Port availability only, not validity of a particular ordered book. */ + public static boolean isPageImplemented(Identifier page) { + 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 (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; + return page.equals(ROCKS) || page.equals(DUNGEONS) || PortLakePlan.isLake(page) + || PortVegetationPlan.isVegetation(page) || PortTerrainFeaturePlan.isSupported(page) + || page.equals(PortTerrainFeaturePlan.SKYLANDS) + || page.equals(PortTerrainFeaturePlan.DENSE_ORES) + || page.equals(PortTerrainFeaturePlan.STAR_FISSURES) + || PortStructurePages.supports(page) || PortCarverPages.supports(page); + } + /** A page never disappears into an Age unless this port can apply its effect. */ public static boolean supports(List pages) { boolean terrain = false; @@ -58,13 +78,18 @@ public final class PortAuthoredWriting { int skylands = 0; int denseOres = 0; int starFissures = 0; + int weather = 0; Map terrainFeatures = new HashMap<>(); + Map structures = new HashMap<>(); + Map carvers = new HashMap<>(); for (Identifier page : pages) { if (pendingMaterial != null && !(pendingMaterial.equals("rivencraft:block") && (page.equals(ROCKS) || PortTerrainFeaturePlan.acceptsMaterial(page)) || pendingMaterial.equals("rivencraft:fluid") && PortLakePlan.isLake(page))) return false; if (isTerrain(page) && !terrain) { terrain = true; + } else if (PortWeatherMode.forPage(page) != null && weather++ == 0) { + // A single independent weather policy for this Age. } else if (controller(page) != null && controllers++ == 0) { // An explicit controller needs an authored pool. } else if (isBlockPage(page) && pendingMaterial == null @@ -91,6 +116,12 @@ public final class PortAuthoredWriting { } else if (page.equals(PortTerrainFeaturePlan.STAR_FISSURES) && starFissures < PortTerrainFeaturePlan.MAX_PER_FEATURE) { starFissures++; + } else if (PortCarverPages.supports(page) + && carvers.getOrDefault(page, 0) < PortTerrainFeaturePlan.MAX_PER_FEATURE) { + carvers.merge(page, 1, Integer::sum); + } else if (PortStructurePages.supports(page) + && structures.getOrDefault(page, 0) < PortTerrainFeaturePlan.MAX_PER_FEATURE) { + structures.merge(page, 1, Integer::sum); } else if (PortVegetationPlan.isVegetation(page) && vegetation < MAX_VEGETATION) { vegetation++; } else if (page.equals(DUNGEONS) && dungeons < MAX_DUNGEONS) { @@ -103,11 +134,17 @@ public final class PortAuthoredWriting { biomes++; } else return false; } - if (pendingMaterial != null || (rocks > 0 || vegetation > 0) + if (pendingMaterial != null || (rocks > 0 || pages.stream().anyMatch(PortVegetationPlan::requiresSurface)) && (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false; if ((dungeons > 0 || lakes > 0) && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN) || pages.contains(CAVE_TERRAIN))) return false; if (denseOres > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false; + if (!carvers.isEmpty() && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false; + PortAgeTerrain structureTerrain = pages.contains(VOID_TERRAIN) ? PortAgeTerrain.VOID + : pages.contains(FLAT_TERRAIN) ? PortAgeTerrain.FLAT + : pages.contains(CAVE_TERRAIN) ? PortAgeTerrain.CAVE : PortAgeTerrain.STANDARD; + if (structures.keySet().stream().anyMatch(page -> !PortStructurePages.supportsTerrain(page, structureTerrain))) + return false; if (pages.contains(PortTerrainFeaturePlan.FLOATING_ISLANDS) && pages.contains(CAVE_TERRAIN)) return false; if (pages.stream().anyMatch(PortTerrainFeaturePlan::isSurface) && (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false; @@ -184,8 +221,10 @@ public final class PortAuthoredWriting { boolean skylands = pages.contains(PortTerrainFeaturePlan.SKYLANDS); int denseOres = (int) pages.stream().filter(PortTerrainFeaturePlan.DENSE_ORES::equals).count(); boolean starFissures = pages.contains(PortTerrainFeaturePlan.STAR_FISSURES); - return choices.isEmpty() && !skylands && denseOres == 0 && !starFissures ? null - : new PortTerrainFeaturePlan(choices, skylands, denseOres, starFissures); + List structures = pages.stream().filter(PortStructurePages::supports).toList(); + List carvers = pages.stream().filter(PortCarverPages::supports).toList(); + return choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty() ? null + : new PortTerrainFeaturePlan(choices, skylands, denseOres, starFissures, structures, carvers); } private static List solidMaterials(List pages, long seed, Identifier feature) { @@ -255,19 +294,19 @@ public final class PortAuthoredWriting { if (!supports(pages)) throw new IllegalArgumentException( "Unsupported terrain or biome-page combination in this port slice"); List biomes = new ArrayList<>(); + PortStructurePages.validateRegistry(pages, registries); + PortCarverPages.validateRegistry(pages, registries); PortBiomePlan.Distribution distribution = null; boolean caveTerrain = pages.contains(CAVE_TERRAIN); boolean hasSurfaceBiome = false; boolean hasOreBiome = false; for (Identifier page : pages) { - if (isTerrain(page) || page.equals(ROCKS) || page.equals(DUNGEONS) - || page.equals(PortTerrainFeaturePlan.SKYLANDS) - || page.equals(PortTerrainFeaturePlan.DENSE_ORES) - || page.equals(PortTerrainFeaturePlan.STAR_FISSURES) - || PortLakePlan.isLake(page) || PortTerrainFeaturePlan.isSupported(page) || isBlockPage(page) - || PortVegetationPlan.isVegetation(page)) continue; PortBiomePlan.Distribution selected = controller(page); if (selected != null) { distribution = selected; continue; } + // supports() has already checked the whole book. Resolve only biome + // pages here, not every page absent from a growing exclusion list. + var entry = PortSymbolPages.find(page); + if (entry == null || !entry.category().equals("rivencraft:biome")) continue; Identifier biome = PortRegistryPages.biomeId(page); var registry = registries.lookupOrThrow(Registries.BIOME); var holder = registry.get(ResourceKey.create(Registries.BIOME, biome)) @@ -279,7 +318,7 @@ public final class PortAuthoredWriting { if (net.dnorberg.rivencraft.age.PortDenseOres.hasOres(holder)) hasOreBiome = true; biomes.add(biome); } - if (pages.stream().anyMatch(PortVegetationPlan::isVegetation) + if (pages.stream().anyMatch(PortVegetationPlan::requiresSurface) && !biomes.isEmpty() && !hasSurfaceBiome) throw new IllegalArgumentException("Vegetation pages need at least one surface Overworld biome"); if (pages.contains(PortTerrainFeaturePlan.DENSE_ORES) && !biomes.isEmpty() && !hasOreBiome) diff --git a/ports/mc263/src/main/resources/assets/rivencraft/sounds.json b/ports/mc263/src/main/resources/assets/rivencraft/sounds.json deleted file mode 100644 index 27995c8..0000000 --- a/ports/mc263/src/main/resources/assets/rivencraft/sounds.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "linking.link-fissure": { - "sounds": [{"name": "rivencraft:linking/link-fissure"}] - } -} diff --git a/ports/mc263/src/main/resources/data/rivencraft/worldgen/feature/poplar_trees.json b/ports/mc263/src/main/resources/data/rivencraft/worldgen/feature/poplar_trees.json new file mode 100644 index 0000000..4beb141 --- /dev/null +++ b/ports/mc263/src/main/resources/data/rivencraft/worldgen/feature/poplar_trees.json @@ -0,0 +1,8 @@ +{ + "type": "minecraft:weighted_random_selector", + "features": [ + {"data": "minecraft:red_poplar_leaf_litter", "weight": 200}, + {"data": "minecraft:orange_poplar_leaf_litter", "weight": 240}, + {"data": "minecraft:yellow_poplar_leaf_litter", "weight": 90} + ] +} diff --git a/ports/mc263/src/main/resources/data/rivencraft/worldgen/placed_feature/fallen_poplar_trees.json b/ports/mc263/src/main/resources/data/rivencraft/worldgen/placed_feature/fallen_poplar_trees.json new file mode 100644 index 0000000..5c3ff39 --- /dev/null +++ b/ports/mc263/src/main/resources/data/rivencraft/worldgen/placed_feature/fallen_poplar_trees.json @@ -0,0 +1,12 @@ +{ + "feature": "minecraft:fallen_poplar_tree", + "placement": [ + {"type": "minecraft:in_square"}, + {"type": "minecraft:surface_water_depth_filter", "max_water_depth": 0}, + {"type": "minecraft:heightmap", "heightmap": "OCEAN_FLOOR"}, + { + "type": "minecraft:block_predicate_filter", + "predicate": {"type": "minecraft:would_survive", "state": "minecraft:poplar_sapling"} + } + ] +} diff --git a/ports/mc263/src/main/resources/data/rivencraft/worldgen/placed_feature/poplar_trees.json b/ports/mc263/src/main/resources/data/rivencraft/worldgen/placed_feature/poplar_trees.json new file mode 100644 index 0000000..bb5d443 --- /dev/null +++ b/ports/mc263/src/main/resources/data/rivencraft/worldgen/placed_feature/poplar_trees.json @@ -0,0 +1,9 @@ +{ + "feature": "rivencraft:poplar_trees", + "placement": [ + {"type": "minecraft:count", "count": 6}, + {"type": "minecraft:in_square"}, + {"type": "minecraft:surface_water_depth_filter", "max_water_depth": 0}, + {"type": "minecraft:heightmap", "heightmap": "OCEAN_FLOOR"} + ] +} diff --git a/ports/mc263/src/main/resources/rivencraft.mixins.json b/ports/mc263/src/main/resources/rivencraft.mixins.json new file mode 100644 index 0000000..3e36dc3 --- /dev/null +++ b/ports/mc263/src/main/resources/rivencraft.mixins.json @@ -0,0 +1,8 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.dnorberg.rivencraft.mixin", + "compatibilityLevel": "JAVA_25", + "mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin"], + "injectors": {"defaultRequire": 1} +} diff --git a/ports/mc263/src/main/templates/META-INF/neoforge.mods.toml b/ports/mc263/src/main/templates/META-INF/neoforge.mods.toml index 7d5632b..e7f9bab 100644 --- a/ports/mc263/src/main/templates/META-INF/neoforge.mods.toml +++ b/ports/mc263/src/main/templates/META-INF/neoforge.mods.toml @@ -4,15 +4,18 @@ issueTrackerURL="https://git.paddedcell.org/dnorberg/rivencraft/issues" [[mods]] modId="${mod_id}" version="${mod_version}" -displayName="${mod_name} 26.3 Alpha" +displayName="${mod_name} 26.3 Beta" displayURL="https://git.paddedcell.org/dnorberg/rivencraft" authors="dnorberg and contributors" description=''' -Experimental Minecraft 26.3 alpha. Provides a runtime Age prototype and basic -Linking Book travel. Most Rivencraft gameplay has not yet been ported. Use a -test world and keep backups; this alpha is not compatible with 1.21.1 worlds. +Experimental Minecraft 26.3 beta with books, writing stations and selected +authored Age generation. Full 1.21.1 gameplay parity is still in progress. +Use a test world and keep backups; 1.21.1 worlds are not compatible. ''' +[[mixins]] +config="rivencraft.mixins.json" + [[dependencies.${mod_id}]] modId="neoforge" type="required"