Public Access
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be8440ddb5 | ||
|
|
f13bedd26f | ||
|
|
40770841ee | ||
|
|
38e40ddba3 | ||
|
|
a7a73d3c17 | ||
|
|
48fbae9be9 | ||
|
|
c223f3fdca | ||
|
|
d75154fa9b |
No files matched your search
@@ -7,9 +7,22 @@ of [Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to
|
||||
Minecraft 1.21.1 on NeoForge. Mystcraft veterans will recognize the writing
|
||||
flow, D'ni-inspired glyphs, libraries, crystals, and instability. Rivencraft
|
||||
also lets you author modern terrain, biomes, structures, seasons, and celestial
|
||||
bodies. It is an active pre-public-release project, not a byte-for-byte copy of
|
||||
bodies. It is an independently developed project, not a byte-for-byte copy of
|
||||
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, structure and vegetation pages, independent
|
||||
Age weather/lighting, seeded and colorable Overworld/Nether/End/Static skies,
|
||||
independent Sun/Moon controls, multiple bodies and eclipses, seeded biome and
|
||||
feature completion, and random structure selection. Nether/End noise-biome
|
||||
materials now follow their native rules. The beta does not yet provide full
|
||||
1.21.1 gameplay or optional-integration parity.
|
||||
See the [26.3 beta guide](ports/mc263/README.md) and
|
||||
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.106-beta.1.md).
|
||||
Keep the builds and their saves separate, and back up worlds before updating.
|
||||
|
||||
## Getting started
|
||||
|
||||
1. Make an Ink Vial from two black dyes, a glass bottle, and a water bucket.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 238 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
File diff suppressed because it is too large.
Load diff
+1456
-4
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,85 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.106-beta.1
|
||||
|
||||
Collects the player-validated port slices .99–.106 since 1.0.1.98-beta.1.
|
||||
This beta is not yet feature-complete with Minecraft 1.21.1.
|
||||
|
||||
## Books and travel
|
||||
|
||||
- Fallback Age platforms now use the dedicated non-collectible platform block
|
||||
from 1.21.1: opaque crystal texture, glass-like breaking strength, no drops
|
||||
or pick-block item. Existing ordinary Crystal Block platforms are unchanged.
|
||||
- Ported automatic Return Links for successful handheld Descriptive Book
|
||||
travel. `descriptive_books.return_link_mode` in `config/rivencraft-local.toml`
|
||||
defaults to **DISABLED**. `ON_CREATION` issues one on the first arrival per
|
||||
Age/world with that mode enabled; `ALWAYS` issues one on each arrival.
|
||||
- Return Links preserve departure dimension, position and facing. They are
|
||||
issued after Disarm and drop at the destination if inventory is full.
|
||||
First-arrival records persist across reload, reseeding and restart.
|
||||
- Successful handheld Linking and Descriptive Book travel now leaves the
|
||||
active book at departure as a recoverable open-book entity. Both hands are
|
||||
supported. Failed travel or canceled entity spawning restores the book.
|
||||
- Dropped books reuse the existing textures and skyward-facing model. They
|
||||
have a one-second pickup delay, no ordinary five-minute expiry, slow decay
|
||||
and water/rain damage, matching 1.21.1. Their components and decay counter
|
||||
persist through restart.
|
||||
|
||||
## Page collections and binding
|
||||
|
||||
- Ported Notebooks and Collation Folders with their existing editor and assets,
|
||||
capacities of 252 and 27 positions, sparse page storage, search, scrolling,
|
||||
page movement and inventory transfers. Link Panels are rejected by the editor.
|
||||
- Notebook crafting uses the existing leather/string pattern. The held
|
||||
collection is locked against ordinary menu movement. Escape closes the menu
|
||||
with search focused; search still consumes inventory/hotbar shortcuts.
|
||||
- Corrected the port's keyboard and mouse-button mappings, including arrow
|
||||
movement, right-click handling and scrollbar dragging.
|
||||
- The Book Binder accepts collection imports: shift-click to append pages or
|
||||
click a carried collection on the strip to insert them. Empty positions are
|
||||
skipped and occupied page order is retained. Imports validate all inputs and
|
||||
capacity before committing; successful imports return the empty collection.
|
||||
- Added category Symbol Portfolios to Creative, including registry-derived
|
||||
biome, material and fluid pages. Large categories split into volumes of up
|
||||
to 252 pages. Portfolios use the editor and Binder import flow.
|
||||
- Sealed Notebooks open into Folders containing 16 random pages using the
|
||||
1.21.1 pool/exclusions. Survival consumes one; Creative retains the item.
|
||||
Full inventories receive a dropped Folder. No new recipes are invented for
|
||||
Folders, Portfolios or Sealed Notebooks; discovery/library loot is pending.
|
||||
|
||||
## Writing Desk
|
||||
|
||||
- Ported the two-block Writing Desk, its existing model/textures, selected-page
|
||||
and white paper-stack display, and Legacy GUI atlases.
|
||||
- Ported 88 tabs cycled four at a time, category/search/sort/ALL views, collection
|
||||
and loose-page storage, page movement/extraction, and server-owned catalog
|
||||
synchronization. Link Panels cannot be stored or copied in the desk.
|
||||
- Copy previews are virtual. Taking a copy consumes one paper and one ink by
|
||||
default while retaining the original page. `writing.symbol_ink_cost` is
|
||||
configurable in `rivencraft-local.toml`.
|
||||
- Vials fill a 16-unit meter and return vanilla glass bottles. Refills are
|
||||
event-driven and require bottle space. Full inventory prevents shift-copy
|
||||
consumption. Unknown pages are retained but cannot be copied unresolved.
|
||||
- Ported crafting and adapted its recipe-unlock advancement to 26.3.
|
||||
Removing either desk half removes its partner and drops contents once.
|
||||
- Further corrections to the desk model, focused on page/paper placement,
|
||||
remain tracked for both versions in issue #113.
|
||||
|
||||
## Compatibility and validation
|
||||
|
||||
- Requires Minecraft **26.3**, NeoForge **26.3.0.99**, and **Java 25**. Dynamic
|
||||
Dimensions and Bad Packets are not required. The 1.21.1 implementation is
|
||||
unchanged. Use matching client/server builds; Age catalog protocol remains 10.
|
||||
- Player in-game checks passed. Both build targets passed. Targeted disposable
|
||||
server fixtures covered ownership recovery, collection/copy transactions,
|
||||
save/network codecs, block removal, reload/reseed and restart persistence.
|
||||
Servers were stopped and test worlds removed. This was not a full GameTest run.
|
||||
- Full writing recovery, remaining discovery/stations, instability, crystal
|
||||
portals, progression awards and optional integration parity remain unfinished.
|
||||
JEI guides and discovery loot for the newly ported interactions are pending.
|
||||
Unsupported effects remain marked red; Creative availability is not parity.
|
||||
- Back up saves. New Native Cave profiles from .97 onward cannot be read by
|
||||
earlier betas, and newly ported items/entities also require the newer build.
|
||||
Keep 1.21.1 and 26.3 saves separate. Machine acceleration (#112) and Native
|
||||
Cave default biome-pool policy (#111) remain separate planned work.
|
||||
|
||||
Report issues and help improve the wiki at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -0,0 +1,13 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.41-beta.1
|
||||
|
||||
This beta expands Age generation on Minecraft 26.3.
|
||||
|
||||
- Descriptive Books with no authored symbol pages now choose a seeded random terrain and biome distribution. The chosen plan is saved with the Age and restored after restart.
|
||||
- A Block material page followed by a Rocks feature page now generates small clusters of that block in newly generated surface chunks. This first feature path supports Standard, Amplified, Island, and Flat terrain; Cave and Void combinations remain unsupported.
|
||||
- Expanded dedicated-server checks cover random Age plans and authored Rocks generation across restart.
|
||||
|
||||
The player verified the Rocks feature in game. Random feature selection, duplicate feature pages, unpaired materials, and instability scoring are not yet ported. Existing chunks are not regenerated.
|
||||
|
||||
This 26.3 port is still experimental and does not yet include all features of the separate Minecraft 1.21.1 version. Use a test world and keep a backup. Minecraft 1.21.1 saves are not supported by this beta.
|
||||
|
||||
Requires Minecraft 26.3, NeoForge 26.3.0.99 or a compatible 26.3 build, and Java 25.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.54-beta.1
|
||||
|
||||
This beta collects the player-validated Age feature port slices since
|
||||
1.0.1.41-beta.1. It is not yet feature-complete with Minecraft 1.21.1.
|
||||
|
||||
## Age generation
|
||||
|
||||
- Rocks now use the established 1.21.1 forest-rock placement. Unmodified
|
||||
material-bearing features choose seeded materials independently; a preceding
|
||||
compatible Block or Fluid page selects the material explicitly.
|
||||
- Added Flowers, Trees, Huge Trees, Dungeons, Surface Lakes, Deep Lakes, and
|
||||
Small Lakes using the 1.21.1 placement rules adapted to 26.3.
|
||||
- Added Floating Islands, Tendrils, Skylands, Spheres, Spikes, and Obelisks
|
||||
using the existing Rivencraft generation algorithms.
|
||||
- Added blue Crystal Formations with budding crystal and initially seeded buds.
|
||||
- Added Dense Ores: repeated pages replay eligible biome ore placements,
|
||||
including compatible modded ores and explicitly tagged custom placements.
|
||||
- Added Star Fissures: one Legacy-shaped rift near the first arrival point,
|
||||
a starfield at the dimension's minimum Y, and return travel to the shared
|
||||
Overworld spawn with the original fissure sound. Cave roofs are preserved.
|
||||
- Feature plans, materials, repeated occurrences, and fissure completion
|
||||
persist across restart. Existing generated chunks are not redecorated.
|
||||
|
||||
## Current limits
|
||||
|
||||
- Authored features retain terrain restrictions. Most repeated features allow
|
||||
up to 32 occurrences; exceeding the current authoring cap blocks assembly.
|
||||
Skylands and Star Fissures retain their single-effect behavior when repeated.
|
||||
- Put a material page immediately before a compatible feature. Dangling
|
||||
material assignment, full Legacy grammar recovery, instability, and random
|
||||
feature selection are not yet ported. Pageless books randomize terrain and
|
||||
biome distribution, not the newly authored feature list.
|
||||
- 26.3 combines base terrain, surface, and carving. Added solid volumes run
|
||||
after that combined operation and before decoration, so base carvers do not
|
||||
cut those added volumes.
|
||||
- The full 1.21.1 writing/discovery loop, structures, environmental systems,
|
||||
crystal portals, and optional integration parity remain in progress. The
|
||||
presence of a Creative page is not a promise that its effect is ported.
|
||||
|
||||
Build and static checks passed. The player reported in-game passes for these
|
||||
slices. Earlier server smoke coverage was expanded, but the later generation
|
||||
slices were manually validated; this release does not claim a new full
|
||||
dedicated-server regression pass.
|
||||
|
||||
Requires Minecraft **26.3**, NeoForge **26.3.0.99** or a compatible 26.3 build,
|
||||
and **Java 25**. Dynamic Dimensions and Bad Packets are not required by this
|
||||
26.3 build. Do not use Minecraft 1.21.1 dependencies or saves with it. Back up
|
||||
the entire world before updating; beta-save compatibility is not guaranteed.
|
||||
|
||||
Report issues and help with documentation at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -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).
|
||||
@@ -0,0 +1,59 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.71-beta.1
|
||||
|
||||
This beta collects the player-tested lighting and sky port slices since
|
||||
1.0.1.65-beta.1. It is not yet feature-complete with Minecraft 1.21.1.
|
||||
|
||||
## Lighting and seeded environments
|
||||
|
||||
- Added Normal, Dark, and Bright lighting pages with saved per-Age settings.
|
||||
- Bright works in Cave terrain too, without adding skylight or weather.
|
||||
- Bright supplies effective ambient light level 15 to native combined-light
|
||||
checks and the white ambient rendering attribute. Vanilla darkness-based
|
||||
monster checks respond to that lighting; there is no separate mob-spawn
|
||||
veto. Mobs that ignore light retain their normal rules.
|
||||
- Missing weather and lighting controllers now resolve deterministically from
|
||||
the Age seed, using independent choices and Legacy-style Normal weighting.
|
||||
Explicit pages win; Cave terrain still forces weather Off. Existing Ages
|
||||
keep their saved choices rather than rerolling on load.
|
||||
|
||||
## Sky styles
|
||||
|
||||
- Added Overworld, Nether, End, and animated Static Sky pages, independently
|
||||
of terrain, weather, and lighting. Choices persist on reload and restart.
|
||||
- Nether Sky uses a reddish fog backdrop; End Sky uses the native End sky.
|
||||
These are visual styles, not Nether/End terrain or gameplay conversions:
|
||||
dimension geometry, coordinate scale, bed/water rules, and dragon behavior
|
||||
are not changed.
|
||||
- Static Sky adapts the existing Legacy-style renderer: six independent
|
||||
48×48 animated gray noise faces. A cached cube and small dynamic texture
|
||||
avoid rebuilding thousands of per-cell vertices each frame. Client-only
|
||||
rendering resources are released with the sky renderer.
|
||||
- Non-Overworld backgrounds have transparent clouds and a derived day
|
||||
timeline that avoids Overworld sky/fog/cloud color tracks while retaining
|
||||
gameplay and illumination schedules.
|
||||
- Updated supported-page feedback and added operator sky/environment probes.
|
||||
|
||||
## Current limits and testing
|
||||
|
||||
- Missing Sky pages still default to Overworld; random sky selection, color
|
||||
pages and individual celestial bodies remain pending. Number-controlled
|
||||
Static Sky resolution is tracked separately in issue #109 for both versions.
|
||||
- The writing/discovery loop, instability, portals, and optional-integration
|
||||
parity are still incomplete. Creative availability is not a support claim;
|
||||
check page tooltips and the beta guide for supported combinations.
|
||||
- Previously generated chunks are not regenerated. Save compatibility during
|
||||
the beta is not guaranteed; keep backups and keep both version tracks apart.
|
||||
|
||||
The player reported in-game passes for the included slices, including Static
|
||||
Sky. Build checks passed for both version tracks. Targeted disposable-server
|
||||
checks covered environment defaults, explicit choices, registered sky/lighting
|
||||
variants, persistence and restart, including Cave + Bright and Static Sky.
|
||||
This is not a claim of complete dedicated-server or optional-mod coverage.
|
||||
Test servers were stopped and temporary worlds removed.
|
||||
|
||||
Requires Minecraft **26.3**, NeoForge **26.3.0.99** and **Java 25**. Dynamic
|
||||
Dimensions and Bad Packets are not required by this build. The 1.21.1 release
|
||||
and its dependencies are unchanged.
|
||||
|
||||
Report issues and help improve the wiki at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -0,0 +1,53 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.78-beta.1
|
||||
|
||||
Collects the player-validated port slices since 1.0.1.71-beta.1. This beta is
|
||||
not yet feature-complete with the Minecraft 1.21.1 version.
|
||||
|
||||
## Sky and color writing
|
||||
|
||||
- Missing Sky pages now select a deterministic seeded style. Books with
|
||||
Normal Time and no explicit Sky select celestial-compatible Overworld Sky.
|
||||
Explicit Sky pages always win; weather and lighting choices stay independent.
|
||||
- Ported Red, Green, Blue, Black and White color pages. Preceding Number pages
|
||||
control component strength; unnumbered colors use seeded values. Colors mix
|
||||
before clamping the completed group. Colors before Sky tint that sky; a
|
||||
trailing color group also targets the sky.
|
||||
- Added an Ambient Color controller for an independent ambient-light tint.
|
||||
Color alone does not change effective illumination or monster-spawn rules.
|
||||
- Native, End and animated Static backgrounds use saved Age color settings.
|
||||
|
||||
## Normal Time celestial controls
|
||||
|
||||
- Length pages control the native day cycle; a preceding Number fine-tunes
|
||||
length. Zero Length freezes the cycle at a seeded time or an authored Phase,
|
||||
without interpolation bounce or changing the shared Overworld clock.
|
||||
- Rising, Zenith, Setting and Nadir Phase pages choose the starting phase.
|
||||
Clockwise and Counterclockwise control direction. North, East, South and
|
||||
West Angle pages choose orbit bearing. Preceding Number pages fine-tune
|
||||
Phase and Angle in 15-degree increments.
|
||||
- Small, Normal and Large Celestial size Normal Time's sun to 0.5×, 1× and 2×.
|
||||
A preceding Number overrides size from 0.25× to 3×. As in 1.21.1, the paired
|
||||
moon remains normal-sized. Size does not alter illumination or day length.
|
||||
- Settings persist through reload/restart and remain stable when reseeding
|
||||
future terrain. Older profiles retain native/default values.
|
||||
- Added operator diagnostics for environment, color and celestial profiles.
|
||||
|
||||
## Compatibility and testing
|
||||
|
||||
- Catalog protocol is now 6: use matching client/server builds.
|
||||
- Player in-game checks passed for the included slices. Build checks and
|
||||
targeted disposable-server diagnostics covered numeric modifiers, catalog
|
||||
round trips, unchanged sampled lighting, reload, reseed and restart.
|
||||
- Standalone Sun/Moon controllers, multiple bodies, per-body colors, eclipses,
|
||||
full writing/discovery, instability, portals and optional-integration parity
|
||||
remain incomplete. Creative availability does not imply full page support.
|
||||
- Number-controlled Static Sky resolution remains tracked in issue #109;
|
||||
time-page naming improvements remain tracked in issue #110.
|
||||
- Existing chunks are not regenerated. Save compatibility is not guaranteed
|
||||
during beta; keep backups and keep the two Minecraft version tracks apart.
|
||||
|
||||
Requires Minecraft **26.3**, NeoForge **26.3.0.99**, and **Java 25**. Dynamic
|
||||
Dimensions and Bad Packets are not required. Minecraft 1.21.1 is unchanged.
|
||||
|
||||
Report issues and help improve the wiki at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -0,0 +1,83 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.93-beta.1
|
||||
|
||||
Collects the player-validated port slices .79–.93 since 1.0.1.78-beta.1.
|
||||
This beta is not yet feature-complete with Minecraft 1.21.1.
|
||||
|
||||
## Celestial writing and rendering
|
||||
|
||||
- Ported standalone Sun and Moon pages and multiple independently authored
|
||||
bodies, up to ten of each. Each controller consumes its own ordered Color,
|
||||
Length, Phase, Direction, Angle and Size modifiers; Number pages fine-tune
|
||||
supported values. Normal Time retains its paired sun/moon behavior.
|
||||
- Color groups before Normal Time tint both bodies; independent Sun/Moon
|
||||
groups can use different colors. Authored moons use full-moon sprites as in
|
||||
1.21.1; native Normal Time retains calendar moon phases.
|
||||
- Authored bodies render over Overworld, Nether, End and animated Static
|
||||
backgrounds. Opaque centers retain transparent glows. Eclipse alignment
|
||||
affects body color, background shading and gameplay sky light.
|
||||
- Bare Sun/Moon pages receive independent seeded colors, sizes, bearings,
|
||||
phases, directions and lengths, including stopped cycles. Explicit modifier
|
||||
groups retain their authored behavior.
|
||||
- Books without celestial controllers seed-complete 0–3 suns and 0–3 moons.
|
||||
Empty skies do not accidentally draw a vanilla pair. Explicit controllers
|
||||
suppress extra-body completion. Link-Panel-only random Ages also receive
|
||||
seeded sky/fog colors using the 1.21.1 palette.
|
||||
- All resolved settings are saved, not rerolled by viewing a book, restarting,
|
||||
or reseeding future terrain. Existing Age profiles remain unchanged.
|
||||
|
||||
## Biome and feature completion
|
||||
|
||||
- Explicit biome-distribution pages can now seed-complete missing biome
|
||||
pools. Single selects one; other nonnative controllers require two distinct
|
||||
biomes. Fully omitted pools use the seeded continuation rule, bounded by the
|
||||
available biome pool. Authored pools fill only the missing minimum.
|
||||
- Supplied biome order and duplicate weights are retained. Two distinct
|
||||
supplied biomes do not acquire an unwanted third biome. Cave pools use
|
||||
recognized Cave/Nether biomes; Void does not inject The Void alongside
|
||||
supplied biome pages.
|
||||
- Unpaired Block/Fluid pages receive seeded terrain-compatible implied
|
||||
features. Consecutive materials resolve independently; explicit pairs remain
|
||||
authoritative. Unsupported pairings and the current feature caps remain.
|
||||
- Link-Panel-only Ages roll 0–2 distinct material feature types with Legacy
|
||||
rank weighting and compatible registered solids/source fluids. Additional
|
||||
rare rolls cover Skylands, Crystal Formations and Star Fissures.
|
||||
- Random Ages also roll 0–2 distinct structure families, filtered by terrain,
|
||||
registry availability and the chosen biome pool. Newer structures, including
|
||||
26.3 Abandoned Camps, are rare candidates. An intentionally empty selection
|
||||
disables page-controlled structure sets rather than enabling all of them.
|
||||
- Cave terrain now supports authored and randomly selected Dungeons, matching
|
||||
1.21.1. Dungeon placement retains eight monster-room attempts per page and
|
||||
modern build-height anchors. Flat/Void Dungeons and Cave lakes remain
|
||||
unsupported.
|
||||
|
||||
## Nether and End material correction
|
||||
|
||||
- Ported 1.21.1's biome-specific material-rule layer for noise terrain. Nether
|
||||
biomes in Cave Ages now use their native Netherrack, Nylium, Soul Sand/Soil,
|
||||
Basalt and Blackstone rules instead of Overworld grass. End biomes use End
|
||||
Stone. Other biomes retain the original terrain rules.
|
||||
- Geometry, biome selection, dry Cave aquifers and other environment settings
|
||||
are preserved. Flat/Void layers and authored feature materials are unchanged.
|
||||
- Existing Ages pick up the correction after reload, but only new chunks
|
||||
change. Already generated grass is not replaced.
|
||||
|
||||
## Compatibility and validation
|
||||
|
||||
- Requires Minecraft **26.3**, NeoForge **26.3.0.99**, and **Java 25**. Dynamic
|
||||
Dimensions and Bad Packets are not required. Minecraft 1.21.1 is unchanged.
|
||||
- Catalog protocol is **10**: use matching client/server builds.
|
||||
- Player in-game checks passed for all included slices. Builds and targeted
|
||||
disposable-server checks covered writing, seeded completion, codecs/catalog
|
||||
round trips, terrain materials, reload, reseed and restart. Temporary servers
|
||||
and test worlds were removed. This is not a full automated GameTest run.
|
||||
- Full writing recovery, discovery/stations, instability, portals and optional
|
||||
integration parity remain unfinished. Random-page eligibility config also
|
||||
remains pending; valuable/hazardous random materials currently lack the
|
||||
finished version's instability scoring. Creative availability does not imply
|
||||
full page support. Number-controlled Static resolution (#109) and time-page
|
||||
naming (#110) remain separate work.
|
||||
- Beta save compatibility is not guaranteed. Back up worlds and keep the two
|
||||
Minecraft version tracks and saves separate.
|
||||
|
||||
Report issues and help improve the wiki at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -0,0 +1,63 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.98-beta.1
|
||||
|
||||
Collects the player-validated port slices .94–.98 since 1.0.1.93-beta.1.
|
||||
This beta is not yet feature-complete with Minecraft 1.21.1.
|
||||
|
||||
## Seeded terrain and biome completion
|
||||
|
||||
- Books without a Terrain page now choose terrain using the 1.21.1 weighted
|
||||
selector. Incompatible choices are replaced with a seeded compatible terrain;
|
||||
explicit Terrain pages retain their authored behavior.
|
||||
- Omitted biome controllers now use the seeded weighted distribution selector.
|
||||
Supplied biome pages are retained, with missing minimum slots completed.
|
||||
Void without biome pages still uses The Void. Link-Panel-only random Ages
|
||||
retain their existing selection path.
|
||||
- Missing nonnative biome pools account for authored structures. Single chooses
|
||||
a terrain-compatible biome satisfying all structure requirements where one
|
||||
exists. Multiple-biome distributions add compatible biomes for unmet structure
|
||||
requirements before completing their ordinary minimum. Explicit Single
|
||||
choices are never replaced.
|
||||
- New Native Cave Ages freeze the full recognized Cave/Nether biome pool and
|
||||
distribute it using seeded Tiny regions, matching the 1.21.1 implementation.
|
||||
This includes recognized modded cave biomes. Existing empty Native Cave
|
||||
profiles retain their previous Lush Caves fallback; explicit Single is unchanged.
|
||||
- Removing Nether-style biomes from the default Native Cave pool is separate
|
||||
planned work for both versions in issue #111, not part of this release.
|
||||
|
||||
## Accelerated pages
|
||||
|
||||
- Ported the Accelerated environment page. Repeated pages stack, each adding
|
||||
three extra random positions per eligible block section per tick near players.
|
||||
- Defaults match 1.21.1: at most eight already-loaded candidate chunks and
|
||||
16,384 extra random-tick attempts per Age tick, shared fairly across sections.
|
||||
No extra chunks are force-loaded.
|
||||
- Limits are configurable in `config/rivencraft-local.toml`, under
|
||||
`accelerated_age`. A zero attempt budget disables the extra ticks.
|
||||
- Page counts persist across unload/reload, reseeding and restart. Existing
|
||||
Ages default to zero. Pageless random Ages do not gain Accelerated automatically.
|
||||
- This currently affects random-ticking blocks such as crops and saplings.
|
||||
Machine/block-entity acceleration is deferred for both versions under issue
|
||||
#112; entities and scheduled block ticks are also unaffected. Instability
|
||||
scoring remains part of the unfinished instability port.
|
||||
- Operator command `/rivencraft_port age acceleration <name>` reports the
|
||||
saved page count and current budgets.
|
||||
|
||||
## Compatibility and validation
|
||||
|
||||
- Requires Minecraft **26.3**, NeoForge **26.3.0.99**, and **Java 25**. Dynamic
|
||||
Dimensions and Bad Packets are not required. Minecraft 1.21.1 is unchanged.
|
||||
- Catalog protocol remains **10**. Use matching client/server builds.
|
||||
- Player in-game checks passed. Builds and targeted disposable-server checks
|
||||
covered seeded completion, Native Cave sampling, tick-budget arithmetic,
|
||||
unload/reload, reseeding and restart. Test servers were stopped and their
|
||||
worlds removed. This was not a full automated GameTest run.
|
||||
- Back up worlds before updating. New populated Native Cave profiles cannot
|
||||
be read by earlier beta builds; do not downgrade those saves. Keep Minecraft
|
||||
1.21.1 and 26.3 saves separate.
|
||||
- Full writing recovery, discovery/stations, instability, crystal portals and
|
||||
optional integration parity remain unfinished. Random-page eligibility config
|
||||
and modded biome surface-rule integration also remain pending. Creative page
|
||||
availability does not imply full support.
|
||||
|
||||
Report issues and help improve the wiki at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -121,9 +121,80 @@ def generatePortLanguage = tasks.register('generatePortLanguage', GeneratePortLa
|
||||
}
|
||||
sourceSets.main.resources.srcDir generatePortLanguage
|
||||
|
||||
// Derive registered presentation variants without duplicating vanilla gameplay settings.
|
||||
abstract class GeneratePortDimensionTypes extends DefaultTask {
|
||||
@InputFile abstract RegularFileProperty getMinecraftJar()
|
||||
@OutputDirectory abstract DirectoryProperty getOutputDir()
|
||||
|
||||
@TaskAction void generate() {
|
||||
new java.util.zip.ZipFile(minecraftJar.get().asFile).withCloseable { archive ->
|
||||
def entry = archive.getEntry('data/minecraft/dimension_type/overworld.json')
|
||||
if (entry == null) throw new GradleException('Missing vanilla Overworld dimension type')
|
||||
String template = archive.getInputStream(entry).withCloseable { it.getText('UTF-8') }
|
||||
def dayEntry = archive.getEntry('data/minecraft/timeline/day.json')
|
||||
if (dayEntry == null) throw new GradleException('Missing vanilla day timeline')
|
||||
def day = archive.getInputStream(dayEntry).withCloseable {
|
||||
new groovy.json.JsonSlurper().parseText(it.getText('UTF-8'))
|
||||
}
|
||||
// Keep gameplay schedules and illumination, but do not tint these
|
||||
// native non-Overworld sky backgrounds blue/black with the day cycle.
|
||||
['fog_color', 'sky_color', 'cloud_color'].each { day.tracks.remove("minecraft:visual/${it}".toString()) }
|
||||
// Markers are global per clock and already owned by minecraft:day.
|
||||
day.remove('time_markers')
|
||||
File dayFile = new File(outputDir.get().asFile, 'data/rivencraft/timeline/age_sky_day.json')
|
||||
dayFile.parentFile.mkdirs()
|
||||
dayFile.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(day)) + '\n'
|
||||
File timelineTag = new File(outputDir.get().asFile, 'data/rivencraft/tags/timeline/age_sky.json')
|
||||
timelineTag.parentFile.mkdirs()
|
||||
timelineTag.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson([
|
||||
values: ['#minecraft:universal', 'rivencraft:age_sky_day',
|
||||
'minecraft:moon', 'minecraft:early_game']])) + '\n'
|
||||
['overworld', 'nether', 'end', 'static'].each { sky ->
|
||||
['age_normal', 'age_dark', 'age_bright', 'age_cave_bright'].each { lighting ->
|
||||
if (sky == 'overworld' && lighting == 'age_normal') return
|
||||
String name = lighting + (sky == 'overworld' ? '' : "_sky_${sky}")
|
||||
def definition = new groovy.json.JsonSlurper().parseText(template)
|
||||
definition.has_skylight = lighting in ['age_normal', 'age_bright']
|
||||
definition.ambient_light = lighting in ['age_bright', 'age_cave_bright'] ? 1.0 : 0.0
|
||||
// 26.3's lightmap reads this attribute, not ambient_light.
|
||||
if (definition.ambient_light == 1.0)
|
||||
definition.attributes['minecraft:visual/ambient_light_color'] = '#ffffff'
|
||||
if (sky != 'overworld') {
|
||||
definition.skybox = sky == 'end' ? 'end' : 'none'
|
||||
definition.attributes['minecraft:visual/fog_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#181318' : '#6b2725'
|
||||
definition.attributes['minecraft:visual/sky_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#000000' : '#6b2725'
|
||||
definition.attributes['minecraft:visual/cloud_color'] = '#00000000'
|
||||
if (sky == 'nether') {
|
||||
definition.attributes['minecraft:visual/fog_start_distance'] = 10.0
|
||||
definition.attributes['minecraft:visual/fog_end_distance'] = 96.0
|
||||
}
|
||||
definition.timelines = '#rivencraft:age_sky'
|
||||
}
|
||||
File result = new File(outputDir.get().asFile, "data/rivencraft/dimension_type/${name}.json")
|
||||
result.parentFile.mkdirs()
|
||||
result.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(definition)) + '\n'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def generatePortDimensionTypes = tasks.register('generatePortDimensionTypes', GeneratePortDimensionTypes) {
|
||||
dependsOn 'createMinecraftArtifacts'
|
||||
minecraftJar.set(layout.buildDirectory.file("moddev/artifacts/minecraft-patched-${neo_version}.jar"))
|
||||
outputDir.set(layout.buildDirectory.dir('generated/resources/dimensionTypes'))
|
||||
}
|
||||
sourceSets.main.resources.srcDir generatePortDimensionTypes
|
||||
|
||||
tasks.named('processResources', ProcessResources).configure {
|
||||
// Reuse the existing sprite and base model without copying binary assets.
|
||||
from('../../src/main/resources') {
|
||||
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.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'
|
||||
@@ -132,6 +203,23 @@ tasks.named('processResources', ProcessResources).configure {
|
||||
include 'assets/rivencraft/textures/gui/symbolcomponents.png'
|
||||
include 'assets/rivencraft/models/item/page_link_panel.json'
|
||||
include 'assets/rivencraft/textures/item/ink_vial.png'
|
||||
include 'assets/rivencraft/textures/item/notebook.png'
|
||||
include 'assets/rivencraft/textures/item/collation_folder.png'
|
||||
include 'assets/rivencraft/models/item/notebook.json'
|
||||
include 'assets/rivencraft/models/item/collation_folder.json'
|
||||
include 'assets/rivencraft/textures/item/sealed_notebook.png'
|
||||
include 'assets/rivencraft/textures/item/symbol_portfolio.png'
|
||||
include 'assets/rivencraft/models/item/sealed_notebook.json'
|
||||
include 'assets/rivencraft/models/item/symbol_portfolio.json'
|
||||
include 'assets/rivencraft/textures/item/writing_desk.png'
|
||||
include 'assets/rivencraft/textures/block/writing_desk.png'
|
||||
include 'assets/rivencraft/textures/gui/writing_desk_legacy.png'
|
||||
include 'assets/rivencraft/textures/gui/scrollbar.png'
|
||||
include 'assets/rivencraft/models/item/writing_desk.json'
|
||||
include 'assets/rivencraft/models/block/writing_desk.json'
|
||||
include 'assets/rivencraft/models/block/writing_desk_extension.json'
|
||||
include 'assets/rivencraft/blockstates/writing_desk.json'
|
||||
include 'data/rivencraft/loot_table/blocks/writing_desk.json'
|
||||
include 'assets/rivencraft/textures/item/ink_vial.png.mcmeta'
|
||||
include 'assets/rivencraft/models/item/ink_vial.json'
|
||||
include 'assets/rivencraft/models/item/crystal_shard.json'
|
||||
@@ -144,6 +232,8 @@ tasks.named('processResources', ProcessResources).configure {
|
||||
include 'assets/rivencraft/textures/gui/notebook_pages.png'
|
||||
include 'assets/rivencraft/textures/gui/bookui_pager.png'
|
||||
include 'assets/rivencraft/textures/block/crystal_block.png'
|
||||
include 'assets/rivencraft/textures/entity/linking_book.png'
|
||||
include 'assets/rivencraft/textures/entity/descriptive_book.png'
|
||||
include 'assets/rivencraft/textures/block/book_binder.png'
|
||||
include 'assets/rivencraft/textures/block/book_binder_press.png'
|
||||
include 'assets/rivencraft/textures/gui/book_binder_legacy.png'
|
||||
@@ -162,6 +252,8 @@ tasks.named('processResources', ProcessResources).configure {
|
||||
include 'data/rivencraft/loot_table/blocks/book_binder.json'
|
||||
include 'assets/rivencraft/models/block/crystal_block.json'
|
||||
include 'assets/rivencraft/blockstates/crystal_block.json'
|
||||
include 'assets/rivencraft/models/block/cloud_platform.json'
|
||||
include 'assets/rivencraft/blockstates/cloud_platform.json'
|
||||
include 'assets/rivencraft/blockstates/budding_crystal.json'
|
||||
include 'assets/rivencraft/blockstates/small_crystal_bud.json'
|
||||
include 'assets/rivencraft/blockstates/medium_crystal_bud.json'
|
||||
|
||||
@@ -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.39-beta.1
|
||||
mod_version=1.0.1.106-beta.1
|
||||
@@ -6,19 +6,61 @@
|
||||
"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.sky.overworld.description": "Use the native Overworld sky. Terrain and lighting remain independently authored.",
|
||||
"symbol.rivencraft.sky.nether.description": "Use a Nether-style reddish fog backdrop. Authored celestials render over it; terrain and gameplay rules remain independent.",
|
||||
"symbol.rivencraft.sky.end.description": "Use the native End sky backdrop with authored celestials over it, without End terrain or a dragon fight.",
|
||||
"symbol.rivencraft.sky.static.description": "Animated static surrounds the Age, behind authored celestials. Preceding colors tint it; otherwise it is gray.",
|
||||
"symbol.rivencraft.lighting.ambient_color": "Ambient Light Color",
|
||||
"symbol.rivencraft.lighting.ambient_color.description": "Apply preceding shared colors to ambient light. Use Bright separately for full effective ambient lighting.",
|
||||
"symbol.rivencraft.number.description": "Before Color: value × 10 per channel. Before Length: value × 0.08 days. Before Phase/Angle: value × 15 degrees. Before Size: 0.25× to 3× (0-24).",
|
||||
"symbol.rivencraft.size.small.description": "Before Sun/Moon: 0.5× body size. Before Normal Time: size the sun only. A preceding Number overrides the size.",
|
||||
"symbol.rivencraft.size.normal.description": "Before Sun/Moon: 1× body size. Before Normal Time: size the sun only. A preceding Number selects 0.25× through 3× (0-24).",
|
||||
"symbol.rivencraft.size.large.description": "Before Sun/Moon: 2× body size. Before Normal Time: size the sun only. A preceding Number selects 0.25× through 3× (0-24).",
|
||||
"symbol.rivencraft.angle.north.description": "Before Normal Time, Sun or Moon: set orbit bearing to 0 degrees. A preceding Number overrides Angle in 15-degree steps.",
|
||||
"symbol.rivencraft.angle.east.description": "Before Normal Time, Sun or Moon: set orbit bearing to 90 degrees. A preceding Number overrides Angle in 15-degree steps.",
|
||||
"symbol.rivencraft.angle.south.description": "Before Normal Time, Sun or Moon: set orbit bearing to 180 degrees. A preceding Number overrides Angle in 15-degree steps.",
|
||||
"symbol.rivencraft.angle.west.description": "Before Normal Time, Sun or Moon: set orbit bearing to 270 degrees. A preceding Number overrides Angle in 15-degree steps.",
|
||||
"symbol.rivencraft.phase.rising.description": "Before a celestial controller: start the sun or standalone moon at rising. A preceding Number overrides Phase in 15-degree steps from Nadir.",
|
||||
"symbol.rivencraft.phase.zenith.description": "Before a celestial controller: start the sun or standalone moon overhead. Zero Length holds this position.",
|
||||
"symbol.rivencraft.phase.setting.description": "Before a celestial controller: start the sun or standalone moon at setting. A preceding Number overrides Phase in 15-degree steps from Nadir.",
|
||||
"symbol.rivencraft.phase.nadir.description": "Before a celestial controller: start the sun or standalone moon below the world. Zero Length holds this position.",
|
||||
"symbol.rivencraft.direction.clockwise.description": "Before Normal Time, Sun or Moon: advance this Age's celestial cycle in its usual direction.",
|
||||
"symbol.rivencraft.direction.counterclockwise.description": "Before Normal Time, Sun or Moon: reverse this Age's celestial cycle. Does not change the Overworld clock.",
|
||||
"symbol.rivencraft.celestial.normal_time.description": "Add a sun/moon pair. Preceding modifiers apply to this pair; Size affects its sun only. Colors tint both. Limit ten suns and ten moons per Age.",
|
||||
"symbol.rivencraft.celestial.sun.description": "Add a sun. A bare page gets saved seeded color, size and orbit controls. Written modifiers preserve authored behavior and reset per controller. Moon alignment causes eclipses; limit ten suns.",
|
||||
"symbol.rivencraft.celestial.moon.description": "Add a moon with weaker light. A bare page gets saved seeded color, size and orbit controls. Written modifiers preserve authored behavior and reset per controller. Sun alignment causes eclipses; limit ten moons.",
|
||||
"symbol.rivencraft.period.zero.description": "Before Normal Time, Sun or Moon: freeze at the written Phase, or a saved seeded time if omitted. A preceding Number overrides the Length.",
|
||||
"symbol.rivencraft.period.half.description": "Before Normal Time, Sun or Moon: a half-length cycle (twice as fast). A preceding Number sets Length to value × 0.08 days.",
|
||||
"symbol.rivencraft.period.full.description": "Before Normal Time, Sun or Moon: a normal-length cycle. A preceding Number sets Length to value × 0.08 days.",
|
||||
"symbol.rivencraft.period.double.description": "Before Normal Time, Sun or Moon: a double-length cycle (half as fast). A preceding Number sets Length to value × 0.08 days.",
|
||||
"symbol.rivencraft.color.red.description": "Add red to the next Sky, Ambient Light Color or celestial controller; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.color.green.description": "Add green to the next Sky, Ambient Light Color or celestial controller; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.color.blue.description": "Add blue to the next Sky, Ambient Light Color or celestial controller; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.color.white.description": "Add equally to all RGB channels. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.color.black.description": "Subtract equally from all RGB channels. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.lighting.normal.description": "Use normal skylight and ambient brightness. Cave terrain remains without skylight.",
|
||||
"symbol.rivencraft.lighting.dark.description": "Remove skylight from this Age; placed light sources still work.",
|
||||
"symbol.rivencraft.lighting.bright.description": "Provide full ambient brightness and effective light level 15. Vanilla light-based spawn rules apply, including in Cave terrain without skylight.",
|
||||
"symbol.rivencraft.weather.fast.description": "Use the faster Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
|
||||
"symbol.rivencraft.weather.slow.description": "Use the slower Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
|
||||
"symbol.rivencraft.weather.off.description": "Keep this Age clear without changing Overworld weather.",
|
||||
"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 +75,36 @@
|
||||
"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": "Modify the next compatible feature. Without a following feature, choose a compatible feature from the Age seed.",
|
||||
"symbol.rivencraft.biome.native.description": "Use the terrain's default biome source. Cave uses Tiny regions from recognized Cave/Nether biomes. Do not include Biome pages.",
|
||||
"symbol.rivencraft.feature.dungeons.description": "Add monster-room dungeon attempts throughout surface noise or Cave terrain, including Nether cave biomes. Not Flat or Void.",
|
||||
"symbol.rivencraft.biome.single.description": "Use one biome across the Age. If omitted, choose an eligible biome from the Age seed.",
|
||||
"symbol.rivencraft.terrain.flat.description": "A level, biome-decorated landscape with its foundation at Y=0.",
|
||||
"symbol.rivencraft.environment.accelerated.description": "Adds extra random block ticks near players in this Age. Repeated pages stack within server tick budgets; does not speed up entities or machines.",
|
||||
"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."
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.dnorberg.rivencraft;
|
||||
|
||||
import net.neoforged.neoforge.common.ModConfigSpec;
|
||||
|
||||
/** Server-owned gameplay policies adapted from 1.21.1. */
|
||||
public final class PortConfig {
|
||||
public enum ReturnLinkMode { ON_CREATION, ALWAYS, DISABLED }
|
||||
public static final ModConfigSpec SPEC;
|
||||
public static final ModConfigSpec.IntValue SYMBOL_INK_COST;
|
||||
public static final ModConfigSpec.EnumValue<ReturnLinkMode> RETURN_LINK_MODE;
|
||||
public static final ModConfigSpec.IntValue ACCELERATED_MAX_CHUNKS;
|
||||
public static final ModConfigSpec.IntValue ACCELERATED_MAX_ATTEMPTS;
|
||||
|
||||
static {
|
||||
var builder = new ModConfigSpec.Builder();
|
||||
builder.push("descriptive_books");
|
||||
RETURN_LINK_MODE = builder.comment("Controls automatic Return Links for successful Descriptive Book travel.",
|
||||
"ON_CREATION issues one on the first arrival per Age/world; ALWAYS issues one each arrival; DISABLED issues none.")
|
||||
.defineEnum("return_link_mode", ReturnLinkMode.DISABLED);
|
||||
builder.pop();
|
||||
builder.push("writing");
|
||||
SYMBOL_INK_COST = builder.comment("Ink units consumed for one Writing Desk symbol copy.")
|
||||
.defineInRange("symbol_ink_cost", 1, 1, 16);
|
||||
builder.pop();
|
||||
builder.push("accelerated_age");
|
||||
ACCELERATED_MAX_CHUNKS = builder.comment("Maximum already-loaded player-nearby chunks given extra random ticks per Age tick.")
|
||||
.defineInRange("max_loaded_chunks", 8, 1, 64);
|
||||
ACCELERATED_MAX_ATTEMPTS = builder.comment("Maximum extra random-tick positions sampled per Age tick, shared fairly across loaded sections; zero disables Accelerated.")
|
||||
.defineInRange("max_random_tick_attempts_per_tick", 16384, 0, 65536);
|
||||
builder.pop();
|
||||
SPEC = builder.build();
|
||||
}
|
||||
|
||||
private PortConfig() {}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import net.dnorberg.rivencraft.age.AgeCatalogSync;
|
||||
import net.dnorberg.rivencraft.block.BuddingCrystalBlock;
|
||||
import net.dnorberg.rivencraft.block.PortBookBinderBlock;
|
||||
import net.dnorberg.rivencraft.block.InkMixerBlock;
|
||||
import net.dnorberg.rivencraft.block.StarFissureBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
||||
@@ -26,6 +28,7 @@ import net.dnorberg.rivencraft.writing.LinkingBookRecipe;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
@@ -54,6 +57,28 @@ public final class Rivencraft {
|
||||
public static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MOD_ID);
|
||||
private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MOD_ID);
|
||||
private static final DeferredRegister<net.minecraft.world.entity.EntityType<?>> ENTITIES =
|
||||
DeferredRegister.create(Registries.ENTITY_TYPE, MOD_ID);
|
||||
public static final Supplier<net.minecraft.world.entity.EntityType<net.dnorberg.rivencraft.entity.DroppedBookEntity>>
|
||||
DROPPED_BOOK = ENTITIES.register("dropped_book", () -> net.minecraft.world.entity.EntityType.Builder
|
||||
.<net.dnorberg.rivencraft.entity.DroppedBookEntity>of(
|
||||
net.dnorberg.rivencraft.entity.DroppedBookEntity::new, net.minecraft.world.entity.MobCategory.MISC)
|
||||
.sized(0.65F, 0.4F).clientTrackingRange(10).updateInterval(20)
|
||||
.build(net.minecraft.resources.ResourceKey.create(Registries.ENTITY_TYPE,
|
||||
Identifier.fromNamespaceAndPath(MOD_ID, "dropped_book"))));
|
||||
private static final DeferredRegister<SoundEvent> SOUNDS =
|
||||
DeferredRegister.create(Registries.SOUND_EVENT, MOD_ID);
|
||||
public static final Supplier<SoundEvent> LINK = sound("linking.link");
|
||||
public static final Supplier<SoundEvent> LINK_DISARM = sound("linking.link-disarm");
|
||||
public static final Supplier<SoundEvent> LINK_INTRA = sound("linking.link-intra");
|
||||
public static final Supplier<SoundEvent> LINK_FISSURE = sound("linking.link-fissure");
|
||||
public static final Supplier<SoundEvent> LINK_PORTAL = sound("linking.link-portal");
|
||||
public static final Supplier<SoundEvent> METEOR_ROAR = sound("entity.meteor.roar");
|
||||
|
||||
private static Supplier<SoundEvent> sound(String name) {
|
||||
return SOUNDS.register(name, () -> SoundEvent.createVariableRangeEvent(
|
||||
Identifier.fromNamespaceAndPath(MOD_ID, name)));
|
||||
}
|
||||
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
|
||||
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID);
|
||||
private static final DeferredRegister<MenuType<?>> MENUS =
|
||||
@@ -66,6 +91,10 @@ public final class Rivencraft {
|
||||
public static final Supplier<DataComponentType<Identifier>> AGE_BOOK_ID = COMPONENTS.registerComponentType(
|
||||
"age_book_id", builder -> builder.persistent(Identifier.CODEC)
|
||||
.networkSynchronized(Identifier.STREAM_CODEC));
|
||||
public static final Supplier<DataComponentType<net.dnorberg.rivencraft.writing.NotebookContents>> NOTEBOOK_CONTENTS =
|
||||
COMPONENTS.registerComponentType("notebook_contents", builder -> builder
|
||||
.persistent(net.dnorberg.rivencraft.writing.NotebookContents.CODEC)
|
||||
.networkSynchronized(net.dnorberg.rivencraft.writing.NotebookContents.STREAM_CODEC));
|
||||
public static final Supplier<DataComponentType<LinkBookTarget>> LINK_BOOK_TARGET = COMPONENTS.registerComponentType(
|
||||
"link_book_target", builder -> builder.persistent(LinkBookTarget.CODEC)
|
||||
.networkSynchronized(LinkBookTarget.STREAM_CODEC));
|
||||
@@ -94,6 +123,16 @@ public final class Rivencraft {
|
||||
|
||||
public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block",
|
||||
Block::new, () -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK));
|
||||
public static final DeferredBlock<net.dnorberg.rivencraft.block.CloudPlatformBlock> CLOUD_PLATFORM =
|
||||
BLOCKS.registerBlock("cloud_platform", net.dnorberg.rivencraft.block.CloudPlatformBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK)
|
||||
.mapColor(MapColor.COLOR_BLUE).strength(0.3F).noLootTable());
|
||||
public static final DeferredBlock<StarFissureBlock> STAR_FISSURE =
|
||||
BLOCKS.registerBlock("star_fissure", StarFissureBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.END_PORTAL).lightLevel(state -> 6));
|
||||
public static final Supplier<BlockEntityType<StarFissureBlockEntity>>
|
||||
STAR_FISSURE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("star_fissure", () -> new BlockEntityType<>(
|
||||
StarFissureBlockEntity::new, STAR_FISSURE.get()));
|
||||
public static final DeferredBlock<PortBookBinderBlock> BOOK_BINDER = BLOCKS.registerBlock(
|
||||
"book_binder", PortBookBinderBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
||||
@@ -112,6 +151,25 @@ public final class Rivencraft {
|
||||
new BlockEntityType<>(InkMixerBlockEntity::new, INK_MIXER.get()));
|
||||
public static final Supplier<MenuType<InkMixerMenu>> INK_MIXER_MENU =
|
||||
MENUS.register("ink_mixer", () -> IMenuTypeExtension.create(InkMixerMenu::new));
|
||||
public static final DeferredBlock<net.dnorberg.rivencraft.block.WritingDeskBlock> WRITING_DESK =
|
||||
BLOCKS.registerBlock("writing_desk", net.dnorberg.rivencraft.block.WritingDeskBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
||||
public static final DeferredItem<BlockItem> WRITING_DESK_ITEM = ITEMS.registerSimpleBlockItem(WRITING_DESK);
|
||||
public static final Supplier<BlockEntityType<net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity>> WRITING_DESK_BLOCK_ENTITY =
|
||||
BLOCK_ENTITY_TYPES.register("writing_desk", () -> new BlockEntityType<>(
|
||||
net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity::new, WRITING_DESK.get()));
|
||||
public static final Supplier<MenuType<net.dnorberg.rivencraft.menu.WritingDeskMenu>> WRITING_DESK_MENU =
|
||||
MENUS.register("writing_desk", () -> IMenuTypeExtension.create(net.dnorberg.rivencraft.menu.WritingDeskMenu::new));
|
||||
public static final Supplier<MenuType<net.dnorberg.rivencraft.menu.NotebookMenu>> NOTEBOOK_MENU =
|
||||
MENUS.register("notebook", () -> IMenuTypeExtension.create(net.dnorberg.rivencraft.menu.NotebookMenu::new));
|
||||
public static final DeferredItem<net.dnorberg.rivencraft.item.NotebookItem> NOTEBOOK =
|
||||
ITEMS.registerItem("notebook", net.dnorberg.rivencraft.item.NotebookItem::new, properties -> properties.stacksTo(1));
|
||||
public static final DeferredItem<net.dnorberg.rivencraft.item.CollationFolderItem> COLLATION_FOLDER =
|
||||
ITEMS.registerItem("collation_folder", net.dnorberg.rivencraft.item.CollationFolderItem::new, properties -> properties.stacksTo(1));
|
||||
public static final DeferredItem<net.dnorberg.rivencraft.item.SymbolPortfolioItem> SYMBOL_PORTFOLIO =
|
||||
ITEMS.registerItem("symbol_portfolio", net.dnorberg.rivencraft.item.SymbolPortfolioItem::new, properties -> properties.stacksTo(1));
|
||||
public static final DeferredItem<net.dnorberg.rivencraft.item.SealedNotebookItem> SEALED_NOTEBOOK =
|
||||
ITEMS.registerItem("sealed_notebook", net.dnorberg.rivencraft.item.SealedNotebookItem::new, properties -> properties.stacksTo(16));
|
||||
public static final DeferredItem<BlockItem> CRYSTAL_BLOCK_ITEM = ITEMS.registerSimpleBlockItem(CRYSTAL_BLOCK);
|
||||
public static final DeferredBlock<BuddingCrystalBlock> BUDDING_CRYSTAL = BLOCKS.registerBlock(
|
||||
"budding_crystal", BuddingCrystalBlock::new,
|
||||
@@ -144,9 +202,12 @@ public final class Rivencraft {
|
||||
public static final Supplier<RecipeSerializer<LinkingBookRecipe>> LINKING_BOOK_RECIPE =
|
||||
RECIPE_SERIALIZERS.register("linking_book", () -> LinkingBookRecipe.SERIALIZER);
|
||||
|
||||
public Rivencraft(IEventBus modEventBus) {
|
||||
public Rivencraft(IEventBus modEventBus, net.neoforged.fml.ModContainer container) {
|
||||
container.registerConfig(net.neoforged.fml.config.ModConfig.Type.LOCAL, PortConfig.SPEC);
|
||||
BLOCKS.register(modEventBus);
|
||||
SOUNDS.register(modEventBus);
|
||||
ITEMS.register(modEventBus);
|
||||
ENTITIES.register(modEventBus);
|
||||
BLOCK_ENTITY_TYPES.register(modEventBus);
|
||||
MENUS.register(modEventBus);
|
||||
COMPONENTS.register(modEventBus);
|
||||
@@ -155,6 +216,8 @@ 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(net.dnorberg.rivencraft.age.PortAcceleratedTicks::onLevelTickPost);
|
||||
NeoForge.EVENT_BUS.addListener(PortAgeCommands::register);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogout);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
@@ -9,17 +11,23 @@ import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Full server level catalog, sent on login and after a runtime Age is added. */
|
||||
public record AgeCatalogPayload(int version, List<Identifier> dimensions) implements CustomPacketPayload {
|
||||
public record AgeCatalogPayload(int version, List<Identifier> dimensions,
|
||||
Map<Identifier, PortAgeColors> colors,
|
||||
Map<Identifier, PortDayCycle> dayCycles) implements CustomPacketPayload {
|
||||
public static final Type<AgeCatalogPayload> TYPE = new Type<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age_catalog"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AgeCatalogPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.VAR_INT, AgeCatalogPayload::version,
|
||||
Identifier.STREAM_CODEC.apply(ByteBufCodecs.list()), AgeCatalogPayload::dimensions,
|
||||
ByteBufCodecs.map(HashMap::new, Identifier.STREAM_CODEC, PortAgeColors.STREAM_CODEC), AgeCatalogPayload::colors,
|
||||
ByteBufCodecs.map(HashMap::new, Identifier.STREAM_CODEC, PortDayCycle.STREAM_CODEC), AgeCatalogPayload::dayCycles,
|
||||
AgeCatalogPayload::new
|
||||
);
|
||||
|
||||
public AgeCatalogPayload {
|
||||
dimensions = List.copyOf(dimensions);
|
||||
colors = Map.copyOf(colors);
|
||||
dayCycles = Map.copyOf(dayCycles);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,7 +21,7 @@ public final class AgeCatalogSync {
|
||||
}
|
||||
|
||||
public static void registerPayloads(RegisterPayloadHandlersEvent event) {
|
||||
var registrar = event.registrar("1");
|
||||
var registrar = event.registrar("10");
|
||||
registrar.playToClient(AgeCatalogPayload.TYPE, AgeCatalogPayload.STREAM_CODEC);
|
||||
registrar.playToServer(AgeCatalogAckPayload.TYPE, AgeCatalogAckPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
@@ -77,7 +77,8 @@ public final class AgeCatalogSync {
|
||||
State state = state(server);
|
||||
state.acknowledged.remove(player.getUUID());
|
||||
var dimensions = server.levelKeys().stream().map(key -> key.identifier()).sorted().toList();
|
||||
PacketDistributor.sendToPlayer(player, new AgeCatalogPayload(state.version, dimensions));
|
||||
PacketDistributor.sendToPlayer(player, new AgeCatalogPayload(state.version, dimensions,
|
||||
RuntimeAgeBackend.savedColors(server), RuntimeAgeBackend.savedDayCycles(server)));
|
||||
}
|
||||
|
||||
private static void acknowledge(ServerPlayer player, int version) {
|
||||
|
||||
@@ -24,7 +24,25 @@ final class AgeLevelIndex extends SavedData {
|
||||
.forGetter(index -> index.biomePlans),
|
||||
Codec.unboundedMap(Identifier.CODEC,
|
||||
Codec.STRING.xmap(PortAgeTerrain::valueOf, PortAgeTerrain::name))
|
||||
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain)
|
||||
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortRockPlan.CODEC).optionalFieldOf("rocks", Map.of())
|
||||
.forGetter(index -> index.rocks),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortVegetationPlan.CODEC)
|
||||
.optionalFieldOf("vegetation", Map.of()).forGetter(index -> index.vegetation),
|
||||
Codec.unboundedMap(Identifier.CODEC, Codec.intRange(1, 32))
|
||||
.optionalFieldOf("dungeons", Map.of()).forGetter(index -> index.dungeons),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortLakePlan.CODEC)
|
||||
.optionalFieldOf("lakes", Map.of()).forGetter(index -> index.lakes),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortTerrainFeaturePlan.CODEC)
|
||||
.optionalFieldOf("terrain_features", Map.of()).forGetter(index -> index.terrainFeatures),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortLighting.CODEC)
|
||||
.optionalFieldOf("lighting", Map.of()).forGetter(index -> index.lighting),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortSkyStyle.CODEC)
|
||||
.optionalFieldOf("sky", Map.of()).forGetter(index -> index.sky),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortAgeColors.CODEC)
|
||||
.optionalFieldOf("colors", Map.of()).forGetter(index -> index.colors),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortDayCycle.CODEC)
|
||||
.optionalFieldOf("day_cycles", Map.of()).forGetter(index -> index.dayCycles)
|
||||
).apply(instance, AgeLevelIndex::new));
|
||||
|
||||
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
||||
@@ -38,6 +56,15 @@ final class AgeLevelIndex extends SavedData {
|
||||
private final Map<Identifier, Identifier> biomeOverrides;
|
||||
private final Map<Identifier, PortBiomePlan> biomePlans;
|
||||
private final Map<Identifier, PortAgeTerrain> terrain;
|
||||
private final Map<Identifier, PortRockPlan> rocks;
|
||||
private final Map<Identifier, PortVegetationPlan> vegetation;
|
||||
private final Map<Identifier, Integer> dungeons;
|
||||
private final Map<Identifier, PortLakePlan> lakes;
|
||||
private final Map<Identifier, PortTerrainFeaturePlan> terrainFeatures;
|
||||
private final Map<Identifier, PortLighting> lighting;
|
||||
private final Map<Identifier, PortSkyStyle> sky;
|
||||
private final Map<Identifier, PortAgeColors> colors;
|
||||
private final Map<Identifier, PortDayCycle> dayCycles;
|
||||
|
||||
private AgeLevelIndex() {
|
||||
this.ages = new LinkedHashMap<>();
|
||||
@@ -45,17 +72,43 @@ final class AgeLevelIndex extends SavedData {
|
||||
this.biomeOverrides = new LinkedHashMap<>();
|
||||
this.biomePlans = new LinkedHashMap<>();
|
||||
this.terrain = new LinkedHashMap<>();
|
||||
this.rocks = new LinkedHashMap<>();
|
||||
this.vegetation = new LinkedHashMap<>();
|
||||
this.dungeons = new LinkedHashMap<>();
|
||||
this.lakes = new LinkedHashMap<>();
|
||||
this.terrainFeatures = new LinkedHashMap<>();
|
||||
this.lighting = new LinkedHashMap<>();
|
||||
this.sky = new LinkedHashMap<>();
|
||||
this.colors = new LinkedHashMap<>();
|
||||
this.dayCycles = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
|
||||
Map<Identifier, Identifier> biomeOverrides,
|
||||
Map<Identifier, PortBiomePlan> biomePlans,
|
||||
Map<Identifier, PortAgeTerrain> terrain) {
|
||||
Map<Identifier, PortAgeTerrain> terrain,
|
||||
Map<Identifier, PortRockPlan> rocks,
|
||||
Map<Identifier, PortVegetationPlan> vegetation,
|
||||
Map<Identifier, Integer> dungeons,
|
||||
Map<Identifier, PortLakePlan> lakes,
|
||||
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures,
|
||||
Map<Identifier, PortLighting> lighting,
|
||||
Map<Identifier, PortSkyStyle> sky, Map<Identifier, PortAgeColors> colors,
|
||||
Map<Identifier, PortDayCycle> dayCycles) {
|
||||
this.ages = new LinkedHashMap<>(ages);
|
||||
this.retired = new LinkedHashSet<>(retired);
|
||||
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
||||
this.biomePlans = new LinkedHashMap<>(biomePlans);
|
||||
this.terrain = new LinkedHashMap<>(terrain);
|
||||
this.rocks = new LinkedHashMap<>(rocks);
|
||||
this.vegetation = new LinkedHashMap<>(vegetation);
|
||||
this.dungeons = new LinkedHashMap<>(dungeons);
|
||||
this.lakes = new LinkedHashMap<>(lakes);
|
||||
this.terrainFeatures = new LinkedHashMap<>(terrainFeatures);
|
||||
this.lighting = new LinkedHashMap<>(lighting);
|
||||
this.sky = new LinkedHashMap<>(sky);
|
||||
this.colors = new LinkedHashMap<>(colors);
|
||||
this.dayCycles = new LinkedHashMap<>(dayCycles);
|
||||
}
|
||||
|
||||
Map<Identifier, Long> entries() {
|
||||
@@ -81,13 +134,37 @@ final class AgeLevelIndex extends SavedData {
|
||||
return terrain.getOrDefault(id, PortAgeTerrain.STANDARD);
|
||||
}
|
||||
|
||||
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain) {
|
||||
PortRockPlan rocks(Identifier id) { return rocks.get(id); }
|
||||
PortVegetationPlan vegetation(Identifier id) { return vegetation.get(id); }
|
||||
int dungeons(Identifier id) { return dungeons.getOrDefault(id, 0); }
|
||||
PortLakePlan lakes(Identifier id) { return lakes.get(id); }
|
||||
PortTerrainFeaturePlan terrainFeatures(Identifier id) { return terrainFeatures.get(id); }
|
||||
PortLighting lighting(Identifier id) { return lighting.getOrDefault(id, PortLighting.NORMAL); }
|
||||
PortSkyStyle sky(Identifier id) { return sky.getOrDefault(id, PortSkyStyle.OVERWORLD); }
|
||||
PortAgeColors colors(Identifier id) { return colors.getOrDefault(id, PortAgeColors.NATIVE); }
|
||||
Map<Identifier, PortAgeColors> colors() { return Map.copyOf(colors); }
|
||||
PortDayCycle dayCycle(Identifier id) { return dayCycles.getOrDefault(id, PortDayCycle.NATIVE); }
|
||||
Map<Identifier, PortDayCycle> dayCycles() { return Map.copyOf(dayCycles); }
|
||||
|
||||
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light, PortSkyStyle skyStyle,
|
||||
PortAgeColors colorProfile, PortDayCycle dayCycle) {
|
||||
if (contains(id)) {
|
||||
throw new IllegalArgumentException("Age already exists: " + id);
|
||||
}
|
||||
ages.put(id, seed);
|
||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||
if (rockPlan != null) rocks.put(id, rockPlan);
|
||||
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
|
||||
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
|
||||
if (lakePlan != null) lakes.put(id, lakePlan);
|
||||
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
|
||||
if (light != PortLighting.NORMAL) lighting.put(id, light);
|
||||
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
|
||||
if (!colorProfile.equals(PortAgeColors.NATIVE)) colors.put(id, colorProfile);
|
||||
if (!dayCycle.equals(PortDayCycle.NATIVE)) dayCycles.put(id, dayCycle);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
@@ -96,16 +173,37 @@ final class AgeLevelIndex extends SavedData {
|
||||
biomeOverrides.remove(id);
|
||||
biomePlans.remove(id);
|
||||
terrain.remove(id);
|
||||
rocks.remove(id);
|
||||
vegetation.remove(id);
|
||||
dungeons.remove(id);
|
||||
lakes.remove(id);
|
||||
terrainFeatures.remove(id);
|
||||
lighting.remove(id);
|
||||
sky.remove(id);
|
||||
colors.remove(id);
|
||||
dayCycles.remove(id);
|
||||
retired.add(id);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
|
||||
PortAgeTerrain ageTerrain) {
|
||||
PortAgeTerrain ageTerrain, PortRockPlan rockPlan,
|
||||
PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light,
|
||||
PortSkyStyle skyStyle, PortAgeColors colorProfile, PortDayCycle dayCycle) {
|
||||
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
|
||||
ages.put(id, seed);
|
||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||
if (rockPlan != null) rocks.put(id, rockPlan);
|
||||
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
|
||||
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
|
||||
if (lakePlan != null) lakes.put(id, lakePlan);
|
||||
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
|
||||
if (light != PortLighting.NORMAL) lighting.put(id, light);
|
||||
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
|
||||
if (!colorProfile.equals(PortAgeColors.NATIVE)) colors.put(id, colorProfile);
|
||||
if (!dayCycle.equals(PortDayCycle.NATIVE)) dayCycles.put(id, dayCycle);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import net.dnorberg.rivencraft.PortConfig;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.neoforged.neoforge.event.tick.LevelTickEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||
import net.minecraft.util.RandomSource;
|
||||
|
||||
/** Extra random block ticks from authored Accelerated pages, bounded to loaded player chunks. */
|
||||
public final class PortAcceleratedTicks {
|
||||
public static final Identifier PAGE = Identifier.fromNamespaceAndPath("rivencraft", "environment/accelerated");
|
||||
|
||||
private PortAcceleratedTicks() {}
|
||||
|
||||
public static int pageCount(List<Identifier> pages) {
|
||||
return (int) pages.stream().filter(PAGE::equals).count();
|
||||
}
|
||||
|
||||
public static void configure(ServerLevel level, int pages) {
|
||||
if (!(level instanceof PortAgeLevel)) throw new IllegalArgumentException("Not a Rivencraft Age");
|
||||
level.getDataStorage().computeIfAbsent(PortAccelerationSettings.TYPE).setPages(pages);
|
||||
level.getDataStorage().saveAndJoin();
|
||||
}
|
||||
|
||||
public static int pages(ServerLevel level) {
|
||||
return level.getDataStorage().computeIfAbsent(PortAccelerationSettings.TYPE).pages();
|
||||
}
|
||||
|
||||
public static void onLevelTickPost(LevelTickEvent.Post event) {
|
||||
if (event.getLevel() instanceof PortAgeLevel level) tick(level);
|
||||
}
|
||||
|
||||
static void tick(ServerLevel level) {
|
||||
int maximumAttempts = PortConfig.ACCELERATED_MAX_ATTEMPTS.get();
|
||||
if (maximumAttempts == 0) return;
|
||||
int pages = pages(level);
|
||||
if (pages == 0) return;
|
||||
|
||||
List<ChunkPos> chunks = loadedPlayerChunks(level);
|
||||
if (chunks.isEmpty()) return;
|
||||
int maximumChunks = Math.min(chunks.size(), PortConfig.ACCELERATED_MAX_CHUNKS.get());
|
||||
int first = Math.floorMod(level.getServer().getTickCount(), chunks.size());
|
||||
List<TickSection> eligible = new ArrayList<>();
|
||||
for (int offset = 0; offset < maximumChunks; offset++) {
|
||||
ChunkPos pos = chunks.get((first + offset) % chunks.size());
|
||||
LevelChunk chunk = level.getChunkSource().getChunkNow(pos.x(), pos.z());
|
||||
if (chunk == null) continue;
|
||||
LevelChunkSection[] sections = chunk.getSections();
|
||||
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
||||
LevelChunkSection section = sections[sectionIndex];
|
||||
if (!section.isRandomlyTickingBlocks()) continue;
|
||||
int baseY = SectionPos.sectionToBlockCoord(chunk.getSectionYFromSectionIndex(sectionIndex));
|
||||
eligible.add(new TickSection(pos, baseY, section));
|
||||
}
|
||||
}
|
||||
if (eligible.isEmpty()) return;
|
||||
|
||||
// Legacy attempts three positions per random-ticking section per page. Divide
|
||||
// the bounded total between sections before ticking any one of them, so a
|
||||
// lower section cannot exhaust the entire budget ahead of surface crops.
|
||||
int total = attemptBudget(pages, eligible.size(), maximumAttempts);
|
||||
int perSection = total / eligible.size();
|
||||
int remainder = total % eligible.size();
|
||||
int firstSection = Math.floorMod(level.getServer().getTickCount(), eligible.size());
|
||||
RandomSource random = level.getRandom();
|
||||
for (int offset = 0; offset < eligible.size(); offset++) {
|
||||
TickSection candidate = eligible.get((firstSection + offset) % eligible.size());
|
||||
int samples = perSection + (offset < remainder ? 1 : 0);
|
||||
for (int sample = 0; sample < samples; sample++) {
|
||||
int x = random.nextInt(16);
|
||||
int y = random.nextInt(16);
|
||||
int z = random.nextInt(16);
|
||||
BlockState state = candidate.section().getBlockState(x, y, z);
|
||||
if (state.isRandomlyTicking()) {
|
||||
state.randomTick(level, new BlockPos(candidate.chunk().getMinBlockX() + x,
|
||||
candidate.baseY() + y, candidate.chunk().getMinBlockZ() + z), random);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int attemptBudget(int pages, int sections, int maximumAttempts) {
|
||||
if (pages < 0 || sections < 0 || maximumAttempts < 0)
|
||||
throw new IllegalArgumentException("Negative accelerated tick budget input");
|
||||
if (sections == 0) return 0;
|
||||
long perSection = 3L * pages;
|
||||
return perSection > maximumAttempts / (long) sections
|
||||
? maximumAttempts : (int) (perSection * sections);
|
||||
}
|
||||
|
||||
private static List<ChunkPos> loadedPlayerChunks(ServerLevel level) {
|
||||
Set<ChunkPos> chunks = new LinkedHashSet<>();
|
||||
for (var player : level.players()) {
|
||||
ChunkPos center = player.chunkPosition();
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
int chunkX = center.x() + x;
|
||||
int chunkZ = center.z() + z;
|
||||
if (level.getChunkSource().hasChunk(chunkX, chunkZ)) {
|
||||
chunks.add(new ChunkPos(chunkX, chunkZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(chunks);
|
||||
}
|
||||
|
||||
private record TickSection(ChunkPos chunk, int baseY, LevelChunkSection section) {}
|
||||
}
|
||||
@@ -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 authored page count, retained across unload, restart and reseeding. */
|
||||
final class PortAccelerationSettings extends SavedData {
|
||||
private static final Codec<PortAccelerationSettings> CODEC = Codec.intRange(0, Integer.MAX_VALUE)
|
||||
.fieldOf("pages").codec().xmap(PortAccelerationSettings::new, settings -> settings.pages);
|
||||
static final SavedDataType<PortAccelerationSettings> TYPE = new SavedDataType<>(
|
||||
Identifier.fromNamespaceAndPath("rivencraft", "acceleration"),
|
||||
() -> new PortAccelerationSettings(0), CODEC);
|
||||
private int pages;
|
||||
|
||||
private PortAccelerationSettings(int pages) { this.pages = pages; }
|
||||
int pages() { return pages; }
|
||||
void setPages(int pages) {
|
||||
if (pages < 0) throw new IllegalArgumentException("Negative Accelerated page count");
|
||||
this.pages = pages;
|
||||
setDirty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector3fc;
|
||||
|
||||
/** Frozen shared-color writing, adapted from 1.21.1 AgeSkyProfile. -1 means native. */
|
||||
public record PortAgeColors(int sky, int ambient, int celestial) {
|
||||
public static final PortAgeColors NATIVE = new PortAgeColors(-1, -1);
|
||||
public static final Codec<PortAgeColors> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.intRange(-1, 0xffffff).fieldOf("sky").forGetter(PortAgeColors::sky),
|
||||
Codec.intRange(-1, 0xffffff).fieldOf("ambient").forGetter(PortAgeColors::ambient),
|
||||
Codec.intRange(-1, 0xffffff).optionalFieldOf("celestial", -1).forGetter(PortAgeColors::celestial)
|
||||
).apply(instance, PortAgeColors::new));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, PortAgeColors> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.INT, PortAgeColors::sky, ByteBufCodecs.INT, PortAgeColors::ambient,
|
||||
ByteBufCodecs.INT, PortAgeColors::celestial, PortAgeColors::new);
|
||||
public static final Identifier AMBIENT = Identifier.fromNamespaceAndPath("rivencraft", "lighting/ambient_color");
|
||||
private static volatile Map<Identifier, PortAgeColors> clientColors = Map.of();
|
||||
|
||||
public PortAgeColors {
|
||||
if (sky < -1 || sky > 0xffffff || ambient < -1 || ambient > 0xffffff
|
||||
|| celestial < -1 || celestial > 0xffffff)
|
||||
throw new IllegalArgumentException("Invalid Age color");
|
||||
}
|
||||
|
||||
public PortAgeColors(int sky, int ambient) { this(sky, ambient, -1); }
|
||||
|
||||
public static void updateClient(Map<Identifier, PortAgeColors> colors) { clientColors = Map.copyOf(colors); }
|
||||
|
||||
public static PortAgeColors forLevel(Level level) {
|
||||
if (level instanceof PortAgeLevel age) return age.colors();
|
||||
return level.isClientSide() ? clientColors.getOrDefault(level.dimension().identifier(), NATIVE) : NATIVE;
|
||||
}
|
||||
|
||||
public static boolean isColor(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "color/red", "color/green", "color/blue", "color/white", "color/black" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static int number(Identifier page) {
|
||||
if (!page.getNamespace().equals("rivencraft") || !page.getPath().startsWith("number/")) return -1;
|
||||
try {
|
||||
int value = Integer.parseInt(page.getPath().substring(7));
|
||||
return value >= 0 && value <= 24 ? value : -1;
|
||||
} catch (NumberFormatException exception) { return -1; }
|
||||
}
|
||||
|
||||
public static boolean isModifier(Identifier page) { return isColor(page) || number(page) >= 0 || page.equals(AMBIENT); }
|
||||
|
||||
public static boolean validNumbers(List<Identifier> pages) {
|
||||
for (int i = 0; i < pages.size(); i++)
|
||||
if (number(pages.get(i)) >= 0 && (i + 1 == pages.size()
|
||||
|| !isColor(pages.get(i + 1)) && !PortDayCycle.isPeriod(pages.get(i + 1))
|
||||
&& !PortDayCycle.isPhase(pages.get(i + 1)) && !PortDayCycle.isAngle(pages.get(i + 1))
|
||||
&& !PortDayCycle.isSize(pages.get(i + 1)))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static PortAgeColors compile(List<Identifier> pages, long seed) {
|
||||
if (!validNumbers(pages)) throw new IllegalArgumentException("Number pages must precede a supported Color, Length, Phase, Angle, or Size modifier");
|
||||
int red = 0, green = 0, blue = 0, sky = -1, ambient = -1, celestial = -1;
|
||||
boolean written = false;
|
||||
for (int i = 0; i < pages.size(); i++) {
|
||||
Identifier page = pages.get(i);
|
||||
if (isColor(page)) {
|
||||
int amount = i > 0 ? number(pages.get(i - 1)) : -1;
|
||||
if (amount < 0) amount = new Random(seed ^ (0x9E3779B97F4A7C15L * (i + 1L))).nextInt(25);
|
||||
amount *= 10;
|
||||
written = true;
|
||||
switch (page.getPath()) {
|
||||
case "color/red" -> red += amount;
|
||||
case "color/green" -> green += amount;
|
||||
case "color/blue" -> blue += amount;
|
||||
case "color/white" -> { red += amount; green += amount; blue += amount; }
|
||||
case "color/black" -> { red -= amount; green -= amount; blue -= amount; }
|
||||
}
|
||||
} else if (PortSkyStyle.forPage(page) != null || page.equals(AMBIENT)
|
||||
|| PortDayCycle.isController(page)) {
|
||||
if (PortDayCycle.isController(page)) celestial = -1;
|
||||
if (written) {
|
||||
int rgb = clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
|
||||
if (page.equals(AMBIENT)) ambient = rgb;
|
||||
else if (PortDayCycle.isController(page)) celestial = rgb;
|
||||
else sky = rgb;
|
||||
}
|
||||
red = green = blue = 0;
|
||||
written = false;
|
||||
}
|
||||
}
|
||||
if (written) sky = clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
|
||||
return new PortAgeColors(sky, ambient, celestial);
|
||||
}
|
||||
|
||||
/** Only pageless random Ages receive a random sky tint, as in 1.21.1. */
|
||||
public static PortAgeColors resolve(List<Identifier> pages, long seed) {
|
||||
if (!pages.isEmpty()) return compile(pages, seed);
|
||||
var random = new Random(seed ^ 0x534B5950524F464CL);
|
||||
random.nextInt(5); // Root sky-style draw precedes the sky-color draw.
|
||||
return new PortAgeColors(randomColor(random, true), -1);
|
||||
}
|
||||
|
||||
/** Shared palette; retain .85's truncation for already-defined body defaults. */
|
||||
static int randomColor(Random random, boolean rounded) {
|
||||
int[] palette = {0xB74A50, 0xD9834B, 0xDBC574, 0x5AAB80, 0x6FA8DF, 0x9870B9, 0xD5DBE1, 0x252531};
|
||||
int count = 1 + random.nextInt(3), red = 0, green = 0, blue = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int color = palette[random.nextInt(palette.length)];
|
||||
red += color >> 16 & 255;
|
||||
green += color >> 8 & 255;
|
||||
blue += color & 255;
|
||||
}
|
||||
int offset = rounded ? count / 2 : 0;
|
||||
return (red + offset) / count << 16 | (green + offset) / count << 8 | (blue + offset) / count;
|
||||
}
|
||||
|
||||
private static int clamp(int value) { return Math.max(0, Math.min(255, value)); }
|
||||
public static Vector3fc vector(int rgb) {
|
||||
return new Vector3f((rgb >> 16 & 255) / 255f, (rgb >> 8 & 255) / 255f, (rgb & 255) / 255f);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,63 @@
|
||||
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;
|
||||
private PortAgeColors colors = PortAgeColors.NATIVE;
|
||||
private PortDayCycle dayCycle = PortDayCycle.NATIVE;
|
||||
|
||||
PortDayCycle dayCycle() { return dayCycle == null ? PortDayCycle.NATIVE : dayCycle; }
|
||||
void configureDayCycle(PortDayCycle cycle) {
|
||||
this.dayCycle = cycle;
|
||||
environmentAttributes().invalidateTickCache();
|
||||
updateSkyBrightness();
|
||||
}
|
||||
|
||||
PortAgeColors colors() { return colors == null ? PortAgeColors.NATIVE : colors; }
|
||||
void configureColors(PortAgeColors colors) {
|
||||
this.colors = colors;
|
||||
environmentAttributes().invalidateTickCache();
|
||||
}
|
||||
|
||||
PortAgeLevel(MinecraftServer server, ResourceKey<Level> key, LevelStem stem,
|
||||
long biomeZoomSeed, PortAgeTerrain terrain) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -25,19 +23,22 @@ public final class PortAgeTravel {
|
||||
if (!AgeCatalogSync.ready(player, id)) {
|
||||
return "Age catalog not acknowledged by client; remaining in source dimension";
|
||||
}
|
||||
BlockPos landing = findLanding(target);
|
||||
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 = 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));
|
||||
boolean moved = player.teleportTo(target, landing.getX() + 0.5, landing.getY(), landing.getZ() + 0.5,
|
||||
Set.of(), player.getYRot(), player.getXRot(), true);
|
||||
return moved ? null : "Teleportation failed; remaining in source dimension";
|
||||
}
|
||||
|
||||
private static BlockPos findLanding(ServerLevel target) {
|
||||
if (target.getChunkSource().getGenerator() instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator noise
|
||||
&& noise.generatorSettings().value().noiseSettings().equals(target.registryAccess()
|
||||
.lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES)
|
||||
.value().noiseSettings())) {
|
||||
static BlockPos findLanding(ServerLevel target) {
|
||||
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
|
||||
// at that height only when no footed opening can be found.
|
||||
@@ -54,36 +55,50 @@ 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 (target.getChunkSource().getGenerator()) {
|
||||
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) {
|
||||
var generator = target.getChunkSource().getGenerator();
|
||||
if (generator instanceof PortFeatureChunkGenerator features) generator = features.base();
|
||||
return generator instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator noise
|
||||
&& noise.generatorSettings().value().noiseSettings().equals(target.registryAccess()
|
||||
.lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES)
|
||||
.value().noiseSettings());
|
||||
}
|
||||
|
||||
private static BlockPos makePlatform(ServerLevel target, BlockPos foot) {
|
||||
@@ -97,7 +112,7 @@ public final class PortAgeTravel {
|
||||
}
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
target.setBlock(foot.offset(x, -1, z), Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 3);
|
||||
target.setBlock(foot.offset(x, -1, z), Rivencraft.CLOUD_PLATFORM.get().defaultBlockState(), 3);
|
||||
}
|
||||
}
|
||||
target.setBlock(foot, Blocks.AIR.defaultBlockState(), 3);
|
||||
@@ -106,11 +121,45 @@ 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()
|
||||
&& target.getBlockState(foot).getCollisionShape(target, foot).isEmpty()
|
||||
&& target.getBlockState(foot.above()).getCollisionShape(target, foot.above()).isEmpty();
|
||||
&& passable(target, foot) && passable(target, foot.above());
|
||||
}
|
||||
|
||||
private static boolean passable(ServerLevel level, BlockPos pos) {
|
||||
var state = level.getBlockState(pos);
|
||||
return state.getCollisionShape(level, pos).isEmpty() || state.is(net.minecraft.tags.BlockTags.LEAVES);
|
||||
}
|
||||
|
||||
/** Same local-then-surface safe-return search used by 1.21.1 fissures. */
|
||||
public static java.util.Optional<BlockPos> findFootedDestination(ServerLevel level, BlockPos origin) {
|
||||
var local = BlockPos.betweenClosedStream(origin.offset(-4, -2, -4), origin.offset(4, 2, 4))
|
||||
.map(BlockPos::immutable).sorted(java.util.Comparator.comparingDouble(pos -> pos.distSqr(origin)))
|
||||
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
|
||||
.filter(pos -> hasFooting(level, pos)).findFirst();
|
||||
if (local.isPresent()) return local;
|
||||
for (int radius = 1; radius <= 64; radius++) {
|
||||
final int ring = radius;
|
||||
var found = BlockPos.betweenClosedStream(origin.offset(-ring, 0, -ring), origin.offset(ring, 0, ring))
|
||||
.filter(pos -> Math.max(Math.abs(pos.getX() - origin.getX()), Math.abs(pos.getZ() - origin.getZ())) == ring)
|
||||
.map(BlockPos::immutable)
|
||||
.flatMap(pos -> {
|
||||
BlockPos surface = new BlockPos(pos.getX(),
|
||||
level.getHeight(Heightmap.Types.MOTION_BLOCKING, pos.getX(), pos.getZ()), pos.getZ());
|
||||
return java.util.stream.Stream.of(surface, surface.above(), surface.below(),
|
||||
surface.above(2), surface.below(2));
|
||||
})
|
||||
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
|
||||
.filter(pos -> hasFooting(level, pos)).findFirst();
|
||||
if (found.isPresent()) return found;
|
||||
}
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** World-wide record of Ages for which an ON_CREATION Return Link was issued. */
|
||||
final class PortAgeVisits extends SavedData {
|
||||
static final Codec<PortAgeVisits> CODEC = Identifier.CODEC.listOf().fieldOf("ages").codec()
|
||||
.xmap(PortAgeVisits::new, visits -> visits.ages.stream().sorted().toList());
|
||||
static final SavedDataType<PortAgeVisits> TYPE = new SavedDataType<>(
|
||||
Identifier.fromNamespaceAndPath("rivencraft", "age_visits"),
|
||||
() -> new PortAgeVisits(List.of()), CODEC);
|
||||
private final Set<Identifier> ages;
|
||||
|
||||
PortAgeVisits(List<Identifier> ages) { this.ages = new HashSet<>(ages); }
|
||||
boolean hasVisited(Identifier age) { return ages.contains(age); }
|
||||
boolean markFirstVisit(Identifier age) {
|
||||
if (!ages.add(age)) return false;
|
||||
setDirty();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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<PortAgeLevel, Integer> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Disposable fixture for atomic collection imports and real Binder assembly. */
|
||||
final class PortBinderCollectionProbe {
|
||||
private static final BlockPos BINDER = new BlockPos(0, 64, 0);
|
||||
private static final BlockPos CHEST = new BlockPos(1, 64, 0);
|
||||
private static final List<Identifier> EXPECTED = List.of(id("weather/off"), id("lighting/bright"),
|
||||
id("terrain/flat"), id("biome/single"));
|
||||
private PortBinderCollectionProbe() {}
|
||||
private static Identifier id(String path) { return Identifier.fromNamespaceAndPath("rivencraft", path); }
|
||||
private static ItemStack page(String path) { return PageItem.createSymbolPage(id(path)); }
|
||||
private static ItemStack collection(boolean folder, List<ItemStack> pages) {
|
||||
ItemStack result = new ItemStack(folder ? Rivencraft.COLLATION_FOLDER.get() : Rivencraft.NOTEBOOK.get());
|
||||
result.set(Rivencraft.NOTEBOOK_CONTENTS.get(), new NotebookContents(pages));
|
||||
return result;
|
||||
}
|
||||
|
||||
static void create(ServerLevel level) {
|
||||
PortAgeTravel.findLanding(level);
|
||||
level.setBlock(BINDER, Rivencraft.BOOK_BINDER.get().defaultBlockState(), 3);
|
||||
level.setBlock(CHEST, Blocks.CHEST.defaultBlockState(), 3);
|
||||
var binder = (PortBookBinderBlockEntity) level.getBlockEntity(BINDER);
|
||||
var chest = (Container) level.getBlockEntity(CHEST);
|
||||
ItemStack folder = collection(true, List.of(page("weather/off"), ItemStack.EMPTY, page("lighting/bright")));
|
||||
ItemStack before = folder.copy();
|
||||
for (int i = 0; i < PortBookBinderBlockEntity.PAGE_COUNT; i++)
|
||||
binder.setItem(PortBookBinderBlockEntity.FIRST_PAGE + i, page("terrain/flat"));
|
||||
if (!binder.importCollection(folder, 0).isEmpty() || binder.nextPageIndex() != 252
|
||||
|| !ItemStack.isSameItemSameComponents(before, folder)
|
||||
|| binder.symbolPages().stream().anyMatch(symbol -> !symbol.equals(id("terrain/flat"))))
|
||||
throw new IllegalStateException("Full Binder import was not atomic");
|
||||
binder.clearSymbolPages();
|
||||
binder.setItem(PortBookBinderBlockEntity.PANEL, PageItem.createLinkPanelPage());
|
||||
if (!binder.importCollection(collection(true, List.of(page("weather/off"), PageItem.createLinkPanelPage())), 0).isEmpty()
|
||||
|| binder.hasSymbolPages() || binder.getItem(PortBookBinderBlockEntity.PANEL).isEmpty())
|
||||
throw new IllegalStateException("Conflicting Link Panel import was not atomic");
|
||||
binder.setItem(PortBookBinderBlockEntity.PANEL, ItemStack.EMPTY);
|
||||
if (!binder.importCollection(folder, -1).isEmpty() || !binder.importCollection(folder, 1).isEmpty())
|
||||
throw new IllegalStateException("Invalid collection insertion point accepted");
|
||||
binder.insertPage(0, page("terrain/flat"));
|
||||
var player = net.neoforged.neoforge.common.util.FakePlayerFactory.get(level,
|
||||
new com.mojang.authlib.GameProfile(java.util.UUID.fromString("5225a1ed-5ead-44b5-a63c-c78e7dca86f5"), "RivenBinderProbe"));
|
||||
player.setPos(0.5, 64, 2.5);
|
||||
var menu = new PortBookBinderMenu(1, player.getInventory(), binder);
|
||||
try {
|
||||
menu.setCarried(folder);
|
||||
if (!menu.clickMenuButton(player, PortBookBinderMenu.STRIP_CLICK_BASE + 1)
|
||||
|| menu.getCarried().get(Rivencraft.NOTEBOOK_CONTENTS.get()).pageCount() != 0
|
||||
|| !binder.symbolPages().equals(EXPECTED.subList(0, 3)))
|
||||
throw new IllegalStateException("Cursor collection order/empty-cover regression");
|
||||
chest.setItem(0, menu.getCarried().copy());
|
||||
menu.setCarried(ItemStack.EMPTY);
|
||||
player.getInventory().setItem(9, collection(false, List.of(page("biome/single"))));
|
||||
menu.quickMoveStack(player, PortBookBinderBlockEntity.SIZE);
|
||||
ItemStack emptied = player.getInventory().getItem(9);
|
||||
if (!emptied.is(Rivencraft.NOTEBOOK.get())
|
||||
|| emptied.get(Rivencraft.NOTEBOOK_CONTENTS.get()).pageCount() != 0
|
||||
|| !binder.symbolPages().equals(EXPECTED))
|
||||
throw new IllegalStateException("Shift collection import regression");
|
||||
chest.setItem(1, emptied.copy());
|
||||
binder.setItem(PortBookBinderBlockEntity.COVER, new ItemStack(Items.LEATHER));
|
||||
binder.setItem(PortBookBinderBlockEntity.PANEL, PageItem.createLinkPanelPage());
|
||||
menu.assemble(player, "Collection Fixture104");
|
||||
ItemStack book = menu.getCarried();
|
||||
if (!book.is(Rivencraft.DESCRIPTIVE_BOOK.get()) || !EXPECTED.equals(book.get(Rivencraft.BOOK_SYMBOLS.get()))
|
||||
|| !book.has(Rivencraft.AGE_BOOK_ID.get()) || binder.hasSymbolPages()
|
||||
|| !binder.getItem(PortBookBinderBlockEntity.COVER).isEmpty()
|
||||
|| !binder.getItem(PortBookBinderBlockEntity.PANEL).isEmpty())
|
||||
throw new IllegalStateException("Collection-backed Binder assembly regression");
|
||||
chest.setItem(2, book.copy());
|
||||
menu.setCarried(ItemStack.EMPTY);
|
||||
} finally { player.getInventory().clearContent(); }
|
||||
verify(level);
|
||||
}
|
||||
|
||||
static void verify(ServerLevel level) {
|
||||
if (!(level.getBlockEntity(CHEST) instanceof Container chest)) throw new IllegalStateException("Fixture chest missing");
|
||||
if (!chest.getItem(0).is(Rivencraft.COLLATION_FOLDER.get()) || !chest.getItem(1).is(Rivencraft.NOTEBOOK.get()))
|
||||
throw new IllegalStateException("Empty collection covers were not preserved");
|
||||
for (int i = 0; i < 2; i++) if (chest.getItem(i).get(Rivencraft.NOTEBOOK_CONTENTS.get()).pageCount() != 0)
|
||||
throw new IllegalStateException("Imported pages remained in a collection");
|
||||
if (!EXPECTED.equals(chest.getItem(2).get(Rivencraft.BOOK_SYMBOLS.get())))
|
||||
throw new IllegalStateException("Authored book page order was not preserved");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.data.worldgen.material.EndMaterialRules;
|
||||
import net.minecraft.data.worldgen.material.NetherMaterialRules;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.material.MaterialRules;
|
||||
|
||||
/** Root AgeDimensionService.withBiomeMaterialRules adapted to 26.3's rule registry. */
|
||||
final class PortBiomeMaterialRules {
|
||||
private PortBiomeMaterialRules() {}
|
||||
|
||||
static Holder<NoiseGeneratorSettings> apply(HolderLookup.Provider registries, NoiseGeneratorSettings base) {
|
||||
var biomes = registries.lookupOrThrow(Registries.BIOME);
|
||||
var rules = registries.lookupOrThrow(Registries.MATERIAL_RULE);
|
||||
var materials = MaterialRules.sequence(
|
||||
MaterialRules.ifTrue(MaterialRules.isBiome(biomes, Biomes.THE_END, Biomes.END_HIGHLANDS,
|
||||
Biomes.END_MIDLANDS, Biomes.SMALL_END_ISLANDS, Biomes.END_BARRENS),
|
||||
MaterialRules.getRule(rules, EndMaterialRules.END)),
|
||||
MaterialRules.ifTrue(MaterialRules.isBiome(biomes, Biomes.NETHER_WASTES, Biomes.SOUL_SAND_VALLEY,
|
||||
Biomes.CRIMSON_FOREST, Biomes.WARPED_FOREST, Biomes.BASALT_DELTAS),
|
||||
MaterialRules.getRule(rules, NetherMaterialRules.NETHER)),
|
||||
base.materialRule().value());
|
||||
return Holder.direct(new NoiseGeneratorSettings(base.noiseSettings(), base.defaultBlock(), base.defaultFluid(),
|
||||
base.noiseRouter(), Holder.direct(materials), base.spawnTarget(), base.seaLevel(), base.disableMobGeneration(),
|
||||
base.aquifers(), base.useLegacyRandomSource(), base.debugFunctions()));
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Frozen authored biome choice; repeated entries retain their selection weight. */
|
||||
/** Frozen biome choice; Native may carry an explicit Cave pool. Repeats retain weight. */
|
||||
public record PortBiomePlan(Distribution distribution, List<Identifier> biomes) {
|
||||
public enum Distribution { NATIVE, SINGLE, TILED, GRID, TINY, SMALL, MEDIUM, LARGE, HUGE }
|
||||
|
||||
@@ -17,8 +17,7 @@ public record PortBiomePlan(Distribution distribution, List<Identifier> biomes)
|
||||
|
||||
public PortBiomePlan {
|
||||
biomes = List.copyOf(biomes);
|
||||
if (distribution == Distribution.NATIVE && !biomes.isEmpty()
|
||||
|| distribution == Distribution.SINGLE && biomes.size() != 1
|
||||
if (distribution == null || distribution == Distribution.SINGLE && biomes.size() != 1
|
||||
|| distribution != Distribution.NATIVE && distribution != Distribution.SINGLE && biomes.size() < 2)
|
||||
throw new IllegalArgumentException("Invalid authored biome distribution");
|
||||
}
|
||||
|
||||
@@ -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<Identifier> pages) {
|
||||
Map<Identifier, Integer> 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<Identifier> 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<Identifier> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
|
||||
/** One frozen authored orbit, using the same angular motion as 1.21.1 Body. */
|
||||
public record PortCelestialBody(int kind, double period, int stoppedTime, long originTick,
|
||||
int phaseTick, boolean clockwise, int bearing, float size, int color) {
|
||||
public static final Codec<PortCelestialBody> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.intRange(1, 2).fieldOf("kind").forGetter(PortCelestialBody::kind),
|
||||
Codec.doubleRange(0, 2).fieldOf("period").forGetter(PortCelestialBody::period),
|
||||
Codec.intRange(0, 23999).fieldOf("stopped_time").forGetter(PortCelestialBody::stoppedTime),
|
||||
Codec.LONG.fieldOf("origin_tick").forGetter(PortCelestialBody::originTick),
|
||||
Codec.intRange(0, 23999).fieldOf("phase_tick").forGetter(PortCelestialBody::phaseTick),
|
||||
Codec.BOOL.fieldOf("clockwise").forGetter(PortCelestialBody::clockwise),
|
||||
Codec.intRange(-1, 359).fieldOf("bearing").forGetter(PortCelestialBody::bearing),
|
||||
Codec.floatRange(0.25F, 3F).fieldOf("size").forGetter(PortCelestialBody::size),
|
||||
Codec.intRange(-1, 0xffffff).fieldOf("color").forGetter(PortCelestialBody::color)
|
||||
).apply(instance, PortCelestialBody::new));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, PortCelestialBody> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.VAR_INT, PortCelestialBody::kind, ByteBufCodecs.DOUBLE, PortCelestialBody::period,
|
||||
ByteBufCodecs.VAR_INT, PortCelestialBody::stoppedTime, ByteBufCodecs.VAR_LONG, PortCelestialBody::originTick,
|
||||
ByteBufCodecs.VAR_INT, PortCelestialBody::phaseTick, ByteBufCodecs.BOOL, PortCelestialBody::clockwise,
|
||||
ByteBufCodecs.INT, PortCelestialBody::bearing, ByteBufCodecs.FLOAT, PortCelestialBody::size,
|
||||
ByteBufCodecs.INT, PortCelestialBody::color, PortCelestialBody::new);
|
||||
|
||||
public PortCelestialBody {
|
||||
if (kind != PortDayCycle.SUN && kind != PortDayCycle.MOON || !Double.isFinite(period)
|
||||
|| period < 0 || period > 2 || stoppedTime < 0 || stoppedTime >= 24000
|
||||
|| phaseTick < 0 || phaseTick >= 24000 || bearing < -1 || bearing > 359
|
||||
|| !Float.isFinite(size) || size < 0.25F || size > 3 || color < -1 || color > 0xffffff)
|
||||
throw new IllegalArgumentException("Invalid celestial body");
|
||||
}
|
||||
|
||||
public static PortCelestialBody of(PortDayCycle cycle, int kind, int color, boolean pairedMoon) {
|
||||
int offset = pairedMoon ? 12000 : 0;
|
||||
return new PortCelestialBody(kind, cycle.period(), (cycle.stoppedTime() + offset) % 24000,
|
||||
cycle.originTick(), (cycle.phaseTick() + offset) % 24000, cycle.clockwise(),
|
||||
cycle.bearing(), pairedMoon ? 1F : cycle.sunSize(), color);
|
||||
}
|
||||
|
||||
public float angle(double nativeTicks) {
|
||||
double time = period == 0 ? stoppedTime
|
||||
: (clockwise ? 1 : -1) * (nativeTicks - originTick) / period + phaseTick;
|
||||
return (float) (((time - 6000D) % 24000D) * (360D / 24000D));
|
||||
}
|
||||
|
||||
public float light(long nativeTicks) {
|
||||
return Math.max(0F, (float) Math.cos(Math.toRadians(angle(nativeTicks))))
|
||||
* (kind == PortDayCycle.MOON ? 0.18F : 1F);
|
||||
}
|
||||
|
||||
public net.minecraft.world.phys.Vec3 direction(double ticks) {
|
||||
double orbit = Math.toRadians(angle(ticks)), axis = Math.toRadians(bearing < 0 ? -90 : bearing);
|
||||
double lateral = Math.sin(orbit);
|
||||
return new net.minecraft.world.phys.Vec3(Math.sin(axis) * lateral, Math.cos(orbit), Math.cos(axis) * lateral);
|
||||
}
|
||||
|
||||
/** Root AgeSkyProfile.eclipseFor: contact uses the central quarter-size disc, not its halo. */
|
||||
public float eclipse(java.util.List<PortCelestialBody> bodies, double ticks) {
|
||||
float strongest = 0;
|
||||
var direction = direction(ticks);
|
||||
double radius = Math.atan((kind == PortDayCycle.SUN ? 30D : 20D) * size * 0.25D / 100D);
|
||||
for (var other : bodies) {
|
||||
if (other.kind == kind) continue;
|
||||
double separation = Math.acos(Math.max(-1D, Math.min(1D, direction.dot(other.direction(ticks)))));
|
||||
double contact = radius + Math.atan((other.kind == PortDayCycle.SUN ? 30D : 20D) * other.size * 0.25D / 100D);
|
||||
strongest = Math.max(strongest, (float) Math.max(0D, 1D - separation / contact));
|
||||
}
|
||||
return strongest;
|
||||
}
|
||||
|
||||
public static float eclipseAt(java.util.List<PortCelestialBody> bodies, double ticks) {
|
||||
float strongest = 0;
|
||||
for (var body : bodies) if (body.kind == PortDayCycle.SUN && body.direction(ticks).y > 0D)
|
||||
strongest = Math.max(strongest, body.eclipse(bodies, ticks));
|
||||
return strongest;
|
||||
}
|
||||
|
||||
/** Shared client/server-checkable RGB rule; alpha is handled by the renderer. */
|
||||
public int eclipseColor(float eclipse) {
|
||||
int rgb = color < 0 ? 0xffffff : color;
|
||||
float corona = kind == PortDayCycle.SUN ? (float) Math.pow(eclipse, 8D) * 0.8F : 0F;
|
||||
int red = channel(rgb >> 16 & 255, eclipse, corona);
|
||||
int green = channel(rgb >> 8 & 255, eclipse, corona);
|
||||
int blue = channel(rgb & 255, eclipse, corona);
|
||||
return red << 16 | green << 8 | blue;
|
||||
}
|
||||
|
||||
private int channel(int value, float eclipse, float corona) {
|
||||
return Math.round(kind == PortDayCycle.MOON ? value * (1F - eclipse) : value + (255 - value) * corona);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.item.SymbolPortfolioItem;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/** Disposable-world boundary, booster-use and persistence checks. */
|
||||
final class PortCollectionExtrasProbe {
|
||||
private static final BlockPos CHEST = new BlockPos(0, 64, 0);
|
||||
private PortCollectionExtrasProbe() {}
|
||||
private static Identifier id(String path) { return Identifier.fromNamespaceAndPath("rivencraft", path); }
|
||||
private static NotebookContents contents(ItemStack stack) { return stack.get(Rivencraft.NOTEBOOK_CONTENTS.get()); }
|
||||
private static List<Identifier> symbols(ItemStack collection) {
|
||||
return contents(collection).pages().stream().map(page -> page.get(Rivencraft.SYMBOL_PAGE_ID.get())).toList();
|
||||
}
|
||||
private static void folder(ItemStack stack) {
|
||||
if (!stack.is(Rivencraft.COLLATION_FOLDER.get()) || contents(stack).pageCount() != 16
|
||||
|| symbols(stack).stream().anyMatch(id -> !PortSymbolPages.sealedCandidateIds().contains(id)))
|
||||
throw new IllegalStateException("Sealed Notebook page pool/count regression");
|
||||
}
|
||||
|
||||
static void create(ServerLevel level) {
|
||||
PortAgeTravel.findLanding(level);
|
||||
level.setBlock(CHEST, Blocks.CHEST.defaultBlockState(), 3);
|
||||
var chest = (Container) level.getBlockEntity(CHEST);
|
||||
var pages = new java.util.ArrayList<ItemStack>();
|
||||
for (int i = 0; i < 253; i++) pages.add(PageItem.createSymbolPage(id(i % 2 == 0 ? "terrain/flat" : "weather/off")));
|
||||
var volumes = SymbolPortfolioItem.createVolumes(id("feature"), pages);
|
||||
if (volumes.size() != 2 || contents(volumes.get(0)).pageCount() != 252
|
||||
|| contents(volumes.get(1)).pageCount() != 1
|
||||
|| !volumes.stream().flatMap(volume -> symbols(volume).stream()).toList()
|
||||
.equals(pages.stream().map(page -> page.get(Rivencraft.SYMBOL_PAGE_ID.get())).toList())
|
||||
|| !SymbolPortfolioItem.createVolumes(id("feature"), List.of()).isEmpty())
|
||||
throw new IllegalStateException("Portfolio volume boundary/order regression");
|
||||
chest.setItem(0, volumes.get(0));
|
||||
chest.setItem(1, volumes.get(1));
|
||||
var actual = net.dnorberg.rivencraft.registry.ModCreativeTabs.creativeSymbolPortfolios(level.registryAccess());
|
||||
var allPages = PortSymbolPages.categories().stream().flatMap(category -> PortSymbolPages.pages(category, level.registryAccess()).stream())
|
||||
.map(page -> page.get(Rivencraft.SYMBOL_PAGE_ID.get())).toList();
|
||||
if (!actual.stream().flatMap(volume -> symbols(volume).stream()).toList().equals(allPages))
|
||||
throw new IllegalStateException("Creative portfolios omit/duplicate/reorder pages");
|
||||
var player = net.neoforged.neoforge.common.util.FakePlayerFactory.get(level,
|
||||
new com.mojang.authlib.GameProfile(java.util.UUID.fromString("790b69dd-b62b-45be-9017-caf16515169a"), "RivenExtrasProbe"));
|
||||
player.setPos(0.5, 64, 2.5);
|
||||
try {
|
||||
player.getAbilities().instabuild = false;
|
||||
player.getRandom().setSeed(105);
|
||||
var first = net.dnorberg.rivencraft.item.SealedNotebookItem.open(player);
|
||||
player.getRandom().setSeed(105);
|
||||
var repeat = net.dnorberg.rivencraft.item.SealedNotebookItem.open(player);
|
||||
folder(first);
|
||||
if (!symbols(first).equals(symbols(repeat))) throw new IllegalStateException("Booster seeded selection changed");
|
||||
chest.setItem(2, first);
|
||||
for (var hand : InteractionHand.values()) {
|
||||
player.getInventory().clearContent();
|
||||
player.setItemInHand(InteractionHand.OFF_HAND, ItemStack.EMPTY);
|
||||
player.setItemInHand(hand, new ItemStack(Rivencraft.SEALED_NOTEBOOK.get()));
|
||||
Rivencraft.SEALED_NOTEBOOK.get().use(level, player, hand);
|
||||
folder(player.getItemInHand(hand));
|
||||
}
|
||||
player.getInventory().clearContent();
|
||||
player.setItemInHand(InteractionHand.OFF_HAND, ItemStack.EMPTY);
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Rivencraft.SEALED_NOTEBOOK.get(), 2));
|
||||
Rivencraft.SEALED_NOTEBOOK.get().use(level, player, InteractionHand.MAIN_HAND);
|
||||
if (player.getMainHandItem().getCount() != 1) throw new IllegalStateException("Booster stack consumption regression");
|
||||
player.getAbilities().instabuild = true;
|
||||
Rivencraft.SEALED_NOTEBOOK.get().use(level, player, InteractionHand.MAIN_HAND);
|
||||
if (!player.getMainHandItem().is(Rivencraft.SEALED_NOTEBOOK.get()) || player.getMainHandItem().getCount() != 1)
|
||||
throw new IllegalStateException("Creative booster consumed its input");
|
||||
player.getAbilities().instabuild = false;
|
||||
for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++)
|
||||
player.getInventory().setItem(slot, new ItemStack(Items.COBBLESTONE, 64));
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Rivencraft.SEALED_NOTEBOOK.get(), 2));
|
||||
Rivencraft.SEALED_NOTEBOOK.get().use(level, player, InteractionHand.MAIN_HAND);
|
||||
var dropped = level.getEntitiesOfClass(ItemEntity.class, new AABB(-3, 60, -3, 4, 70, 6),
|
||||
entity -> entity.getItem().is(Rivencraft.COLLATION_FOLDER.get()));
|
||||
if (dropped.size() != 1 || player.getMainHandItem().getCount() != 1)
|
||||
throw new IllegalStateException("Full-inventory booster delivery regression");
|
||||
folder(dropped.getFirst().getItem());
|
||||
dropped.getFirst().discard();
|
||||
} finally {
|
||||
player.getAbilities().instabuild = false;
|
||||
player.getInventory().clearContent();
|
||||
player.setItemInHand(InteractionHand.OFF_HAND, ItemStack.EMPTY);
|
||||
}
|
||||
verify(level);
|
||||
}
|
||||
|
||||
static void verify(ServerLevel level) {
|
||||
if (!(level.getBlockEntity(CHEST) instanceof Container chest)) throw new IllegalStateException("Fixture chest missing");
|
||||
if (!chest.getItem(0).is(Rivencraft.SYMBOL_PORTFOLIO.get()) || contents(chest.getItem(0)).pageCount() != 252
|
||||
|| !chest.getItem(1).is(Rivencraft.SYMBOL_PORTFOLIO.get()) || contents(chest.getItem(1)).pageCount() != 1)
|
||||
throw new IllegalStateException("Portfolio persistence regression");
|
||||
folder(chest.getItem(2));
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.AmethystClusterBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/** 26.3 port of the 1.21.1 surface crystal mound with budding hearts and seeded buds. */
|
||||
final class PortCrystalFormationsFeature {
|
||||
private static final long SALT = 0x4352595354414C53L;
|
||||
private static final int CHANCE = 15;
|
||||
|
||||
private PortCrystalFormationsFeature() {}
|
||||
|
||||
record Site(int x, int z, long seed) {}
|
||||
|
||||
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
|
||||
long seed = mix(ageSeed ^ SALT ^ (long) chunk.x() * 341873128712L
|
||||
^ (long) chunk.z() * 132897987541L ^ (long) occurrence * 0x9E3779B97F4A7C15L);
|
||||
RandomSource random = RandomSource.create(seed);
|
||||
if (random.nextInt(CHANCE) != 0) return null;
|
||||
return new Site(chunk.getMinBlockX() + 5 + random.nextInt(6),
|
||||
chunk.getMinBlockZ() + 5 + random.nextInt(6), seed);
|
||||
}
|
||||
|
||||
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, int occurrence) {
|
||||
Site site = site(ageSeed, chunk.getPos(), occurrence);
|
||||
if (site == null) return;
|
||||
RandomSource random = RandomSource.create(site.seed() ^ 0x42554444494E474CL);
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, site.x(), site.z());
|
||||
if (y <= level.getMinY() + 1 || y + 4 >= level.getMaxY()) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
int x = site.x() + dx;
|
||||
int z = site.z() + dz;
|
||||
if (Math.abs(level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - y) > 1) return;
|
||||
BlockState floor = level.getBlockState(pos.set(x, y - 1, z));
|
||||
if (!floor.isFaceSturdy(level, pos, Direction.UP) || !floor.getFluidState().isEmpty()
|
||||
|| floor.hasBlockEntity()) return;
|
||||
for (int h = 0; h < 4; h++) {
|
||||
BlockState existing = level.getBlockState(pos.set(x, y + h, z));
|
||||
if (!existing.isAir() && !existing.canBeReplaced()) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
boolean center = dx == 0 && dz == 0;
|
||||
int height = center ? 3 : random.nextInt(3) == 0 ? 2 : 1;
|
||||
for (int h = 0; h < height; h++) {
|
||||
boolean budding = h == height - 1 && (center || random.nextBoolean());
|
||||
level.setBlock(pos.set(site.x() + dx, y + h, site.z() + dz),
|
||||
budding ? Rivencraft.BUDDING_CRYSTAL.get().defaultBlockState()
|
||||
: Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 2);
|
||||
}
|
||||
if (level.getBlockState(pos.set(site.x() + dx, y + height - 1, site.z() + dz))
|
||||
.is(Rivencraft.BUDDING_CRYSTAL) && random.nextBoolean()) {
|
||||
level.setBlock(pos.set(site.x() + dx, y + height, site.z() + dz),
|
||||
Rivencraft.SMALL_CRYSTAL_BUD.get().defaultBlockState()
|
||||
.setValue(AmethystClusterBlock.FACING, Direction.UP), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.clock.ClockInstance;
|
||||
import net.minecraft.world.clock.ClockManager;
|
||||
import net.minecraft.world.clock.WorldClock;
|
||||
import net.minecraft.world.clock.WorldClocks;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Authored native celestial controls. Never mutates the shared clock. */
|
||||
public record PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize, int bodyMode,
|
||||
List<PortCelestialBody> bodies) {
|
||||
public static final int PAIRED = 0, SUN = 1, MOON = 2, NONE = 3;
|
||||
public static final PortDayCycle NATIVE = new PortDayCycle(false, 1, 0);
|
||||
public static final Identifier NORMAL_TIME = Identifier.fromNamespaceAndPath("rivencraft", "celestial/normal_time");
|
||||
public static final Codec<PortDayCycle> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.BOOL.fieldOf("authored").forGetter(PortDayCycle::authored),
|
||||
Codec.doubleRange(0, 2).fieldOf("period").forGetter(PortDayCycle::period),
|
||||
Codec.intRange(0, 23999).fieldOf("stopped_time").forGetter(PortDayCycle::stoppedTime),
|
||||
Codec.LONG.optionalFieldOf("origin_tick", 0L).forGetter(PortDayCycle::originTick),
|
||||
Codec.intRange(0, 23999).optionalFieldOf("phase_tick", 0).forGetter(PortDayCycle::phaseTick),
|
||||
Codec.BOOL.optionalFieldOf("clockwise", true).forGetter(PortDayCycle::clockwise),
|
||||
Codec.intRange(-1, 359).optionalFieldOf("bearing", -1).forGetter(PortDayCycle::bearing),
|
||||
Codec.floatRange(0.25F, 3F).optionalFieldOf("sun_size", 1F).forGetter(PortDayCycle::sunSize),
|
||||
Codec.intRange(0, 3).optionalFieldOf("body_mode", PAIRED).forGetter(PortDayCycle::bodyMode),
|
||||
PortCelestialBody.CODEC.listOf().optionalFieldOf("bodies", List.of()).forGetter(PortDayCycle::bodies)
|
||||
).apply(instance, PortDayCycle::new));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, PortDayCycle> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.BOOL, PortDayCycle::authored, ByteBufCodecs.DOUBLE, PortDayCycle::period,
|
||||
ByteBufCodecs.VAR_INT, PortDayCycle::stoppedTime, ByteBufCodecs.VAR_LONG, PortDayCycle::originTick,
|
||||
ByteBufCodecs.VAR_INT, PortDayCycle::phaseTick, ByteBufCodecs.BOOL, PortDayCycle::clockwise,
|
||||
ByteBufCodecs.INT, PortDayCycle::bearing, ByteBufCodecs.FLOAT, PortDayCycle::sunSize,
|
||||
ByteBufCodecs.VAR_INT, PortDayCycle::bodyMode,
|
||||
PortCelestialBody.STREAM_CODEC.apply(ByteBufCodecs.list()), PortDayCycle::bodies, PortDayCycle::new);
|
||||
private static volatile Map<Identifier, PortDayCycle> clientCycles = Map.of();
|
||||
|
||||
public PortDayCycle {
|
||||
bodies = List.copyOf(bodies);
|
||||
if (!Double.isFinite(period) || period < 0 || period > 2 || stoppedTime < 0 || stoppedTime >= 24000
|
||||
|| phaseTick < 0 || phaseTick >= 24000 || bearing < -1 || bearing >= 360
|
||||
|| !Float.isFinite(sunSize) || sunSize < 0.25F || sunSize > 3F
|
||||
|| bodyMode < PAIRED || bodyMode > NONE
|
||||
|| bodyMode == NONE && (!authored || !bodies.isEmpty()))
|
||||
throw new IllegalArgumentException("Invalid day cycle");
|
||||
if (bodies.stream().filter(body -> body.kind() == SUN).count() > 10
|
||||
|| bodies.stream().filter(body -> body.kind() == MOON).count() > 10)
|
||||
throw new IllegalArgumentException("An Age may have at most ten suns and ten moons");
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize, int bodyMode) {
|
||||
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, bearing, sunSize, bodyMode, List.of());
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize) {
|
||||
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, bearing, sunSize, PAIRED);
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime) {
|
||||
this(authored, period, stoppedTime, 0, 0, true);
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
long originTick, int phaseTick, boolean clockwise) {
|
||||
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, -1);
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
long originTick, int phaseTick, boolean clockwise, int bearing) {
|
||||
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, bearing, 1F);
|
||||
}
|
||||
|
||||
public static boolean isPeriod(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "period/full", "period/half", "period/double", "period/zero" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isPhase(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "phase/rising", "phase/zenith", "phase/setting", "phase/nadir" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isDirection(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && (page.getPath().equals("direction/clockwise")
|
||||
|| page.getPath().equals("direction/counterclockwise"));
|
||||
}
|
||||
|
||||
public static boolean isAngle(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "angle/north", "angle/east", "angle/south", "angle/west" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean supports(Identifier page) {
|
||||
return isController(page) || isPeriod(page) || isPhase(page) || isDirection(page) || isAngle(page) || isSize(page);
|
||||
}
|
||||
|
||||
public static boolean isController(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "celestial/normal_time", "celestial/sun", "celestial/moon" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isSize(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "size/small", "size/normal", "size/large" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean validWriting(List<Identifier> pages) {
|
||||
int periods = 0, phases = 0, directions = 0, angles = 0, sizes = 0, suns = 0, moons = 0;
|
||||
for (Identifier page : pages) {
|
||||
if (isPeriod(page) && ++periods > 1 || isPhase(page) && ++phases > 1
|
||||
|| isDirection(page) && ++directions > 1 || isAngle(page) && ++angles > 1
|
||||
|| isSize(page) && ++sizes > 1) return false;
|
||||
if (isController(page)) {
|
||||
if (!page.getPath().equals("celestial/moon")) suns++;
|
||||
if (!page.getPath().equals("celestial/sun")) moons++;
|
||||
if (suns > 10 || moons > 10) return false;
|
||||
periods = phases = directions = angles = sizes = 0;
|
||||
}
|
||||
}
|
||||
return periods + phases + directions + angles + sizes == 0;
|
||||
}
|
||||
|
||||
public static PortDayCycle compile(List<Identifier> pages, long seed) {
|
||||
return compile(pages, seed, 0);
|
||||
}
|
||||
|
||||
/** Creation-only completion. Exact diagnostic fixtures can still use compile. */
|
||||
public static PortDayCycle resolve(List<Identifier> pages, long seed, long nativeTicks) {
|
||||
if (!validWriting(pages) || !PortAgeColors.validNumbers(pages))
|
||||
throw new IllegalArgumentException("Invalid celestial modifier order");
|
||||
if (pages.stream().anyMatch(PortDayCycle::isController)) return compile(pages, seed, nativeTicks);
|
||||
// Match root AgeSkyProfile.random's count draw positions: sky style,
|
||||
// mixed sky color, then independent 0..3 sun and moon counts.
|
||||
var random = new Random(seed ^ 0x534B5950524F464CL);
|
||||
random.nextInt(5);
|
||||
randomColor(random);
|
||||
int suns = random.nextInt(4), moons = random.nextInt(4);
|
||||
var bodies = new java.util.ArrayList<PortCelestialBody>();
|
||||
for (int i = 0; i < suns + moons; i++)
|
||||
bodies.addAll(randomUnmodified(seed, i, nativeTicks, i < suns ? SUN : MOON).bodies);
|
||||
if (bodies.isEmpty()) return new PortDayCycle(true, 1, 0, 0, 0, true, -1, 1F, NONE);
|
||||
var first = bodies.getFirst();
|
||||
return new PortDayCycle(true, first.period(), first.stoppedTime(), first.originTick(), first.phaseTick(),
|
||||
first.clockwise(), first.bearing(), first.size(), first.kind(), bodies);
|
||||
}
|
||||
|
||||
public static PortDayCycle compile(List<Identifier> pages, long seed, long nativeTicks) {
|
||||
if (!validWriting(pages)) throw new IllegalArgumentException("Place at most one of each celestial modifier before its controller; limit ten suns and ten moons");
|
||||
if (pages.stream().filter(PortDayCycle::isController).count() <= 1)
|
||||
return compileOne(pages, seed, nativeTicks, 0);
|
||||
var bodies = new java.util.ArrayList<PortCelestialBody>();
|
||||
PortDayCycle first = null;
|
||||
int start = 0, ordinal = 0;
|
||||
for (int i = 0; i < pages.size(); i++) if (isController(pages.get(i))) {
|
||||
var cycle = compileOne(pages.subList(start, i + 1), seed, nativeTicks, ordinal++);
|
||||
if (first == null) first = cycle;
|
||||
// Prefix retains original page indices for seeded unnumbered colors.
|
||||
int color = PortAgeColors.compile(pages.subList(0, i + 1), seed).celestial();
|
||||
if (!cycle.bodies.isEmpty()) bodies.addAll(cycle.bodies);
|
||||
else {
|
||||
if (cycle.bodyMode != MOON) bodies.add(PortCelestialBody.of(cycle, SUN, color, false));
|
||||
if (cycle.bodyMode != SUN) bodies.add(PortCelestialBody.of(cycle, MOON, color, cycle.bodyMode == PAIRED));
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
return new PortDayCycle(first.authored, first.period, first.stoppedTime, first.originTick,
|
||||
first.phaseTick, first.clockwise, first.bearing, first.sunSize, first.bodyMode, bodies);
|
||||
}
|
||||
|
||||
private static PortDayCycle compileOne(List<Identifier> pages, long seed, long nativeTicks, int ordinal) {
|
||||
var controller = pages.stream().filter(PortDayCycle::isController).findFirst().orElse(null);
|
||||
if (controller == null) return NATIVE;
|
||||
int mode = controller.getPath().equals("celestial/sun") ? SUN
|
||||
: controller.getPath().equals("celestial/moon") ? MOON : PAIRED;
|
||||
if (mode != PAIRED && !hasBodyModifiers(pages)) return randomUnmodified(seed, ordinal, nativeTicks, mode);
|
||||
double period = 1;
|
||||
Integer phase = null;
|
||||
boolean clockwise = true;
|
||||
int bearing = -1;
|
||||
float size = 1F;
|
||||
for (int i = 0; i < pages.size(); i++) {
|
||||
Identifier page = pages.get(i);
|
||||
int number = i > 0 ? PortAgeColors.number(pages.get(i - 1)) : -1;
|
||||
if (isPhase(page)) {
|
||||
int degrees = number >= 0 ? number * 15 % 360 : switch (page.getPath()) {
|
||||
case "phase/rising" -> 90;
|
||||
case "phase/zenith" -> 180;
|
||||
case "phase/setting" -> 270;
|
||||
default -> 0;
|
||||
};
|
||||
// Same conversion as 1.21.1 AgeDimensionService.applyInitialCelestialPhase:
|
||||
// the written angle starts at Nadir, while native time zero is sunrise.
|
||||
phase = (degrees * 24000 / 360 + 18000) % 24000;
|
||||
} else if (isAngle(page)) {
|
||||
bearing = number >= 0 ? number * 15 % 360 : switch (page.getPath()) {
|
||||
case "angle/east" -> 90;
|
||||
case "angle/south" -> 180;
|
||||
case "angle/west" -> 270;
|
||||
default -> 0;
|
||||
};
|
||||
} else if (isSize(page)) {
|
||||
size = number >= 0 ? (float) (0.25D + number * (2.75D / 24D)) : switch (page.getPath()) {
|
||||
case "size/small" -> 0.5F;
|
||||
case "size/large" -> 2F;
|
||||
default -> 1F;
|
||||
};
|
||||
} else if (isDirection(page)) {
|
||||
clockwise = page.getPath().equals("direction/clockwise");
|
||||
} else if (isPeriod(page)) period = number >= 0 ? number * 0.08 : switch (page.getPath()) {
|
||||
case "period/half" -> 0.5;
|
||||
case "period/double" -> 2;
|
||||
case "period/zero" -> 0;
|
||||
default -> 1;
|
||||
};
|
||||
}
|
||||
int stopped = period == 0 ? phase != null ? phase
|
||||
: new Random(seed ^ 0x4441594359434C45L).nextInt(24000) : 0;
|
||||
return new PortDayCycle(true, period, stopped, phase != null ? nativeTicks : 0,
|
||||
phase == null ? 0 : phase, clockwise, bearing, size, mode);
|
||||
}
|
||||
|
||||
private static boolean hasBodyModifiers(List<Identifier> pages) {
|
||||
boolean color = false, controls = false;
|
||||
for (var page : pages) {
|
||||
if (isController(page)) return controls || color;
|
||||
if (PortAgeColors.isColor(page)) color = true;
|
||||
else if (PortSkyStyle.forPage(page) != null || page.equals(PortAgeColors.AMBIENT)) color = false;
|
||||
if (isPeriod(page) || isPhase(page) || isDirection(page) || isAngle(page) || isSize(page)) controls = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static PortDayCycle randomUnmodified(long seed, int ordinal, long nativeTicks, int mode) {
|
||||
// Independent per-controller stream: unrelated terrain/sky/weather pages
|
||||
// cannot reshuffle the bodies. Use root random-Age bounds, not native defaults.
|
||||
var random = new Random(seed ^ 0x43454C4553544941L ^ (ordinal + 1L) * 0x9E3779B97F4A7C15L);
|
||||
double period = random.nextInt(7) == 0 ? 0D : 0.2D + random.nextDouble() * 1.8D;
|
||||
int phase = random.nextInt(24000), bearing = random.nextInt(360);
|
||||
float size = 0.75F + random.nextFloat();
|
||||
boolean clockwise = random.nextBoolean();
|
||||
int color = randomColor(random);
|
||||
var cycle = new PortDayCycle(true, period, period == 0 ? phase : 0, nativeTicks,
|
||||
phase, clockwise, bearing, size, mode);
|
||||
return new PortDayCycle(true, period, cycle.stoppedTime, nativeTicks, phase, clockwise, bearing, size,
|
||||
mode, List.of(PortCelestialBody.of(cycle, mode, color, false)));
|
||||
}
|
||||
|
||||
private static int randomColor(Random random) {
|
||||
return PortAgeColors.randomColor(random, false);
|
||||
}
|
||||
|
||||
public boolean singleBody() { return authored && (bodyMode == SUN || bodyMode == MOON); }
|
||||
public boolean customLight() { return singleBody() || !bodies.isEmpty(); }
|
||||
|
||||
/** Linear authored orbit, as in 1.21.1 Body; zero degrees is Zenith. */
|
||||
public float bodyAngle(long nativeTicks) {
|
||||
return (float) (((time(nativeTicks) - 6000D) % 24000D) * (360D / 24000D));
|
||||
}
|
||||
|
||||
public float bodyLight(long nativeTicks) {
|
||||
if (bodyMode == NONE) return 0;
|
||||
if (!bodies.isEmpty()) {
|
||||
float sun = 0, moon = 0;
|
||||
for (var body : bodies) {
|
||||
if (body.kind() == SUN) sun += body.light(nativeTicks) * (1F - 0.85F * body.eclipse(bodies, nativeTicks));
|
||||
else moon += body.light(nativeTicks);
|
||||
}
|
||||
return Math.min(1F, sun > 0 ? sun : moon);
|
||||
}
|
||||
float altitude = Math.max(0F, (float) Math.cos(Math.toRadians(bodyAngle(nativeTicks))));
|
||||
return altitude * (bodyMode == MOON ? 0.18F : 1F);
|
||||
}
|
||||
|
||||
public long ticks(long nativeTicks) {
|
||||
if (!authored) return nativeTicks;
|
||||
return (long) Math.floor(time(nativeTicks));
|
||||
}
|
||||
|
||||
private double time(long nativeTicks) {
|
||||
return !authored ? nativeTicks : period == 0 ? stoppedTime
|
||||
: (clockwise ? 1 : -1) * ((double) nativeTicks - originTick) / period + phaseTick;
|
||||
}
|
||||
|
||||
public static long nativeTicks(Level level) {
|
||||
return level.clockManager().getInstance(level.registryAccess().lookupOrThrow(
|
||||
net.minecraft.core.registries.Registries.WORLD_CLOCK).getOrThrow(WorldClocks.OVERWORLD)).totalTicks();
|
||||
}
|
||||
|
||||
public static void updateClient(Map<Identifier, PortDayCycle> cycles) { clientCycles = Map.copyOf(cycles); }
|
||||
public static PortDayCycle forLevel(Level level) {
|
||||
if (level instanceof PortAgeLevel age) return age.dayCycle();
|
||||
return level.isClientSide() ? clientCycles.getOrDefault(level.dimension().identifier(), NATIVE) : NATIVE;
|
||||
}
|
||||
|
||||
/** Timelines keep this manager; profiles are read lazily after level construction/catalog sync. */
|
||||
public static ClockManager timelines(Level level, ClockManager nativeManager) {
|
||||
Map<Holder<WorldClock>, ClockInstance> wrappers = new HashMap<>();
|
||||
return definition -> {
|
||||
if (!definition.is(WorldClocks.OVERWORLD)) return nativeManager.getInstance(definition);
|
||||
return wrappers.computeIfAbsent(definition, ignored -> new ClockInstance() {
|
||||
private ClockInstance source() { return nativeManager.getInstance(definition); }
|
||||
public long totalTicks() { return forLevel(level).ticks(source().totalTicks()); }
|
||||
public float partialTick() {
|
||||
PortDayCycle cycle = forLevel(level);
|
||||
if (!cycle.authored) return source().partialTick();
|
||||
if (cycle.period == 0) return 0;
|
||||
double time = cycle.time(source().totalTicks());
|
||||
return (float) (time - Math.floor(time)
|
||||
+ (cycle.clockwise ? 1 : -1) * source().partialTick() / cycle.period);
|
||||
}
|
||||
public float rate() {
|
||||
PortDayCycle cycle = forLevel(level);
|
||||
return !cycle.authored ? source().rate() : cycle.period == 0 ? 0
|
||||
: (float) ((cycle.clockwise ? 1 : -1) * source().rate() / cycle.period);
|
||||
}
|
||||
public boolean isPaused() { return source().isPaused() || forLevel(level).authored && forLevel(level).period == 0; }
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.feature.AbstractOreFeature;
|
||||
import net.minecraft.world.level.levelgen.placement.FeaturePlacer;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
import net.neoforged.neoforge.common.Tags;
|
||||
|
||||
/** Replays each eligible biome ore placement once per page, matching Rivencraft 1.21.1. */
|
||||
public final class PortDenseOres {
|
||||
public static final TagKey<PlacedFeature> EXTRA_ORES = TagKey.create(Registries.PLACED_FEATURE,
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "dense_ores"));
|
||||
|
||||
private PortDenseOres() {}
|
||||
|
||||
public static boolean hasOres(Holder<Biome> biome) {
|
||||
return oreFeatures(biome).anyMatch(PortDenseOres::isOre);
|
||||
}
|
||||
|
||||
/** Modern ore features carry their targets directly rather than an OreConfiguration. */
|
||||
public static boolean isOre(Holder<PlacedFeature> holder) {
|
||||
if (holder.is(EXTRA_ORES)) return true;
|
||||
if (!(holder.value().feature().value() instanceof AbstractOreFeature ore)) return false;
|
||||
return ore.targetStates().stream().anyMatch(target -> isOreBlock(target.state()));
|
||||
}
|
||||
|
||||
private static boolean isOreBlock(BlockState state) {
|
||||
if (state.is(Tags.BlockItems.ORES.block())) return true;
|
||||
String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
|
||||
return path.endsWith("_ore") || path.contains("_ore_");
|
||||
}
|
||||
|
||||
private static java.util.stream.Stream<Holder<PlacedFeature>> oreFeatures(Holder<Biome> biome) {
|
||||
return biome.value().getGenerationSettings().features().stream().flatMap(HolderSet::stream);
|
||||
}
|
||||
|
||||
public static void place(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator generator,
|
||||
long ageSeed, int count) {
|
||||
if (count == 0) return;
|
||||
Set<Holder<Biome>> biomes = new HashSet<>();
|
||||
ChunkPos.rangeClosed(chunk.getPos(), 1).forEach(pos -> {
|
||||
for (var section : level.getChunk(pos.x(), pos.z()).getSections())
|
||||
section.getBiomes().getAll(biomes::add);
|
||||
});
|
||||
biomes.retainAll(generator.getBiomeSource().possibleBiomes());
|
||||
// Collect once per chunk, not once per repeated page. Stable registry-ID
|
||||
// order and per-occurrence seeds retain the 1.21.1 placement sequence.
|
||||
Map<Identifier, PlacedFeature> ores = new TreeMap<>();
|
||||
for (Holder<Biome> biome : biomes) {
|
||||
oreFeatures(biome).filter(PortDenseOres::isOre).forEach(feature ->
|
||||
feature.unwrapKey().ifPresent(key -> ores.putIfAbsent(key.identifier(), feature.value())));
|
||||
}
|
||||
BlockPos origin = new BlockPos(chunk.getPos().getMinBlockX(), level.getMinY(),
|
||||
chunk.getPos().getMinBlockZ());
|
||||
FeaturePlacer placer = new FeaturePlacer(level, generator);
|
||||
for (int occurrence = 0; occurrence < count; occurrence++) {
|
||||
for (var entry : ores.entrySet()) {
|
||||
long seed = mix(ageSeed ^ (long) origin.getX() * 341873128712L
|
||||
^ (long) origin.getZ() * 132897987541L
|
||||
^ (long) entry.getKey().hashCode() * 0x9E3779B97F4A7C15L
|
||||
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
placer.placeWithBiomeCheck(entry.getValue(), RandomSource.create(seed), origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.entity.DroppedBookEntity;
|
||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||
import net.dnorberg.rivencraft.item.PortHandheldTravel;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.storage.TagValueInput;
|
||||
import net.minecraft.world.level.storage.TagValueOutput;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.neoforged.neoforge.common.util.FakePlayerFactory;
|
||||
|
||||
/** Disposable-world checks for book ownership and the new entity save format. */
|
||||
final class PortDroppedBookProbe {
|
||||
private PortDroppedBookProbe() {}
|
||||
|
||||
static void create(ServerLevel level) {
|
||||
PortAgeTravel.findLanding(level);
|
||||
var player = FakePlayerFactory.get(level, new com.mojang.authlib.GameProfile(
|
||||
UUID.fromString("4db5c544-0a0c-421d-a128-c520bda64642"), "RivenBookProbe"));
|
||||
player.setPos(0.5, 64, 0.5);
|
||||
try {
|
||||
for (var hand : InteractionHand.values()) {
|
||||
ItemStack book = new ItemStack(hand == InteractionHand.MAIN_HAND
|
||||
? Rivencraft.LINKING_BOOK.get() : Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
book.set(DataComponents.CUSTOM_NAME, Component.literal("Dropped fixture " + hand));
|
||||
book.set(Rivencraft.LINK_BOOK_TARGET.get(), new LinkBookTarget(
|
||||
Identifier.fromNamespaceAndPath("minecraft", "overworld"), 12.25, 80.5, -42.75, 135, -20));
|
||||
player.setItemInHand(hand, book);
|
||||
String failure = PortHandheldTravel.travel(player, hand, book, () -> "expected failure");
|
||||
if (!"expected failure".equals(failure) || player.getItemInHand(hand) != book)
|
||||
throw new IllegalStateException("Failed travel consumed the book");
|
||||
try {
|
||||
PortHandheldTravel.travel(player, hand, book, () -> { throw new IllegalStateException("fixture"); });
|
||||
throw new AssertionError("Fixture exception was swallowed");
|
||||
} catch (IllegalStateException expected) {
|
||||
if (!"fixture".equals(expected.getMessage()) || player.getItemInHand(hand) != book) throw expected;
|
||||
}
|
||||
java.util.function.Consumer<net.neoforged.neoforge.event.entity.EntityJoinLevelEvent> cancel = event -> {
|
||||
if (event.getLevel() == level && event.getEntity() instanceof DroppedBookEntity) event.setCanceled(true);
|
||||
};
|
||||
net.neoforged.neoforge.common.NeoForge.EVENT_BUS.addListener(cancel);
|
||||
try {
|
||||
failure = PortHandheldTravel.travel(player, hand, book, () -> null);
|
||||
if (failure != null || player.getItemInHand(hand) != book)
|
||||
throw new IllegalStateException("Rejected dropped entity consumed the book");
|
||||
} finally { net.neoforged.neoforge.common.NeoForge.EVENT_BUS.unregister(cancel); }
|
||||
failure = PortHandheldTravel.travel(player, hand, book, () -> null);
|
||||
if (failure != null || !player.getItemInHand(hand).isEmpty())
|
||||
throw new IllegalStateException("Successful travel retained the active book");
|
||||
}
|
||||
} finally { player.getInventory().clearContent(); }
|
||||
verify(level);
|
||||
}
|
||||
|
||||
static void verify(ServerLevel level) {
|
||||
var books = level.getEntitiesOfClass(DroppedBookEntity.class, new AABB(-2, 60, -2, 3, 68, 3));
|
||||
if (books.size() != 2) throw new IllegalStateException("Expected two dropped books, found " + books.size());
|
||||
for (var entity : books) {
|
||||
var output = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, level.registryAccess());
|
||||
entity.saveWithoutId(output);
|
||||
var copy = new DroppedBookEntity(Rivencraft.DROPPED_BOOK.get(), level);
|
||||
copy.load(TagValueInput.create(ProblemReporter.DISCARDING, level.registryAccess(), output.buildResult()));
|
||||
if (!ItemStack.isSameItemSameComponents(copy.getItem(), entity.getItem())
|
||||
|| copy.getAge() != -32768 || copy.getItem().getCount() != 1)
|
||||
throw new IllegalStateException("Dropped book persistence regression");
|
||||
var copied = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, level.registryAccess());
|
||||
copy.saveWithoutId(copied);
|
||||
if (copied.buildResult().getIntOr("BookDecayTicks", -1)
|
||||
!= output.buildResult().getIntOr("BookDecayTicks", -2))
|
||||
throw new IllegalStateException("Dropped book decay counter was not preserved");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Seed-completes missing environment controllers once, before an Age is saved. */
|
||||
public record PortEnvironmentSelection(PortWeatherMode weather, PortLighting lighting, PortSkyStyle sky) {
|
||||
public static PortEnvironmentSelection resolve(List<Identifier> pages, long seed, PortAgeTerrain terrain) {
|
||||
PortLighting lighting = pages.stream().map(PortLighting::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x4C49474854494E47L), PortLighting.values()));
|
||||
PortWeatherMode weather = terrain == PortAgeTerrain.CAVE ? PortWeatherMode.OFF
|
||||
: pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x574541544845525FL), PortWeatherMode.values()));
|
||||
return new PortEnvironmentSelection(weather, lighting, PortSkyStyle.resolve(pages, seed));
|
||||
}
|
||||
|
||||
private static <T> T rankedDefault(Random random, T[] choices) {
|
||||
// LegacyRankChoice: the first (Normal) option is rank 1, the rest rank 2.
|
||||
// Its rank-1 weight is the complete rare group plus one. Derive this
|
||||
// from the supported pool, rather than selecting unported weather.
|
||||
int[] weights = new int[choices.length];
|
||||
Arrays.fill(weights, 1);
|
||||
weights[0] = choices.length;
|
||||
return PortRandomAgeSelection.weighted(random, List.of(choices), weights);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicate;
|
||||
import net.minecraft.world.level.levelgen.feature.BlockBlobFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.LakeFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider;
|
||||
import net.minecraft.world.level.levelgen.placement.BiomeFilter;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
import net.minecraft.world.level.levelgen.placement.RarityFilter;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
|
||||
/** Delegates terrain generation and applies frozen Age-scoped features. */
|
||||
public final class PortFeatureChunkGenerator extends ChunkGenerator {
|
||||
private static final ResourceKey<PlacedFeature> FOREST_ROCK = ResourceKey.create(
|
||||
Registries.PLACED_FEATURE, Identifier.withDefaultNamespace("forest_rock"));
|
||||
private static final ResourceKey<PlacedFeature> DUNGEONS = ResourceKey.create(
|
||||
Registries.PLACED_FEATURE, Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "dungeons"));
|
||||
private static final TagKey<Block> ROCK_GROUND = TagKey.create(
|
||||
Registries.BLOCK, Identifier.withDefaultNamespace("forest_rock_can_place_on"));
|
||||
public static final MapCodec<PortFeatureChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
ChunkGenerator.CODEC.fieldOf("base").forGetter(generator -> generator.base),
|
||||
PortRockPlan.CODEC.optionalFieldOf("rocks").forGetter(generator -> Optional.ofNullable(generator.rocks)),
|
||||
PortVegetationPlan.CODEC.optionalFieldOf("vegetation")
|
||||
.forGetter(generator -> Optional.ofNullable(generator.vegetation)),
|
||||
com.mojang.serialization.Codec.intRange(0, 32).optionalFieldOf("dungeons", 0)
|
||||
.forGetter(generator -> generator.dungeons),
|
||||
PortLakePlan.CODEC.optionalFieldOf("lakes")
|
||||
.forGetter(generator -> Optional.ofNullable(generator.lakes)),
|
||||
PortTerrainFeaturePlan.CODEC.optionalFieldOf("terrain_features")
|
||||
.forGetter(generator -> Optional.ofNullable(generator.terrainFeatures)),
|
||||
com.mojang.serialization.Codec.LONG.fieldOf("age_seed").forGetter(generator -> generator.ageSeed)
|
||||
).apply(instance, (base, rocks, vegetation, dungeons, lakes, terrainFeatures, seed) ->
|
||||
new PortFeatureChunkGenerator(base, rocks.orElse(null), vegetation.orElse(null),
|
||||
dungeons, lakes.orElse(null), terrainFeatures.orElse(null), seed)));
|
||||
|
||||
private final ChunkGenerator base;
|
||||
private final PortRockPlan rocks;
|
||||
private final PortVegetationPlan vegetation;
|
||||
private final int dungeons;
|
||||
private final PortLakePlan lakes;
|
||||
private final PortTerrainFeaturePlan terrainFeatures;
|
||||
private final long ageSeed;
|
||||
|
||||
public PortFeatureChunkGenerator(ChunkGenerator base, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, long ageSeed) {
|
||||
super(base.getBiomeSource(), base::getBiomeGenerationSettings);
|
||||
this.base = base;
|
||||
this.rocks = rocks;
|
||||
this.vegetation = vegetation;
|
||||
this.dungeons = dungeons;
|
||||
this.lakes = lakes;
|
||||
this.terrainFeatures = terrainFeatures;
|
||||
this.ageSeed = ageSeed;
|
||||
}
|
||||
|
||||
public ChunkGenerator base() { return base; }
|
||||
public PortRockPlan rocks() { return rocks; }
|
||||
public PortVegetationPlan vegetation() { return vegetation; }
|
||||
public int dungeons() { return dungeons; }
|
||||
public PortLakePlan lakes() { return lakes; }
|
||||
public PortTerrainFeaturePlan terrainFeatures() { return terrainFeatures; }
|
||||
|
||||
@Override protected MapCodec<? extends ChunkGenerator> codec() { return CODEC; }
|
||||
@Override public void validate() { base.validate(); }
|
||||
@Override public ChunkPos getOrigin(RandomState random) { return base.getOrigin(random); }
|
||||
@Override public java.util.Optional<Identifier> getTypeNameForDataFixer() { return base.getTypeNameForDataFixer(); }
|
||||
@Override public CompletableFuture<ChunkAccess> createBiomes(RandomState random, Blender blender,
|
||||
StructureManager structures, ChunkAccess chunk) {
|
||||
return base.createBiomes(random, blender, structures, chunk);
|
||||
}
|
||||
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
|
||||
RandomState random, long seed) {
|
||||
if (controlsStructures()) {
|
||||
var lookup = PortStructurePages.filtered(sets, terrainFeatures.structures(), getBiomeSource());
|
||||
var planning = PortStructurePages.planningBiomes(getBiomeSource(), sets, terrainFeatures.structures());
|
||||
if (base instanceof PortFlatChunkGenerator || base instanceof net.minecraft.world.level.levelgen.FlatLevelSource)
|
||||
return ChunkGeneratorStructureState.createForFlat(random, seed, getOrigin(random), planning,
|
||||
lookup.listElements().map(holder -> (Holder<StructureSet>) holder));
|
||||
return ChunkGeneratorStructureState.createForNormal(random, seed, getOrigin(random), planning, lookup);
|
||||
}
|
||||
return base.createState(sets, random, seed);
|
||||
}
|
||||
|
||||
private boolean controlsStructures() {
|
||||
return terrainFeatures != null && (terrainFeatures.structureControl() || !terrainFeatures.structures().isEmpty());
|
||||
}
|
||||
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
|
||||
StructureManager structures, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||
if (controlsStructures()) super.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||
else base.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||
}
|
||||
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) {
|
||||
base.createReferences(level, structures, chunk);
|
||||
}
|
||||
@Override public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level,
|
||||
HolderSet<Structure> targets, BlockPos origin, int radius, boolean skipKnown) {
|
||||
return base.findNearestMapStructure(level, targets, origin, radius, skipKnown);
|
||||
}
|
||||
@Override public void spawnOriginalMobs(WorldGenRegion level) { base.spawnOriginalMobs(level); }
|
||||
@Override public int getSpawnHeight(LevelHeightAccessor height) { return base.getSpawnHeight(height); }
|
||||
@Override public int getGenDepth() { return base.getGenDepth(); }
|
||||
@Override public int getSeaLevel() { return base.getSeaLevel(); }
|
||||
@Override public int getMinY() { return base.getMinY(); }
|
||||
@Override public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender,
|
||||
RandomState random, StructureManager structures, BiomeManager biomes,
|
||||
WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
|
||||
CompletableFuture<ChunkAccess> built = base.buildTerrain(
|
||||
chunk, blender, random, structures, biomes, carverBiomeRegion, possibleBiomes);
|
||||
if (terrainFeatures == null || !terrainFeatures.skylands() && terrainFeatures.choices().isEmpty()) return built;
|
||||
return built.thenApply(result -> {
|
||||
// Match 1.21.1: filter the base terrain once, then add the authored
|
||||
// solid volumes, irrespective of the Skylands page's written position.
|
||||
if (terrainFeatures.skylands()) PortSkylandsFeature.filter(result, ageSeed);
|
||||
java.util.Map<Identifier, Integer> occurrences = new java.util.HashMap<>();
|
||||
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
|
||||
if (PortTerrainFeaturePlan.isSurface(choice.page())) continue;
|
||||
int occurrence = occurrences.merge(choice.page(), 1, Integer::sum) - 1;
|
||||
long featureSeed = ageSeed ^ (long) occurrence * 0x9E3779B97F4A7C15L;
|
||||
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
|
||||
if (choice.page().equals(PortTerrainFeaturePlan.FLOATING_ISLANDS))
|
||||
PortFloatingIslandFeature.generate(result, featureSeed, material);
|
||||
else if (choice.page().equals(PortTerrainFeaturePlan.TENDRILS))
|
||||
PortTendrilFeature.generate(result, featureSeed, material);
|
||||
else if (choice.page().equals(PortTerrainFeaturePlan.SPHERES))
|
||||
PortSpheresFeature.generate(result, featureSeed, material);
|
||||
else throw new IllegalStateException("Unsupported terrain feature: " + choice.page());
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
@Override public int getBaseHeight(int x, int z, Heightmap.Types type,
|
||||
LevelHeightAccessor height, RandomState random) {
|
||||
return base.getBaseHeight(x, z, type, height, random);
|
||||
}
|
||||
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState random) {
|
||||
return base.getBaseColumn(x, z, height, random);
|
||||
}
|
||||
@Override public void addDebugScreenInfo(List<String> info, RandomState random,
|
||||
BlockPos pos, SamplerContext context) {
|
||||
base.addDebugScreenInfo(info, random, pos, context);
|
||||
}
|
||||
@Override public void refreshFeaturesPerStep() {
|
||||
base.refreshFeaturesPerStep();
|
||||
super.refreshFeaturesPerStep();
|
||||
}
|
||||
|
||||
@Override public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk,
|
||||
StructureManager structures) {
|
||||
base.applyBiomeDecoration(level, chunk, structures);
|
||||
if (terrainFeatures != null) {
|
||||
PortDenseOres.place(level, chunk, this, ageSeed, terrainFeatures.denseOres());
|
||||
int spikeOccurrence = 0;
|
||||
int obeliskOccurrence = 0;
|
||||
int crystalOccurrence = 0;
|
||||
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
|
||||
if (!PortTerrainFeaturePlan.isSurface(choice.page())) continue;
|
||||
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
|
||||
if (choice.page().equals(PortTerrainFeaturePlan.SPIKES))
|
||||
PortSpikesFeature.generate(level, chunk, ageSeed, material, spikeOccurrence++);
|
||||
else if (choice.page().equals(PortTerrainFeaturePlan.OBELISKS))
|
||||
PortObelisksFeature.generate(level, chunk, ageSeed, material, obeliskOccurrence++);
|
||||
else if (choice.page().equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS))
|
||||
PortCrystalFormationsFeature.generate(level, chunk, ageSeed, crystalOccurrence++);
|
||||
else throw new IllegalStateException("Unsupported surface feature: " + choice.page());
|
||||
}
|
||||
}
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
long chunkSeed = ageSeed ^ (long) chunkPos.x() * 341873128712L
|
||||
^ (long) chunkPos.z() * 132897987541L;
|
||||
BlockPos origin = new BlockPos(chunkPos.getMinBlockX(), level.getMinY(), chunkPos.getMinBlockZ());
|
||||
if (rocks != null) {
|
||||
PlacedFeature vanilla = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
.getOrThrow(FOREST_ROCK).value();
|
||||
var placement = vanilla.placement().stream()
|
||||
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList();
|
||||
for (int occurrence = 0; occurrence < rocks.materials().size(); occurrence++) {
|
||||
RandomSource random = RandomSource.create(chunkSeed
|
||||
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
BlockState material = BuiltInRegistries.BLOCK.getValue(
|
||||
rocks.materials().get(occurrence)).defaultBlockState();
|
||||
PlacedFeature authored = new PlacedFeature(Holder.direct(new BlockBlobFeature(
|
||||
material, BlockPredicate.matchesTag(ROCK_GROUND))), placement);
|
||||
authored.place(level, this, random, origin);
|
||||
}
|
||||
}
|
||||
if (dungeons > 0) {
|
||||
PlacedFeature authored = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
.getOrThrow(DUNGEONS).value();
|
||||
for (int occurrence = 0; occurrence < dungeons; occurrence++) {
|
||||
RandomSource random = RandomSource.create(chunkSeed
|
||||
^ 0x44554E47454F4E53L ^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
authored.place(level, this, random, origin);
|
||||
}
|
||||
}
|
||||
if (lakes != null) {
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||
java.util.Map<PortLakePlan.Choice, Integer> occurrences = new java.util.HashMap<>();
|
||||
for (PortLakePlan.Choice choice : lakes.choices()) {
|
||||
PlacedFeature vanilla = registry.getOrThrow(ResourceKey.create(
|
||||
Registries.PLACED_FEATURE, PortLakePlan.placedFeature(choice.page()))).value();
|
||||
if (!(vanilla.feature().value() instanceof LakeFeature lake))
|
||||
throw new IllegalStateException("Lake placement no longer references a LakeFeature: " + choice.page());
|
||||
BlockState fluid = BuiltInRegistries.BLOCK.getValue(choice.fluid()).defaultBlockState();
|
||||
LakeFeature feature = new LakeFeature(BlockStateProvider.holderOf(fluid), lake.barrier(),
|
||||
lake.canPlaceFeature(), lake.canReplaceWithAirOrFluid(), lake.canReplaceWithBarrier());
|
||||
PlacedFeature authored = new PlacedFeature(Holder.direct(feature), vanilla.placement().stream()
|
||||
.filter(modifier -> !(modifier instanceof BiomeFilter))
|
||||
.map(modifier -> !PortLakePlan.isDeep(choice.page()) && modifier instanceof RarityFilter
|
||||
? RarityFilter.onAverageOnceEvery(12) : modifier)
|
||||
.toList());
|
||||
int occurrence = occurrences.merge(choice, 1, Integer::sum) - 1;
|
||||
RandomSource random = RandomSource.create(chunkSeed
|
||||
^ (long) choice.hashCode() * 0x9E3779B97F4A7C15L
|
||||
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
authored.place(level, this, random, origin);
|
||||
}
|
||||
}
|
||||
if (vegetation != null) {
|
||||
int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 8);
|
||||
Holder<Biome> biome = level.getBiome(new BlockPos(
|
||||
chunkPos.getMinBlockX() + 8, surface, chunkPos.getMinBlockZ() + 8));
|
||||
boolean surfaceBiome = !biome.is(BiomeTags.IS_END) && !PortRegistryPages.isCaveBiome(biome);
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||
List<BlockPos> 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());
|
||||
authored.place(level, this, random, origin);
|
||||
if (PortVegetationPlan.addsLogs(page)) mushroomHosts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.QuartPos;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
|
||||
/** 1.21.1 floating-island shape adapted to the 26.3 chunk terrain stage. */
|
||||
final class PortFloatingIslandFeature {
|
||||
private static final long SALT = 0x464C4F4154494E47L;
|
||||
private static final int SOURCE_RANGE = 3;
|
||||
private static final int CHANCE = 128;
|
||||
|
||||
private PortFloatingIslandFeature() {}
|
||||
|
||||
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||
var target = chunk.getPos();
|
||||
for (int sourceX = target.x() - SOURCE_RANGE; sourceX <= target.x() + SOURCE_RANGE; sourceX++) {
|
||||
for (int sourceZ = target.z() - SOURCE_RANGE; sourceZ <= target.z() + SOURCE_RANGE; sourceZ++) {
|
||||
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
|
||||
^ (long) sourceX * 341873128712L ^ (long) sourceZ * 132897987541L));
|
||||
if (random.nextInt(CHANCE) != 0) continue;
|
||||
int centerX = sourceX * 16 + random.nextInt(16);
|
||||
int centerZ = sourceZ * 16 + random.nextInt(16);
|
||||
int centerY = Math.min(chunk.getMaxY() - 30, 168 + random.nextInt(48));
|
||||
if (centerY < chunk.getMinY() + 24) continue;
|
||||
int radiusX = 24 + random.nextInt(18);
|
||||
int radiusZ = 24 + random.nextInt(18);
|
||||
fill(chunk, ageSeed, material, centerX, centerY, centerZ, radiusX, radiusZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void fill(ChunkAccess chunk, long ageSeed, BlockState material,
|
||||
int centerX, int centerY, int centerZ, int radiusX, int radiusZ) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
int worldX = originX + localX;
|
||||
double dx = (worldX - centerX) / (double) radiusX;
|
||||
for (int localZ = 0; localZ < 16; localZ++) {
|
||||
int worldZ = originZ + localZ;
|
||||
double dz = (worldZ - centerZ) / (double) radiusZ;
|
||||
double distance = dx * dx + dz * dz;
|
||||
if (distance >= 1.0) continue;
|
||||
long detail = mix(ageSeed ^ (long) centerX * 341873128712L
|
||||
^ (long) centerZ * 132897987541L ^ (long) worldX * 42317861L
|
||||
^ (long) worldZ * 374761393L);
|
||||
int top = centerY + (int) Math.round(3 * (1 - distance)) + Math.floorMod(detail, 3) - 1;
|
||||
int depth = Math.max(1, (int) Math.round(3 + 17 * Math.pow(1 - distance, 1.5)));
|
||||
// Biomes are already populated by createBiomes in 26.3, so
|
||||
// sampling this chunk preserves its authored biome distribution.
|
||||
var biome = chunk.getNoiseBiome(QuartPos.fromBlock(worldX), QuartPos.fromBlock(top),
|
||||
QuartPos.fromBlock(worldZ));
|
||||
BlockState soil = biome.is(BiomeTags.IS_END) || PortRegistryPages.isCaveBiome(biome)
|
||||
? material : Blocks.DIRT.defaultBlockState();
|
||||
BlockState cap = soil == material ? material : Blocks.GRASS_BLOCK.defaultBlockState();
|
||||
for (int y = Math.max(top - depth, chunk.getMinY()); y <= top; y++) {
|
||||
pos.set(worldX, y, worldZ);
|
||||
if (!chunk.getBlockState(pos).isAir()) continue;
|
||||
BlockState state = y == top ? cap : y >= top - 3 ? soil : material;
|
||||
chunk.setBlockState(pos, state);
|
||||
surface.update(localX, y, localZ, state);
|
||||
floor.update(localX, y, localZ, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Ordered lake pages with fluid choices frozen when the Age is bound. */
|
||||
public record PortLakePlan(List<Choice> choices) {
|
||||
public static final Codec<PortLakePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Choice.CODEC.listOf().fieldOf("choices").forGetter(PortLakePlan::choices)
|
||||
).apply(instance, PortLakePlan::new));
|
||||
|
||||
public record Choice(Identifier page, Identifier fluid) {
|
||||
public static final Codec<Choice> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.fieldOf("page").forGetter(Choice::page),
|
||||
Identifier.CODEC.fieldOf("fluid").forGetter(Choice::fluid)
|
||||
).apply(instance, Choice::new));
|
||||
|
||||
public Choice {
|
||||
if (!isLake(page) || fluid == null) throw new IllegalArgumentException("Invalid lake choice");
|
||||
}
|
||||
}
|
||||
|
||||
public PortLakePlan {
|
||||
choices = List.copyOf(choices);
|
||||
if (choices.isEmpty() || choices.size() > 32)
|
||||
throw new IllegalArgumentException("Lake plan must contain 1–32 choices");
|
||||
}
|
||||
|
||||
public static boolean isLake(Identifier page) {
|
||||
if (page == null || !page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
|
||||
return switch (page.getPath()) {
|
||||
case "feature/surface_lakes", "feature/deep_lakes", "feature/lava_lakes" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isDeep(Identifier page) { return page.getPath().equals("feature/deep_lakes"); }
|
||||
|
||||
public static Identifier placedFeature(Identifier page) {
|
||||
if (!isLake(page)) throw new IllegalArgumentException("Unsupported lake page " + page);
|
||||
return Identifier.withDefaultNamespace(isDeep(page) ? "lake_lava_underground" : "lake_lava_surface");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.Locale;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Authored lighting choices using statically registered, client-synchronized types. */
|
||||
public enum PortLighting {
|
||||
NORMAL, DARK, BRIGHT;
|
||||
|
||||
public static final Codec<PortLighting> CODEC = Codec.STRING.xmap(
|
||||
name -> valueOf(name.toUpperCase(Locale.ROOT)), mode -> mode.name().toLowerCase(Locale.ROOT));
|
||||
|
||||
public static PortLighting forPage(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
case "lighting/normal" -> NORMAL;
|
||||
case "lighting/dark" -> DARK;
|
||||
case "lighting/bright" -> BRIGHT;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isBright(Level level) {
|
||||
return level.dimensionTypeRegistration().unwrapKey().map(key -> {
|
||||
Identifier id = key.identifier();
|
||||
return id.getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& (id.getPath().equals("age_bright") || id.getPath().equals("age_cave_bright")
|
||||
|| id.getPath().startsWith("age_bright_sky_")
|
||||
|| id.getPath().startsWith("age_cave_bright_sky_"));
|
||||
}).orElse(false);
|
||||
}
|
||||
|
||||
Holder<DimensionType> dimensionType(MinecraftServer server, PortAgeTerrain terrain,
|
||||
Holder<DimensionType> overworldType, PortSkyStyle sky) {
|
||||
boolean cave = terrain == PortAgeTerrain.CAVE;
|
||||
if (this == NORMAL && !cave && sky == PortSkyStyle.OVERWORLD) return overworldType;
|
||||
String variant = this == BRIGHT ? (cave ? "age_cave_bright" : "age_bright")
|
||||
: this == DARK || cave ? "age_dark" : "age_normal";
|
||||
if (sky != PortSkyStyle.OVERWORLD) variant += "_sky_" + sky.name().toLowerCase(Locale.ROOT);
|
||||
return server.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(
|
||||
ResourceKey.create(Registries.DIMENSION_TYPE,
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, variant)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.menu.NotebookMenu;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Disposable-world menu, ownership, codec and chest-persistence fixtures. */
|
||||
final class PortNotebookProbe {
|
||||
private static final BlockPos CHEST = new BlockPos(0, 64, 0);
|
||||
private PortNotebookProbe() {}
|
||||
|
||||
static void create(ServerLevel level) {
|
||||
PortAgeTravel.findLanding(level);
|
||||
level.setBlock(CHEST, Blocks.CHEST.defaultBlockState(), 3);
|
||||
Container chest = (Container) level.getBlockEntity(CHEST);
|
||||
var player = net.neoforged.neoforge.common.util.FakePlayerFactory.get(level,
|
||||
new com.mojang.authlib.GameProfile(java.util.UUID.fromString("1043b9f3-f348-4a73-b585-e0de13fd5c27"), "RivenNotebookProbe"));
|
||||
int output = 0;
|
||||
try {
|
||||
for (boolean folder : List.of(false, true)) for (var hand : InteractionHand.values()) {
|
||||
player.getInventory().clearContent();
|
||||
player.getInventory().setSelectedSlot(0);
|
||||
ItemStack collection = new ItemStack(folder ? Rivencraft.COLLATION_FOLDER.get() : Rivencraft.NOTEBOOK.get());
|
||||
player.setItemInHand(hand, collection);
|
||||
var menu = new NotebookMenu(1, player.getInventory(), hand);
|
||||
int capacity = CollationFolderItem.capacity(collection);
|
||||
if (!menu.stillValid(player) || menu.pageCapacity() != capacity
|
||||
|| menu.getSlot(0).mayPlace(PageItem.createLinkPanelPage())
|
||||
|| menu.getSlot(0).mayPlace(new ItemStack(net.minecraft.world.item.Items.DIAMOND)))
|
||||
throw new IllegalStateException("Notebook capacity/page acceptance regression");
|
||||
ItemStack page = PageItem.createSymbolPage(Identifier.fromNamespaceAndPath("rivencraft", "terrain/flat"));
|
||||
player.getInventory().setItem(9, page.copyWithCount(3));
|
||||
menu.clicked(NotebookMenu.NOTEBOOK_SLOTS, 0, ContainerInput.QUICK_MOVE, player);
|
||||
if (!player.getInventory().getItem(9).isEmpty() || contents(collection).pageCount() != 3)
|
||||
throw new IllegalStateException("Notebook shift insertion regression");
|
||||
for (int i = 0; i < 3; i++) menu.clicked(i, 0, ContainerInput.QUICK_MOVE, player);
|
||||
if (contents(collection).pageCount() != 0) throw new IllegalStateException("Notebook shift extraction regression");
|
||||
menu.getSlot(capacity - 1).set(page.copy());
|
||||
if (contents(collection).pageCount() != 1 || !contents(collection).pages().get(0).isEmpty()
|
||||
|| contents(collection).pages().get(capacity - 1).isEmpty())
|
||||
throw new IllegalStateException("Notebook sparse position regression");
|
||||
if (!menu.clickMenuButton(player, NotebookMenu.MOVE_PREVIOUS_BASE + capacity - 1)
|
||||
|| !contents(collection).pages().get(capacity - 1).isEmpty())
|
||||
throw new IllegalStateException("Notebook page movement regression");
|
||||
if (folder && menu.getSlot(capacity).mayPlace(page))
|
||||
throw new IllegalStateException("Folder exceeded 27 positions");
|
||||
menu.clicked(NotebookMenu.NOTEBOOK_SLOTS + 27, 0, ContainerInput.PICKUP, player);
|
||||
if (player.getItemInHand(hand) != collection || !menu.getCarried().isEmpty())
|
||||
throw new IllegalStateException("Notebook active item was movable");
|
||||
var ops = RegistryOps.create(com.mojang.serialization.JsonOps.INSTANCE, level.registryAccess());
|
||||
var encoded = NotebookContents.CODEC.encodeStart(ops, contents(collection)).getOrThrow();
|
||||
var decoded = NotebookContents.CODEC.parse(ops, encoded).getOrThrow();
|
||||
var buffer = new RegistryFriendlyByteBuf(io.netty.buffer.Unpooled.buffer(), level.registryAccess());
|
||||
try {
|
||||
NotebookContents.STREAM_CODEC.encode(buffer, decoded);
|
||||
decoded = NotebookContents.STREAM_CODEC.decode(buffer);
|
||||
if (decoded.pageCount() != 1 || decoded.pages().get(capacity - 2).isEmpty())
|
||||
throw new IllegalStateException("Notebook save/network codec regression");
|
||||
} finally { buffer.release(); }
|
||||
chest.setItem(output++, collection.copy());
|
||||
player.setItemInHand(hand, ItemStack.EMPTY);
|
||||
}
|
||||
} finally { player.getInventory().clearContent(); }
|
||||
verify(level);
|
||||
}
|
||||
|
||||
private static NotebookContents contents(ItemStack stack) {
|
||||
return stack.getOrDefault(Rivencraft.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY);
|
||||
}
|
||||
|
||||
static void verify(ServerLevel level) {
|
||||
if (!(level.getBlockEntity(CHEST) instanceof Container chest)) throw new IllegalStateException("Fixture chest missing");
|
||||
for (int i = 0; i < 4; i++) {
|
||||
var collection = chest.getItem(i);
|
||||
int capacity = i < 2 ? 252 : 27;
|
||||
if (CollationFolderItem.capacity(collection) != capacity || contents(collection).pageCount() != 1
|
||||
|| contents(collection).pages().get(capacity - 2).isEmpty())
|
||||
throw new IllegalStateException("Notebook chest persistence regression: " + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/** 26.3 port of Rivencraft 1.21.1's Legacy-style 4×4 plinth and 2×12×2 shaft. */
|
||||
final class PortObelisksFeature {
|
||||
private static final long SALT = 0x4F42454C49534B53L;
|
||||
private static final int CHANCE = 128;
|
||||
private static final int BASE = 4;
|
||||
private static final int HEIGHT = 12;
|
||||
|
||||
private PortObelisksFeature() {}
|
||||
|
||||
record Site(int x, int z) {}
|
||||
|
||||
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
|
||||
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
|
||||
^ (long) chunk.x() * 341873128712L ^ (long) chunk.z() * 132897987541L
|
||||
^ (long) occurrence * 0x9E3779B97F4A7C15L));
|
||||
if (random.nextInt(CHANCE) != 0) return null;
|
||||
return new Site(chunk.getMinBlockX() + 4 + random.nextInt(8),
|
||||
chunk.getMinBlockZ() + 4 + random.nextInt(8));
|
||||
}
|
||||
|
||||
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
|
||||
Site site = site(ageSeed, chunk.getPos(), occurrence);
|
||||
if (site == null) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
int minSurface = Integer.MAX_VALUE;
|
||||
int maxSurface = Integer.MIN_VALUE;
|
||||
int[][] surface = new int[BASE][BASE];
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
int x = site.x() + dx;
|
||||
int z = site.z() + dz;
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||
if (y <= level.getMinY() + 1) return;
|
||||
BlockState ground = level.getBlockState(pos.set(x, y - 1, z));
|
||||
if (!ground.isFaceSturdy(level, pos, Direction.UP)
|
||||
|| !ground.getFluidState().isEmpty() || ground.hasBlockEntity()) return;
|
||||
surface[dx][dz] = y;
|
||||
minSurface = Math.min(minSurface, y);
|
||||
maxSurface = Math.max(maxSurface, y);
|
||||
}
|
||||
}
|
||||
if (maxSurface - minSurface > 2 || maxSurface + HEIGHT >= level.getMaxY()) return;
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
for (int y = surface[dx][dz]; y <= maxSurface; y++) {
|
||||
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = 1; dx <= 2; dx++) {
|
||||
for (int dz = 1; dz <= 2; dz++) {
|
||||
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++) {
|
||||
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
for (int y = surface[dx][dz]; y <= maxSurface; y++)
|
||||
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||
}
|
||||
}
|
||||
for (int dx = 1; dx <= 2; dx++) {
|
||||
for (int dz = 1; dz <= 2; dz++) {
|
||||
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++)
|
||||
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean open(BlockState state) {
|
||||
return state.getFluidState().isEmpty() && (state.isAir() || state.canBeReplaced());
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
/** Seed-frozen terrain and biome choices for a book with no authored symbols. */
|
||||
public final class PortRandomAgeSelection {
|
||||
private PortRandomAgeSelection() {}
|
||||
|
||||
public record Selection(PortAgeTerrain terrain, PortBiomePlan biomes) {}
|
||||
|
||||
public static Selection select(MinecraftServer server, long seed) {
|
||||
// Match the 1.21.1 Legacy-rank choices: Standard and Flat are common;
|
||||
// the other terrain families are rarer. The sorted order is deliberate.
|
||||
PortAgeTerrain terrain = terrain(seed);
|
||||
if (terrain == PortAgeTerrain.VOID)
|
||||
return new Selection(terrain, new PortBiomePlan(PortBiomePlan.Distribution.SINGLE,
|
||||
List.of(Identifier.withDefaultNamespace("the_void"))));
|
||||
|
||||
PortBiomePlan.Distribution distribution = distribution(seed);
|
||||
List<Identifier> eligible = eligibleBiomes(server, terrain);
|
||||
if (eligible.size() < 2 && distribution != PortBiomePlan.Distribution.NATIVE)
|
||||
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
return new Selection(terrain, new PortBiomePlan(distribution,
|
||||
terrain == PortAgeTerrain.CAVE ? eligible : List.of()));
|
||||
|
||||
int count = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||
if (count > 1) {
|
||||
Random countRandom = new Random(seed ^ 0x42494F4D45434F55L);
|
||||
while (count < eligible.size() && countRandom.nextInt(3) != 0) count++;
|
||||
}
|
||||
List<Identifier> shuffled = new ArrayList<>(eligible);
|
||||
Collections.shuffle(shuffled, new Random(seed ^ 0x42494F4D45504F4FL));
|
||||
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
|
||||
}
|
||||
|
||||
public static List<PortAgeTerrain> terrains() {
|
||||
return List.of(PortAgeTerrain.AMPLIFIED, PortAgeTerrain.CAVE, PortAgeTerrain.FLAT,
|
||||
PortAgeTerrain.ISLAND, PortAgeTerrain.STANDARD, PortAgeTerrain.VOID);
|
||||
}
|
||||
|
||||
public static PortAgeTerrain terrain(long seed) {
|
||||
return weighted(new Random(seed ^ 0x5445525241494E4CL), terrains(), new int[]{1, 1, 5, 1, 6, 1});
|
||||
}
|
||||
|
||||
/** First AgeTraits draw in 1.21.1; shared by random and partly authored Ages. */
|
||||
public static PortBiomePlan.Distribution distribution(long seed) {
|
||||
return weighted(new Random(seed ^ 0x524956454E435241L),
|
||||
List.of(PortBiomePlan.Distribution.NATIVE, PortBiomePlan.Distribution.SINGLE,
|
||||
PortBiomePlan.Distribution.MEDIUM, PortBiomePlan.Distribution.TILED,
|
||||
PortBiomePlan.Distribution.GRID, PortBiomePlan.Distribution.TINY,
|
||||
PortBiomePlan.Distribution.SMALL, PortBiomePlan.Distribution.LARGE,
|
||||
PortBiomePlan.Distribution.HUGE),
|
||||
new int[]{3, 3, 3, 1, 1, 1, 1, 1, 1});
|
||||
}
|
||||
|
||||
public static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
|
||||
if (terrain == PortAgeTerrain.VOID) return List.of(Identifier.withDefaultNamespace("the_void"));
|
||||
if (terrain == PortAgeTerrain.CAVE) {
|
||||
return caveBiomes(server.registryAccess());
|
||||
}
|
||||
List<Identifier> surface = server.overworld().getChunkSource().getGenerator().getBiomeSource()
|
||||
.possibleBiomes().stream().flatMap(holder -> holder.unwrapKey().stream())
|
||||
.map(key -> key.identifier()).distinct().sorted(Comparator.naturalOrder()).toList();
|
||||
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
|
||||
}
|
||||
|
||||
public static List<Identifier> caveBiomes(net.minecraft.core.HolderLookup.Provider registries) {
|
||||
var caves = registries.lookupOrThrow(Registries.BIOME).listElements()
|
||||
.filter(PortRegistryPages::isCaveBiome).map(holder -> holder.key().identifier()).sorted().toList();
|
||||
return caves.isEmpty() ? List.of(Identifier.withDefaultNamespace("lush_caves")) : caves;
|
||||
}
|
||||
|
||||
static <T> T weighted(Random random, List<T> choices, int[] weights) {
|
||||
int total = 0;
|
||||
for (int weight : weights) total += weight;
|
||||
int ticket = random.nextInt(total);
|
||||
for (int index = 0; index < choices.size(); index++) {
|
||||
ticket -= weights[index];
|
||||
if (ticket < 0) return choices.get(index);
|
||||
}
|
||||
throw new IllegalStateException("Random Age ticket was not consumed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Frozen page-free material features and rare formations from 1.21.1. */
|
||||
public final class PortRandomFeatureSelection {
|
||||
private PortRandomFeatureSelection() {}
|
||||
public record Selection(PortRockPlan rocks, PortLakePlan lakes, PortTerrainFeaturePlan terrain, int dungeons) {}
|
||||
|
||||
public static Selection select(net.minecraft.server.MinecraftServer server, long seed,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomes) {
|
||||
var base = select(seed, terrain);
|
||||
var structures = PortRandomStructureSelection.select(server, seed, terrain, biomes);
|
||||
var plan = base.terrain();
|
||||
plan = plan == null ? new PortTerrainFeaturePlan(List.of(), false, 0, false, structures.structures(), List.of(), true)
|
||||
: new PortTerrainFeaturePlan(plan.choices(), plan.skylands(), plan.denseOres(), plan.starFissures(),
|
||||
structures.structures(), plan.carvers(), true);
|
||||
return new Selection(base.rocks(), base.lakes(), plan, structures.dungeons());
|
||||
}
|
||||
|
||||
public static Selection select(long seed, PortAgeTerrain terrain) {
|
||||
var random = new Random(seed ^ 0x4D4154455249414CL);
|
||||
int count = random.nextInt(3);
|
||||
var terrainPage = id("terrain/" + terrain.name().toLowerCase(java.util.Locale.ROOT));
|
||||
var candidates = new ArrayList<PortRankedChoice.Option<Identifier>>();
|
||||
for (String name : List.of("rocks", "floating_islands", "tendrils", "spheres", "spikes", "obelisks",
|
||||
"surface_lakes", "deep_lakes", "lava_lakes")) {
|
||||
var page = id("feature/" + name);
|
||||
if (PortAuthoredWriting.supports(List.of(terrainPage, page)))
|
||||
candidates.add(new PortRankedChoice.Option<>(page, rank(name)));
|
||||
}
|
||||
var pages = new ArrayList<Identifier>();
|
||||
pages.add(terrainPage);
|
||||
List<Identifier> solids = PortRegistryPages.solidMaterialIds(), fluids = PortRegistryPages.fluidMaterialIds();
|
||||
for (int selected = 0; selected < count && !candidates.isEmpty();) {
|
||||
Identifier feature = PortRankedChoice.choose(random, candidates);
|
||||
candidates.removeIf(option -> option.value().equals(feature));
|
||||
var materials = PortLakePlan.isLake(feature) ? fluids : solids;
|
||||
if (materials.isEmpty()) continue;
|
||||
pages.add(PortRegistryPages.blockPageId(materials.get(random.nextInt(materials.size()))));
|
||||
pages.add(feature);
|
||||
selected++;
|
||||
}
|
||||
if (terrain != PortAgeTerrain.FLAT && terrain != PortAgeTerrain.VOID
|
||||
&& Math.floorMod(mix(seed ^ 0x534B594C414E4453L), 8) == 0)
|
||||
pages.add(PortTerrainFeaturePlan.SKYLANDS);
|
||||
if (terrain != PortAgeTerrain.CAVE && terrain != PortAgeTerrain.VOID && random.nextInt(10) == 0)
|
||||
pages.add(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS);
|
||||
if (randomStarFissure(seed)) pages.add(PortTerrainFeaturePlan.STAR_FISSURES);
|
||||
return new Selection(PortAuthoredWriting.rockPlan(pages, seed), PortAuthoredWriting.lakePlan(pages, seed),
|
||||
PortAuthoredWriting.terrainFeaturePlan(pages, seed), 0);
|
||||
}
|
||||
|
||||
private static int rank(String name) {
|
||||
return switch (name) {
|
||||
case "surface_lakes", "deep_lakes" -> 1;
|
||||
case "lava_lakes" -> 2;
|
||||
case "spheres", "spikes", "obelisks" -> 3;
|
||||
default -> 4;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean randomStarFissure(long seed) {
|
||||
var random = new Random(mix(seed ^ 0x535441524752414DL));
|
||||
for (int step = 0; step < 27; step++) {
|
||||
int branch = random.nextInt(5);
|
||||
if (branch == 0) return false;
|
||||
if (random.nextInt(9) == 0) return true;
|
||||
if (branch == 1) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Identifier id(String path) { return Identifier.fromNamespaceAndPath("rivencraft", path); }
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
|
||||
/** Creation-only structure completion, adapted from 1.21.1 AgeStructureSelection. */
|
||||
public final class PortRandomStructureSelection {
|
||||
private static final Identifier DUNGEONS = page("dungeons");
|
||||
private PortRandomStructureSelection() {}
|
||||
public record Selection(int dungeons, List<Identifier> structures) {
|
||||
public Selection { structures = List.copyOf(structures); }
|
||||
}
|
||||
|
||||
public static Selection select(MinecraftServer server, long seed, PortAgeTerrain terrain, PortBiomePlan plan) {
|
||||
Set<Identifier> biomes = actualBiomes(server, terrain, plan);
|
||||
var eligible = new ArrayList<PortRankedChoice.Option<Identifier>>();
|
||||
for (String name : List.of("dungeons", "mineshafts", "nether_fortresses", "strongholds", "villages",
|
||||
"trial_chambers", "bastions", "buried_treasure", "shipwrecks", "ocean_ruins", "pillager_outposts",
|
||||
"ruined_portals", "nether_fossils", "ancient_cities", "trail_ruins", "abandoned_camps")) {
|
||||
Identifier feature = page(name);
|
||||
boolean allowed = feature.equals(DUNGEONS)
|
||||
? net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(List.of(
|
||||
Identifier.fromNamespaceAndPath("rivencraft", "terrain/" + terrain.name().toLowerCase(java.util.Locale.ROOT)), feature))
|
||||
&& server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
.get(ResourceKey.create(Registries.PLACED_FEATURE,
|
||||
Identifier.fromNamespaceAndPath("rivencraft", "dungeons"))).isPresent()
|
||||
: PortStructurePages.randomEligible(feature, terrain, biomes, server.registryAccess());
|
||||
if (allowed) eligible.add(new PortRankedChoice.Option<>(feature, rank(name)));
|
||||
}
|
||||
var random = new Random(seed ^ 0x5354525543545552L);
|
||||
int count = random.nextInt(3), dungeons = 0;
|
||||
var structures = new ArrayList<Identifier>();
|
||||
for (int selected = 0; selected < count && !eligible.isEmpty(); selected++) {
|
||||
var feature = PortRankedChoice.choose(random, eligible);
|
||||
if (feature.equals(DUNGEONS)) dungeons++;
|
||||
else structures.add(feature);
|
||||
eligible.removeIf(option -> option.value().equals(feature));
|
||||
}
|
||||
return new Selection(dungeons, structures);
|
||||
}
|
||||
|
||||
private static Set<Identifier> actualBiomes(MinecraftServer server, PortAgeTerrain terrain, PortBiomePlan plan) {
|
||||
if (plan != null && !plan.biomes().isEmpty())
|
||||
return Set.copyOf(plan.biomes());
|
||||
if (terrain == PortAgeTerrain.FLAT) return Set.of(Identifier.withDefaultNamespace("plains"));
|
||||
if (terrain == PortAgeTerrain.VOID) return Set.of(Identifier.withDefaultNamespace("the_void"));
|
||||
if (terrain == PortAgeTerrain.CAVE) return Set.of(Identifier.withDefaultNamespace("lush_caves"));
|
||||
return server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM).getOrThrow(LevelStem.OVERWORLD)
|
||||
.value().generator().getBiomeSource().possibleBiomes().stream()
|
||||
.flatMap(holder -> holder.unwrapKey().stream()).map(key -> key.identifier())
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
private static int rank(String name) {
|
||||
return switch (name) {
|
||||
case "villages", "strongholds", "mineshafts" -> 1;
|
||||
case "dungeons", "nether_fortresses" -> 2;
|
||||
default -> 3;
|
||||
};
|
||||
}
|
||||
private static Identifier page(String name) { return Identifier.fromNamespaceAndPath("rivencraft", "feature/" + name); }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/** The 1.21.1 LegacyRankChoice rule, retained for seeded grammar completion. */
|
||||
final class PortRankedChoice {
|
||||
private PortRankedChoice() {}
|
||||
record Option<T>(T value, int rank) {}
|
||||
|
||||
static <T> T choose(Random random, List<Option<T>> options) {
|
||||
if (options.isEmpty()) throw new IllegalArgumentException("No ranked choices");
|
||||
var counts = new HashMap<Integer, Integer>();
|
||||
int highest = 0;
|
||||
for (var option : options) {
|
||||
counts.merge(option.rank(), 1, Integer::sum);
|
||||
highest = Math.max(highest, option.rank());
|
||||
}
|
||||
var weights = new HashMap<Integer, Integer>();
|
||||
int weight = 1, previousTotal = 0;
|
||||
for (int rank = highest; rank >= 0; rank--) {
|
||||
int count = counts.getOrDefault(rank, 0);
|
||||
if (weight != 1 && count > 0) weight = Math.max(weight, previousTotal / count + 1);
|
||||
weights.put(rank, weight);
|
||||
previousTotal = count * weight;
|
||||
weight++;
|
||||
}
|
||||
int ticket = random.nextInt(options.stream().mapToInt(option -> weights.get(option.rank())).sum());
|
||||
for (var option : options) {
|
||||
ticket -= weights.get(option.rank());
|
||||
if (ticket < 0) return option.value();
|
||||
}
|
||||
throw new IllegalStateException("Ranked choice ticket was not consumed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.dnorberg.rivencraft.PortConfig;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.Prediction;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Called only after successful Descriptive Book travel and its Disarm effect. */
|
||||
public final class PortReturnLinks {
|
||||
private PortReturnLinks() {}
|
||||
|
||||
public static void afterSuccessfulTravel(ServerPlayer player, Identifier age, LinkBookTarget departure) {
|
||||
var mode = PortConfig.RETURN_LINK_MODE.get();
|
||||
if (mode == PortConfig.ReturnLinkMode.DISABLED) return;
|
||||
var storage = player.level().getServer().overworld().getDataStorage();
|
||||
boolean issue = shouldIssue(mode, storage.computeIfAbsent(PortAgeVisits.TYPE), age);
|
||||
if (!issue) return;
|
||||
ItemStack book = createBook(departure);
|
||||
if (!player.getInventory().add(book)) player.drop(book, false, Prediction.SERVER_ONLY);
|
||||
// Persist the once-per-Age marker before the next arrival can issue another.
|
||||
if (mode == PortConfig.ReturnLinkMode.ON_CREATION)
|
||||
storage.saveAndJoin();
|
||||
}
|
||||
|
||||
static boolean shouldIssue(PortConfig.ReturnLinkMode mode, PortAgeVisits visits, Identifier age) {
|
||||
return switch (mode) {
|
||||
case DISABLED -> false;
|
||||
case ALWAYS -> true;
|
||||
case ON_CREATION -> visits.markFirstVisit(age);
|
||||
};
|
||||
}
|
||||
|
||||
static ItemStack createBook(LinkBookTarget departure) {
|
||||
ItemStack book = new ItemStack(Rivencraft.LINKING_BOOK.get());
|
||||
book.set(Rivencraft.LINK_BOOK_TARGET.get(), departure);
|
||||
book.set(DataComponents.CUSTOM_NAME, Component.translatable("item.rivencraft.linking_book.return"));
|
||||
return book;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Frozen, ordered Rocks materials. Old single-material beta Ages remain readable. */
|
||||
public record PortRockPlan(List<Identifier> materials) {
|
||||
public static final Codec<PortRockPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.optionalFieldOf("material").forGetter(plan -> Optional.empty()),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("materials", List.of()).forGetter(PortRockPlan::materials)
|
||||
).apply(instance, (old, current) -> new PortRockPlan(current.isEmpty()
|
||||
? old.map(List::of).orElse(List.of()) : current)));
|
||||
|
||||
public PortRockPlan {
|
||||
materials = List.copyOf(materials);
|
||||
if (materials.isEmpty() || materials.size() > 32)
|
||||
throw new IllegalArgumentException("Rocks plan must contain 1–32 materials");
|
||||
}
|
||||
|
||||
public PortRockPlan(Identifier material) { this(List.of(material)); }
|
||||
}
|
||||
@@ -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<BlockPos> findHosts(ChunkAccess chunk) {
|
||||
List<BlockPos> 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<BlockPos> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Persisted sky styles, including the client-rendered Legacy static background. */
|
||||
public enum PortSkyStyle {
|
||||
OVERWORLD, NETHER, END, STATIC;
|
||||
|
||||
public static final Codec<PortSkyStyle> CODEC = Codec.STRING.xmap(
|
||||
name -> valueOf(name.toUpperCase(Locale.ROOT)), sky -> sky.name().toLowerCase(Locale.ROOT));
|
||||
|
||||
public static PortSkyStyle forPage(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
case "sky/overworld" -> OVERWORLD;
|
||||
case "sky/nether" -> NETHER;
|
||||
case "sky/end" -> END;
|
||||
case "sky/static" -> STATIC;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static PortSkyStyle resolve(List<Identifier> pages, long seed) {
|
||||
return pages.stream().map(PortSkyStyle::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElseGet(() -> pages.stream().anyMatch(PortSkyStyle::isCelestial)
|
||||
? OVERWORLD : random(seed));
|
||||
}
|
||||
|
||||
/** Nether-style Ages use their fog clear color as the visible backdrop. */
|
||||
public static float fogShade(net.minecraft.world.level.Level level) {
|
||||
boolean nether = level.dimensionTypeRegistration().unwrapKey().map(key ->
|
||||
key.identifier().getNamespace().equals("rivencraft")
|
||||
&& key.identifier().getPath().endsWith("_sky_nether")).orElse(false);
|
||||
var cycle = PortDayCycle.forLevel(level);
|
||||
return nether && !cycle.bodies().isEmpty()
|
||||
? 1F - 0.65F * PortCelestialBody.eclipseAt(cycle.bodies(), PortDayCycle.nativeTicks(level)) : 1F;
|
||||
}
|
||||
|
||||
private static boolean isCelestial(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
|
||||
return switch (page.getPath()) {
|
||||
// Preserve the established compatible-default rule for written controllers.
|
||||
case "celestial/normal_time", "celestial/sun", "celestial/moon" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static PortSkyStyle random(long seed) {
|
||||
// Match the first draw in 1.21.1 AgeSkyProfile.random: its equally
|
||||
// likely INHERITED and OVERWORLD slots currently share the native
|
||||
// Overworld presentation here. Do not add unported colors/bodies.
|
||||
int choice = new Random(seed ^ 0x534B5950524F464CL).nextInt(5);
|
||||
return switch (choice) {
|
||||
case 0, 1 -> OVERWORLD;
|
||||
case 2 -> NETHER;
|
||||
case 3 -> END;
|
||||
case 4 -> STATIC;
|
||||
default -> throw new IllegalStateException("Invalid random sky choice");
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* 26.3 port of Rivencraft's subtractive Skylands filter, adapted from
|
||||
* Mystcraft Legacy's SymbolSkylands (LGPL-3.0-or-later). Keeps upper solid
|
||||
* terrain above a seven-octave cutoff near Y=76 and removes liquids.
|
||||
*/
|
||||
final class PortSkylandsFeature {
|
||||
private static final long SALT = 0x534B594C414E4453L;
|
||||
private static final int BASE_CUTOFF = 76;
|
||||
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
|
||||
|
||||
private PortSkylandsFeature() {}
|
||||
|
||||
static void filter(ChunkAccess chunk, long ageSeed) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
int minY = chunk.getMinY();
|
||||
int maxY = chunk.getMaxY();
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
for (int localZ = 0; localZ < 16; localZ++) {
|
||||
int cutoff = cutoff(ageSeed, originX + localX, originZ + localZ);
|
||||
for (int y = minY; y < maxY; y++) {
|
||||
pos.set(originX + localX, y, originZ + localZ);
|
||||
BlockState state = chunk.getBlockState(pos);
|
||||
if (!removes(state, y, cutoff)) continue;
|
||||
chunk.setBlockState(pos, AIR);
|
||||
surface.update(localX, y, localZ, AIR);
|
||||
floor.update(localX, y, localZ, AIR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean removes(BlockState state, int y, int cutoff) {
|
||||
return !state.isAir() && (y <= cutoff || state.liquid());
|
||||
}
|
||||
|
||||
static int cutoff(long ageSeed, int x, int z) {
|
||||
double variation = 0.0D;
|
||||
int cellSize = 128;
|
||||
double amplitude = 12.0D;
|
||||
for (int octave = 0; octave < 7; octave++) {
|
||||
variation += valueNoise(ageSeed ^ SALT ^ (long) octave * 0x9E3779B97F4A7C15L,
|
||||
x, z, cellSize) * amplitude;
|
||||
cellSize /= 2;
|
||||
amplitude /= 2.0D;
|
||||
}
|
||||
return BASE_CUTOFF + (int) Math.round(variation);
|
||||
}
|
||||
|
||||
private static double valueNoise(long seed, int x, int z, int cellSize) {
|
||||
int cellX = Math.floorDiv(x, cellSize);
|
||||
int cellZ = Math.floorDiv(z, cellSize);
|
||||
double u = smooth((x - cellX * cellSize) / (double) cellSize);
|
||||
double v = smooth((z - cellZ * cellSize) / (double) cellSize);
|
||||
double north = lerp(value(seed, cellX, cellZ), value(seed, cellX + 1, cellZ), u);
|
||||
double south = lerp(value(seed, cellX, cellZ + 1), value(seed, cellX + 1, cellZ + 1), u);
|
||||
return lerp(north, south, v);
|
||||
}
|
||||
|
||||
private static double value(long seed, int x, int z) {
|
||||
long hash = mix(seed ^ (long) x * 0xD1B54A32D192ED03L ^ (long) z * 0xABC98388FB8FAC03L);
|
||||
return (hash >>> 11) * 0x1.0p-52 - 1.0D;
|
||||
}
|
||||
|
||||
private static double smooth(double value) { return value * value * (3.0D - 2.0D * value); }
|
||||
|
||||
private static double lerp(double first, double second, double amount) {
|
||||
return first + (second - first) * amount;
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.Random;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* 26.3 adaptation of Rivencraft 1.21.1's solid sphere nodes, derived from
|
||||
* Mystcraft Legacy's MapGenSpheresMyst (Mystcraft contributors,
|
||||
* LGPL-3.0-or-later, commit 9bc8ddc). Writes stay within the target chunk.
|
||||
*/
|
||||
final class PortSpheresFeature {
|
||||
private static final int SOURCE_RANGE = 8;
|
||||
private static final long SALT = 0x53504845524553L;
|
||||
|
||||
private PortSpheresFeature() {}
|
||||
|
||||
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||
var target = chunk.getPos();
|
||||
Random master = new Random(ageSeed ^ SALT);
|
||||
long xSeed = master.nextLong();
|
||||
long zSeed = master.nextLong();
|
||||
for (int sx = target.x() - SOURCE_RANGE; sx <= target.x() + SOURCE_RANGE; sx++) {
|
||||
for (int sz = target.z() - SOURCE_RANGE; sz <= target.z() + SOURCE_RANGE; sz++) {
|
||||
Random source = new Random((long) sx * xSeed ^ (long) sz * zSeed ^ ageSeed ^ SALT);
|
||||
if (source.nextFloat() > 0.05F) continue;
|
||||
double x = sx * 16 + source.nextInt(16);
|
||||
double y = source.nextInt(source.nextInt(192) + 1) + 32;
|
||||
double z = sz * 16 + source.nextInt(16);
|
||||
trace(chunk, material, source.nextLong(), x, y, z,
|
||||
1.0F + source.nextFloat() * 4.0F, 0, 0, -1, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void trace(ChunkAccess chunk, BlockState material, long seed,
|
||||
double x, double y, double z, float radius, float yaw, float pitch,
|
||||
int step, int length) {
|
||||
Random random = new Random(seed);
|
||||
if (length <= 0) {
|
||||
int maximum = SOURCE_RANGE * 16 - 16;
|
||||
length = maximum - random.nextInt(maximum / 4);
|
||||
}
|
||||
boolean singleNode = step == -1;
|
||||
if (singleNode) step = length / 2;
|
||||
int splitAt = random.nextInt(length / 2) + length / 4;
|
||||
boolean slowPitch = random.nextInt(6) == 0;
|
||||
float yawDrift = 0;
|
||||
float pitchDrift = 0;
|
||||
double middleX = chunk.getPos().getMinBlockX() + 8;
|
||||
double middleZ = chunk.getPos().getMinBlockZ() + 8;
|
||||
for (; step < length; step++) {
|
||||
double nodeRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
|
||||
float horizontal = Mth.cos(pitch);
|
||||
x += Mth.cos(yaw) * horizontal;
|
||||
y += Mth.sin(pitch);
|
||||
z += Mth.sin(yaw) * horizontal;
|
||||
pitch *= slowPitch ? 0.92F : 0.7F;
|
||||
pitch += pitchDrift * 0.1F;
|
||||
yaw += yawDrift * 0.1F;
|
||||
pitchDrift = pitchDrift * 0.9F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
|
||||
yawDrift = yawDrift * 0.75F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
|
||||
if (!singleNode && step == splitAt && radius > 1.0F) {
|
||||
trace(chunk, material, random.nextLong(), x, y, z,
|
||||
random.nextFloat() * 0.5F + 0.5F, yaw - Mth.HALF_PI, pitch / 3, step, length);
|
||||
trace(chunk, material, random.nextLong(), x, y, z,
|
||||
random.nextFloat() * 0.5F + 0.5F, yaw + Mth.HALF_PI, pitch / 3, step, length);
|
||||
return;
|
||||
}
|
||||
if (!singleNode && random.nextInt(4) == 0) continue;
|
||||
double dx = x - middleX;
|
||||
double dz = z - middleZ;
|
||||
double remaining = length - step;
|
||||
double reach = radius + 18;
|
||||
if (dx * dx + dz * dz - remaining * remaining > reach * reach) return;
|
||||
if (Math.abs(dx) <= 16 + nodeRadius * 2 && Math.abs(dz) <= 16 + nodeRadius * 2)
|
||||
fillNode(chunk, material, x, y, z, nodeRadius);
|
||||
if (singleNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillNode(ChunkAccess chunk, BlockState material,
|
||||
double centerX, double centerY, double centerZ, double radius) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
int minX = Math.max(0, Mth.floor(centerX - radius) - originX - 1);
|
||||
int maxX = Math.min(16, Mth.floor(centerX + radius) - originX + 2);
|
||||
int minZ = Math.max(0, Mth.floor(centerZ - radius) - originZ - 1);
|
||||
int maxZ = Math.min(16, Mth.floor(centerZ + radius) - originZ + 2);
|
||||
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - radius) - 1);
|
||||
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + radius) + 2);
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int worldY = minY; worldY < maxY; worldY++) {
|
||||
double dy = (worldY + 0.5D - centerY) / radius;
|
||||
for (int localZ = minZ; localZ < maxZ; localZ++) {
|
||||
double dz = (originZ + localZ + 0.5D - centerZ) / radius;
|
||||
for (int localX = minX; localX < maxX; localX++) {
|
||||
double dx = (originX + localX + 0.5D - centerX) / radius;
|
||||
if (!insideSphere(dx, dy, dz)) continue;
|
||||
pos.set(originX + localX, worldY, originZ + localZ);
|
||||
BlockState previous = chunk.getBlockState(pos);
|
||||
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
|
||||
chunk.setBlockState(pos, material);
|
||||
surface.update(localX, worldY, localZ, material);
|
||||
floor.update(localX, worldY, localZ, material);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean insideSphere(double dx, double dy, double dz) {
|
||||
return dx * dx + dy * dy + dz * dz < 1.0D;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* 26.3 port of Rivencraft 1.21.1's rare surface spikes, adapted from
|
||||
* Mystcraft Legacy's WorldGenSpikesAdv (Mystcraft contributors,
|
||||
* LGPL-3.0-or-later, commit 9bc8ddc). The footprint stays within one chunk.
|
||||
*/
|
||||
final class PortSpikesFeature {
|
||||
private static final long SALT = 0x5350494B4553L;
|
||||
|
||||
private PortSpikesFeature() {}
|
||||
|
||||
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
RandomSource random = RandomSource.create(ageSeed ^ SALT
|
||||
^ (long) originX * 341873128712L ^ (long) originZ * 132897987541L
|
||||
^ (long) occurrence * 0x9E3779B97F4A7C15L);
|
||||
if (random.nextInt(18) != 0) return;
|
||||
int x = originX + 4 + random.nextInt(8);
|
||||
int z = originZ + 4 + random.nextInt(8);
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||
int height = 6 + random.nextInt(32);
|
||||
int width = 1 + random.nextInt(4);
|
||||
if (y < level.getMinY() + 1 || y + height >= level.getMaxY()) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
if (!level.isEmptyBlock(pos.set(x, y, z))) return;
|
||||
for (int px = x - width; px <= x + width; px++) {
|
||||
for (int pz = z - width; pz <= z + width; pz++) {
|
||||
if (!insideFootprint(px - x, pz - z, width)) continue;
|
||||
if (!level.getBlockState(pos.set(px, y - 1, pz)).isSolid()) return;
|
||||
}
|
||||
}
|
||||
for (int px = x - width; px <= x + width; px++) {
|
||||
for (int pz = z - width; pz <= z + width; pz++) {
|
||||
if (!insideFootprint(px - x, pz - z, width)) continue;
|
||||
int top = y + 1 + random.nextInt(random.nextInt(height) + 1);
|
||||
for (int py = y; py < top; py++) {
|
||||
pos.set(px, py, pz);
|
||||
if (level.isEmptyBlock(pos)) level.setBlock(pos, material, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean insideFootprint(int dx, int dz, int width) {
|
||||
return dx * dx + dz * dz <= width * width + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/**
|
||||
* Ports Rivencraft 1.21.1's Legacy fissure footprint/carving.
|
||||
* Derived from Mystcraft's WorldGenMystStarFissure, LGPL-3.0,
|
||||
* copyright XCompWiz and Mystcraft contributors.
|
||||
*/
|
||||
public final class PortStarFissureFeature {
|
||||
private static final long SALT = 0x5354415246495353L;
|
||||
|
||||
private PortStarFissureFeature() {}
|
||||
|
||||
/** Per-Age data prevents revisits/restarts from carving the same fissure again. */
|
||||
private static final class Arrival extends SavedData {
|
||||
private static final Codec<Arrival> CODEC = BlockPos.CODEC.optionalFieldOf("spawn").codec()
|
||||
.xmap(Arrival::new, data -> Optional.ofNullable(data.spawn));
|
||||
private static final SavedDataType<Arrival> TYPE = new SavedDataType<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "star_fissure_arrival"),
|
||||
() -> new Arrival(Optional.empty()), CODEC);
|
||||
private BlockPos spawn;
|
||||
|
||||
private Arrival(Optional<BlockPos> spawn) { this.spawn = spawn.orElse(null); }
|
||||
}
|
||||
|
||||
public static BlockPos savedSpawn(ServerLevel level) {
|
||||
return level.getDataStorage().computeIfAbsent(Arrival.TYPE).spawn;
|
||||
}
|
||||
|
||||
public static void generateOnce(ServerLevel level, BlockPos spawn, boolean caveTerrain) {
|
||||
Arrival data = level.getDataStorage().computeIfAbsent(Arrival.TYPE);
|
||||
if (data.spawn != null) return;
|
||||
generate(level, level.getSeed(), spawn, caveTerrain);
|
||||
data.spawn = spawn.immutable();
|
||||
data.setDirty();
|
||||
}
|
||||
|
||||
/** Legacy chooses 8–23 blocks after the spawn chunk, then grows along +Z. */
|
||||
public static List<BlockPos> footprint(long ageSeed, BlockPos spawn) {
|
||||
long chunkX = spawn.getX() >> 4;
|
||||
long chunkZ = spawn.getZ() >> 4;
|
||||
Random random = new Random(ageSeed ^ SALT ^ chunkX ^ (chunkZ << 32));
|
||||
int originX = ((int) chunkX << 4) + random.nextInt(16) + 8;
|
||||
int originZ = ((int) chunkZ << 4) + random.nextInt(16) + 8;
|
||||
int length = random.nextInt(8) + 10;
|
||||
int[][] intervals = new int[length][2];
|
||||
List<BlockPos> columns = new ArrayList<>();
|
||||
addRow(columns, originX, originZ, 0, 0, 0);
|
||||
for (int row = 1; row < length; row++) {
|
||||
int range = length + Math.min(row, length - row) + 1;
|
||||
int right = (random.nextInt(range) - (range >> 1) + intervals[row - 1][1]) / 2;
|
||||
int left = (random.nextInt(range) - (range >> 1) + intervals[row - 1][0]) / 2;
|
||||
if (left > right) {
|
||||
int swap = left;
|
||||
left = right;
|
||||
right = swap;
|
||||
}
|
||||
left = Math.min(left, intervals[row - 1][1]);
|
||||
right = Math.max(right, intervals[row - 1][0]);
|
||||
intervals[row][0] = left;
|
||||
intervals[row][1] = right;
|
||||
addRow(columns, originX, originZ, row, left, right);
|
||||
}
|
||||
return List.copyOf(columns);
|
||||
}
|
||||
|
||||
private static void addRow(List<BlockPos> columns, int originX, int originZ, int row, int left, int right) {
|
||||
for (int x = left; x <= right; x++) {
|
||||
// Only X/Z are used; the real floor is the dimension's minimum Y.
|
||||
columns.add(new BlockPos(originX + x, 0, originZ + row * 2));
|
||||
columns.add(new BlockPos(originX + x, 0, originZ + row * 2 + 1));
|
||||
}
|
||||
}
|
||||
|
||||
private static void generate(ServerLevel level, long ageSeed, BlockPos spawn, boolean caveTerrain) {
|
||||
int floorY = level.getMinY();
|
||||
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
|
||||
for (BlockPos column : footprint(ageSeed, spawn)) {
|
||||
int top = level.getHeight(Heightmap.Types.WORLD_SURFACE, column.getX(), column.getZ());
|
||||
if (caveTerrain) {
|
||||
for (int y = Math.max(spawn.getY(), floorY + 1); y < top; y++) {
|
||||
if (level.getBlockState(mutable.set(column.getX(), y, column.getZ())).is(Blocks.BEDROCK)) {
|
||||
top = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int y = floorY + 1; y < top; y++)
|
||||
level.setBlock(mutable.set(column.getX(), y, column.getZ()), Blocks.AIR.defaultBlockState(), 2);
|
||||
level.setBlock(mutable.set(column.getX(), floorY, column.getZ()),
|
||||
Rivencraft.STAR_FISSURE.get().defaultBlockState(), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
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<Identifier, Identifier> 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<Identifier> CONTROLLED_SETS = Stream.concat(SETS.values().stream(),
|
||||
Stream.of(Identifier.withDefaultNamespace("nether_complexes")))
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
private static final Set<Identifier> CAVE_ONLY = Set.of(
|
||||
page("nether_fortresses"), page("bastions"), page("nether_fossils"));
|
||||
private static final Set<Identifier> 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<Identifier> 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<Identifier, Identifier> 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<Identifier> pages) {
|
||||
Map<Identifier, Integer> 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<Identifier> 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)));
|
||||
}
|
||||
|
||||
/** Random choices need at least one normally eligible biome; authored pages may override it. */
|
||||
public static boolean randomEligible(Identifier page, PortAgeTerrain terrain, Set<Identifier> biomes,
|
||||
HolderLookup.Provider registries) {
|
||||
if (!supportsTerrain(page, terrain)) return false;
|
||||
var set = registries.lookupOrThrow(Registries.STRUCTURE_SET)
|
||||
.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)));
|
||||
if (set.isEmpty()) return false;
|
||||
var compatible = compatibleBiomes(set.get().value());
|
||||
return compatible.isEmpty() || biomes.stream().anyMatch(compatible::contains);
|
||||
}
|
||||
|
||||
public static Set<Identifier> compatibleBiomes(Identifier page, HolderLookup.Provider registries) {
|
||||
if (!supports(page)) return Set.of();
|
||||
return registries.lookupOrThrow(Registries.STRUCTURE_SET)
|
||||
.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)))
|
||||
.map(holder -> compatibleBiomes(holder.value())).orElse(Set.of());
|
||||
}
|
||||
|
||||
private static Set<Identifier> compatibleBiomes(StructureSet set) {
|
||||
return set.structures().stream()
|
||||
.flatMap(entry -> entry.structure().value().biomes().stream())
|
||||
.flatMap(holder -> holder.unwrapKey().stream()).map(key -> key.identifier())
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
private static Map<Identifier, Integer> enabled(List<Identifier> pages) {
|
||||
Map<Identifier, Integer> result = new HashMap<>();
|
||||
for (Identifier page : pages) result.merge(SETS.get(page), 1, Integer::sum);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean authored(Structure structure, HolderLookup.Provider registries, List<Identifier> 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<StructureSet> sets, List<Identifier> pages) {
|
||||
List<Holder<Biome>> 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<? extends BiomeSource> codec() {
|
||||
throw new UnsupportedOperationException("Planning biome sources are never serialized");
|
||||
}
|
||||
@Override protected Stream<Holder<Biome>> 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<StructureSet> filtered(HolderLookup<StructureSet> source, List<Identifier> pages,
|
||||
BiomeSource actual) {
|
||||
Map<Identifier, Integer> counts = enabled(pages);
|
||||
Map<ResourceKey<StructureSet>, Holder.Reference<StructureSet>> copies = new HashMap<>();
|
||||
return new HolderLookup<>() {
|
||||
@Override public Optional<Holder.Reference<StructureSet>> get(ResourceKey<StructureSet> 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<HolderSet.Named<StructureSet>> get(TagKey<StructureSet> key) { return source.get(key); }
|
||||
@Override public Stream<Holder.Reference<StructureSet>> listElements() {
|
||||
return source.listElements().flatMap(holder -> get(holder.key()).stream());
|
||||
}
|
||||
@Override public Stream<HolderSet.Named<StructureSet>> listTags() { return source.listTags(); }
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean hasBiome(StructureSet set, BiomeSource source) {
|
||||
Set<Holder<Biome>> 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<StructureSet> {
|
||||
private static final HolderOwner<StructureSet> OWNER = new HolderOwner<>() {};
|
||||
private LocalReference(ResourceKey<StructureSet> key, StructureSet value) { super(Type.STAND_ALONE, OWNER, key, value); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.Random;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* 26.3 adaptation of Rivencraft 1.21.1's winding solid volumes, originally
|
||||
* adapted from Mystcraft Legacy's SymbolTendrils and MapGenCavesMyst (LGPL-3.0-or-later).
|
||||
* Each chunk reconstructs the neighboring source paths to preserve continuous borders.
|
||||
*/
|
||||
final class PortTendrilFeature {
|
||||
private static final long SALT = 0x54454E4452494C53L;
|
||||
private static final int SOURCE_RANGE = 8;
|
||||
private static final int SOURCE_RATE = 15;
|
||||
private static final int SOURCE_SIZE = 18;
|
||||
|
||||
private PortTendrilFeature() {}
|
||||
|
||||
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||
var target = chunk.getPos();
|
||||
Random master = new Random(ageSeed ^ SALT);
|
||||
long xSeed = master.nextLong();
|
||||
long zSeed = master.nextLong();
|
||||
for (int sourceX = target.x() - SOURCE_RANGE; sourceX <= target.x() + SOURCE_RANGE; sourceX++) {
|
||||
for (int sourceZ = target.z() - SOURCE_RANGE; sourceZ <= target.z() + SOURCE_RANGE; sourceZ++) {
|
||||
Random source = new Random((long) sourceX * xSeed ^ (long) sourceZ * zSeed ^ ageSeed ^ SALT);
|
||||
int nodes = source.nextInt(source.nextInt(source.nextInt(SOURCE_SIZE) + 1) + 1);
|
||||
if (source.nextInt(SOURCE_RATE) != 0) continue;
|
||||
for (int node = 0; node < nodes; node++) {
|
||||
double x = sourceX * 16 + source.nextInt(16);
|
||||
double y = source.nextInt(source.nextInt(120) + 8);
|
||||
double z = sourceZ * 16 + source.nextInt(16);
|
||||
int branches = 1;
|
||||
if (source.nextInt(4) == 0) {
|
||||
trace(chunk, material, source.nextLong(), x, y, z,
|
||||
1.0F + source.nextFloat() * 6.0F, 0.0F, 0.0F, -1, -1, 0.5D);
|
||||
branches += source.nextInt(4);
|
||||
}
|
||||
for (int branch = 0; branch < branches; branch++) {
|
||||
float yaw = source.nextFloat() * Mth.TWO_PI;
|
||||
float pitch = (source.nextFloat() - 0.5F) / 4.0F;
|
||||
float radius = source.nextFloat() * 2.0F + source.nextFloat();
|
||||
if (source.nextInt(10) == 0)
|
||||
radius *= source.nextFloat() * source.nextFloat() * 3.0F + 1.0F;
|
||||
trace(chunk, material, source.nextLong(), x, y, z,
|
||||
radius, yaw, pitch, 0, 0, 1.0D);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void trace(ChunkAccess chunk, BlockState material, long seed,
|
||||
double x, double y, double z, float radius, float yaw, float pitch,
|
||||
int step, int length, double verticalScale) {
|
||||
Random random = new Random(seed);
|
||||
if (length <= 0) {
|
||||
int maximum = SOURCE_RANGE * 16 - 16;
|
||||
length = maximum - random.nextInt(maximum / 4);
|
||||
}
|
||||
boolean singleNode = step == -1;
|
||||
if (singleNode) step = length / 2;
|
||||
int splitAt = random.nextInt(length / 2) + length / 4;
|
||||
boolean slowPitch = random.nextInt(6) == 0;
|
||||
float yawDrift = 0.0F;
|
||||
float pitchDrift = 0.0F;
|
||||
double middleX = chunk.getPos().getMinBlockX() + 8;
|
||||
double middleZ = chunk.getPos().getMinBlockZ() + 8;
|
||||
for (; step < length; step++) {
|
||||
double horizontalRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
|
||||
double verticalRadius = horizontalRadius * verticalScale;
|
||||
float horizontalMotion = Mth.cos(pitch);
|
||||
x += Mth.cos(yaw) * horizontalMotion;
|
||||
y += Mth.sin(pitch);
|
||||
z += Mth.sin(yaw) * horizontalMotion;
|
||||
pitch *= slowPitch ? 0.92F : 0.7F;
|
||||
pitch += pitchDrift * 0.1F;
|
||||
yaw += yawDrift * 0.1F;
|
||||
pitchDrift = pitchDrift * 0.9F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
|
||||
yawDrift = yawDrift * 0.75F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
|
||||
if (!singleNode && step == splitAt && radius > 1.0F) {
|
||||
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
|
||||
yaw - Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
|
||||
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
|
||||
yaw + Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
|
||||
return;
|
||||
}
|
||||
if (!singleNode && random.nextInt(4) == 0) continue;
|
||||
double distanceX = x - middleX;
|
||||
double distanceZ = z - middleZ;
|
||||
double remaining = length - step;
|
||||
double reach = radius + 18.0D;
|
||||
if (distanceX * distanceX + distanceZ * distanceZ - remaining * remaining > reach * reach) return;
|
||||
if (x >= middleX - 16.0D - horizontalRadius * 2.0D
|
||||
&& x <= middleX + 16.0D + horizontalRadius * 2.0D
|
||||
&& z >= middleZ - 16.0D - horizontalRadius * 2.0D
|
||||
&& z <= middleZ + 16.0D + horizontalRadius * 2.0D) {
|
||||
fillNode(chunk, material, x, y, z, horizontalRadius, verticalRadius);
|
||||
}
|
||||
if (singleNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillNode(ChunkAccess chunk, BlockState material, double centerX, double centerY,
|
||||
double centerZ, double horizontalRadius, double verticalRadius) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
int minX = Math.max(0, Mth.floor(centerX - horizontalRadius) - originX - 1);
|
||||
int maxX = Math.min(16, Mth.floor(centerX + horizontalRadius) - originX + 2);
|
||||
int minZ = Math.max(0, Mth.floor(centerZ - horizontalRadius) - originZ - 1);
|
||||
int maxZ = Math.min(16, Mth.floor(centerZ + horizontalRadius) - originZ + 2);
|
||||
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - verticalRadius) - 1);
|
||||
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + verticalRadius) + 2);
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int worldY = minY; worldY < maxY; worldY++) {
|
||||
double dy = (worldY + 0.5D - centerY) / verticalRadius;
|
||||
if (dy <= -0.7D || dy * dy >= 1.0D) continue;
|
||||
for (int localZ = minZ; localZ < maxZ; localZ++) {
|
||||
double dz = (originZ + localZ + 0.5D - centerZ) / horizontalRadius;
|
||||
for (int localX = minX; localX < maxX; localX++) {
|
||||
double dx = (originX + localX + 0.5D - centerX) / horizontalRadius;
|
||||
if (dx * dx + dy * dy + dz * dz >= 1.0D) continue;
|
||||
pos.set(originX + localX, worldY, originZ + localZ);
|
||||
BlockState previous = chunk.getBlockState(pos);
|
||||
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
|
||||
chunk.setBlockState(pos, material);
|
||||
surface.update(localX, worldY, localZ, material);
|
||||
floor.update(localX, worldY, localZ, material);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
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<Choice> choices, boolean skylands, int denseOres,
|
||||
boolean starFissures, List<Identifier> structures, List<Identifier> carvers,
|
||||
boolean structureControl) {
|
||||
public static final Identifier FLOATING_ISLANDS = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "feature/floating_islands");
|
||||
public static final Identifier TENDRILS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/tendrils");
|
||||
public static final Identifier SKYLANDS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/skylands");
|
||||
public static final Identifier SPHERES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spheres");
|
||||
public static final Identifier SPIKES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spikes");
|
||||
public static final Identifier OBELISKS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/obelisks");
|
||||
public static final Identifier CRYSTAL_FORMATIONS = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "feature/crystal_formations");
|
||||
public static final Identifier CRYSTAL_MATERIAL = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "crystal_block");
|
||||
public static final Identifier DENSE_ORES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dense_ores");
|
||||
public static final Identifier STAR_FISSURES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/star_fissures");
|
||||
public static final int MAX_PER_FEATURE = 32;
|
||||
public static final Codec<PortTerrainFeaturePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Choice.CODEC.listOf().fieldOf("choices").forGetter(PortTerrainFeaturePlan::choices),
|
||||
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),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("structures", List.of()).forGetter(PortTerrainFeaturePlan::structures),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("carvers", List.of()).forGetter(PortTerrainFeaturePlan::carvers),
|
||||
Codec.BOOL.optionalFieldOf("structure_control", false).forGetter(PortTerrainFeaturePlan::structureControl)
|
||||
).apply(instance, PortTerrainFeaturePlan::new));
|
||||
|
||||
public record Choice(Identifier page, Identifier material) {
|
||||
public static final Codec<Choice> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.fieldOf("page").forGetter(Choice::page),
|
||||
Identifier.CODEC.fieldOf("material").forGetter(Choice::material)
|
||||
).apply(instance, Choice::new));
|
||||
|
||||
public Choice {
|
||||
if (!isSupported(page) || material == null)
|
||||
throw new IllegalArgumentException("Invalid terrain feature choice");
|
||||
if (CRYSTAL_FORMATIONS.equals(page) && !CRYSTAL_MATERIAL.equals(material))
|
||||
throw new IllegalArgumentException("Crystal formations use Rivencraft crystal, not a Block modifier");
|
||||
}
|
||||
}
|
||||
|
||||
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 && structures.isEmpty() && carvers.isEmpty() && !structureControl)
|
||||
throw new IllegalArgumentException("Terrain feature plan must not be empty");
|
||||
var counts = new HashMap<Identifier, Integer>();
|
||||
for (Choice choice : choices) {
|
||||
if (counts.merge(choice.page(), 1, Integer::sum) > MAX_PER_FEATURE)
|
||||
throw new IllegalArgumentException("Terrain feature plan permits at most 32 occurrences per feature");
|
||||
}
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
|
||||
boolean starFissures, List<Identifier> structures, List<Identifier> carvers) {
|
||||
this(choices, skylands, denseOres, starFissures, structures, carvers, false);
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
|
||||
boolean starFissures, List<Identifier> structures) {
|
||||
this(choices, skylands, denseOres, starFissures, structures, List.of());
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres, boolean starFissures) {
|
||||
this(choices, skylands, denseOres, starFissures, List.of());
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres) {
|
||||
this(choices, skylands, denseOres, false);
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands) { this(choices, skylands, 0, false); }
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices) { this(choices, false, 0, false); }
|
||||
|
||||
public static boolean isSupported(Identifier page) {
|
||||
return FLOATING_ISLANDS.equals(page) || TENDRILS.equals(page) || SPHERES.equals(page) || isSurface(page);
|
||||
}
|
||||
|
||||
public static boolean acceptsMaterial(Identifier page) {
|
||||
return isSupported(page) && !CRYSTAL_FORMATIONS.equals(page);
|
||||
}
|
||||
|
||||
public static boolean isSurface(Identifier page) {
|
||||
return SPIKES.equals(page) || OBELISKS.equals(page) || CRYSTAL_FORMATIONS.equals(page);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Ordered, frozen plants and log decorations; not all require surface terrain. */
|
||||
public record PortVegetationPlan(List<Identifier> 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<PortVegetationPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.listOf().fieldOf("pages").forGetter(PortVegetationPlan::pages)
|
||||
).apply(instance, PortVegetationPlan::new));
|
||||
|
||||
public PortVegetationPlan {
|
||||
pages = List.copyOf(pages);
|
||||
if (pages.isEmpty() || pages.size() > 32 || pages.stream().anyMatch(page -> !isVegetation(page)))
|
||||
throw new IllegalArgumentException("Vegetation plan requires 1–32 supported pages");
|
||||
}
|
||||
|
||||
public static boolean isVegetation(Identifier page) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
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<PortWeatherMode> 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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<PortWeatherSettings> CODEC = PortWeatherMode.CODEC
|
||||
.fieldOf("mode").codec().xmap(PortWeatherSettings::new, settings -> settings.mode);
|
||||
static final SavedDataType<PortWeatherSettings> 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.WritingDeskBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/** Disposable server fixture for copying transactions, page moves and two-part removal. */
|
||||
final class PortWritingDeskProbe {
|
||||
private static final BlockPos POS = new BlockPos(0, 64, 0);
|
||||
private PortWritingDeskProbe() {}
|
||||
private static ItemStack page(String path) { return PageItem.createSymbolPage(Identifier.fromNamespaceAndPath("rivencraft", path)); }
|
||||
|
||||
static void create(ServerLevel level) {
|
||||
PortAgeTravel.findLanding(level);
|
||||
var player = net.neoforged.neoforge.common.util.FakePlayerFactory.get(level,
|
||||
new com.mojang.authlib.GameProfile(java.util.UUID.fromString("f92ad9ee-1ff0-4fe2-bcd3-994d4c035933"), "RivenDeskProbe"));
|
||||
player.setPos(0.5, 64, 2.5);
|
||||
// New Minecraft drops Container block-entity contents during removal.
|
||||
// Removing either half must remove its partner and drop contents once.
|
||||
for (Direction facing : Direction.Plane.HORIZONTAL) for (boolean extension : List.of(false, true)) {
|
||||
var state = Rivencraft.WRITING_DESK.get().defaultBlockState().setValue(WritingDeskBlock.FACING, facing);
|
||||
level.setBlock(POS, state, 3);
|
||||
Rivencraft.WRITING_DESK.get().setPlacedBy(level, POS, state, player, new ItemStack(Rivencraft.WRITING_DESK_ITEM.get()));
|
||||
BlockPos other = POS.relative(facing.getClockWise());
|
||||
if (!level.getBlockState(other).getValue(WritingDeskBlock.EXTENSION))
|
||||
throw new IllegalStateException("Desk extension not placed");
|
||||
var desk = (WritingDeskBlockEntity) level.getBlockEntity(POS);
|
||||
desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER, 3));
|
||||
level.destroyBlock(extension ? other : POS, true);
|
||||
if (level.getBlockState(POS).is(Rivencraft.WRITING_DESK.get()) || level.getBlockState(other).is(Rivencraft.WRITING_DESK.get()))
|
||||
throw new IllegalStateException("Desk partner survived removal");
|
||||
var drops = level.getEntitiesOfClass(net.minecraft.world.entity.item.ItemEntity.class,
|
||||
new net.minecraft.world.phys.AABB(-3, 60, -3, 4, 69, 4));
|
||||
int paper = drops.stream().filter(e -> e.getItem().is(Items.PAPER)).mapToInt(e -> e.getItem().getCount()).sum();
|
||||
int desks = drops.stream().filter(e -> e.getItem().is(Rivencraft.WRITING_DESK_ITEM.get())).mapToInt(e -> e.getItem().getCount()).sum();
|
||||
if (paper != 3 || desks != 1) throw new IllegalStateException("Desk removal duplicated/lost drops: " + paper + "/" + desks);
|
||||
drops.forEach(net.minecraft.world.entity.Entity::discard);
|
||||
}
|
||||
var state = Rivencraft.WRITING_DESK.get().defaultBlockState();
|
||||
level.setBlock(POS, state, 3);
|
||||
Rivencraft.WRITING_DESK.get().setPlacedBy(level, POS, state, player, new ItemStack(Rivencraft.WRITING_DESK_ITEM.get()));
|
||||
var desk = (WritingDeskBlockEntity) level.getBlockEntity(POS);
|
||||
try {
|
||||
if (desk.canPlaceItem(0, PageItem.createLinkPanelPage())) throw new IllegalStateException("Desk accepted Link Panel");
|
||||
desk.setItem(0, page("terrain/flat"));
|
||||
var folder = new ItemStack(Rivencraft.COLLATION_FOLDER.get());
|
||||
folder.set(Rivencraft.NOTEBOOK_CONTENTS.get(), new NotebookContents(List.of(page("weather/off"))));
|
||||
desk.setItem(1, folder);
|
||||
desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER, 2));
|
||||
desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(Rivencraft.INK_VIAL.get()));
|
||||
if (desk.inkRemaining() != 16 || !desk.getItem(WritingDeskBlockEntity.RETURN).is(Items.GLASS_BOTTLE))
|
||||
throw new IllegalStateException("Desk ink exchange regression");
|
||||
var menu = new WritingDeskMenu(1, player.getInventory(), desk);
|
||||
menu.broadcastChanges();
|
||||
if (menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem().isEmpty() || desk.inkRemaining() != 16)
|
||||
throw new IllegalStateException("Desk virtual result consumed inputs");
|
||||
menu.clicked(WritingDeskMenu.COPY_RESULT, 0, ContainerInput.PICKUP, player);
|
||||
if (menu.getCarried().isEmpty() || desk.inkRemaining() != 15
|
||||
|| desk.getItem(WritingDeskBlockEntity.PAPER).getCount() != 1 || desk.getItem(0).isEmpty())
|
||||
throw new IllegalStateException("Desk copy did not consume exactly one paper/ink");
|
||||
menu.setCarried(ItemStack.EMPTY);
|
||||
for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++)
|
||||
player.getInventory().setItem(slot, new ItemStack(Items.COBBLESTONE, 64));
|
||||
if (!menu.quickMoveStack(player, WritingDeskMenu.COPY_RESULT).isEmpty() || desk.inkRemaining() != 15)
|
||||
throw new IllegalStateException("Full inventory consumed copy inputs");
|
||||
player.getInventory().clearContent();
|
||||
if (!menu.clickMenuButton(player, WritingDeskMenu.movePageButtonId(1, 0, 1, 26)))
|
||||
throw new IllegalStateException("Desk sparse page movement failed");
|
||||
if (!menu.clickMenuButton(player, WritingDeskMenu.extractPageButtonId(1, 26, -1)) || menu.getCarried().isEmpty())
|
||||
throw new IllegalStateException("Desk page extraction failed");
|
||||
menu.setCarried(ItemStack.EMPTY);
|
||||
for (int i = 0; i < 21; i++) menu.clickMenuButton(player, WritingDeskMenu.NEXT_TABS);
|
||||
if (menu.firstVisibleTab() != 84 || !menu.clickMenuButton(player, 87)) throw new IllegalStateException("88 desk tabs not reachable");
|
||||
desk.setItem(87, page("lighting/bright"));
|
||||
menu.clickMenuButton(player, WritingDeskMenu.PAGE_BUTTON_BASE);
|
||||
menu.broadcastChanges();
|
||||
} finally { player.getInventory().clearContent(); }
|
||||
verify(level);
|
||||
}
|
||||
|
||||
static void verify(ServerLevel level) {
|
||||
if (!(level.getBlockEntity(POS) instanceof WritingDeskBlockEntity desk)
|
||||
|| desk.activeCollection() != 87 || desk.inkRemaining() != 15
|
||||
|| desk.getItem(WritingDeskBlockEntity.PAPER).getCount() != 1 || desk.getItem(87).isEmpty()
|
||||
|| desk.displayPage().isEmpty()) throw new IllegalStateException("Desk persistence regression");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -57,7 +56,8 @@ public final class RuntimeAgeBackend {
|
||||
for (Map.Entry<Identifier, Long> age : index.entries().entrySet()) {
|
||||
try {
|
||||
load(server, age.getKey(), age.getValue(), index.biomePlan(age.getKey()),
|
||||
index.terrain(age.getKey()));
|
||||
index.terrain(age.getKey()), index.rocks(age.getKey()), index.vegetation(age.getKey()),
|
||||
index.dungeons(age.getKey()), index.lakes(age.getKey()), index.terrainFeatures(age.getKey()));
|
||||
} catch (RuntimeException exception) {
|
||||
Rivencraft.LOGGER.error("Could not restore Age {}. Its saved index remains intact.", age.getKey(), exception);
|
||||
}
|
||||
@@ -75,12 +75,88 @@ public final class RuntimeAgeBackend {
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
|
||||
return create(server, id, seed, biomePlan, terrain, null);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, null);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, 0);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, null);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, null);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
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) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, PortLighting.NORMAL);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
|
||||
PortLighting lighting) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, lighting, PortSkyStyle.OVERWORLD);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
|
||||
PortLighting lighting, PortSkyStyle sky) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, lighting, sky, PortAgeColors.NATIVE);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
|
||||
PortLighting lighting, PortSkyStyle sky, PortAgeColors colors) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, lighting, sky, colors, PortDayCycle.NATIVE);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
|
||||
PortLighting lighting, PortSkyStyle sky, PortAgeColors colors, PortDayCycle dayCycle) {
|
||||
checkServerThread(server);
|
||||
ageName(id);
|
||||
AgeLevelIndex index = index(server);
|
||||
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
|
||||
ServerLevel level = load(server, id, seed, biomePlan, terrain);
|
||||
index.add(id, seed, biomePlan, terrain);
|
||||
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, lighting, sky, colors, dayCycle);
|
||||
((PortAgeLevel) level).configureWeather(weather);
|
||||
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures, lighting, sky, colors, dayCycle);
|
||||
// A newly bound book may be used immediately. Persist its Age identity
|
||||
// before publishing the catalog so a later crash cannot leave that
|
||||
// book pointing at a world omitted from the on-disk index.
|
||||
@@ -100,7 +176,9 @@ public final class RuntimeAgeBackend {
|
||||
public static ServerLevel reload(MinecraftServer server, Identifier id) {
|
||||
checkServerThread(server);
|
||||
long seed = requireSeed(server, id);
|
||||
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id));
|
||||
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id),
|
||||
index(server).rocks(id), index(server).vegetation(id), index(server).dungeons(id), index(server).lakes(id),
|
||||
index(server).terrainFeatures(id));
|
||||
AgeCatalogSync.onCatalogChanged(server);
|
||||
return level;
|
||||
}
|
||||
@@ -112,18 +190,23 @@ public final class RuntimeAgeBackend {
|
||||
long previousSeed = requireSeed(server, id);
|
||||
PortBiomePlan biomePlan = index.biomePlan(id);
|
||||
PortAgeTerrain terrain = index.terrain(id);
|
||||
PortRockPlan rocks = index.rocks(id);
|
||||
PortVegetationPlan vegetation = index.vegetation(id);
|
||||
int dungeons = index.dungeons(id);
|
||||
PortLakePlan lakes = index.lakes(id);
|
||||
PortTerrainFeaturePlan terrainFeatures = index.terrainFeatures(id);
|
||||
if (previousSeed == seed) return;
|
||||
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
||||
if (loaded) unloadLoaded(server, id);
|
||||
try {
|
||||
if (loaded) load(server, id, seed, biomePlan, terrain);
|
||||
if (loaded) load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
index.reseed(id, seed);
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
if (loaded) AgeCatalogSync.onCatalogChanged(server);
|
||||
} catch (RuntimeException exception) {
|
||||
if (loaded) {
|
||||
try {
|
||||
load(server, id, previousSeed, biomePlan, terrain);
|
||||
load(server, id, previousSeed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
AgeCatalogSync.onCatalogChanged(server);
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
@@ -141,8 +224,17 @@ public final class RuntimeAgeBackend {
|
||||
long seed = requireSeed(server, id);
|
||||
PortBiomePlan biomePlan = index.biomePlan(id);
|
||||
PortAgeTerrain terrain = index.terrain(id);
|
||||
PortRockPlan rocks = index.rocks(id);
|
||||
PortVegetationPlan vegetation = index.vegetation(id);
|
||||
int dungeons = index.dungeons(id);
|
||||
PortLakePlan lakes = index.lakes(id);
|
||||
PortTerrainFeaturePlan terrainFeatures = index.terrainFeatures(id);
|
||||
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
||||
ArchivePaths paths = archivePaths(server, name);
|
||||
PortLighting lighting = index.lighting(id);
|
||||
PortSkyStyle sky = index.sky(id);
|
||||
PortAgeColors colors = index.colors(id);
|
||||
PortDayCycle dayCycle = index.dayCycle(id);
|
||||
if (loaded) unloadLoaded(server, id);
|
||||
// Persist the tombstone before moving files: a crash mid-archive must
|
||||
// not let an old book silently regenerate this identity.
|
||||
@@ -159,14 +251,15 @@ public final class RuntimeAgeBackend {
|
||||
}
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
try {
|
||||
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain);
|
||||
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, lighting, sky, colors, dayCycle);
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
}
|
||||
if (loaded) {
|
||||
try {
|
||||
load(server, id, seed, biomePlan, terrain);
|
||||
load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
}
|
||||
@@ -182,6 +275,16 @@ public final class RuntimeAgeBackend {
|
||||
return index(server).entries();
|
||||
}
|
||||
|
||||
public static Map<Identifier, PortAgeColors> savedColors(MinecraftServer server) {
|
||||
checkServerThread(server);
|
||||
return index(server).colors();
|
||||
}
|
||||
|
||||
public static Map<Identifier, PortDayCycle> savedDayCycles(MinecraftServer server) {
|
||||
checkServerThread(server);
|
||||
return index(server).dayCycles();
|
||||
}
|
||||
|
||||
/** Includes retired IDs, which must never be silently reused by a new book. */
|
||||
public static boolean isNameTaken(MinecraftServer server, Identifier id) {
|
||||
checkServerThread(server);
|
||||
@@ -263,15 +366,14 @@ public final class RuntimeAgeBackend {
|
||||
}
|
||||
|
||||
private static BiomeSource authoredBiomeSource(MinecraftServer server, PortBiomePlan plan, long seed) {
|
||||
if (plan.distribution() == PortBiomePlan.Distribution.NATIVE)
|
||||
throw new IllegalArgumentException("Native biome distribution has no authored source");
|
||||
var registry = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
List<Holder<Biome>> biomes = plan.biomes().stream().map(id ->
|
||||
(Holder<Biome>) registry.get(ResourceKey.create(Registries.BIOME, id))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + id)))
|
||||
.toList();
|
||||
return switch (plan.distribution()) {
|
||||
case NATIVE -> throw new IllegalStateException("Native is not an override");
|
||||
case NATIVE -> biomes.size() == 1 ? new FixedBiomeSource(biomes.getFirst())
|
||||
: new PortScaledBiomeSource(HolderSet.direct(biomes), seed, 0);
|
||||
case SINGLE -> new FixedBiomeSource(biomes.getFirst());
|
||||
case TILED, GRID -> new CheckerboardColumnBiomeSource(HolderSet.direct(biomes),
|
||||
plan.distribution() == PortBiomePlan.Distribution.GRID ? 0 : 4);
|
||||
@@ -282,12 +384,26 @@ public final class RuntimeAgeBackend {
|
||||
}
|
||||
|
||||
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures) {
|
||||
return load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, index(server).lighting(id), index(server).sky(id), index(server).colors(id), index(server).dayCycle(id));
|
||||
}
|
||||
|
||||
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortLighting lighting, PortSkyStyle sky,
|
||||
PortAgeColors colors, PortDayCycle dayCycle) {
|
||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
||||
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
||||
if (biomePlan != null && biomePlan.distribution() == PortBiomePlan.Distribution.NATIVE
|
||||
&& !biomePlan.biomes().isEmpty() && terrain != PortAgeTerrain.CAVE)
|
||||
throw new IllegalArgumentException("Only Native Cave terrain may carry an explicit native biome pool");
|
||||
|
||||
// Noise terrains reuse the Overworld biome source unless a
|
||||
// non-Native authored distribution replaces it.
|
||||
// non-Native authored distribution or a frozen Native Cave pool replaces it.
|
||||
// LevelStem.seedOverride controls chunk random state and getSeed().
|
||||
LevelStem overworldStem = server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM)
|
||||
.getValue(LevelStem.OVERWORLD);
|
||||
@@ -339,7 +455,8 @@ public final class RuntimeAgeBackend {
|
||||
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biomeId);
|
||||
}
|
||||
}
|
||||
if (biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE) {
|
||||
if (biomePlan != null && (biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE
|
||||
|| terrain == PortAgeTerrain.CAVE && !biomePlan.biomes().isEmpty())) {
|
||||
source = authoredBiomeSource(server, biomePlan, seed);
|
||||
}
|
||||
var settings = switch (terrain) {
|
||||
@@ -351,13 +468,16 @@ public final class RuntimeAgeBackend {
|
||||
case CAVE -> dryCaveSettings(server);
|
||||
case FLAT, VOID -> throw new IllegalStateException("Flat terrain should use FlatLevelSource");
|
||||
};
|
||||
generator = new NoiseBasedChunkGenerator(source, settings);
|
||||
generator = new NoiseBasedChunkGenerator(source, PortBiomeMaterialRules.apply(server.registryAccess(), settings.value()));
|
||||
}
|
||||
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
|
||||
if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
|
||||
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
|
||||
LevelStem stem = new LevelStem(lighting.dimensionType(server, terrain, overworldStem.type(), sky),
|
||||
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);
|
||||
((PortAgeLevel) level).configureColors(colors);
|
||||
((PortAgeLevel) level).configureDayCycle(dayCycle);
|
||||
|
||||
// Publish only after the constructor succeeds. If the load event fails,
|
||||
// roll back the map and close the newly opened chunk/entity stores.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Solid crystal-textured arrival footing that cannot be collected. */
|
||||
public final class CloudPlatformBlock extends Block {
|
||||
public CloudPlatformBlock(Properties properties) { super(properties); }
|
||||
|
||||
@Override
|
||||
protected ItemStack getCloneItemStack(LevelReader level, BlockPos pos, BlockState state, boolean includeData) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.InsideBlockEffectApplier;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.EndPortalBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.portal.TeleportTransition;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
/** Non-obtainable starfield portal returning entities to the shared Overworld spawn. */
|
||||
public final class StarFissureBlock extends EndPortalBlock {
|
||||
private static final VoxelShape SHAPE = Block.box(0, 0, 0, 16, 1.6, 16);
|
||||
|
||||
public StarFissureBlock(BlockBehaviour.Properties properties) { super(properties); }
|
||||
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new StarFissureBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void animateTick(BlockState state, Level level, BlockPos pos, net.minecraft.util.RandomSource random) {
|
||||
// Legacy fissures have a starfield, not vanilla End portal smoke.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
|
||||
return SHAPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity,
|
||||
InsideBlockEffectApplier effects, boolean precise) {
|
||||
// Do not inherit End credits or End-dimension travel from the vanilla block.
|
||||
if (entity.canUsePortal(false)) entity.setAsInsidePortal(this, pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos) {
|
||||
if (level.dimension() == Level.OVERWORLD) return null;
|
||||
ServerLevel overworld = level.getServer().overworld();
|
||||
if (overworld == null) return null;
|
||||
BlockPos spawn = overworld.getRespawnData().pos();
|
||||
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.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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
|
||||
import net.dnorberg.rivencraft.writing.WritingDeskCatalog;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.Containers;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.SimpleMenuProvider;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.EnumProperty;
|
||||
import net.minecraft.world.level.block.state.properties.BooleanProperty;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
public final class WritingDeskBlock extends BaseEntityBlock {
|
||||
public static final EnumProperty<Direction> FACING = BlockStateProperties.HORIZONTAL_FACING;
|
||||
public static final BooleanProperty EXTENSION = BooleanProperty.create("extension");
|
||||
|
||||
public WritingDeskBlock(Properties properties) {
|
||||
super(properties);
|
||||
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH).setValue(EXTENSION, false));
|
||||
}
|
||||
|
||||
@Override protected void createBlockStateDefinition(StateDefinition.Builder<net.minecraft.world.level.block.Block, BlockState> builder) {
|
||||
builder.add(FACING, EXTENSION);
|
||||
}
|
||||
@Override public BlockState getStateForPlacement(BlockPlaceContext context) {
|
||||
Direction facing = context.getHorizontalDirection().getOpposite();
|
||||
BlockPos extension = context.getClickedPos().relative(facing.getClockWise());
|
||||
return context.getLevel().getBlockState(extension).canBeReplaced(context)
|
||||
? defaultBlockState().setValue(FACING, facing) : null;
|
||||
}
|
||||
@Override public void setPlacedBy(Level level, BlockPos pos, BlockState state,
|
||||
@Nullable LivingEntity placer, ItemStack stack) {
|
||||
super.setPlacedBy(level, pos, state, placer, stack);
|
||||
level.setBlock(pos.relative(state.getValue(FACING).getClockWise()),
|
||||
state.setValue(EXTENSION, true), 3);
|
||||
}
|
||||
@Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; }
|
||||
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return state.getValue(EXTENSION) ? null : new WritingDeskBlockEntity(pos, state);
|
||||
}
|
||||
@Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
|
||||
Player player, BlockHitResult hit) {
|
||||
BlockPos controller = state.getValue(EXTENSION)
|
||||
? pos.relative(state.getValue(FACING).getCounterClockWise()) : pos;
|
||||
if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer
|
||||
&& level.getBlockEntity(controller) instanceof WritingDeskBlockEntity desk) {
|
||||
serverPlayer.openMenu(new SimpleMenuProvider(
|
||||
(id, inventory, ignored) -> new WritingDeskMenu(id, inventory, desk),
|
||||
Component.translatable("container.rivencraft.writing_desk")),
|
||||
buffer -> {
|
||||
buffer.writeBlockPos(controller);
|
||||
WritingDeskCatalog.write(buffer, WritingDeskCatalog.available(desk));
|
||||
});
|
||||
}
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
@Override protected void affectNeighborsAfterRemoval(BlockState state, net.minecraft.server.level.ServerLevel level,
|
||||
BlockPos pos, boolean movedByPiston) {
|
||||
BlockPos other = pos.relative(state.getValue(FACING).getClockWise(), state.getValue(EXTENSION) ? -1 : 1);
|
||||
BlockState otherState = level.getBlockState(other);
|
||||
if (otherState.is(this) && otherState.getValue(EXTENSION) != state.getValue(EXTENSION)
|
||||
&& otherState.getValue(FACING) == state.getValue(FACING))
|
||||
level.setBlock(other, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(), 35);
|
||||
Containers.updateNeighboursAfterDestroy(state, level, pos);
|
||||
}
|
||||
}
|
||||
+30
@@ -112,6 +112,36 @@ public final class PortBookBinderBlockEntity extends BlockEntity implements Cont
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Validate the complete collection before moving any page, as in 1.21.1. */
|
||||
public ItemStack importCollection(ItemStack collection, int index) {
|
||||
if (!net.dnorberg.rivencraft.item.CollationFolderItem.isCollection(collection)
|
||||
|| collection.getCount() != 1) return ItemStack.EMPTY;
|
||||
var contents = collection.getOrDefault(Rivencraft.NOTEBOOK_CONTENTS.get(),
|
||||
net.dnorberg.rivencraft.writing.NotebookContents.EMPTY);
|
||||
int end = nextPageIndex();
|
||||
if (index < 0 || index > end) return ItemStack.EMPTY;
|
||||
ItemStack panel = ItemStack.EMPTY;
|
||||
List<ItemStack> writing = new ArrayList<>();
|
||||
for (ItemStack page : contents.pages()) {
|
||||
if (page.isEmpty()) continue;
|
||||
if (!page.is(Rivencraft.PAGE.get()) || page.getCount() != 1) return ItemStack.EMPTY;
|
||||
if (PageItem.isLinkPanel(page)) {
|
||||
if (!panel.isEmpty() || !items.get(PANEL).isEmpty()) return ItemStack.EMPTY;
|
||||
panel = page;
|
||||
} else if (validSymbolPage(page)) writing.add(page);
|
||||
else return ItemStack.EMPTY;
|
||||
}
|
||||
if (end + writing.size() > PAGE_COUNT || writing.isEmpty() && panel.isEmpty()) return ItemStack.EMPTY;
|
||||
if (!panel.isEmpty()) items.set(PANEL, panel.copy());
|
||||
for (int current = end - 1; current >= index; current--)
|
||||
items.set(FIRST_PAGE + current + writing.size(), items.get(FIRST_PAGE + current));
|
||||
for (ItemStack page : writing) items.set(FIRST_PAGE + index++, page.copy());
|
||||
ItemStack emptied = collection.copy();
|
||||
emptied.set(Rivencraft.NOTEBOOK_CONTENTS.get(), net.dnorberg.rivencraft.writing.NotebookContents.EMPTY);
|
||||
setChanged();
|
||||
return emptied;
|
||||
}
|
||||
|
||||
public ItemStack takePage(int index) {
|
||||
int end = nextPageIndex();
|
||||
if (index < 0 || index >= end) return ItemStack.EMPTY;
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.entity.TheEndPortalBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
public final class StarFissureBlockEntity extends TheEndPortalBlockEntity {
|
||||
public StarFissureBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), pos, state);
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.dnorberg.rivencraft.writing.InkVialExchange;
|
||||
import net.dnorberg.rivencraft.writing.PageContent;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.ClientGamePacketListener;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.ContainerHelper;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/** Collection tabs, paper, ink vials and empty-vial return are owned by the block. */
|
||||
public final class WritingDeskBlockEntity extends BlockEntity implements Container {
|
||||
public static final int COLLECTIONS = 88;
|
||||
public static final int INK = COLLECTIONS;
|
||||
public static final int RETURN = INK + 1;
|
||||
public static final int PAPER = INK + 2;
|
||||
/** Unused by new desks; retained so old saves cannot silently lose slot contents. */
|
||||
public static final int LEGACY_OUTPUT = INK + 3;
|
||||
public static final int SIZE = INK + 4;
|
||||
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
private int activeCollection;
|
||||
private int inkRemaining;
|
||||
private ItemStack displayPage = ItemStack.EMPTY;
|
||||
|
||||
public WritingDeskBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(Rivencraft.WRITING_DESK_BLOCK_ENTITY.get(), pos, state);
|
||||
}
|
||||
|
||||
public int activeCollection() { return activeCollection; }
|
||||
public int inkRemaining() { return inkRemaining; }
|
||||
public ItemStack displayPage() { return displayPage; }
|
||||
public void setDisplayPage(ItemStack page) {
|
||||
ItemStack next = page.isEmpty() ? ItemStack.EMPTY : page.copyWithCount(1);
|
||||
if (!ItemStack.matches(displayPage, next)) {
|
||||
displayPage = next;
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
public void setActiveCollection(int index) {
|
||||
if (index >= 0 && index < COLLECTIONS && activeCollection != index) {
|
||||
activeCollection = index;
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override public int getContainerSize() { return SIZE; }
|
||||
@Override public boolean isEmpty() { return items.stream().allMatch(ItemStack::isEmpty); }
|
||||
@Override public ItemStack getItem(int slot) { return items.get(slot); }
|
||||
@Override public ItemStack removeItem(int slot, int amount) {
|
||||
ItemStack removed = ContainerHelper.removeItem(items, slot, amount);
|
||||
if (!removed.isEmpty()) {
|
||||
if (slot < COLLECTIONS) displayPage = ItemStack.EMPTY;
|
||||
setChanged();
|
||||
if (slot == RETURN) refillEmptyInkBar();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
@Override public ItemStack removeItemNoUpdate(int slot) { return ContainerHelper.takeItem(items, slot); }
|
||||
@Override public void setItem(int slot, ItemStack stack) {
|
||||
items.set(slot, stack);
|
||||
if (stack.getCount() > getMaxStackSize()) stack.setCount(getMaxStackSize());
|
||||
if (slot < COLLECTIONS) displayPage = ItemStack.EMPTY;
|
||||
setChanged();
|
||||
if (slot == INK || slot == RETURN) refillEmptyInkBar();
|
||||
}
|
||||
@Override public boolean stillValid(Player player) {
|
||||
return level != null && level.isLoaded(worldPosition)
|
||||
&& level.getBlockEntity(worldPosition) == this
|
||||
&& player.distanceToSqr(worldPosition.getX() + 0.5, worldPosition.getY() + 0.5,
|
||||
worldPosition.getZ() + 0.5) <= 64.0;
|
||||
}
|
||||
@Override public boolean canPlaceItem(int slot, ItemStack stack) {
|
||||
if (slot < COLLECTIONS) return CollationFolderItem.isCollection(stack)
|
||||
&& stack.getOrDefault(Rivencraft.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY)
|
||||
.pages().stream().noneMatch(page -> PageItem.content(page).kind() == PageContent.Kind.LINK_PANEL)
|
||||
|| stack.is(Rivencraft.PAGE.get())
|
||||
&& PageItem.content(stack).kind() != PageContent.Kind.LINK_PANEL;
|
||||
return switch (slot) {
|
||||
case INK -> stack.is(Rivencraft.INK_VIAL.get());
|
||||
case RETURN -> false;
|
||||
case PAPER -> stack.is(Items.PAPER);
|
||||
case LEGACY_OUTPUT -> false;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/** Consume one vial only when the empty meter can return its glass bottle. */
|
||||
private void refillEmptyInkBar() {
|
||||
if (level != null && !level.isClientSide() && inkRemaining == 0) {
|
||||
int addedInk = InkVialExchange.exchange(items, INK, RETURN);
|
||||
if (addedInk > 0) {
|
||||
inkRemaining = addedInk;
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The output is virtual; inputs are committed only after a result can be given to a player. */
|
||||
public boolean canConsumeCopyMaterials() {
|
||||
ItemStack paper = items.get(PAPER);
|
||||
if (!canPlaceItem(PAPER, paper) || paper.isEmpty()) return false;
|
||||
int cost = net.dnorberg.rivencraft.PortConfig.SYMBOL_INK_COST.get();
|
||||
return InkVialExchange.canPay(items, INK, RETURN, inkRemaining, cost);
|
||||
}
|
||||
|
||||
public boolean consumeCopyMaterials() {
|
||||
if (!canConsumeCopyMaterials()) return false;
|
||||
int cost = net.dnorberg.rivencraft.PortConfig.SYMBOL_INK_COST.get();
|
||||
if (inkRemaining < cost) {
|
||||
inkRemaining += InkVialExchange.exchange(items, INK, RETURN);
|
||||
}
|
||||
inkRemaining -= cost;
|
||||
removeItem(PAPER, 1);
|
||||
setChanged();
|
||||
if (inkRemaining == 0) refillEmptyInkBar();
|
||||
return true;
|
||||
}
|
||||
@Override public void setLevel(Level level) {
|
||||
super.setLevel(level);
|
||||
refillEmptyInkBar();
|
||||
}
|
||||
@Override public void clearContent() {
|
||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
inkRemaining = 0;
|
||||
displayPage = ItemStack.EMPTY;
|
||||
setChanged();
|
||||
}
|
||||
@Override protected void loadAdditional(ValueInput input) {
|
||||
super.loadAdditional(input);
|
||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
ContainerHelper.loadAllItems(input, items);
|
||||
activeCollection = Math.clamp(input.getIntOr("ActiveCollection", 0), 0, COLLECTIONS - 1);
|
||||
inkRemaining = Math.clamp(input.getIntOr("InkRemaining", 0), 0, InkVialItem.CAPACITY);
|
||||
displayPage = input.read("DisplayPage", ItemStack.CODEC).orElse(ItemStack.EMPTY);
|
||||
refillEmptyInkBar();
|
||||
}
|
||||
@Override protected void saveAdditional(ValueOutput output) {
|
||||
super.saveAdditional(output);
|
||||
ContainerHelper.saveAllItems(output, items);
|
||||
output.putInt("ActiveCollection", activeCollection);
|
||||
output.putInt("InkRemaining", inkRemaining);
|
||||
if (!displayPage.isEmpty()) output.store("DisplayPage", ItemStack.CODEC, displayPage);
|
||||
}
|
||||
@Override public Packet<ClientGamePacketListener> getUpdatePacket() {
|
||||
return ClientboundBlockEntityDataPacket.create(this);
|
||||
}
|
||||
@Override public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
|
||||
return saveWithoutMetadata(registries);
|
||||
}
|
||||
@Override public void setChanged() {
|
||||
super.setChanged();
|
||||
if (level != null && !level.isClientSide()) {
|
||||
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,23 @@ public final class AgeCatalogClient {
|
||||
private AgeCatalogClient() {
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void logout(net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent.LoggingOut event) {
|
||||
net.dnorberg.rivencraft.age.PortAgeColors.updateClient(java.util.Map.of());
|
||||
net.dnorberg.rivencraft.age.PortDayCycle.updateClient(java.util.Map.of());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void register(RegisterClientPayloadHandlersEvent event) {
|
||||
event.register(net.dnorberg.rivencraft.network.WritingDeskCatalogPayload.TYPE, ClientWritingDeskCatalog::handle);
|
||||
event.register(AgeCatalogPayload.TYPE, (payload, context) -> {
|
||||
if (!(context.listener() instanceof ClientPacketListener listener)) return;
|
||||
net.dnorberg.rivencraft.age.PortAgeColors.updateClient(payload.colors());
|
||||
net.dnorberg.rivencraft.age.PortDayCycle.updateClient(payload.dayCycles());
|
||||
if (listener.getLevel() != null) {
|
||||
listener.getLevel().environmentAttributes().invalidateTickCache();
|
||||
listener.getLevel().updateSkyBrightness();
|
||||
}
|
||||
listener.levels = payload.dimensions().stream()
|
||||
.map(id -> ResourceKey.<Level>create(Registries.DIMENSION, id))
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
|
||||
import net.dnorberg.rivencraft.network.WritingDeskCatalogPayload;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
||||
|
||||
public final class ClientWritingDeskCatalog {
|
||||
private ClientWritingDeskCatalog() {}
|
||||
|
||||
public static void handle(WritingDeskCatalogPayload payload, IPayloadContext context) {
|
||||
context.enqueueWork(() -> {
|
||||
var player = Minecraft.getInstance().player;
|
||||
if (player != null && player.containerMenu instanceof WritingDeskMenu menu
|
||||
&& menu.containerId == payload.containerId()) {
|
||||
menu.setCatalog(payload.symbols());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.entity.DroppedBookEntity;
|
||||
import net.minecraft.client.model.geom.ModelLayers;
|
||||
import net.minecraft.client.model.object.book.BookModel;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.entity.EntityRenderer;
|
||||
import net.minecraft.client.renderer.entity.EntityRendererProvider;
|
||||
import net.minecraft.client.renderer.entity.state.EntityRenderState;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Uses the same skyward-facing open book and textures as the 1.21.1 renderer. */
|
||||
public final class DroppedBookRenderer extends EntityRenderer<DroppedBookEntity, DroppedBookRenderer.State> {
|
||||
private static final Identifier LINKING = Identifier.fromNamespaceAndPath("rivencraft", "textures/entity/linking_book.png");
|
||||
private static final Identifier DESCRIPTIVE = Identifier.fromNamespaceAndPath("rivencraft", "textures/entity/descriptive_book.png");
|
||||
private final BookModel model;
|
||||
|
||||
public DroppedBookRenderer(EntityRendererProvider.Context context) {
|
||||
super(context);
|
||||
model = new BookModel(context.bakeLayer(ModelLayers.BOOK));
|
||||
shadowRadius = 0.25F;
|
||||
}
|
||||
|
||||
public static final class State extends EntityRenderState {
|
||||
float yaw;
|
||||
boolean descriptive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State createRenderState() { return new State(); }
|
||||
|
||||
@Override
|
||||
public void extractRenderState(DroppedBookEntity entity, State state, float partialTick) {
|
||||
super.extractRenderState(entity, state, partialTick);
|
||||
state.yaw = entity.getYRot();
|
||||
state.descriptive = entity.getItem().is(Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) {
|
||||
pose.pushPose();
|
||||
pose.translate(0.0F, 0.0625F, 0.0F);
|
||||
pose.rotateDegrees(Axis.YP, state.yaw + 90.0F);
|
||||
pose.rotateDegrees(Axis.ZP, 90.0F);
|
||||
pose.scale(0.8F, 0.8F, 0.8F);
|
||||
collector.submitModel(model, BookModel.State.forAnimation(0, 0, 0, 1.2F), pose,
|
||||
state.descriptive ? DESCRIPTIVE : LINKING, state.lightCoords, OverlayTexture.NO_OVERLAY,
|
||||
state.outlineColor);
|
||||
pose.popPose();
|
||||
super.submit(state, pose, collector, camera);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import java.util.Locale;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.menu.NotebookMenu;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** A page surface modelled on Legacy's folder GUI, backed by the Notebook menu. */
|
||||
public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu> {
|
||||
private static final int SURFACE_TOP = 20;
|
||||
private static final int SURFACE_BOTTOM = 132;
|
||||
private static final int SURFACE_RIGHT = 156;
|
||||
private static final int PAGE_WIDTH = 30;
|
||||
private static final int PAGE_HEIGHT = 40;
|
||||
private static final int PAGE_STEP_X = 31;
|
||||
private static final int PAGE_STEP_Y = 41;
|
||||
private static final int PAGE_COLUMNS = 5;
|
||||
private static final Identifier PAGE_ATLAS = texture("gui/notebook_pages");
|
||||
private EditBox textInput;
|
||||
private boolean draggingScrollbar;
|
||||
private boolean surfaceClickPendingRelease;
|
||||
private int selectedSlot = -1;
|
||||
private int scroll;
|
||||
|
||||
public NotebookScreen(NotebookMenu menu, Inventory inventory, Component title) {
|
||||
super(menu, inventory, title, 176, 213);
|
||||
inventoryLabelY = 123;
|
||||
}
|
||||
|
||||
@Override public boolean isPauseScreen() { return false; }
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
super.init();
|
||||
Button sortButton = Button.builder(Component.literal("AZ"), button -> {})
|
||||
.bounds(leftPos, topPos, 18, 18).build();
|
||||
Button allButton = Button.builder(Component.literal("ALL"), button -> {})
|
||||
.bounds(leftPos + 20, topPos, 26, 18).build();
|
||||
sortButton.active = false;
|
||||
allButton.active = false;
|
||||
addRenderableWidget(sortButton);
|
||||
addRenderableWidget(allButton);
|
||||
textInput = new EditBox(font, leftPos + 48, topPos, 128, 18,
|
||||
Component.translatable("screen.rivencraft.notebook.search"));
|
||||
textInput.setMaxLength(32);
|
||||
textInput.setHint(Component.translatable("screen.rivencraft.notebook.search"));
|
||||
textInput.setResponder(value -> scroll = 0);
|
||||
addRenderableWidget(textInput);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
super.extractBackground(graphics, mouseX, mouseY, partialTick);
|
||||
graphics.fill(leftPos, topPos + SURFACE_TOP, leftPos + SURFACE_RIGHT,
|
||||
topPos + SURFACE_BOTTOM, 0xB0000000);
|
||||
drawBeveledPanel(graphics, leftPos, topPos + 132, imageWidth, 81);
|
||||
for (int row = 0; row < 3; row++) {
|
||||
for (int column = 0; column < 9; column++) drawSlot(graphics, 7 + column * 18, 134 + row * 18);
|
||||
}
|
||||
for (int column = 0; column < 9; column++) drawSlot(graphics, 7 + column * 18, 192);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
super.extractContents(graphics, mouseX, mouseY, partialTick);
|
||||
int pageSlots = displayedPageSlots();
|
||||
scroll = Math.min(scroll, maxScroll(pageSlots));
|
||||
graphics.nextStratum();
|
||||
graphics.enableScissor(leftPos, topPos + SURFACE_TOP, leftPos + SURFACE_RIGHT, topPos + SURFACE_BOTTOM);
|
||||
for (int index = 0; index < pageSlots; index++) {
|
||||
int x = leftPos + (index % PAGE_COLUMNS) * PAGE_STEP_X;
|
||||
int y = topPos + SURFACE_TOP + (index / PAGE_COLUMNS) * PAGE_STEP_Y - scroll;
|
||||
if (y + PAGE_HEIGHT > topPos + SURFACE_TOP && y < topPos + SURFACE_BOTTOM) drawPage(graphics, index, x, y);
|
||||
}
|
||||
graphics.disableScissor();
|
||||
drawScrollbar(graphics, pageSlots);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void extractTooltip(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
int hovered = pageAt(mouseX, mouseY);
|
||||
if (hovered >= 0 && menu.getCarried().isEmpty() && !menu.getSlot(hovered).getItem().isEmpty()) {
|
||||
ItemStack page = menu.getSlot(hovered).getItem();
|
||||
if (matchesSearch(page)) graphics.setTooltipForNextFrame(font, page, mouseX, mouseY);
|
||||
} else super.extractTooltip(graphics, mouseX, mouseY);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void extractSlots(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
// Collection slots are drawn above as large pages. Do not submit up to
|
||||
// 252 additional item models at their off-screen server-slot positions.
|
||||
for (int index = NotebookMenu.NOTEBOOK_SLOTS; index < menu.slots.size(); index++) {
|
||||
var slot = menu.getSlot(index);
|
||||
if (slot.isActive()) extractSlot(graphics, slot, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0xFF404040, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
double mouseX = event.x(), mouseY = event.y();
|
||||
int button = event.button();
|
||||
if (inScrollbar(mouseX, mouseY) && button == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
draggingScrollbar = true;
|
||||
setScrollFromMouse(mouseY, displayedPageSlots());
|
||||
return true;
|
||||
}
|
||||
int index = pageAt(mouseX, mouseY);
|
||||
if (index >= 0 && (button == InputConstants.MOUSE_BUTTON_LEFT || button == InputConstants.MOUSE_BUTTON_RIGHT)) {
|
||||
ItemStack page = menu.getSlot(index).getItem();
|
||||
if ((!page.isEmpty() && !matchesSearch(page))
|
||||
|| (page.isEmpty() && (!menu.getCarried().is(Rivencraft.PAGE.get())
|
||||
|| PageItem.isLinkPanel(menu.getCarried())))) {
|
||||
return true;
|
||||
}
|
||||
setFocused(null);
|
||||
textInput.setFocused(false);
|
||||
selectedSlot = index;
|
||||
slotClicked(menu.getSlot(index), index, button == InputConstants.MOUSE_BUTTON_RIGHT ? 1 : 0, ContainerInput.PICKUP);
|
||||
surfaceClickPendingRelease = true;
|
||||
return true;
|
||||
}
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseDragged(MouseButtonEvent event, double dragX, double dragY) {
|
||||
double mouseY = event.y();
|
||||
int button = event.button();
|
||||
if (draggingScrollbar && button == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
setScrollFromMouse(mouseY, displayedPageSlots());
|
||||
return true;
|
||||
}
|
||||
return super.mouseDragged(event, dragX, dragY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseReleased(MouseButtonEvent event) {
|
||||
if (draggingScrollbar || surfaceClickPendingRelease) {
|
||||
draggingScrollbar = false;
|
||||
surfaceClickPendingRelease = false;
|
||||
return true;
|
||||
}
|
||||
draggingScrollbar = false;
|
||||
return super.mouseReleased(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
|
||||
if (inSurface(mouseX, mouseY) || inScrollbar(mouseX, mouseY)) {
|
||||
scroll = Math.clamp(scroll - (int)Math.signum(scrollY) * 20,
|
||||
0, maxScroll(displayedPageSlots()));
|
||||
return true;
|
||||
}
|
||||
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(KeyEvent event) {
|
||||
// Let the container close normally, including returning its carried
|
||||
// stack, even while the search field owns keyboard focus.
|
||||
if (event.isEscape()) return super.keyPressed(event);
|
||||
if (textInput != null && textInput.isFocused()) {
|
||||
textInput.keyPressed(event);
|
||||
// EditBox does not consume ordinary letter key presses; the matching charTyped
|
||||
// event handles text input. Do not pass them to the container's inventory keys.
|
||||
return true;
|
||||
}
|
||||
if (selectedSlot >= 0 && (event.isLeft() || event.isRight())) {
|
||||
moveSelected(event.isRight());
|
||||
return true;
|
||||
}
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
|
||||
private int displayedPageSlots() {
|
||||
if (menu.getCarried().is(Rivencraft.PAGE.get())) {
|
||||
return menu.pageCapacity();
|
||||
}
|
||||
for (int index = menu.pageCapacity() - 1; index >= 0; index--) {
|
||||
if (!menu.getSlot(index).getItem().isEmpty()) {
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private boolean matchesSearch(ItemStack page) {
|
||||
return textInput.getValue().isBlank()
|
||||
|| page.getHoverName().getString().toLowerCase(Locale.ROOT)
|
||||
.contains(textInput.getValue().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
private int pageAt(double mouseX, double mouseY) {
|
||||
if (!inSurface(mouseX, mouseY)) return -1;
|
||||
int localX = (int)mouseX - leftPos;
|
||||
int localY = (int)mouseY - topPos - SURFACE_TOP + scroll;
|
||||
int column = localX / PAGE_STEP_X;
|
||||
int index = (localY / PAGE_STEP_Y) * PAGE_COLUMNS + column;
|
||||
return column < PAGE_COLUMNS && localX % PAGE_STEP_X < PAGE_WIDTH
|
||||
&& localY % PAGE_STEP_Y < PAGE_HEIGHT && index < displayedPageSlots()
|
||||
? index : -1;
|
||||
}
|
||||
|
||||
private boolean inSurface(double mouseX, double mouseY) {
|
||||
return mouseX >= leftPos && mouseX < leftPos + SURFACE_RIGHT
|
||||
&& mouseY >= topPos + SURFACE_TOP && mouseY < topPos + SURFACE_BOTTOM;
|
||||
}
|
||||
|
||||
private boolean inScrollbar(double mouseX, double mouseY) {
|
||||
return mouseX >= leftPos + SURFACE_RIGHT && mouseX < leftPos + imageWidth
|
||||
&& mouseY >= topPos + SURFACE_TOP && mouseY < topPos + SURFACE_BOTTOM;
|
||||
}
|
||||
|
||||
private int maxScroll(int pageCount) {
|
||||
int rows = (pageCount + PAGE_COLUMNS - 1) / PAGE_COLUMNS;
|
||||
return Math.max(0, rows * PAGE_STEP_Y - (SURFACE_BOTTOM - SURFACE_TOP));
|
||||
}
|
||||
|
||||
private void setScrollFromMouse(double mouseY, int pageCount) {
|
||||
int maximum = maxScroll(pageCount);
|
||||
double fraction = (mouseY - topPos - SURFACE_TOP - 10.0)
|
||||
/ (SURFACE_BOTTOM - SURFACE_TOP - 20.0);
|
||||
scroll = Math.clamp((int)(Math.clamp(fraction, 0.0, 1.0) * maximum), 0, maximum);
|
||||
}
|
||||
|
||||
private void drawScrollbar(GuiGraphicsExtractor graphics, int pageCount) {
|
||||
int x = leftPos + SURFACE_RIGHT;
|
||||
int y = topPos + SURFACE_TOP;
|
||||
int height = SURFACE_BOTTOM - SURFACE_TOP;
|
||||
graphics.fill(x, y, x + 20, y + height, 0xFF858585);
|
||||
graphics.outline(x, y, 20, height, 0xFF262626);
|
||||
for (int line = y + 5; line < y + height - 4; line += 5) {
|
||||
graphics.fill(x + 4, line, x + 16, line + 1, 0xFFB7B7B7);
|
||||
}
|
||||
int maximum = maxScroll(pageCount);
|
||||
int thumbY = y + 4 + (maximum == 0 ? 0 : scroll * (height - 23) / maximum);
|
||||
graphics.fill(x + 4, thumbY, x + 16, thumbY + 15, 0xFFC9C9C9);
|
||||
graphics.outline(x + 4, thumbY, 12, 15, 0xFF414141);
|
||||
}
|
||||
|
||||
private void drawPage(GuiGraphicsExtractor graphics, int index, int x, int y) {
|
||||
ItemStack page = menu.getSlot(index).getItem();
|
||||
boolean dimmed = page.isEmpty() || !matchesSearch(page);
|
||||
// Legacy's Notebook draws the atlas' native 30x40 page, not a stretched item icon.
|
||||
graphics.nextStratum();
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, PAGE_ATLAS, x, y, 156, 0, PAGE_WIDTH, PAGE_HEIGHT, 256, 256);
|
||||
if (!page.isEmpty() && page.has(Rivencraft.SYMBOL_PAGE_ID.get())) {
|
||||
graphics.nextStratum();
|
||||
ItemStack glyph = page.copy();
|
||||
glyph.set(Rivencraft.PAGE_GLYPH_ONLY.get(), true);
|
||||
graphics.pose().pushMatrix();
|
||||
graphics.pose().translate(x + 1, y + 6);
|
||||
graphics.pose().scale(1.75F, 1.75F);
|
||||
graphics.fakeItem(glyph, 0, 0);
|
||||
graphics.pose().popMatrix();
|
||||
graphics.nextStratum();
|
||||
}
|
||||
if (dimmed) {
|
||||
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0xB8000000);
|
||||
} else if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.isPageImplemented(
|
||||
page.get(Rivencraft.SYMBOL_PAGE_ID.get()))) {
|
||||
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0x55FF3333);
|
||||
}
|
||||
if (index == selectedSlot && !page.isEmpty()) {
|
||||
graphics.outline(x, y, PAGE_WIDTH, PAGE_HEIGHT, 0xFFFFD061);
|
||||
}
|
||||
}
|
||||
|
||||
private static Identifier texture(String path) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/" + path + ".png");
|
||||
}
|
||||
|
||||
private void moveSelected(boolean next) {
|
||||
int target = selectedSlot + (next ? 1 : -1);
|
||||
if (selectedSlot < 0 || target < 0 || target >= NotebookMenu.NOTEBOOK_SLOTS
|
||||
|| menu.getSlot(selectedSlot).getItem().isEmpty()) return;
|
||||
int buttonId = (next ? NotebookMenu.MOVE_NEXT_BASE : NotebookMenu.MOVE_PREVIOUS_BASE) + selectedSlot;
|
||||
if (menu.clickMenuButton(minecraft.player, buttonId)) {
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, buttonId);
|
||||
selectedSlot = target;
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawBeveledPanel(GuiGraphicsExtractor graphics, int x, int y, int width, int height) {
|
||||
graphics.fill(x, y, x + width, y + height, 0xFFC6C6C6);
|
||||
graphics.fill(x, y, x + width, y + 2, 0xFFFFFFFF);
|
||||
graphics.fill(x, y, x + 2, y + height, 0xFFFFFFFF);
|
||||
graphics.fill(x, y + height - 2, x + width, y + height, 0xFF555555);
|
||||
graphics.fill(x + width - 2, y, x + width, y + height, 0xFF555555);
|
||||
}
|
||||
|
||||
private void drawSlot(GuiGraphicsExtractor graphics, int x, int y) {
|
||||
graphics.fill(leftPos + x, topPos + y, leftPos + x + 18, topPos + y + 18, 0xFF8B8B8B);
|
||||
graphics.fill(leftPos + x, topPos + y, leftPos + x + 18, topPos + y + 1, 0xFF373737);
|
||||
graphics.fill(leftPos + x, topPos + y, leftPos + x + 1, topPos + y + 18, 0xFF373737);
|
||||
graphics.fill(leftPos + x + 17, topPos + y, leftPos + x + 18, topPos + y + 18, 0xFFFFFFFF);
|
||||
graphics.fill(leftPos + x, topPos + y + 17, leftPos + x + 18, topPos + y + 18, 0xFFFFFFFF);
|
||||
}
|
||||
}
|
||||
+10
@@ -14,10 +14,20 @@ import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
|
||||
public final class PortBookBinderClientEvents {
|
||||
private PortBookBinderClientEvents() {}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerBlockEntityRenderers(
|
||||
net.neoforged.neoforge.client.event.EntityRenderersEvent.RegisterRenderers event) {
|
||||
event.registerBlockEntityRenderer(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), context -> new StarFissureRenderer());
|
||||
event.registerEntityRenderer(Rivencraft.DROPPED_BOOK.get(), DroppedBookRenderer::new);
|
||||
event.registerBlockEntityRenderer(Rivencraft.WRITING_DESK_BLOCK_ENTITY.get(), WritingDeskRenderer::new);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerScreens(RegisterMenuScreensEvent event) {
|
||||
event.register(Rivencraft.BOOK_BINDER_MENU.get(), PortBookBinderScreen::new);
|
||||
event.register(Rivencraft.INK_MIXER_MENU.get(), InkMixerScreen::new);
|
||||
event.register(Rivencraft.NOTEBOOK_MENU.get(), NotebookScreen::new);
|
||||
event.register(Rivencraft.WRITING_DESK_MENU.get(), WritingDeskScreen::new);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
|
||||
@@ -94,6 +94,8 @@ public final class PortBookBinderScreen extends AbstractContainerScreen<PortBook
|
||||
} else if (PageItem.isLinkPanel(page)) {
|
||||
graphics.fill(x + 5, y + 6, x + 22, y + 17, 0xFF000000);
|
||||
} else {
|
||||
if (!PortAuthoredWriting.isPageImplemented(page.get(Rivencraft.SYMBOL_PAGE_ID.get())))
|
||||
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0x88FF3333);
|
||||
// Render the full-size transparent glyph through the working
|
||||
// item pipeline, without its small parchment-backed icon.
|
||||
ItemStack glyphOnly = page.copy();
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.commands.RenderPass;
|
||||
import com.mojang.renderpearl.api.pipeline.BlendFunction;
|
||||
import com.mojang.renderpearl.api.pipeline.ColorTargetState;
|
||||
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
|
||||
import com.mojang.renderpearl.api.pipeline.RenderPipeline;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.MoonPhase;
|
||||
import net.neoforged.api.distmarker.Dist;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.client.event.RegisterRenderPipelinesEvent;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/** Legacy's opaque central disc, separate from the native transparent glow. */
|
||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
|
||||
public final class PortCelestialDiscs implements AutoCloseable {
|
||||
private static final Identifier SUN = Identifier.withDefaultNamespace("sun");
|
||||
private static final RenderPipeline CORE = RenderPipelines.CELESTIAL.toBuilder()
|
||||
.withLocation(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "pipeline/celestial_core"))
|
||||
.withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)).build();
|
||||
private final Map<TextureAtlasSprite, GpuBuffer> cores = new IdentityHashMap<>();
|
||||
private TextureAtlasSprite atlasGeneration;
|
||||
|
||||
@SubscribeEvent public static void register(RegisterRenderPipelinesEvent event) { event.registerPipeline(CORE); }
|
||||
|
||||
public void draw(RenderPass pass, TextureAtlas atlas, boolean moon, MoonPhase phase,
|
||||
Matrix4f transform, int color, float alpha) {
|
||||
if (alpha <= 0) return;
|
||||
var generation = atlas.getSprite(SUN);
|
||||
if (generation != atlasGeneration) {
|
||||
close();
|
||||
atlasGeneration = generation;
|
||||
}
|
||||
var sprite = moon ? atlas.getSprite(Identifier.withDefaultNamespace("moon/" + phase.getSerializedName())) : generation;
|
||||
var buffer = cores.computeIfAbsent(sprite, ignored -> build(sprite, moon));
|
||||
var indices = RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
|
||||
var indexBuffer = indices.getBuffer(6);
|
||||
var rgb = net.dnorberg.rivencraft.age.PortAgeColors.vector(color);
|
||||
var uniforms = RenderSystem.getDynamicUniforms().writeTransform(transform, new Vector4f(rgb, alpha));
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(CORE));
|
||||
RenderSystem.bindDefaultUniforms(pass);
|
||||
pass.setUniform("DynamicTransforms", uniforms);
|
||||
pass.setUniform("Sampler0", atlas.getTextureView(), atlas.getSampler());
|
||||
pass.setVertexBuffer(0, buffer.slice());
|
||||
pass.setIndexBuffer(indexBuffer, indices.type());
|
||||
pass.drawIndexed(6, 1, 0, 0, 0);
|
||||
}
|
||||
|
||||
private static GpuBuffer build(TextureAtlasSprite sprite, boolean moon) {
|
||||
var format = DefaultVertexFormat.POSITION_TEX;
|
||||
try (var bytes = ByteBufferBuilder.exactlySized(4 * format.getVertexSize())) {
|
||||
var builder = new BufferBuilder(bytes, PrimitiveTopology.QUADS, format);
|
||||
// Same 8/32 central area as root's ordinary-alpha second pass.
|
||||
float u0 = sprite.getU(0.375F), u1 = sprite.getU(0.625F);
|
||||
float v0 = sprite.getV(0.375F), v1 = sprite.getV(0.625F);
|
||||
builder.addVertex(-0.25F, 0, -0.25F).setUv(moon ? u1 : u0, moon ? v1 : v0);
|
||||
builder.addVertex(0.25F, 0, -0.25F).setUv(moon ? u0 : u1, moon ? v1 : v0);
|
||||
builder.addVertex(0.25F, 0, 0.25F).setUv(moon ? u0 : u1, moon ? v0 : v1);
|
||||
builder.addVertex(-0.25F, 0, 0.25F).setUv(moon ? u1 : u0, moon ? v0 : v1);
|
||||
try (var mesh = builder.buildOrThrow()) {
|
||||
return RenderSystem.getDevice().createBuffer(() -> "Rivencraft celestial core", 40, mesh.vertexBuffer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cores.values().forEach(GpuBuffer::close);
|
||||
cores.clear();
|
||||
atlasGeneration = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.MeshData;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
||||
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalDouble;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/** Legacy's six 48x48 noise faces, uploaded once per animation frame rather than per cell. */
|
||||
public final class PortStaticSky implements AutoCloseable {
|
||||
private static final int GRID = 48;
|
||||
private final DynamicTexture texture = new DynamicTexture("Rivencraft static sky", GRID * 6, GRID, false);
|
||||
private final GpuBuffer cube = buildCube();
|
||||
private long uploadedFrame = Long.MIN_VALUE;
|
||||
private int uploadedTint = -1;
|
||||
|
||||
public void render(RenderTarget target, GpuBufferSlice fog, long frame, int tint) {
|
||||
if (tint < 0) tint = 0x929292;
|
||||
if (uploadedFrame != frame || uploadedTint != tint) {
|
||||
RandomSource random = RandomSource.create(frame);
|
||||
var image = texture.getPixels();
|
||||
for (int face = 0; face < 6; face++) {
|
||||
for (int y = 0; y < GRID; y++) {
|
||||
for (int x = 0; x < GRID; x++) {
|
||||
int noise = 65 + random.nextInt(160);
|
||||
int red = (tint >> 16 & 255) * noise / 255;
|
||||
int green = (tint >> 8 & 255) * noise / 255;
|
||||
int blue = (tint & 255) * noise / 255;
|
||||
image.setPixel(face * GRID + x, y, 0xff000000 | red << 16 | green << 8 | blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
texture.upload();
|
||||
uploadedFrame = frame;
|
||||
uploadedTint = tint;
|
||||
}
|
||||
RenderSystem.setShaderFog(fog);
|
||||
var indices = RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
|
||||
var indexBuffer = indices.getBuffer(36);
|
||||
var transform = RenderSystem.getDynamicUniforms().writeTransform(RenderSystem.getModelViewMatrixCopy());
|
||||
try (var pass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(
|
||||
() -> "Rivencraft static sky", target.getColorTextureView(), Optional.empty(),
|
||||
target.getDepthTextureView(), OptionalDouble.empty())) {
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(RenderPipelines.END_SKY));
|
||||
RenderSystem.bindDefaultUniforms(pass);
|
||||
pass.setUniform("DynamicTransforms", transform);
|
||||
pass.setUniform("Sampler0", texture.getTextureView(), texture.getSampler());
|
||||
pass.setVertexBuffer(0, cube.slice());
|
||||
pass.setIndexBuffer(indexBuffer, indices.type());
|
||||
pass.drawIndexed(36, 1, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static GpuBuffer buildCube() {
|
||||
var format = DefaultVertexFormat.POSITION_TEX_COLOR;
|
||||
try (var bytes = ByteBufferBuilder.exactlySized(24 * format.getVertexSize())) {
|
||||
var builder = new BufferBuilder(bytes, PrimitiveTopology.QUADS, format);
|
||||
for (int face = 0; face < 6; face++) {
|
||||
var pose = new Matrix4f();
|
||||
switch (face) {
|
||||
case 1 -> pose.rotationX((float) Math.PI / 2);
|
||||
case 2 -> pose.rotationX((float) -Math.PI / 2);
|
||||
case 3 -> pose.rotationX((float) Math.PI);
|
||||
case 4 -> pose.rotationZ((float) Math.PI / 2);
|
||||
case 5 -> pose.rotationZ((float) -Math.PI / 2);
|
||||
}
|
||||
float u0 = face / 6f, u1 = (face + 1) / 6f;
|
||||
builder.addVertex(pose, -100, -100, -100).setUv(u0, 0).setColor(-1);
|
||||
builder.addVertex(pose, -100, -100, 100).setUv(u0, 1).setColor(-1);
|
||||
builder.addVertex(pose, 100, -100, 100).setUv(u1, 1).setColor(-1);
|
||||
builder.addVertex(pose, 100, -100, -100).setUv(u1, 0).setColor(-1);
|
||||
}
|
||||
try (MeshData mesh = builder.buildOrThrow()) {
|
||||
return RenderSystem.getDevice().createBuffer(() -> "Rivencraft static sky cube", 40, mesh.vertexBuffer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cube.close();
|
||||
texture.close();
|
||||
}
|
||||
}
|
||||
+10
-8
@@ -5,6 +5,7 @@ import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.special.SpecialModelRenderer;
|
||||
@@ -18,7 +19,8 @@ public final class PortSymbolPageRenderer implements SpecialModelRenderer<PortSy
|
||||
public PageArt extractArgument(ItemStack stack) {
|
||||
Identifier symbol = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
return symbol == null ? null : new PageArt(symbol,
|
||||
stack.getOrDefault(Rivencraft.PAGE_GLYPH_ONLY.get(), false));
|
||||
stack.getOrDefault(Rivencraft.PAGE_GLYPH_ONLY.get(), false),
|
||||
PortAuthoredWriting.isPageImplemented(symbol) ? 0xFFFFFFFF : 0xFFFF6666);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -30,10 +32,10 @@ public final class PortSymbolPageRenderer implements SpecialModelRenderer<PortSy
|
||||
: PortSymbolPageTextures.forItem(art.symbol());
|
||||
collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture),
|
||||
(matrix, vertices) -> {
|
||||
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<PortSy
|
||||
}
|
||||
|
||||
private static void vertex(VertexConsumer vertices, PoseStack.Pose pose,
|
||||
float x, float y, float u, float v, int light, int overlay) {
|
||||
vertices.addVertex(pose, x, y, 0.5F).setColor(255, 255, 255, 255)
|
||||
float x, float y, float u, float v, int light, int overlay, int color) {
|
||||
vertices.addVertex(pose, x, y, 0.5F).setColor(color)
|
||||
.setUv(u, v).setOverlay(overlay).setLight(light).setNormal(pose, 0, 0, 1);
|
||||
}
|
||||
|
||||
public record PageArt(Identifier symbol, boolean glyphOnly) {}
|
||||
public record PageArt(Identifier symbol, boolean glyphOnly, int color) {}
|
||||
|
||||
public record Unbaked() implements SpecialModelRenderer.Unbaked<PageArt> {
|
||||
public static final Unbaked INSTANCE = new Unbaked();
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.blockentity.AbstractEndPortalRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.state.EndPortalRenderState;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.core.Direction;
|
||||
|
||||
/** Retains 1.21.1's UP-at-zero/DOWN-at-one planes with the modern starfield renderer. */
|
||||
public final class StarFissureRenderer extends AbstractEndPortalRenderer<StarFissureBlockEntity, EndPortalRenderState> {
|
||||
@Override
|
||||
public EndPortalRenderState createRenderState() { return new EndPortalRenderState(); }
|
||||
|
||||
@Override
|
||||
public void submit(EndPortalRenderState state, PoseStack pose, SubmitNodeCollector collector,
|
||||
CameraRenderState camera) {
|
||||
if (state.facesToShow.contains(Direction.UP)) {
|
||||
pose.pushPose();
|
||||
pose.translate(0, -1, 0);
|
||||
submitCube(List.of(Direction.UP), RenderTypes.endPortal(), pose, collector);
|
||||
pose.popPose();
|
||||
}
|
||||
if (state.facesToShow.contains(Direction.DOWN)) {
|
||||
pose.pushPose();
|
||||
pose.translate(0, 1, 0);
|
||||
submitCube(List.of(Direction.DOWN), RenderTypes.endPortal(), pose, collector);
|
||||
pose.popPose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
/** Carries extracted sky presentation values without live-world reads during drawing. */
|
||||
public interface StaticSkyState {
|
||||
int rivencraft$bodyOverlay();
|
||||
void rivencraft$bodyOverlay(int mode);
|
||||
record Body(int kind, float angle, int bearing, float size, int color, float eclipse) {}
|
||||
java.util.List<Body> rivencraft$bodies();
|
||||
void rivencraft$bodies(java.util.List<Body> bodies);
|
||||
long rivencraft$staticFrame();
|
||||
void rivencraft$staticFrame(long frame);
|
||||
int rivencraft$skyTint();
|
||||
void rivencraft$skyTint(int color);
|
||||
int rivencraft$orbitBearing();
|
||||
void rivencraft$orbitBearing(int bearing);
|
||||
float rivencraft$sunSize();
|
||||
void rivencraft$sunSize(float size);
|
||||
int rivencraft$celestialTint();
|
||||
void rivencraft$celestialTint(int color);
|
||||
int rivencraft$bodyMode();
|
||||
void rivencraft$bodyMode(int mode);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import net.dnorberg.rivencraft.block.WritingDeskBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
|
||||
import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
|
||||
import net.minecraft.client.renderer.item.ItemModelResolver;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** The existing selected-page and white-paper display, extracted for 26.3. */
|
||||
public final class WritingDeskRenderer implements BlockEntityRenderer<WritingDeskBlockEntity, WritingDeskRenderer.State> {
|
||||
private final ItemModelResolver resolver;
|
||||
public WritingDeskRenderer(BlockEntityRendererProvider.Context context) { resolver = context.itemModelResolver(); }
|
||||
public static final class State extends BlockEntityRenderState {
|
||||
final ItemStackRenderState paper = new ItemStackRenderState();
|
||||
final ItemStackRenderState page = new ItemStackRenderState();
|
||||
int layers;
|
||||
float rotation;
|
||||
}
|
||||
@Override public State createRenderState() { return new State(); }
|
||||
@Override public void extractRenderState(WritingDeskBlockEntity desk, State state, float partialTick,
|
||||
Vec3 camera, ModelFeatureRenderer.CrumblingOverlay overlay) {
|
||||
BlockEntityRenderer.super.extractRenderState(desk, state, partialTick, camera, overlay);
|
||||
int count = desk.getItem(WritingDeskBlockEntity.PAPER).getCount();
|
||||
state.layers = count == 0 ? 0 : Math.min(4, 1 + count / 16);
|
||||
state.rotation = 180 - desk.getBlockState().getValue(WritingDeskBlock.FACING).toYRot();
|
||||
resolver.updateForTopItem(state.paper, new ItemStack(Items.PAPER), ItemDisplayContext.FIXED, desk.getLevel(), null, 0);
|
||||
resolver.updateForTopItem(state.page, desk.displayPage(), ItemDisplayContext.FIXED, desk.getLevel(), null, 0);
|
||||
}
|
||||
@Override public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) {
|
||||
pose.pushPose();
|
||||
pose.translate(0.5, 0, 0.5);
|
||||
pose.rotateDegrees(Axis.YP, state.rotation);
|
||||
for (int i = 0; i < state.layers; i++) draw(state.paper, pose, collector, state.lightCoords,
|
||||
1.22, 0.27 + i * 0.008, -0.14, 0.55F);
|
||||
if (!state.page.isEmpty()) draw(state.page, pose, collector, state.lightCoords, 1.0, 0.572, 0, 0.8F);
|
||||
pose.popPose();
|
||||
}
|
||||
private static void draw(ItemStackRenderState item, PoseStack pose, SubmitNodeCollector collector,
|
||||
int light, double x, double y, double z, float scale) {
|
||||
pose.pushPose();
|
||||
pose.translate(x, y, z);
|
||||
pose.rotateDegrees(Axis.XP, 90);
|
||||
pose.scale(scale, scale, scale);
|
||||
item.submit(pose, collector, light, OverlayTexture.NO_OVERLAY, 0);
|
||||
pose.popPose();
|
||||
}
|
||||
@Override public boolean shouldRenderOffScreen() { return true; }
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.writing.PageContent;
|
||||
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
|
||||
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
|
||||
/** Legacy desk atlas layout, with four visible tabs out of the desk's 88 collections. */
|
||||
public final class WritingDeskScreen extends AbstractContainerScreen<WritingDeskMenu> {
|
||||
private static final Identifier PAGE_ATLAS = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "textures/gui/notebook_pages.png");
|
||||
private static final Identifier LEGACY_DESK = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "textures/gui/writing_desk_legacy.png");
|
||||
private static final Identifier SCROLLBAR = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "textures/gui/scrollbar.png");
|
||||
private int lastCollection = -1;
|
||||
private int selectedPage;
|
||||
private int scrollRows;
|
||||
private boolean allMode;
|
||||
private boolean sortAlphabetically;
|
||||
private int selectedCollection = -1;
|
||||
private Identifier selectedCategory;
|
||||
private EditBox search;
|
||||
private Button categoryButton;
|
||||
private Button allButton;
|
||||
private Button sortButton;
|
||||
private boolean draggingScrollbar;
|
||||
private record DisplayPage(int collection, int index, ItemStack stack) {}
|
||||
private DisplayPage pageDragSource;
|
||||
private double pageDragStartX;
|
||||
private double pageDragStartY;
|
||||
private boolean draggingPage;
|
||||
|
||||
private boolean isPhysicalPageView() {
|
||||
return !allMode && !sortAlphabetically && selectedCategory == null
|
||||
&& (search == null || search.getValue().isBlank());
|
||||
}
|
||||
|
||||
public WritingDeskScreen(WritingDeskMenu menu, Inventory inventory, Component title) {
|
||||
super(menu, inventory, title, 409, 186);
|
||||
titleLabelX = 222;
|
||||
}
|
||||
|
||||
@Override public boolean isPauseScreen() { return false; }
|
||||
|
||||
@Override protected void init() {
|
||||
super.init();
|
||||
sortButton = addRenderableWidget(Button.builder(Component.literal("AZ"), button -> {
|
||||
sortAlphabetically = !sortAlphabetically;
|
||||
scrollRows = 0;
|
||||
}).bounds(leftPos + 58, topPos, 20, 18)
|
||||
.tooltip(Tooltip.create(Component.translatable("screen.rivencraft.writing_desk.sort"))).build());
|
||||
allButton = addRenderableWidget(Button.builder(Component.literal("ALL"), button -> {
|
||||
allMode = !allMode;
|
||||
scrollRows = 0;
|
||||
if (!allMode) {
|
||||
selectedCollection = -1;
|
||||
selectedPage = 0;
|
||||
if (minecraft != null && minecraft.gameMode != null)
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, WritingDeskMenu.PAGE_BUTTON_BASE);
|
||||
}
|
||||
}).bounds(leftPos + 80, topPos, 28, 18)
|
||||
.tooltip(Tooltip.create(Component.translatable("screen.rivencraft.writing_desk.all"))).build());
|
||||
search = new EditBox(font, leftPos + 110, topPos, 101, 18,
|
||||
Component.translatable("screen.rivencraft.writing_desk.search"));
|
||||
search.setMaxLength(64);
|
||||
search.setHint(Component.translatable("screen.rivencraft.writing_desk.search"));
|
||||
search.setResponder(value -> scrollRows = 0);
|
||||
addRenderableWidget(search);
|
||||
categoryButton = addRenderableWidget(Button.builder(Component.literal("C"), button -> {
|
||||
List<Identifier> categories = menu.catalog().stream()
|
||||
.map(SymbolDisplayMetadata::category).distinct().sorted().toList();
|
||||
int next = selectedCategory == null ? 0 : categories.indexOf(selectedCategory) + 1;
|
||||
selectedCategory = next >= categories.size() ? null : categories.get(next);
|
||||
categoryButton.setMessage(Component.literal(selectedCategory == null ? "C" : "C*"));
|
||||
scrollRows = 0;
|
||||
}).bounds(leftPos + 213, topPos, 18, 18).build());
|
||||
}
|
||||
|
||||
private List<DisplayPage> filteredPages() {
|
||||
String query = search == null ? "" : search.getValue().trim().toLowerCase(Locale.ROOT);
|
||||
List<DisplayPage> entries = new ArrayList<>();
|
||||
int from = allMode ? 0 : menu.activeCollection();
|
||||
int until = allMode ? WritingDeskBlockEntity.COLLECTIONS : from + 1;
|
||||
for (int collection = from; collection < until; collection++) {
|
||||
List<ItemStack> pages = pages(collection);
|
||||
int last = -1;
|
||||
for (int i = 0; i < pages.size(); i++) if (!pages.get(i).isEmpty()) last = i;
|
||||
// A notebook has exactly its normal GUI capacity; a loose page has one position.
|
||||
if (isPhysicalPageView()) last = menu.pageCapacity(collection) - 1;
|
||||
for (int i = 0; i <= last; i++) {
|
||||
ItemStack page = i < pages.size() ? pages.get(i) : ItemStack.EMPTY;
|
||||
if (page.isEmpty() && (allMode || sortAlphabetically || !query.isEmpty() || selectedCategory != null)) continue;
|
||||
if (!page.isEmpty() && !query.isEmpty()
|
||||
&& !page.getHoverName().getString().toLowerCase(Locale.ROOT).contains(query)
|
||||
&& !PageItem.content(page).symbol().map(symbol -> symbol.id().toString()
|
||||
.toLowerCase(Locale.ROOT).contains(query)).orElse(false)) continue;
|
||||
if (!page.isEmpty() && selectedCategory != null) {
|
||||
Identifier id = PageItem.content(page).symbol().map(symbol -> symbol.id()).orElse(null);
|
||||
if (menu.catalog().stream().noneMatch(metadata -> metadata.id().equals(id)
|
||||
&& metadata.category().equals(selectedCategory))) continue;
|
||||
}
|
||||
entries.add(new DisplayPage(collection, i, page));
|
||||
}
|
||||
}
|
||||
if (sortAlphabetically) entries.sort(Comparator
|
||||
.comparing((DisplayPage entry) -> entry.stack().getHoverName().getString(), String.CASE_INSENSITIVE_ORDER)
|
||||
.thenComparingInt(DisplayPage::collection).thenComparingInt(DisplayPage::index));
|
||||
return entries;
|
||||
}
|
||||
|
||||
@Override public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
int x = leftPos;
|
||||
int y = topPos;
|
||||
// Legacy GuiWritingDesk draws the entire 176x166 panel, not a stretched swatch.
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, LEGACY_DESK, x + 233, y + 20, 0, 0, 176, 166, 256, 256);
|
||||
drawTabControl(graphics, 20, 203, menu.firstVisibleTab() > 0);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int tab = menu.firstVisibleTab() + i;
|
||||
int tabY = y + 29 + i * 37;
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, LEGACY_DESK, x, tabY, 0, 166, 58, 37, 256, 256,
|
||||
tab == menu.activeCollection() && !allMode ? 0xFF8080FF : -1);
|
||||
// Legacy numbers its Writing Desk collections from zero.
|
||||
drawNumeral(graphics, tab, x + (tab < 25 ? 8 : 4), tabY + 3, 19);
|
||||
ItemStack collection = menu.getSlot(tab).getItem();
|
||||
if (!collection.isEmpty()) {
|
||||
String name = collection.getHoverName().getString();
|
||||
float scale = Math.min(1F, 50F / Math.max(1, font.width(name)));
|
||||
graphics.pose().pushMatrix();
|
||||
graphics.pose().translate(x + 4, tabY + 25);
|
||||
graphics.pose().scale(scale, scale);
|
||||
graphics.text(font, name, 0, 0, 0xFF404040, false);
|
||||
graphics.pose().popMatrix();
|
||||
}
|
||||
}
|
||||
drawTabControl(graphics, 177, 212, menu.firstVisibleTab() < WritingDeskBlockEntity.COLLECTIONS - 4);
|
||||
// GuiElementVSlider: 4-pixel caps, repeated 22-pixel track, 12x15 thumb.
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, SCROLLBAR, x + 213, y + 20, 0, 0, 20, 4, 32, 32);
|
||||
for (int offset = 4; offset < 162; offset += 22)
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, SCROLLBAR, x + 213, y + 20 + offset, 0, 4, 20, Math.min(22, 162 - offset), 32, 32);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, SCROLLBAR, x + 213, y + 182, 0, 26, 20, 4, 32, 32);
|
||||
int totalRows = (filteredPages().size() + 4) / 5;
|
||||
int maxScroll = Math.max(0, totalRows - 4);
|
||||
scrollRows = Math.clamp(scrollRows, 0, maxScroll);
|
||||
int thumbY = y + 24 + (maxScroll == 0 ? 0 : scrollRows * 143 / maxScroll);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, SCROLLBAR, x + 217, thumbY, 20, draggingScrollbar ? 15 : 0, 12, 15, 32, 32);
|
||||
graphics.fill(x + 365, y + 27, x + 381, y + 97, 0xFF303035);
|
||||
graphics.fill(x + 366, y + 28, x + 380, y + 96, 0xFF6666AA);
|
||||
int filled = 68 * menu.inkRemaining() / 16;
|
||||
if (filled > 0) graphics.fill(x + 366, y + 96 - filled, x + 380, y + 96, 0xFF15151B);
|
||||
}
|
||||
|
||||
private void drawTabControl(GuiGraphicsExtractor graphics, int y, int textureY, boolean enabled) {
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, LEGACY_DESK, leftPos, topPos + y, 0, textureY, 58, 9, 256, 256,
|
||||
enabled ? -1 : 0xFF666666);
|
||||
}
|
||||
|
||||
@Override protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
// Legacy leaves the inventory grid unlabeled.
|
||||
}
|
||||
|
||||
@Override public void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
super.extractContents(graphics, mouseX, mouseY, partialTick);
|
||||
if (sortAlphabetically) graphics.outline(leftPos + 58, topPos, 20, 18, 0xFFFFD061);
|
||||
if (allMode) graphics.outline(leftPos + 80, topPos, 28, 18, 0xFFFFD061);
|
||||
renderCollection(graphics);
|
||||
if (draggingPage && pageDragSource != null) {
|
||||
DisplayPage target = pageAtMouse(mouseX, mouseY);
|
||||
if (target != null && canMovePage(pageDragSource, target)) {
|
||||
int x = leftPos + 58 + ((int) mouseX - leftPos - 58) / 31 * 31;
|
||||
int y = topPos + 20 + ((int) mouseY - topPos - 20) / 41 * 41;
|
||||
graphics.outline(x, y, 30, 40, 0xFF57D478);
|
||||
}
|
||||
graphics.fakeItem(pageDragSource.stack(), mouseX - 8, mouseY - 8);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override protected void extractTooltip(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
super.extractTooltip(graphics, mouseX, mouseY);
|
||||
if (mouseX >= leftPos + 365 && mouseX < leftPos + 381
|
||||
&& mouseY >= topPos + 27 && mouseY < topPos + 97) {
|
||||
graphics.setTooltipForNextFrame(font, Component.translatable("screen.rivencraft.writing_desk.ink",
|
||||
menu.inkRemaining(), 16), mouseX, mouseY);
|
||||
}
|
||||
if (mouseX >= leftPos + 213 && mouseX < leftPos + 231
|
||||
&& mouseY >= topPos && mouseY < topPos + 18) {
|
||||
graphics.setTooltipForNextFrame(font, Component.translatable("screen.rivencraft.writing_desk.category",
|
||||
selectedCategory == null
|
||||
? Component.translatable("screen.rivencraft.writing_desk.all_categories")
|
||||
: Component.literal(selectedCategory.toString())), mouseX, mouseY);
|
||||
}
|
||||
if (mouseX >= leftPos && mouseX < leftPos + 58) {
|
||||
int localY = mouseY - topPos;
|
||||
if (localY >= 20 && localY < 29)
|
||||
graphics.setTooltipForNextFrame(font, Component.translatable("screen.rivencraft.writing_desk.previous_tabs"), mouseX, mouseY);
|
||||
else if (localY >= 177 && localY < 186)
|
||||
graphics.setTooltipForNextFrame(font, Component.translatable("screen.rivencraft.writing_desk.next_tabs"), mouseX, mouseY);
|
||||
else if (mouseX < leftPos + 34 && localY >= 29 && localY < 177) {
|
||||
int tab = menu.firstVisibleTab() + (localY - 29) / 37;
|
||||
graphics.setTooltipForNextFrame(font, Component.translatable("screen.rivencraft.writing_desk.tab_symbol",
|
||||
tab), mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<ItemStack> pages(int collectionIndex) {
|
||||
ItemStack collection = menu.getSlot(collectionIndex).getItem();
|
||||
if (collection.is(Rivencraft.PAGE.get())) return List.of(collection);
|
||||
if (CollationFolderItem.isCollection(collection)) {
|
||||
return collection.getOrDefault(Rivencraft.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private void renderCollection(GuiGraphicsExtractor graphics) {
|
||||
if (lastCollection != menu.activeCollection()) {
|
||||
lastCollection = menu.activeCollection();
|
||||
scrollRows = 0;
|
||||
selectedPage = 0;
|
||||
selectedCollection = -1;
|
||||
}
|
||||
List<DisplayPage> entries = filteredPages();
|
||||
for (int i = scrollRows * 5; i < entries.size() && i < scrollRows * 5 + 20; i++) {
|
||||
int column = i % 5;
|
||||
int row = i / 5 - scrollRows;
|
||||
DisplayPage entry = entries.get(i);
|
||||
ItemStack page = entry.stack();
|
||||
int x = leftPos + 58 + column * 31;
|
||||
int y = topPos + 20 + row * 41;
|
||||
drawPage(graphics, page, x, y);
|
||||
if (page.isEmpty()) graphics.fill(x, y, x + 30, y + 40, 0xB8000000);
|
||||
int active = selectedCollection >= 0 ? selectedCollection : menu.activeCollection();
|
||||
if (entry.collection() == active && entry.index() == selectedPage && !page.isEmpty())
|
||||
graphics.outline(x, y, 30, 40, 0xFFFFD061);
|
||||
}
|
||||
int active = selectedCollection >= 0 ? selectedCollection : menu.activeCollection();
|
||||
List<ItemStack> source = pages(active);
|
||||
if (selectedPage >= source.size() || source.get(selectedPage).isEmpty()) return;
|
||||
ItemStack selected = source.get(selectedPage);
|
||||
drawPage(graphics, selected, leftPos + 265, topPos + 26);
|
||||
graphics.fill(leftPos + 261, topPos + 81, leftPos + 360, topPos + 95, 0xFF202020);
|
||||
var symbol = PageItem.content(selected).symbol();
|
||||
if (symbol.isPresent() && PageItem.isCorrupted(symbol.orElseThrow())) {
|
||||
graphics.enableScissor(leftPos + 263, topPos + 82, leftPos + 358, topPos + 94);
|
||||
graphics.text(font, selected.getHoverName(), leftPos + 263, topPos + 84,
|
||||
0xFFE0E0E0, false);
|
||||
graphics.disableScissor();
|
||||
} else {
|
||||
String title = font.plainSubstrByWidth(selected.getHoverName().getString(), 95);
|
||||
graphics.text(font, title, leftPos + 263, topPos + 84, 0xFFE0E0E0, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawPage(GuiGraphicsExtractor graphics, ItemStack page, int x, int y) {
|
||||
graphics.nextStratum();
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, PAGE_ATLAS, x, y, 156, 0, 30, 40, 256, 256);
|
||||
if (page.isEmpty()) return;
|
||||
if (page.has(Rivencraft.SYMBOL_PAGE_ID.get())) {
|
||||
graphics.nextStratum();
|
||||
ItemStack glyph = page.copy();
|
||||
glyph.set(Rivencraft.PAGE_GLYPH_ONLY.get(), true);
|
||||
graphics.pose().pushMatrix();
|
||||
graphics.pose().translate(x + 1, y + 6);
|
||||
graphics.pose().scale(1.75F, 1.75F);
|
||||
graphics.fakeItem(glyph, 0, 0);
|
||||
graphics.pose().popMatrix();
|
||||
graphics.nextStratum();
|
||||
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.isPageImplemented(page.get(Rivencraft.SYMBOL_PAGE_ID.get())))
|
||||
graphics.fill(x, y, x + 30, y + 40, 0x55FF3333);
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawNumeral(GuiGraphicsExtractor graphics, int number, int x, int y, int size) {
|
||||
int[] digits = net.dnorberg.rivencraft.writing.PortSymbolNumerals.digitsFor(number);
|
||||
int digitSize = digits.length == 1 ? size : Math.max(8, size * 3 / 4);
|
||||
Identifier components = Identifier.fromNamespaceAndPath("rivencraft", "textures/gui/symbolcomponents.png");
|
||||
for (int digit : digits) {
|
||||
for (int component : net.dnorberg.rivencraft.writing.PortSymbolNumerals.componentsForDigit(digit))
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, components, x, y + (size - digitSize) / 2,
|
||||
(component % 8) * 64, (component / 8) * 64, digitSize, digitSize, 64, 64, 512, 512, 0xFF211A14);
|
||||
x += digitSize + 1;
|
||||
}
|
||||
}
|
||||
|
||||
private DisplayPage pageAtMouse(double mouseX, double mouseY) {
|
||||
int x = (int) Math.floor(mouseX - leftPos - 58);
|
||||
int y = (int) Math.floor(mouseY - topPos - 20);
|
||||
if (x < 0 || x >= 155 || y < 0 || y >= 164 || x % 31 >= 30 || y % 41 >= 40) return null;
|
||||
int index = (y / 41 + scrollRows) * 5 + x / 31;
|
||||
List<DisplayPage> entries = filteredPages();
|
||||
return index < entries.size() ? entries.get(index) : null;
|
||||
}
|
||||
|
||||
private boolean canMovePage(DisplayPage source, DisplayPage target) {
|
||||
return !source.stack().isEmpty()
|
||||
&& (CollationFolderItem.isCollection(menu.getSlot(source.collection()).getItem())
|
||||
|| menu.getSlot(source.collection()).getItem().is(Rivencraft.PAGE.get()))
|
||||
&& (CollationFolderItem.isCollection(menu.getSlot(target.collection()).getItem())
|
||||
|| menu.getSlot(target.collection()).getItem().is(Rivencraft.PAGE.get()))
|
||||
&& (source.collection() != target.collection() || source.index() != target.index());
|
||||
}
|
||||
|
||||
private int extractTargetSlot(double mouseX, double mouseY) {
|
||||
for (Slot slot : menu.slots) {
|
||||
int index = slot.index;
|
||||
if (index >= WritingDeskBlockEntity.COLLECTIONS && index <= WritingDeskMenu.COPY_RESULT) continue;
|
||||
if (!slot.isActive() || !isHovering(slot.x, slot.y, 16, 16, mouseX, mouseY)) continue;
|
||||
ItemStack held = pageDragSource.stack();
|
||||
if (!slot.mayPlace(held)) return -1;
|
||||
ItemStack current = slot.getItem();
|
||||
if (current.isEmpty()) return index;
|
||||
if (index < WritingDeskBlockEntity.COLLECTIONS && CollationFolderItem.isCollection(current)) {
|
||||
List<ItemStack> pages = current.getOrDefault(
|
||||
Rivencraft.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
|
||||
if (pages.size() < CollationFolderItem.capacity(current) || pages.stream().anyMatch(ItemStack::isEmpty))
|
||||
return index;
|
||||
} else if (index > WritingDeskMenu.COPY_RESULT && ItemStack.isSameItemSameComponents(current, held)
|
||||
&& current.getCount() < slot.getMaxStackSize(held)) return index;
|
||||
return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
double mouseX = event.x(), mouseY = event.y();
|
||||
int button = event.button();
|
||||
if (button == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
pageDragSource = null;
|
||||
draggingPage = false;
|
||||
}
|
||||
int surfaceX = (int) Math.floor(mouseX - leftPos - 58);
|
||||
int surfaceY = (int) Math.floor(mouseY - topPos - 20);
|
||||
if ((button == InputConstants.MOUSE_BUTTON_LEFT || button == InputConstants.MOUSE_BUTTON_RIGHT) && !menu.getCarried().isEmpty()
|
||||
&& surfaceX >= 0 && surfaceX < 155 && surfaceY >= 0 && surfaceY < 164) {
|
||||
if (menu.getCarried().is(Rivencraft.PAGE.get())
|
||||
&& PageItem.content(menu.getCarried()).kind() != PageContent.Kind.LINK_PANEL
|
||||
&& surfaceX % 31 < 30 && surfaceY % 41 < 40) {
|
||||
int index = (surfaceY / 41 + scrollRows) * 5 + surfaceX / 31;
|
||||
List<DisplayPage> entries = filteredPages();
|
||||
// Only displayed positions are addressable. Filtering never turns
|
||||
// empty screen space into additional storage for a loose-page tab.
|
||||
int collection = index < entries.size() ? entries.get(index).collection() : menu.activeCollection();
|
||||
int page = index < entries.size() ? entries.get(index).index() : index;
|
||||
if (index < entries.size() && page < menu.pageCapacity(collection)
|
||||
&& minecraft != null && minecraft.gameMode != null) {
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId,
|
||||
WritingDeskMenu.PLACE_PAGE_BUTTON_BASE + collection * NotebookContents.MAX_PAGES + page);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (button == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
if (mouseX >= leftPos && mouseX < leftPos + 58) {
|
||||
int y = (int) mouseY - topPos;
|
||||
if (y >= 20 && y < 29 || y >= 177 && y < 186) {
|
||||
if (minecraft != null && minecraft.gameMode != null)
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId,
|
||||
y < 29 ? WritingDeskMenu.PREVIOUS_TABS : WritingDeskMenu.NEXT_TABS);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (mouseX >= leftPos + 213 && mouseX < leftPos + 233
|
||||
&& mouseY >= topPos + 20 && mouseY < topPos + 186) {
|
||||
draggingScrollbar = true;
|
||||
setScrollFromMouse(mouseY);
|
||||
return true;
|
||||
}
|
||||
int x = (int) Math.floor(mouseX - leftPos - 58);
|
||||
int y = (int) Math.floor(mouseY - topPos - 20);
|
||||
if (x >= 0 && x < 155 && y >= 0 && y < 164) {
|
||||
DisplayPage entry = pageAtMouse(mouseX, mouseY);
|
||||
if (entry != null && !entry.stack().isEmpty()) {
|
||||
selectedCollection = entry.collection();
|
||||
selectedPage = entry.index();
|
||||
if (menu.getCarried().isEmpty()) {
|
||||
pageDragSource = entry;
|
||||
pageDragStartX = mouseX;
|
||||
pageDragStartY = mouseY;
|
||||
}
|
||||
if (minecraft != null && minecraft.gameMode != null) {
|
||||
int id = WritingDeskMenu.ALL_PAGE_BUTTON_BASE
|
||||
+ entry.collection() * NotebookContents.MAX_PAGES + entry.index();
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, id);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (button == InputConstants.MOUSE_BUTTON_LEFT && mouseX >= leftPos && mouseX < leftPos + 58) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int top = topPos + 29 + i * 37;
|
||||
boolean overSlot = mouseX >= leftPos + 36 && mouseX < leftPos + 54
|
||||
&& mouseY >= top + 4 && mouseY < top + 22;
|
||||
if (mouseY >= top && mouseY < top + 37 && !overSlot) {
|
||||
allMode = false;
|
||||
selectedCollection = -1;
|
||||
if (minecraft != null && minecraft.gameMode != null) {
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, menu.firstVisibleTab() + i);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
@Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
|
||||
if (mouseX >= leftPos + 58 && mouseX < leftPos + 233
|
||||
&& mouseY >= topPos + 20 && mouseY < topPos + 186) {
|
||||
int rows = (filteredPages().size() + 4) / 5;
|
||||
scrollRows = Math.clamp(scrollRows - (int) Math.signum(scrollY), 0, Math.max(0, rows - 4));
|
||||
return true;
|
||||
}
|
||||
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
|
||||
}
|
||||
|
||||
private void setScrollFromMouse(double mouseY) {
|
||||
int rows = (filteredPages().size() + 4) / 5;
|
||||
int max = Math.max(0, rows - 4);
|
||||
scrollRows = Math.clamp((int) Math.round((mouseY - topPos - 31.5) * max / 143.0), 0, max);
|
||||
}
|
||||
|
||||
@Override public boolean mouseDragged(MouseButtonEvent event, double dragX, double dragY) {
|
||||
double mouseX = event.x(), mouseY = event.y();
|
||||
int button = event.button();
|
||||
if (button == InputConstants.MOUSE_BUTTON_LEFT && pageDragSource != null) {
|
||||
if (Math.abs(mouseX - pageDragStartX) >= 4 || Math.abs(mouseY - pageDragStartY) >= 4)
|
||||
draggingPage = true;
|
||||
return true;
|
||||
}
|
||||
if (button == InputConstants.MOUSE_BUTTON_LEFT && draggingScrollbar) {
|
||||
setScrollFromMouse(mouseY);
|
||||
return true;
|
||||
}
|
||||
return super.mouseDragged(event, dragX, dragY);
|
||||
}
|
||||
|
||||
@Override public boolean mouseReleased(MouseButtonEvent event) {
|
||||
double mouseX = event.x(), mouseY = event.y();
|
||||
int button = event.button();
|
||||
if (button == InputConstants.MOUSE_BUTTON_LEFT && pageDragSource != null) {
|
||||
if (draggingPage) {
|
||||
DisplayPage target = pageAtMouse(mouseX, mouseY);
|
||||
if (target != null && canMovePage(pageDragSource, target)
|
||||
&& minecraft != null && minecraft.gameMode != null) {
|
||||
selectedCollection = target.collection();
|
||||
selectedPage = target.index();
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId,
|
||||
WritingDeskMenu.movePageButtonId(pageDragSource.collection(), pageDragSource.index(),
|
||||
target.collection(), target.index()));
|
||||
} else if (target == null && minecraft != null && minecraft.gameMode != null) {
|
||||
int targetSlot = extractTargetSlot(mouseX, mouseY);
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId,
|
||||
WritingDeskMenu.extractPageButtonId(pageDragSource.collection(), pageDragSource.index(),
|
||||
targetSlot));
|
||||
}
|
||||
}
|
||||
pageDragSource = null;
|
||||
draggingPage = false;
|
||||
return true;
|
||||
}
|
||||
if (button == InputConstants.MOUSE_BUTTON_LEFT && draggingScrollbar) {
|
||||
draggingScrollbar = false;
|
||||
return true;
|
||||
}
|
||||
return super.mouseReleased(event);
|
||||
}
|
||||
|
||||
@Override public boolean keyPressed(KeyEvent event) {
|
||||
if (event.isEscape()) return super.keyPressed(event);
|
||||
if (search != null && search.isFocused()) {
|
||||
search.keyPressed(event);
|
||||
return true;
|
||||
}
|
||||
if (minecraft != null && minecraft.gameMode != null) {
|
||||
if (event.key() == InputConstants.KEY_PAGEUP) {
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, WritingDeskMenu.PREVIOUS_TABS);
|
||||
return true;
|
||||
}
|
||||
if (event.key() == InputConstants.KEY_PAGEDOWN) {
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, WritingDeskMenu.NEXT_TABS);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package net.dnorberg.rivencraft.entity;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Recoverable open-book entity, with the 1.21.1 decay and weather behavior. */
|
||||
public final class DroppedBookEntity extends ItemEntity {
|
||||
private static final int SLOW_DECAY_INTERVAL = 10_000;
|
||||
private int decayTicks;
|
||||
|
||||
public DroppedBookEntity(EntityType<? extends DroppedBookEntity> type, Level level) { super(type, level); }
|
||||
|
||||
public DroppedBookEntity(Level level, Vec3 position, ItemStack book) {
|
||||
this(Rivencraft.DROPPED_BOOK.get(), level);
|
||||
setPos(position.x, position.y + 0.25, position.z);
|
||||
setDeltaMovement(Vec3.ZERO);
|
||||
setItem(book);
|
||||
setUnlimitedLifetime();
|
||||
setPickUpDelay(20);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
if (!(level() instanceof ServerLevel serverLevel) || isRemoved()) return;
|
||||
if (++decayTicks >= SLOW_DECAY_INTERVAL) {
|
||||
decayTicks = 0;
|
||||
hurtServer(serverLevel, damageSources().starve(), 1.0F);
|
||||
}
|
||||
if (!isRemoved() && isInWaterOrRain()) hurtServer(serverLevel, damageSources().drown(), 1.0F);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAdditionalSaveData(ValueOutput output) {
|
||||
super.addAdditionalSaveData(output);
|
||||
output.putInt("BookDecayTicks", decayTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readAdditionalSaveData(ValueInput input) {
|
||||
super.readAdditionalSaveData(input);
|
||||
decayTicks = Math.clamp(input.getIntOr("BookDecayTicks", 0), 0, SLOW_DECAY_INTERVAL - 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Legacy's compact page collection, using the existing Notebook page editor. */
|
||||
public final class CollationFolderItem extends NotebookItem {
|
||||
public static final int MAX_PAGES = 27;
|
||||
|
||||
public CollationFolderItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
public static boolean isCollection(ItemStack stack) {
|
||||
return stack.is(Rivencraft.NOTEBOOK.get()) || stack.is(Rivencraft.COLLATION_FOLDER.get())
|
||||
|| stack.is(Rivencraft.SYMBOL_PORTFOLIO.get());
|
||||
}
|
||||
|
||||
public static int capacity(ItemStack stack) {
|
||||
return stack.is(Rivencraft.COLLATION_FOLDER.get()) ? MAX_PAGES
|
||||
: stack.is(Rivencraft.NOTEBOOK.get()) || stack.is(Rivencraft.SYMBOL_PORTFOLIO.get())
|
||||
? NotebookContents.MAX_PAGES : 0;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.menu.NotebookMenu;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.SimpleMenuProvider;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** The 1.21.1 collection editor opened through a server-owned menu. */
|
||||
public class NotebookItem extends Item {
|
||||
public NotebookItem(Properties properties) { super(properties); }
|
||||
|
||||
@Override
|
||||
public InteractionResult use(Level level, Player player, InteractionHand hand) {
|
||||
ItemStack notebook = player.getItemInHand(hand);
|
||||
if (player instanceof ServerPlayer serverPlayer) {
|
||||
serverPlayer.openMenu(new SimpleMenuProvider(
|
||||
(id, inventory, ignored) -> new NotebookMenu(id, inventory, hand), notebook.getHoverName()),
|
||||
buffer -> buffer.writeBoolean(hand == InteractionHand.OFF_HAND));
|
||||
}
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
var contents = stack.getOrDefault(Rivencraft.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY);
|
||||
tooltip.accept(Component.translatable("item.rivencraft.notebook.pages", contents.pageCount(),
|
||||
CollationFolderItem.capacity(stack)).withStyle(ChatFormatting.GRAY));
|
||||
if (contents.unresolvedCount() > 0) tooltip.accept(Component.translatable(
|
||||
"item.rivencraft.notebook.unresolved", contents.unresolvedCount()).withStyle(ChatFormatting.RED));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -41,6 +42,22 @@ public final class PageItem extends Item {
|
||||
return stack;
|
||||
}
|
||||
|
||||
public static ItemStack createSymbolPage(net.dnorberg.rivencraft.writing.SymbolReference symbol) {
|
||||
return createSymbolPage(symbol.id());
|
||||
}
|
||||
|
||||
/** Resolved view of the port's existing components; no second saved schema. */
|
||||
public static net.dnorberg.rivencraft.writing.PageContent content(ItemStack stack) {
|
||||
var id = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
if (isLinkPanel(stack)) return net.dnorberg.rivencraft.writing.PageContent.LINK_PANEL;
|
||||
return id == null ? net.dnorberg.rivencraft.writing.PageContent.BLANK
|
||||
: net.dnorberg.rivencraft.writing.PageContent.symbol(
|
||||
new net.dnorberg.rivencraft.writing.SymbolReference(id)).resolve();
|
||||
}
|
||||
|
||||
public static boolean isUsableWriting(ItemStack stack) { return content(stack).isResolvedSymbol(); }
|
||||
public static boolean isCorrupted(net.dnorberg.rivencraft.writing.SymbolReference symbol) { return isCorrupted(symbol.id()); }
|
||||
|
||||
@Override
|
||||
public Component getName(ItemStack stack) {
|
||||
Component customName = stack.get(DataComponents.CUSTOM_NAME);
|
||||
@@ -72,8 +89,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);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import net.dnorberg.rivencraft.entity.DroppedBookEntity;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.Prediction;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Leaves only the active book behind after successful handheld travel. */
|
||||
public final class PortHandheldTravel {
|
||||
private PortHandheldTravel() {}
|
||||
|
||||
public static String travel(ServerPlayer player, InteractionHand hand, ItemStack book,
|
||||
Supplier<String> travel) {
|
||||
if (book.isEmpty() || player.getItemInHand(hand) != book) return "Book is no longer in the active hand";
|
||||
var source = player.level();
|
||||
var departure = player.position();
|
||||
player.setItemInHand(hand, ItemStack.EMPTY);
|
||||
boolean dropped = false;
|
||||
try {
|
||||
String failure = travel.get();
|
||||
if (failure != null) return failure;
|
||||
dropped = source.addFreshEntity(new DroppedBookEntity(source, departure, book));
|
||||
return null;
|
||||
} finally {
|
||||
// Rejected travel or a cancelled entity spawn must not consume the book.
|
||||
if (!dropped) {
|
||||
if (player.getItemInHand(hand).isEmpty()) player.setItemInHand(hand, book);
|
||||
else if (!player.getInventory().add(book)) player.drop(book, false, Prediction.SERVER_ONLY);
|
||||
}
|
||||
player.containerMenu.broadcastChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.Prediction;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** The 1.21.1 booster item, opened into a 16-page Collation Folder. */
|
||||
public final class SealedNotebookItem extends Item {
|
||||
public SealedNotebookItem(Properties properties) { super(properties); }
|
||||
|
||||
@Override
|
||||
public InteractionResult use(Level level, Player player, InteractionHand hand) {
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
if (!(player instanceof ServerPlayer serverPlayer)) return InteractionResult.PASS;
|
||||
ItemStack sealed = player.getItemInHand(hand);
|
||||
ItemStack folder = open(serverPlayer);
|
||||
if (folder.isEmpty()) return InteractionResult.FAIL;
|
||||
if (!player.hasInfiniteMaterials()) sealed.shrink(1);
|
||||
if (sealed.isEmpty()) player.setItemInHand(hand, folder);
|
||||
else if (!player.getInventory().add(folder)) player.drop(folder, false, Prediction.SERVER_ONLY);
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
public static ItemStack open(ServerPlayer player) {
|
||||
var candidates = PortSymbolPages.sealedCandidateIds();
|
||||
if (candidates.isEmpty()) return ItemStack.EMPTY;
|
||||
var pages = new ArrayList<ItemStack>(16);
|
||||
for (int i = 0; i < 16; i++) pages.add(PageItem.createSymbolPage(
|
||||
candidates.get(player.getRandom().nextInt(candidates.size()))));
|
||||
ItemStack folder = new ItemStack(Rivencraft.COLLATION_FOLDER.get());
|
||||
folder.set(Rivencraft.NOTEBOOK_CONTENTS.get(), new NotebookContents(pages));
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Non-craftable, prefilled Legacy-style collections of one symbol-page category. */
|
||||
public final class SymbolPortfolioItem extends NotebookItem {
|
||||
public SymbolPortfolioItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
public static List<ItemStack> createVolumes(Identifier category, List<ItemStack> pages) {
|
||||
if (pages.isEmpty()) return List.of();
|
||||
int capacity = NotebookContents.MAX_PAGES;
|
||||
int volumeCount = (pages.size() + capacity - 1) / capacity;
|
||||
List<ItemStack> volumes = new ArrayList<>(volumeCount);
|
||||
for (int volume = 0; volume < volumeCount; volume++) {
|
||||
ItemStack portfolio = new ItemStack(Rivencraft.SYMBOL_PORTFOLIO.get());
|
||||
int first = volume * capacity;
|
||||
int last = Math.min(first + capacity, pages.size());
|
||||
portfolio.set(Rivencraft.NOTEBOOK_CONTENTS.get(),
|
||||
new NotebookContents(pages.subList(first, last)));
|
||||
Component categoryName = Component.translatableWithFallback(
|
||||
"itemGroup.rivencraft.pages." + category.getPath(), category.getPath());
|
||||
portfolio.set(DataComponents.CUSTOM_NAME, volumeCount == 1
|
||||
? Component.translatable("item.rivencraft.symbol_portfolio.named", categoryName)
|
||||
: Component.translatable("item.rivencraft.symbol_portfolio.volume",
|
||||
categoryName, volume + 1, volumeCount));
|
||||
volumes.add(portfolio);
|
||||
}
|
||||
return List.copyOf(volumes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package net.dnorberg.rivencraft.menu;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.SimpleContainer;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
public final class NotebookMenu extends AbstractContainerMenu {
|
||||
public static final int NOTEBOOK_SLOTS = NotebookContents.MAX_PAGES;
|
||||
public static final int MOVE_PREVIOUS_BASE = 0;
|
||||
public static final int MOVE_NEXT_BASE = NOTEBOOK_SLOTS;
|
||||
private static final int PLAYER_INVENTORY_START = NOTEBOOK_SLOTS;
|
||||
private final Inventory playerInventory;
|
||||
private final InteractionHand hand;
|
||||
private final int openedHotbarSlot;
|
||||
private final NotebookContainer contents;
|
||||
private final int pageCapacity;
|
||||
|
||||
public NotebookMenu(int containerId, Inventory inventory, RegistryFriendlyByteBuf data) {
|
||||
this(containerId, inventory, data.readBoolean() ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND);
|
||||
}
|
||||
|
||||
public NotebookMenu(int containerId, Inventory inventory, InteractionHand hand) {
|
||||
super(Rivencraft.NOTEBOOK_MENU.get(), containerId);
|
||||
this.playerInventory = inventory;
|
||||
this.hand = hand;
|
||||
this.openedHotbarSlot = inventory.getSelectedSlot();
|
||||
this.pageCapacity = CollationFolderItem.capacity(inventory.player.getItemInHand(hand));
|
||||
this.contents = new NotebookContainer(inventory, hand);
|
||||
|
||||
for (int page = 0; page < NOTEBOOK_SLOTS; page++) {
|
||||
// The client draws these pages on a filtered, scrollable surface instead.
|
||||
addSlot(new PageSlot(contents, page, -1000, -1000, page < pageCapacity));
|
||||
}
|
||||
for (int row = 0; row < 3; row++) {
|
||||
for (int column = 0; column < 9; column++) {
|
||||
int inventoryIndex = column + row * 9 + 9;
|
||||
addSlot(playerSlot(inventoryIndex, 8 + column * 18, 135 + row * 18));
|
||||
}
|
||||
}
|
||||
for (int column = 0; column < 9; column++) {
|
||||
addSlot(playerSlot(column, 8 + column * 18, 193));
|
||||
}
|
||||
}
|
||||
|
||||
private Slot playerSlot(int inventoryIndex, int x, int y) {
|
||||
boolean locked = hand == InteractionHand.MAIN_HAND && inventoryIndex == openedHotbarSlot;
|
||||
return locked ? new LockedSlot(playerInventory, inventoryIndex, x, y)
|
||||
: new Slot(playerInventory, inventoryIndex, x, y);
|
||||
}
|
||||
|
||||
public int pageCapacity() { return pageCapacity; }
|
||||
|
||||
@Override
|
||||
public boolean stillValid(Player player) {
|
||||
return player.isAlive()
|
||||
&& (hand == InteractionHand.OFF_HAND || player.getInventory().getSelectedSlot() == openedHotbarSlot)
|
||||
&& CollationFolderItem.isCollection(player.getItemInHand(hand));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack quickMoveStack(Player player, int index) {
|
||||
Slot slot = slots.get(index);
|
||||
if (!slot.hasItem()) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
ItemStack source = slot.getItem();
|
||||
ItemStack original = source.copy();
|
||||
if (index < PLAYER_INVENTORY_START) {
|
||||
if (!moveItemStackTo(source, PLAYER_INVENTORY_START, slots.size(), true)) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
} else {
|
||||
if (!source.is(Rivencraft.PAGE.get())
|
||||
|| PageItem.isLinkPanel(source)
|
||||
|| !moveItemStackTo(source, 0, PLAYER_INVENTORY_START, false)) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
}
|
||||
if (source.isEmpty()) {
|
||||
slot.setByPlayer(ItemStack.EMPTY);
|
||||
} else {
|
||||
slot.setChanged();
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clickMenuButton(Player player, int id) {
|
||||
int source;
|
||||
int target;
|
||||
if (id >= MOVE_PREVIOUS_BASE && id < MOVE_NEXT_BASE) {
|
||||
source = id;
|
||||
target = source - 1;
|
||||
} else if (id >= MOVE_NEXT_BASE && id < MOVE_NEXT_BASE + NOTEBOOK_SLOTS) {
|
||||
source = id - MOVE_NEXT_BASE;
|
||||
target = source + 1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (source < 0 || source >= pageCapacity || target < 0 || target >= pageCapacity
|
||||
|| contents.getItem(source).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
contents.swap(source, target);
|
||||
broadcastChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clicked(int slotId, int button, ContainerInput clickType, Player player) {
|
||||
if (!stillValid(player)) {
|
||||
player.closeContainer();
|
||||
return;
|
||||
}
|
||||
if (clickType == ContainerInput.SWAP
|
||||
&& (button == openedHotbarSlot && hand == InteractionHand.MAIN_HAND
|
||||
|| button == 40 && hand == InteractionHand.OFF_HAND)) {
|
||||
return;
|
||||
}
|
||||
super.clicked(slotId, button, clickType, player);
|
||||
}
|
||||
|
||||
private static final class PageSlot extends Slot {
|
||||
private final boolean enabled;
|
||||
|
||||
private PageSlot(Container container, int slot, int x, int y, boolean enabled) {
|
||||
super(container, slot, x, y);
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@Override public boolean mayPlace(ItemStack stack) {
|
||||
return enabled && stack.is(Rivencraft.PAGE.get())
|
||||
&& !PageItem.isLinkPanel(stack);
|
||||
}
|
||||
@Override public boolean mayPickup(Player player) { return enabled; }
|
||||
@Override public int getMaxStackSize() { return 1; }
|
||||
}
|
||||
|
||||
private static final class LockedSlot extends Slot {
|
||||
private LockedSlot(Container container, int slot, int x, int y) {
|
||||
super(container, slot, x, y);
|
||||
}
|
||||
|
||||
@Override public boolean mayPickup(Player player) { return false; }
|
||||
@Override public boolean mayPlace(ItemStack stack) { return false; }
|
||||
}
|
||||
|
||||
private static final class NotebookContainer extends SimpleContainer {
|
||||
private final Inventory inventory;
|
||||
private final InteractionHand hand;
|
||||
private boolean loading;
|
||||
|
||||
private NotebookContainer(Inventory inventory, InteractionHand hand) {
|
||||
super(NOTEBOOK_SLOTS);
|
||||
this.inventory = inventory;
|
||||
this.hand = hand;
|
||||
ItemStack notebook = inventory.player.getItemInHand(hand);
|
||||
loading = true;
|
||||
List<ItemStack> saved = notebook.getOrDefault(
|
||||
Rivencraft.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
|
||||
for (int index = 0; index < saved.size(); index++) {
|
||||
super.setItem(index, saved.get(index).copy());
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
private void swap(int first, int second) {
|
||||
loading = true;
|
||||
ItemStack held = getItem(first);
|
||||
setItem(first, getItem(second));
|
||||
setItem(second, held);
|
||||
loading = false;
|
||||
setChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChanged() {
|
||||
super.setChanged();
|
||||
ItemStack notebook = inventory.player.getItemInHand(hand);
|
||||
if (loading || !CollationFolderItem.isCollection(notebook)) {
|
||||
return;
|
||||
}
|
||||
int capacity = CollationFolderItem.capacity(notebook);
|
||||
NonNullList<ItemStack> saved = NonNullList.withSize(capacity, ItemStack.EMPTY);
|
||||
for (int index = 0; index < capacity; index++) {
|
||||
ItemStack page = getItem(index);
|
||||
saved.set(index, page.isEmpty() ? ItemStack.EMPTY : page.copyWithCount(1));
|
||||
}
|
||||
notebook.set(Rivencraft.NOTEBOOK_CONTENTS.get(), new NotebookContents(saved));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,14 @@ public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
if (player.level().isClientSide()) return true;
|
||||
int index = id - STRIP_CLICK_BASE;
|
||||
ItemStack carried = getCarried();
|
||||
if (net.dnorberg.rivencraft.item.CollationFolderItem.isCollection(carried)) {
|
||||
ItemStack emptied = blockEntity.importCollection(carried,
|
||||
Math.min(index == 0 ? 0 : index - 1, blockEntity.nextPageIndex()));
|
||||
if (emptied.isEmpty()) return false;
|
||||
setCarried(emptied);
|
||||
broadcastChanges();
|
||||
return true;
|
||||
}
|
||||
if (!carried.isEmpty()) {
|
||||
if (index == 0) {
|
||||
if (!PageItem.isLinkPanel(carried)
|
||||
@@ -145,6 +153,12 @@ public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
} else if (stack.is(Items.LEATHER)) {
|
||||
moved = moveItemStackTo(stack, PortBookBinderBlockEntity.COVER,
|
||||
PortBookBinderBlockEntity.COVER + 1, false);
|
||||
} else if (net.dnorberg.rivencraft.item.CollationFolderItem.isCollection(stack)) {
|
||||
ItemStack emptied = blockEntity.importCollection(stack, blockEntity.nextPageIndex());
|
||||
if (emptied.isEmpty()) return ItemStack.EMPTY;
|
||||
slot.setByPlayer(emptied);
|
||||
broadcastChanges();
|
||||
return original;
|
||||
} else if (PageItem.isLinkPanel(stack)) {
|
||||
moved = moveItemStackTo(stack, PortBookBinderBlockEntity.PANEL,
|
||||
PortBookBinderBlockEntity.PANEL + 1, false);
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
package net.dnorberg.rivencraft.menu;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.dnorberg.rivencraft.writing.PageContent;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.writing.WritingDeskCatalog;
|
||||
import net.dnorberg.rivencraft.network.WritingDeskCatalogPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.neoforged.neoforge.network.PacketDistributor;
|
||||
import java.util.List;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.SimpleContainer;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.DataSlot;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
public final class WritingDeskMenu extends AbstractContainerMenu {
|
||||
public static final int COPY_RESULT = WritingDeskBlockEntity.SIZE;
|
||||
public static final int VISIBLE_TABS = 4;
|
||||
public static final int PREVIOUS_TABS = 90;
|
||||
public static final int NEXT_TABS = 91;
|
||||
public static final int PAGE_BUTTON_BASE = 128;
|
||||
public static final int CATALOG_BUTTON_BASE = 512;
|
||||
public static final int ALL_PAGE_BUTTON_BASE = 32768;
|
||||
public static final int PLACE_PAGE_BUTTON_BASE = 65536;
|
||||
private static final int PAGE_ADDRESS_COUNT = WritingDeskBlockEntity.COLLECTIONS * NotebookContents.MAX_PAGES;
|
||||
public static final int MOVE_PAGE_BUTTON_BASE = PLACE_PAGE_BUTTON_BASE + PAGE_ADDRESS_COUNT;
|
||||
private static final int PLAYER_START = COPY_RESULT + 1;
|
||||
private static final int EXTRACT_TARGET_COUNT = PLAYER_START + 36 + 1;
|
||||
public static final int EXTRACT_PAGE_BUTTON_BASE = MOVE_PAGE_BUTTON_BASE + PAGE_ADDRESS_COUNT * PAGE_ADDRESS_COUNT;
|
||||
private final Container desk;
|
||||
private final WritingDeskBlockEntity blockEntity;
|
||||
private final SimpleContainer result = new SimpleContainer(1);
|
||||
private final boolean clientSide;
|
||||
private int syncedActiveCollection;
|
||||
private int syncedInkRemaining;
|
||||
private int firstVisibleTab;
|
||||
private int selectedPage;
|
||||
private int selectedCollection = -1;
|
||||
private Identifier selectedSymbol;
|
||||
private List<SymbolDisplayMetadata> catalog = List.of();
|
||||
private final ServerPlayer serverViewer;
|
||||
|
||||
public WritingDeskMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) {
|
||||
this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos())
|
||||
instanceof WritingDeskBlockEntity found ? found : null);
|
||||
catalog = WritingDeskCatalog.read(data);
|
||||
}
|
||||
|
||||
public WritingDeskMenu(int id, Inventory inventory, WritingDeskBlockEntity blockEntity) {
|
||||
super(Rivencraft.WRITING_DESK_MENU.get(), id);
|
||||
this.blockEntity = blockEntity;
|
||||
this.desk = blockEntity == null ? new SimpleContainer(WritingDeskBlockEntity.SIZE) : blockEntity;
|
||||
this.clientSide = inventory.player.level().isClientSide();
|
||||
this.serverViewer = inventory.player instanceof ServerPlayer serverPlayer ? serverPlayer : null;
|
||||
if (blockEntity != null && !clientSide) catalog = WritingDeskCatalog.available(blockEntity);
|
||||
firstVisibleTab = blockEntity == null ? 0 : blockEntity.activeCollection() / VISIBLE_TABS * VISIBLE_TABS;
|
||||
addDataSlot(new DataSlot() {
|
||||
@Override public int get() { return firstVisibleTab; }
|
||||
@Override public void set(int value) { firstVisibleTab = value; }
|
||||
});
|
||||
addDataSlot(new DataSlot() {
|
||||
@Override public int get() { return WritingDeskMenu.this.blockEntity == null
|
||||
? syncedActiveCollection : WritingDeskMenu.this.blockEntity.activeCollection(); }
|
||||
@Override public void set(int value) { syncedActiveCollection = value; }
|
||||
});
|
||||
addDataSlot(new DataSlot() {
|
||||
@Override public int get() { return WritingDeskMenu.this.blockEntity == null
|
||||
? syncedInkRemaining : WritingDeskMenu.this.blockEntity.inkRemaining(); }
|
||||
@Override public void set(int value) { syncedInkRemaining = value; }
|
||||
});
|
||||
for (int i = 0; i < WritingDeskBlockEntity.COLLECTIONS; i++) {
|
||||
addSlot(new DeskSlot(desk, i, 37, 34 + (i % VISIBLE_TABS) * 37, 1));
|
||||
}
|
||||
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.INK, 385, 28, 16));
|
||||
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.RETURN, 385, 80, 64));
|
||||
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.PAPER, 241, 28, 64));
|
||||
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.LEGACY_OUTPUT, -1000, -1000, 64));
|
||||
addSlot(new CopySlot(result, 0, 241, 80));
|
||||
for (int row = 0; row < 3; row++) {
|
||||
for (int column = 0; column < 9; column++) {
|
||||
addSlot(new Slot(inventory, column + row * 9 + 9, 241 + column * 18, 104 + row * 18));
|
||||
}
|
||||
}
|
||||
for (int column = 0; column < 9; column++) {
|
||||
addSlot(new Slot(inventory, column, 241 + column * 18, 162));
|
||||
}
|
||||
}
|
||||
|
||||
public int activeCollection() { return syncedActiveCollection; }
|
||||
public int firstVisibleTab() { return firstVisibleTab; }
|
||||
public int inkRemaining() { return syncedInkRemaining; }
|
||||
public int pageCapacity(int collectionIndex) {
|
||||
if (collectionIndex < 0 || collectionIndex >= WritingDeskBlockEntity.COLLECTIONS) return 0;
|
||||
ItemStack collection = desk.getItem(collectionIndex);
|
||||
return CollationFolderItem.isCollection(collection) ? CollationFolderItem.capacity(collection)
|
||||
: collection.is(Rivencraft.PAGE.get()) ? 1 : 0;
|
||||
}
|
||||
public List<SymbolDisplayMetadata> catalog() { return catalog; }
|
||||
public void setCatalog(List<SymbolDisplayMetadata> symbols) { catalog = List.copyOf(symbols); }
|
||||
|
||||
public static int movePageButtonId(int sourceCollection, int sourcePage, int targetCollection, int targetPage) {
|
||||
int source = sourceCollection * NotebookContents.MAX_PAGES + sourcePage;
|
||||
int target = targetCollection * NotebookContents.MAX_PAGES + targetPage;
|
||||
return MOVE_PAGE_BUTTON_BASE + source * PAGE_ADDRESS_COUNT + target;
|
||||
}
|
||||
|
||||
/** targetSlot is -1 for the cursor, otherwise a visible player or desk-tab slot. */
|
||||
public static int extractPageButtonId(int sourceCollection, int sourcePage, int targetSlot) {
|
||||
int source = sourceCollection * NotebookContents.MAX_PAGES + sourcePage;
|
||||
return EXTRACT_PAGE_BUTTON_BASE + source * EXTRACT_TARGET_COUNT + targetSlot + 1;
|
||||
}
|
||||
|
||||
@Override public boolean clickMenuButton(Player player, int id) {
|
||||
if (blockEntity == null || !stillValid(player)) return false;
|
||||
if (id == PREVIOUS_TABS || id == NEXT_TABS) {
|
||||
firstVisibleTab = Math.clamp(firstVisibleTab + (id == NEXT_TABS ? VISIBLE_TABS : -VISIBLE_TABS),
|
||||
0, WritingDeskBlockEntity.COLLECTIONS - VISIBLE_TABS);
|
||||
return true;
|
||||
} else if (id >= 0 && id < WritingDeskBlockEntity.COLLECTIONS) {
|
||||
if (id < firstVisibleTab || id >= firstVisibleTab + VISIBLE_TABS) return false;
|
||||
blockEntity.setActiveCollection(id);
|
||||
selectedPage = 0;
|
||||
selectedCollection = -1;
|
||||
selectedSymbol = null;
|
||||
} else if (id >= EXTRACT_PAGE_BUTTON_BASE
|
||||
&& id < EXTRACT_PAGE_BUTTON_BASE + PAGE_ADDRESS_COUNT * EXTRACT_TARGET_COUNT) {
|
||||
int offset = id - EXTRACT_PAGE_BUTTON_BASE;
|
||||
int source = offset / EXTRACT_TARGET_COUNT;
|
||||
int targetSlot = offset % EXTRACT_TARGET_COUNT - 1;
|
||||
if (!extractPage(source / NotebookContents.MAX_PAGES, source % NotebookContents.MAX_PAGES,
|
||||
targetSlot)) return false;
|
||||
} else if (id >= MOVE_PAGE_BUTTON_BASE && id < MOVE_PAGE_BUTTON_BASE + PAGE_ADDRESS_COUNT * PAGE_ADDRESS_COUNT) {
|
||||
int offset = id - MOVE_PAGE_BUTTON_BASE;
|
||||
int source = offset / PAGE_ADDRESS_COUNT;
|
||||
int target = offset % PAGE_ADDRESS_COUNT;
|
||||
if (!movePage(source / NotebookContents.MAX_PAGES, source % NotebookContents.MAX_PAGES,
|
||||
target / NotebookContents.MAX_PAGES, target % NotebookContents.MAX_PAGES)) return false;
|
||||
} else if (id >= PLACE_PAGE_BUTTON_BASE
|
||||
&& id < PLACE_PAGE_BUTTON_BASE + WritingDeskBlockEntity.COLLECTIONS * NotebookContents.MAX_PAGES) {
|
||||
int offset = id - PLACE_PAGE_BUTTON_BASE;
|
||||
if (!placeCarriedPage(offset / NotebookContents.MAX_PAGES, offset % NotebookContents.MAX_PAGES)) return false;
|
||||
} else if (id >= PAGE_BUTTON_BASE && id < PAGE_BUTTON_BASE + NotebookContents.MAX_PAGES) {
|
||||
selectedPage = id - PAGE_BUTTON_BASE;
|
||||
selectedCollection = -1;
|
||||
selectedSymbol = null;
|
||||
} else if (id >= ALL_PAGE_BUTTON_BASE && id < ALL_PAGE_BUTTON_BASE + WritingDeskBlockEntity.COLLECTIONS * NotebookContents.MAX_PAGES) {
|
||||
int offset = id - ALL_PAGE_BUTTON_BASE;
|
||||
int collection = offset / NotebookContents.MAX_PAGES;
|
||||
int page = offset % NotebookContents.MAX_PAGES;
|
||||
if (pageAt(collection, page).isEmpty()) return false;
|
||||
selectedCollection = collection;
|
||||
selectedPage = page;
|
||||
selectedSymbol = null;
|
||||
} else if (id >= CATALOG_BUTTON_BASE && id < CATALOG_BUTTON_BASE + catalog.size()) {
|
||||
List<SymbolDisplayMetadata> available = WritingDeskCatalog.available(blockEntity);
|
||||
if (!catalog.equals(available)) {
|
||||
refreshCatalog(available);
|
||||
return false;
|
||||
}
|
||||
selectedSymbol = available.get(id - CATALOG_BUTTON_BASE).id();
|
||||
} else return false;
|
||||
refreshOutput();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean placeCarriedPage(int collectionIndex, int pageIndex) {
|
||||
// Resolve the actual storage position on the server, never a filtered list index.
|
||||
ItemStack carried = getCarried();
|
||||
if (clientSide || !storablePage(carried)) return false;
|
||||
ItemStack collection = desk.getItem(collectionIndex);
|
||||
if (CollationFolderItem.isCollection(collection)) {
|
||||
if (pageIndex < 0 || pageIndex >= CollationFolderItem.capacity(collection)) return false;
|
||||
var pages = new java.util.ArrayList<>(collection.getOrDefault(
|
||||
Rivencraft.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages());
|
||||
while (pages.size() <= pageIndex) pages.add(ItemStack.EMPTY);
|
||||
if (!pages.get(pageIndex).isEmpty()) return false;
|
||||
pages.set(pageIndex, carried.copyWithCount(1));
|
||||
ItemStack updated = collection.copy();
|
||||
updated.set(Rivencraft.NOTEBOOK_CONTENTS.get(), new NotebookContents(pages));
|
||||
desk.setItem(collectionIndex, updated);
|
||||
} else return false;
|
||||
carried.shrink(1);
|
||||
setCarried(carried.isEmpty() ? ItemStack.EMPTY : carried);
|
||||
selectedCollection = collectionIndex;
|
||||
selectedPage = pageIndex;
|
||||
selectedSymbol = null;
|
||||
refreshCatalog();
|
||||
broadcastChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean movePage(int sourceCollection, int sourcePage, int targetCollection, int targetPage) {
|
||||
if (!getCarried().isEmpty() || sourceCollection == targetCollection && sourcePage == targetPage) return false;
|
||||
ItemStack sourceBook = desk.getItem(sourceCollection);
|
||||
ItemStack targetBook = desk.getItem(targetCollection);
|
||||
boolean sourceNotebook = CollationFolderItem.isCollection(sourceBook);
|
||||
boolean targetNotebook = CollationFolderItem.isCollection(targetBook);
|
||||
if (sourceNotebook && sourcePage >= CollationFolderItem.capacity(sourceBook)
|
||||
|| targetNotebook && targetPage >= CollationFolderItem.capacity(targetBook)) return false;
|
||||
if ((!sourceNotebook && (!sourceBook.is(Rivencraft.PAGE.get()) || sourcePage != 0))
|
||||
|| (!targetNotebook && (!targetBook.is(Rivencraft.PAGE.get()) || targetPage != 0))) return false;
|
||||
var sourcePages = sourceNotebook ? new java.util.ArrayList<>(sourceBook.getOrDefault(
|
||||
Rivencraft.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()) : null;
|
||||
if (sourceNotebook && (sourcePage >= sourcePages.size() || sourcePages.get(sourcePage).isEmpty())) return false;
|
||||
ItemStack moving = sourceNotebook ? sourcePages.get(sourcePage) : sourceBook;
|
||||
if (moving.getCount() != 1) return false;
|
||||
if (!storablePage(moving)) return false;
|
||||
if (!targetNotebook && targetBook.getCount() != 1) return false;
|
||||
var targetPages = targetNotebook ? sourceCollection == targetCollection ? sourcePages
|
||||
: new java.util.ArrayList<>(targetBook.getOrDefault(
|
||||
Rivencraft.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()) : null;
|
||||
if (targetNotebook) while (targetPages.size() <= targetPage) targetPages.add(ItemStack.EMPTY);
|
||||
ItemStack displaced = targetNotebook ? targetPages.get(targetPage) : targetBook;
|
||||
if (!displaced.isEmpty() && !storablePage(displaced)) return false;
|
||||
if (targetNotebook) targetPages.set(targetPage, moving);
|
||||
if (sourceNotebook) {
|
||||
sourcePages.set(sourcePage, displaced);
|
||||
ItemStack updatedSource = sourceBook.copy();
|
||||
updatedSource.set(Rivencraft.NOTEBOOK_CONTENTS.get(), new NotebookContents(sourcePages));
|
||||
desk.setItem(sourceCollection, updatedSource);
|
||||
} else desk.setItem(sourceCollection, displaced.isEmpty() ? ItemStack.EMPTY : displaced.copyWithCount(1));
|
||||
if (targetNotebook && sourceCollection != targetCollection) {
|
||||
ItemStack updatedTarget = targetBook.copy();
|
||||
updatedTarget.set(Rivencraft.NOTEBOOK_CONTENTS.get(), new NotebookContents(targetPages));
|
||||
desk.setItem(targetCollection, updatedTarget);
|
||||
} else if (!targetNotebook) desk.setItem(targetCollection, moving.copyWithCount(1));
|
||||
selectedCollection = targetCollection;
|
||||
selectedPage = targetPage;
|
||||
selectedSymbol = null;
|
||||
refreshCatalog();
|
||||
broadcastChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean extractPage(int sourceCollection, int sourcePage, int targetSlot) {
|
||||
if (!getCarried().isEmpty() || targetSlot < -1 || targetSlot >= slots.size()) return false;
|
||||
ItemStack sourceBook = desk.getItem(sourceCollection);
|
||||
boolean sourceNotebook = CollationFolderItem.isCollection(sourceBook);
|
||||
if (sourceNotebook && sourcePage >= CollationFolderItem.capacity(sourceBook)) return false;
|
||||
if (!sourceNotebook && (!sourceBook.is(Rivencraft.PAGE.get()) || sourcePage != 0)) return false;
|
||||
var sourcePages = sourceNotebook ? new java.util.ArrayList<>(sourceBook.getOrDefault(
|
||||
Rivencraft.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()) : null;
|
||||
if (sourceNotebook && (sourcePage >= sourcePages.size() || sourcePages.get(sourcePage).isEmpty())) return false;
|
||||
ItemStack moving = sourceNotebook ? sourcePages.get(sourcePage).copyWithCount(1) : sourceBook.copyWithCount(1);
|
||||
if (!sourceNotebook && sourceBook.getCount() != 1) return false;
|
||||
|
||||
if (targetSlot == -1) {
|
||||
setCarried(moving);
|
||||
} else {
|
||||
if (targetSlot >= WritingDeskBlockEntity.COLLECTIONS && targetSlot < PLAYER_START) return false;
|
||||
if (targetSlot < WritingDeskBlockEntity.COLLECTIONS && !storablePage(moving)) return false;
|
||||
Slot target = slots.get(targetSlot);
|
||||
if (!target.isActive() || !target.mayPlace(moving)) return false;
|
||||
ItemStack current = target.getItem();
|
||||
if (targetSlot < WritingDeskBlockEntity.COLLECTIONS && CollationFolderItem.isCollection(current)) {
|
||||
var targetPages = targetSlot == sourceCollection ? sourcePages
|
||||
: new java.util.ArrayList<>(current.getOrDefault(
|
||||
Rivencraft.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages());
|
||||
int position = -1;
|
||||
for (int i = 0; i < CollationFolderItem.capacity(current); i++) {
|
||||
if (i >= targetPages.size() || targetPages.get(i).isEmpty()) {
|
||||
position = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (position < 0) return false;
|
||||
while (targetPages.size() <= position) targetPages.add(ItemStack.EMPTY);
|
||||
targetPages.set(position, moving);
|
||||
if (targetSlot != sourceCollection) {
|
||||
ItemStack updatedTarget = current.copy();
|
||||
updatedTarget.set(Rivencraft.NOTEBOOK_CONTENTS.get(), new NotebookContents(targetPages));
|
||||
target.setByPlayer(updatedTarget);
|
||||
}
|
||||
} else if (current.isEmpty()) {
|
||||
target.setByPlayer(moving);
|
||||
} else if (targetSlot >= PLAYER_START && ItemStack.isSameItemSameComponents(current, moving)
|
||||
&& current.getCount() < target.getMaxStackSize(moving)) {
|
||||
target.setByPlayer(current.copyWithCount(current.getCount() + 1));
|
||||
} else return false;
|
||||
}
|
||||
|
||||
if (sourceNotebook) {
|
||||
sourcePages.set(sourcePage, ItemStack.EMPTY);
|
||||
ItemStack updatedSource = sourceBook.copy();
|
||||
updatedSource.set(Rivencraft.NOTEBOOK_CONTENTS.get(), new NotebookContents(sourcePages));
|
||||
desk.setItem(sourceCollection, updatedSource);
|
||||
} else desk.setItem(sourceCollection, ItemStack.EMPTY);
|
||||
selectedCollection = sourceCollection;
|
||||
selectedPage = sourcePage;
|
||||
selectedSymbol = null;
|
||||
refreshCatalog();
|
||||
broadcastChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override public boolean stillValid(Player player) {
|
||||
return blockEntity != null && blockEntity.stillValid(player);
|
||||
}
|
||||
|
||||
@Override public ItemStack quickMoveStack(Player player, int index) {
|
||||
if (!stillValid(player) || index < 0 || index >= slots.size()) return ItemStack.EMPTY;
|
||||
if (index == COPY_RESULT) {
|
||||
refreshOutput();
|
||||
if (!canCopy()) return ItemStack.EMPTY;
|
||||
ItemStack copy = selectedSource().copyWithCount(1);
|
||||
ItemStack movingCopy = copy.copy();
|
||||
if (!moveItemStackTo(movingCopy, PLAYER_START, slots.size(), true) || !movingCopy.isEmpty()) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
if (!blockEntity.consumeCopyMaterials()) return ItemStack.EMPTY;
|
||||
refreshOutput();
|
||||
return copy;
|
||||
}
|
||||
Slot slot = slots.get(index);
|
||||
if (!slot.isActive()) return ItemStack.EMPTY;
|
||||
if (!slot.hasItem()) return ItemStack.EMPTY;
|
||||
ItemStack moving = slot.getItem();
|
||||
ItemStack original = moving.copy();
|
||||
if (index < COPY_RESULT) {
|
||||
if (!moveItemStackTo(moving, PLAYER_START, slots.size(), true)) return ItemStack.EMPTY;
|
||||
} else if (!moveIntoDesk(moving)) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
if (moving.isEmpty()) slot.setByPlayer(ItemStack.EMPTY);
|
||||
else slot.setChanged();
|
||||
return original;
|
||||
}
|
||||
|
||||
private boolean moveIntoDesk(ItemStack moving) {
|
||||
if (moving.is(Rivencraft.INK_VIAL.get())) return moveItemStackTo(moving,
|
||||
WritingDeskBlockEntity.INK, WritingDeskBlockEntity.INK + 1, false);
|
||||
if (CollationFolderItem.isCollection(moving)) return moveItemStackTo(moving, firstVisibleTab, firstVisibleTab + VISIBLE_TABS, false);
|
||||
if (moving.is(Items.PAPER))
|
||||
return moveItemStackTo(moving, WritingDeskBlockEntity.PAPER,
|
||||
WritingDeskBlockEntity.PAPER + 1, false);
|
||||
if (storablePage(moving)) return moveItemStackTo(moving, firstVisibleTab, firstVisibleTab + VISIBLE_TABS, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean storablePage(ItemStack stack) {
|
||||
return stack.is(Rivencraft.PAGE.get())
|
||||
&& PageItem.content(stack).kind() != PageContent.Kind.LINK_PANEL;
|
||||
}
|
||||
|
||||
private ItemStack selectedSource() {
|
||||
if (selectedSymbol != null) {
|
||||
SymbolReference reference = new SymbolReference(selectedSymbol);
|
||||
return PortSymbolPages.find(reference.id()) != null
|
||||
&& blockEntity != null
|
||||
&& WritingDeskCatalog.available(blockEntity).stream()
|
||||
.anyMatch(entry -> entry.id().equals(selectedSymbol))
|
||||
? PageItem.createSymbolPage(reference) : ItemStack.EMPTY;
|
||||
}
|
||||
int collectionIndex = selectedCollection >= 0 ? selectedCollection
|
||||
: blockEntity == null ? syncedActiveCollection : blockEntity.activeCollection();
|
||||
return pageAt(collectionIndex, selectedPage);
|
||||
}
|
||||
|
||||
private ItemStack pageAt(int collectionIndex, int pageIndex) {
|
||||
if (collectionIndex < 0 || collectionIndex >= WritingDeskBlockEntity.COLLECTIONS
|
||||
|| pageIndex < 0 || pageIndex >= NotebookContents.MAX_PAGES) return ItemStack.EMPTY;
|
||||
ItemStack collection = desk.getItem(collectionIndex);
|
||||
if (collection.is(Rivencraft.PAGE.get())) return pageIndex == 0 ? collection : ItemStack.EMPTY;
|
||||
if (CollationFolderItem.isCollection(collection)) {
|
||||
if (pageIndex >= CollationFolderItem.capacity(collection)) return ItemStack.EMPTY;
|
||||
List<ItemStack> pages = collection.getOrDefault(
|
||||
Rivencraft.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
|
||||
return pageIndex < pages.size() ? pages.get(pageIndex) : ItemStack.EMPTY;
|
||||
}
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
private boolean canCopy() {
|
||||
ItemStack source = selectedSource();
|
||||
return blockEntity != null && source.is(Rivencraft.PAGE.get())
|
||||
&& PageItem.content(source).kind() != PageContent.Kind.BLANK
|
||||
&& PageItem.content(source).kind() != PageContent.Kind.LINK_PANEL
|
||||
&& PageItem.isUsableWriting(source) && blockEntity.canConsumeCopyMaterials();
|
||||
}
|
||||
|
||||
private void refreshOutput() {
|
||||
if (clientSide) return;
|
||||
if (blockEntity != null) blockEntity.setDisplayPage(selectedSource());
|
||||
ItemStack preview = canCopy() ? selectedSource().copyWithCount(1) : ItemStack.EMPTY;
|
||||
if (!ItemStack.matches(result.getItem(0), preview)) result.setItem(0, preview);
|
||||
}
|
||||
|
||||
private void refreshCatalog() {
|
||||
if (clientSide || blockEntity == null) return;
|
||||
refreshCatalog(WritingDeskCatalog.available(blockEntity));
|
||||
}
|
||||
|
||||
private void refreshCatalog(List<SymbolDisplayMetadata> current) {
|
||||
if (clientSide || blockEntity == null) return;
|
||||
if (!current.equals(catalog)) {
|
||||
catalog = current;
|
||||
if (serverViewer != null && serverViewer.containerMenu == this) {
|
||||
PacketDistributor.sendToPlayer(serverViewer, new WritingDeskCatalogPayload(containerId, catalog));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void broadcastChanges() {
|
||||
refreshCatalog();
|
||||
refreshOutput();
|
||||
super.broadcastChanges();
|
||||
}
|
||||
|
||||
@Override public void clicked(int slotId, int button, net.minecraft.world.inventory.ContainerInput clickType,
|
||||
Player player) {
|
||||
if (!stillValid(player)) {
|
||||
player.closeContainer();
|
||||
return;
|
||||
}
|
||||
refreshCatalog();
|
||||
refreshOutput();
|
||||
if (slotId >= 0 && slotId < slots.size() && !slots.get(slotId).isActive()) return;
|
||||
super.clicked(slotId, button, clickType, player);
|
||||
}
|
||||
|
||||
private final class CopySlot extends Slot {
|
||||
private CopySlot(Container container, int index, int x, int y) { super(container, index, x, y); }
|
||||
@Override public boolean mayPlace(ItemStack stack) { return false; }
|
||||
@Override public boolean mayPickup(Player player) {
|
||||
return clientSide ? hasItem() : WritingDeskMenu.this.stillValid(player) && canCopy();
|
||||
}
|
||||
@Override public void onTake(Player player, ItemStack stack) {
|
||||
if (!clientSide && blockEntity.consumeCopyMaterials()) {
|
||||
refreshOutput();
|
||||
}
|
||||
super.onTake(player, stack);
|
||||
}
|
||||
}
|
||||
|
||||
private final class DeskSlot extends Slot {
|
||||
private final int limit;
|
||||
private DeskSlot(Container container, int index, int x, int y, int limit) {
|
||||
super(container, index, x, y);
|
||||
this.limit = limit;
|
||||
}
|
||||
@Override public boolean isActive() {
|
||||
int slot = getContainerSlot();
|
||||
return slot >= WritingDeskBlockEntity.COLLECTIONS
|
||||
|| slot >= firstVisibleTab && slot < firstVisibleTab + VISIBLE_TABS;
|
||||
}
|
||||
@Override public boolean mayPickup(Player player) { return isActive(); }
|
||||
@Override public boolean mayPlace(ItemStack stack) { return isActive() && container.canPlaceItem(getContainerSlot(), stack); }
|
||||
@Override public int getMaxStackSize() { return limit; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
|
||||
import net.dnorberg.rivencraft.age.PortLighting;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.lighting.LevelLightEngine;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
/** Ambient light contributes to effective brightness, not fake block/sky propagation. */
|
||||
@Mixin(LevelLightEngine.class)
|
||||
public abstract class AgeAmbientLightMixin {
|
||||
@Shadow @Final protected LevelHeightAccessor levelHeightAccessor;
|
||||
|
||||
@ModifyReturnValue(method = "getRawBrightness", at = @At("RETURN"))
|
||||
private int rivencraft$ambientBrightness(int brightness) {
|
||||
return levelHeightAccessor instanceof Level level && PortLighting.isBright(level)
|
||||
? Math.max(brightness, 15) : brightness;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import net.dnorberg.rivencraft.age.PortAgeColors;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributeSystem;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(EnvironmentAttributeSystem.Builder.class)
|
||||
public class AgeColorAttributesMixin {
|
||||
@Inject(method = "addDefaultLayers", at = @At("RETURN"))
|
||||
private void rivencraft$colors(Level level, CallbackInfoReturnable<EnvironmentAttributeSystem.Builder> cir) {
|
||||
var id = level.dimension().identifier();
|
||||
if (!id.getNamespace().equals("rivencraft") || !id.getPath().startsWith("age/")) return;
|
||||
var builder = cir.getReturnValue();
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.SKY_COLOR, (base, tick) -> {
|
||||
int rgb = PortAgeColors.forLevel(level).sky();
|
||||
return rgb < 0 ? base : PortAgeColors.vector(rgb);
|
||||
});
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.FOG_COLOR, (base, tick) -> {
|
||||
int rgb = PortAgeColors.forLevel(level).sky();
|
||||
var color = rgb < 0 ? base : PortAgeColors.vector(rgb);
|
||||
float shade = net.dnorberg.rivencraft.age.PortSkyStyle.fogShade(level);
|
||||
return shade == 1F ? color : new org.joml.Vector3f(color).mul(shade);
|
||||
});
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.AMBIENT_LIGHT_COLOR, (base, tick) -> {
|
||||
int rgb = PortAgeColors.forLevel(level).ambient();
|
||||
return rgb < 0 ? base : PortAgeColors.vector(rgb);
|
||||
});
|
||||
}
|
||||
}
|
||||
Loaded 100 of 138 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user