Public Access
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35980338e1 | ||
|
|
be8440ddb5 | ||
|
|
f13bedd26f |
No files matched your search
@@ -17,10 +17,13 @@ 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
|
||||
materials now follow their native rules. Books now have Legacy-style spreads,
|
||||
animated fallback panels and saved destination labels; lectern stands, open-book
|
||||
drops, page collections, stations and basic progression are also ported.
|
||||
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.93-beta.1.md).
|
||||
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.115-beta.1.md).
|
||||
Keep the builds and their saves separate, and back up worlds before updating.
|
||||
|
||||
## Getting started
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 162 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -10,6 +10,77 @@ tracks issues #97–#103 under parent #96.
|
||||
|
||||
## Version boundary
|
||||
|
||||
Local slice `1.0.1.106-beta.1` ports the Writing Desk's two-part block,
|
||||
88-tab inventory, authoritative catalog sync, copying transactions and 1.21.1
|
||||
GUI/model assets. Vial exchange reuses the existing shared helper; symbol copy
|
||||
cost defaults to one. Modern Container removal handles content drops while
|
||||
the block cleans up its partner. The renderer uses extracted item states.
|
||||
Current input helpers also correct Notebook right-click/scrollbar behavior.
|
||||
Page content is a derived view of the existing port components; no saved Page
|
||||
schema is replaced. Progression awards and optional JEI integration remain
|
||||
pending. Client appearance/input/catalog acceptance remains required.
|
||||
|
||||
Local slice `1.0.1.105-beta.1` ports category Symbol Portfolios and Sealed
|
||||
Notebooks, reusing item textures/models and collection menu/import behavior.
|
||||
Portfolio volumes split at 252 pages. Sealed items use 1.21.1's 16-page draw
|
||||
and candidate exclusions, with Survival consumption, Creative retention and
|
||||
full-inventory drop fallback. No recipes are invented; discovery/library loot
|
||||
and JEI integration remain later work. Unported effects are still marked red.
|
||||
|
||||
Local slice `1.0.1.104-beta.1` ports 1.21.1's all-or-nothing collection import
|
||||
to the current Binder menu: shift-click appends and cursor strip-click inserts
|
||||
before a page. Occupied page order is preserved, gaps are compacted and the
|
||||
empty collection is returned. Capacity/panel conflicts are checked before any
|
||||
mutation. This connects the newly ported collections to actual book assembly;
|
||||
the dedicated stored-collection input/UI remains later parity work.
|
||||
|
||||
Local follow-up `1.0.1.103-beta.1` corrects old GLFW key literals retained in
|
||||
the Notebook screen. Escape always reaches the normal container close path,
|
||||
including focused search, and page-movement arrows use 26.3 key helpers.
|
||||
The player reported the search-focused GUI could not be escaped in .102;
|
||||
client keyboard acceptance must be repeated with .103.
|
||||
|
||||
Local slice `1.0.1.102-beta.1` ports Notebook/Collation Folder storage, menu,
|
||||
Creative entries, Notebook crafting and the existing sparse page editor.
|
||||
The 1.21.1 capacities (252/27), Link Panel insertion rejection, held-item
|
||||
locking and search keyboard handling are preserved. The GUI uses 26.3 input
|
||||
events/extraction and the existing glyph-only item pipeline, while retaining
|
||||
the original page atlas proportions. Discovery, sealed notebooks, portfolios,
|
||||
Writing Desk and bulk Binder collection use remain unfinished.
|
||||
|
||||
Local slice `1.0.1.101-beta.1` ports dropped-book entities, their open-book
|
||||
renderer/textures, decay/weather damage and persistent components. Handheld
|
||||
travel detaches only the active book and leaves it at departure after success;
|
||||
failed travel or a cancelled entity spawn restores it. Shared handling covers
|
||||
both book types and hands and keeps Return Link issuance after departure-book
|
||||
handling. The newer render-state-based pickup path replaces the old copy()
|
||||
workaround. Client rendering/pickup remain part of in-game validation.
|
||||
|
||||
Local slice `1.0.1.100-beta.1` ports automatic Return Link policy from 1.21.1
|
||||
for handheld Descriptive Books, default DISABLED. ON_CREATION uses a persistent
|
||||
world-wide per-Age marker; ALWAYS issues on each successful arrival. Exact
|
||||
departure position/facing is captured before teleportation and issuance occurs
|
||||
after Disarm, with inventory-full fallback to a destination item drop. Ordinary
|
||||
Linking Books, failures and operator travel do not issue books. Stand/portal
|
||||
integration remains pending with those systems.
|
||||
|
||||
Local slice `1.0.1.99-beta.1` ports the dedicated non-collectible arrival
|
||||
platform block from 1.21.1, reusing its opaque crystal model and strength 0.3.
|
||||
Fallback landings use it instead of collectible Crystal Blocks. No item,
|
||||
loot, pick-block result, or material Page is available. Existing platforms
|
||||
and the port's landing-search/footprint policy are unchanged.
|
||||
|
||||
Local slice `1.0.1.98-beta.1` ports the 1.21.1 Accelerated environment page:
|
||||
stacking extra random block ticks in already-loaded player-nearby chunks,
|
||||
eight-chunk/16,384-attempt default budgets, fair rotating section allocation,
|
||||
and an Age-local saved page count. The 26.3 LOCAL config holds these limits.
|
||||
Entities, machines and scheduled ticks are unaffected. Existing Ages remain
|
||||
unaccelerated; random Age inclusion and instability scoring are not added.
|
||||
The suspected host-seed biome zoom mismatch was checked against NeoForge's
|
||||
patched ServerLevel constructor: seedOverride already replaces it correctly,
|
||||
so the backend seed path was left unchanged. Native Cave policy issue #111
|
||||
remains separate future work for both Minecraft tracks.
|
||||
|
||||
Minecraft 26.3/NeoForge uses Java 25, while Rivencraft 1.21.1 uses Java 21.
|
||||
Build and publish separate artifacts; do not widen the Minecraft dependency
|
||||
range of the 1.21.1 JAR. The official [26.3 release notes](https://www.minecraft.net/en-us/article/minecraft-java-edition-26-3),
|
||||
@@ -1801,6 +1872,144 @@ fresh chunks after full restart again used Nylium/End Stone. No full feature
|
||||
or entity chunk scans were run. The server was stopped and temporary world
|
||||
deleted. Nothing was committed, pushed or published.
|
||||
|
||||
## Structure-aware biome completion
|
||||
|
||||
`1.0.1.94-beta.1` adapts root `AgeBiomeSelection.completeStructureBiomes` for
|
||||
explicit non-Native biome controllers. It reads compatible biome IDs from
|
||||
the active registered structure sets and shares this lookup with random
|
||||
structure eligibility, avoiding two different compatibility definitions.
|
||||
Repeated structure pages produce one requirement; Dungeons are excluded.
|
||||
Single with no supplied biome chooses from the intersection of all structure
|
||||
requirements and terrain-eligible biomes. No intersection leaves ordinary
|
||||
seeded filling intact. An explicit Single biome is never rewritten.
|
||||
|
||||
Multi-biome completion adds one seeded compatible biome for each unmet
|
||||
requirement where a terrain-eligible candidate exists, before ordinary minimum
|
||||
filling. Root's salts, requirement order and shuffled candidate selection are
|
||||
preserved. Existing biome order and duplicate weights remain, but a structure
|
||||
can intentionally extend a pool beyond the usual minimum. Native and the
|
||||
port's no-controller fallback remain unchanged. Impossible requirements do
|
||||
not become new writing rejections: authored structure overrides still apply.
|
||||
No saved format, network protocol or generated chunks are changed.
|
||||
|
||||
The completed-biome fixture accepts an optional structure page and checks
|
||||
32 seeds for compatible Single/Cave Fortress choices, common Village/Stronghold
|
||||
requirements, explicit End Single precedence, Native behavior, multi-biome
|
||||
prefix preservation and avoiding unnecessary additions for already-compatible
|
||||
or repeated structure pages. Existing biome plan codec checks remain in place.
|
||||
|
||||
The .94 build and disposable server checks passed. Village Single and Tiny
|
||||
fixtures included compatible Plains; Cave Fortress Single selected Crimson
|
||||
Forest. The 32-seed authoring assertions and the preexisting no-structure
|
||||
completion checks passed. Saved generator pools survived unload/reload,
|
||||
terrain reseed and full restart. The server was stopped and its temporary
|
||||
world deleted. No structure-placement chunk scan was needed. Client book
|
||||
authoring remains a manual check; no commit, push or publication occurred.
|
||||
|
||||
## Omitted biome-controller completion
|
||||
|
||||
`1.0.1.95-beta.1` ports the no-controller branch of root `AgeBiomeSelection`
|
||||
and the first `AgeTraits.select` draw. The rank table and seed stream are
|
||||
shared with `PortRandomAgeSelection` rather than duplicated. The production
|
||||
seeded biome compiler uses that default when there is no explicit controller;
|
||||
the registry-only diagnostic overload retains its exact older defaults.
|
||||
Explicit controllers are unchanged. Native rolls with authored biomes, or
|
||||
Single rolls with more than one biome page, fall back by authored count to
|
||||
Single/Tiled/Medium. Other multi-biome rolls retain their distribution and
|
||||
ordinary minimum-only filling, plus .94's structure requirements.
|
||||
|
||||
Void's omitted controller remains Single for empty pools and count-based
|
||||
for supplied biome pages. Cave's omitted Native roll follows the port's
|
||||
existing page-free seeded Single workaround; a native multi-cave source is
|
||||
still not implemented. Explicit Native is not silently converted. No other
|
||||
terrain, weather, lighting, color or body seed stream changes. Saved plans
|
||||
remain frozen through restart and terrain reseed; old Ages are not rewritten.
|
||||
|
||||
The completed-biome diagnostic accepts `auto` and checks 128 seeds for all
|
||||
nine distribution results, independence from unrelated weather pages,
|
||||
authored prefix preservation, one/two-biome fallbacks, Void behavior and
|
||||
structure-aware completion. Existing explicit-controller checks still run.
|
||||
|
||||
The .95 build and disposable-server checks passed. The expanded 128-seed
|
||||
matrix covered all distributions, authored one/two-biome rules and Cave pools.
|
||||
Created automatic Standard, Cave, Void and Cave Fortress fixtures, plus the
|
||||
unchanged full-random command and explicit Single/Native Cave controls.
|
||||
Generator pools survived unload/reload, reseed and full restart. A second seed
|
||||
produced Small regions with three biomes, while explicit Native Cave retained
|
||||
its Lush Caves source. No chunk-generation scan was needed. The server was
|
||||
stopped and its temporary world deleted. Client authoring remains a manual
|
||||
check; nothing was committed, pushed or published.
|
||||
|
||||
## Omitted terrain completion
|
||||
|
||||
`1.0.1.96-beta.1` adapts root `AgeRuntimeProfiles.selectForWriting`. A partly
|
||||
authored book without terrain starts with the shared ranked terrain draw;
|
||||
if unsupported, compatible profiles in stable ID order are indexed using
|
||||
`floorMod(Long.hashCode(seed), count)`. Explicit terrain is never substituted.
|
||||
Unlike root's feature-only candidate test, the port additionally preflights
|
||||
registry-backed biome restrictions to avoid selecting an unloadable Cave
|
||||
combination for supplied surface biomes. This is a deliberate compatibility
|
||||
guard while the broader writing/instability recovery remains unported.
|
||||
|
||||
Binder availability checks now consider whether any supported terrain can
|
||||
host an omitted-terrain book instead of assuming Standard. The server resolves
|
||||
the actual terrain and biome plan once, then passes the resolved internal page
|
||||
view to material/feature compilers. The inferred terrain is appended internally,
|
||||
not inserted before original pages or written back to the book. Original
|
||||
indices and ordered modifiers remain intact. Environment controllers use the
|
||||
selected terrain, while sky colors and celestials still compile from original
|
||||
pages. Page-free random books retain their original path and seed streams.
|
||||
The chosen terrain and generator plans use existing saved fields, so no
|
||||
network protocol change or retroactive terrain reroll is introduced.
|
||||
|
||||
The fixture checks 64 seeds for terrain coverage, deterministic selection,
|
||||
original page prefixes, Cave-only Fortress support, implied-fluid constraints,
|
||||
surface-biome compatibility, explicit terrain precedence and Binder
|
||||
availability. Generator placement still uses the previously ported routines.
|
||||
|
||||
The .96 build and disposable server checks passed. The 64-seed assertions
|
||||
covered all six terrains, deterministic fallback, page-prefix preservation,
|
||||
explicit Flat, surface-biome protection and Cave-only Fortress availability.
|
||||
Created Sky-only, Fortress-only, Water-only, Plains-only, explicit Flat and
|
||||
full-random fixtures. A Water page retained Water as its implied lake fluid;
|
||||
Fortress-only selected Cave with eligible biomes. Plans survived unload/reload,
|
||||
reseed and full restart. The server was stopped and its temporary world
|
||||
deleted. No chunk scan was needed; client Binder checks remain manual.
|
||||
Nothing was committed, pushed or published.
|
||||
|
||||
## Native Cave regions
|
||||
|
||||
`1.0.1.97-beta.1` ports root `AgeBiomeSelection`'s full Native Cave pool and
|
||||
`AgeDimensionService.createBiomeSource`'s Native-to-Tiny mapping. New Native
|
||||
Cave plans retain distribution NATIVE with a saved recognized Cave/Nether
|
||||
list, and the backend uses `PortScaledBiomeSource` at scale 0 (or Fixed for
|
||||
a one-biome pool). Explicit Native biome pages remain prohibited. Both authored
|
||||
and page-free/omitted-controller paths use the same eligibility query; the
|
||||
temporary Native-to-Single Cave conversions are removed.
|
||||
|
||||
Native pool data is permitted only for Cave terrain at backend validation.
|
||||
Other Native terrain sources are unchanged. Empty older Native Cave records
|
||||
still load the old Lush source; old Single choices also remain frozen. New
|
||||
Native pools are saved through the existing biome-plan codec without a new
|
||||
network protocol, but earlier betas cannot decode populated Native pools, so
|
||||
downgrading those saves is not supported. The current pool is passed to random
|
||||
structure eligibility rather than incorrectly assuming Lush Caves.
|
||||
|
||||
The fixture verifies registry-derived pool ordering, old empty Native codec
|
||||
defaults, the actual generator source, deterministic Tiny-region equivalence,
|
||||
horizontal region variation and sampled pool membership without loading
|
||||
chunks. The omitted-controller matrix now expects Native Cave to remain
|
||||
Native with its full pool. This does not implement modded surface rules.
|
||||
|
||||
The .97 build and disposable-server checks passed: explicit and omitted
|
||||
Native Cave, the full-random Native Cave path (seed 114), Native + Fortresses,
|
||||
explicit Single and the legacy Lush-only control. The vanilla registry pool
|
||||
contained nine biomes, including Sulfur Caves and all five Nether biomes;
|
||||
the actual Native source passed Tiny-equivalence and sample-membership checks.
|
||||
Pools survived unload/reload, reseed and full restart. The server was stopped
|
||||
and its temporary world deleted. No chunk scan was needed. Client exploration
|
||||
remains a manual check; nothing was committed, pushed or published.
|
||||
|
||||
## Verification of this first slice
|
||||
|
||||
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
|
||||
@@ -1812,3 +2021,151 @@ passed again after the 26.3 registry slice.
|
||||
On 2026-11-01, the default 1.21.1 GameTest server was rerun and all 93
|
||||
required tests passed, including the desk test. Issue #97 was then closed;
|
||||
remaining 26.3 parity work stays under #98–#104.
|
||||
|
||||
## Progression advancement port
|
||||
|
||||
`1.0.1.107-beta.1` reuses seven root progression JSON files and existing
|
||||
translations. The Open Portal node remains excluded because its Receptacle icon
|
||||
and action are not yet ported. Added server-owned post-commit hooks for Mixer
|
||||
normal/shift output, Desk normal/shift copies, Binder assembly and successful
|
||||
handheld travel whose destination is a saved runtime Age. Inventory and root
|
||||
criteria remain vanilla. Existing saves retain vanilla player advancement data;
|
||||
no action history is reconstructed and no protocol/schema change is required.
|
||||
|
||||
The build and disposable-server fixtures passed loaded data, preview/empty Mixer
|
||||
rejection, both Panel pickup paths, actual Desk copy and Binder assembly awards,
|
||||
and registered-Age entry eligibility. NeoForge FakePlayer uses a no-op advancement
|
||||
tracker; the fixture player overrides it with vanilla PlayerAdvancements rather
|
||||
than changing production awards to accommodate a test double. No client joined;
|
||||
GUI tree/toasts, inventory triggers, actual travel and reconnect persistence are
|
||||
the user validation. The test server was stopped and its world removed.
|
||||
|
||||
## Recipe discovery and advancement background
|
||||
|
||||
`1.0.1.109-beta.1` uses the existing Crystal Block texture for the root
|
||||
advancement background via the modern asset ID. The .108 stone-path correction
|
||||
passed the player's in-game test. Added adapted 1.21.1 discovery advancements
|
||||
for Ink Vial, Ink Mixer, Book Binder and Crystal Block, retaining all original
|
||||
materials, OR requirements and recipe rewards. The 26.3 recipe-unlocked trigger
|
||||
requires `conditions.recipes`, a list. Root 1.21.1 data remains unchanged.
|
||||
|
||||
The build and disposable-server `recipe_discovery_probe` passed loaded
|
||||
recipe/advancement availability, wrong-material negatives, actual matching-material
|
||||
inventory triggers, per-player isolation and direct recipe grants for all six
|
||||
crafting unlocks (including Notebook and Writing Desk). No client joined this
|
||||
fixture. The test server was stopped and its world removed; crystal background
|
||||
and client recipe-book presentation remain manual checks.
|
||||
|
||||
## Lectern book stands
|
||||
|
||||
`1.0.1.110-beta.1` adapts the root stand block/entity/conversion and final
|
||||
renderer transforms to 26.3 extraction/submission. It uses the shared lectern
|
||||
blockstate and lectern-drop loot table; no new art or stand item. Storage sync
|
||||
and persistence use ValueInput/ValueOutput. preRemoveSideEffects receives the
|
||||
old state and handles one stored-item drop; emptying before conversion back to
|
||||
vanilla prevents duplicate recovery.
|
||||
|
||||
The existing book screens accept a stand source. A separate validated packet
|
||||
checks dimension, eight-block distance, loaded chunk, stand identity and exact
|
||||
book components before travel. Descriptive travel now shares the same helper
|
||||
for handheld/stand routes; only handheld books are detached/dropped. Linking
|
||||
travel uses the existing item service. Stand travel retains effects, Return Link
|
||||
configuration and Age-entry awards. Live views and exploration remain unported;
|
||||
unbound books require removal for handheld binding in this slice.
|
||||
|
||||
Build and isolated server checks passed all facing/hand/book/page combinations,
|
||||
occupied/unrelated-item rejection, ownership on retrieval/break, stale/distant/
|
||||
wrong-dimension requests, serialization and full restart persistence. The test
|
||||
server was stopped and world removed. GUI/rendering/actual stand travel and
|
||||
modifier effects require the player's in-game check. Root 1.21.1 is unchanged.
|
||||
|
||||
## Descriptive Book authored-page GUI
|
||||
|
||||
`1.0.1.111-beta.1` adapts the root Legacy cover/front/full-right-page composition
|
||||
and page-index rules to GuiGraphicsExtractor. Glyphs use the existing glyph-only
|
||||
special item model in a later stratum at 128×128, avoiding the dynamic-blit and
|
||||
nested item-outline issues previously fixed in the Binder. Navigation uses SDL
|
||||
mouse constants and current KeyEvent helpers, corner hotspots and page-turn sound.
|
||||
The destination packet remains confined to the front spread; stand source
|
||||
validation is unchanged. Naming widgets stay hidden off the front spread even
|
||||
after resize. Screens do not pause the world.
|
||||
|
||||
Added the root `book_author` component with persistent string and network codec.
|
||||
Binder and handheld descriptive binding assign the authenticated player's profile
|
||||
name; the client never supplies author text. Existing books without it use the
|
||||
unknown-author translation. Existing book_symbols retain written order, duplicates
|
||||
and omitted-controller choices exactly; no Age data or generation behavior changes.
|
||||
Random-Age glyph descriptions/live destination rendering remain unported.
|
||||
|
||||
Build and disposable server checks passed real assembly, authorship, first/last
|
||||
symbol mapping, repeated-symbol and legacy-empty fallbacks, navigation bounds and
|
||||
full-restart book author/page persistence. Shared full-page texture was verified
|
||||
in the JAR. The temporary server was stopped and world removed. Client visuals,
|
||||
turning/input/sound, lectern opening and panel-click travel remain manual checks.
|
||||
|
||||
## Animated link-panel fallback
|
||||
|
||||
`1.0.1.112-beta.1` reuses the root LegacyDestinationPreviewRenderer spiral/noise
|
||||
algorithm in a shared 26.3 GUI extractor helper, removing placeholder labels from
|
||||
both book panels. Shared dimensions/hit-testing retain the existing 132×83 panel
|
||||
and validated hand/stand travel routes. GUI strata enforce cover -> animation ->
|
||||
Legacy frame ordering. Descriptive symbol pages do not expose an active panel.
|
||||
No world/network changes, dependencies, remote audio or chunk loads are introduced.
|
||||
Default teal is retained until destination metadata/tint is ported; this is a
|
||||
fallback, not live destination rendering. Build passed; client visual/click,
|
||||
lectern, scale and page-turn checks remain manual. No server run was required.
|
||||
|
||||
## Linking Book spreads and artwork consolidation
|
||||
|
||||
`1.0.1.113-beta.1` adapts LinkingBookScreen's two spreads, counter, wrapped
|
||||
left-page details, Return Link title suppression and input/page-turn behavior.
|
||||
The front retains validated hand/stand travel and the animated fallback; the
|
||||
final spread has no active destination panel. Vanilla dimension names translate;
|
||||
Age display-name metadata remains pending and other dimensions fall back to IDs.
|
||||
Block coordinates use floor-based BlockPos conversion, including negative values.
|
||||
|
||||
Both screens use PortLegacyBookArtwork adapted directly from the root helper,
|
||||
with shared text/counter/corner routines. The descriptive screen retains its
|
||||
accepted glyph/author behavior. No networking, persistence or server changes.
|
||||
Build and diff checks passed; GUI presentation, controls, long names, lecterns
|
||||
and descriptive-page regression are manual checks. No server was started.
|
||||
|
||||
## Saved destination names
|
||||
|
||||
`1.0.1.114-beta.1` adds optional server-owned Age display metadata and the root
|
||||
persistent/networked `link_destination_name` item component. Successful book-driven
|
||||
Age creation saves the entered title; Linking Book binding snapshots its source
|
||||
Age's title, and automatic Return Links snapshot the departure label. Item tooltip
|
||||
and GUI use the same component/vanilla/ID fallback. Negative tooltip coordinates
|
||||
now floor correctly rather than truncating toward zero.
|
||||
|
||||
The index name map defaults empty for old saves and does not participate in seed,
|
||||
folder or dimension identity selection. Names are visible only for active IDs;
|
||||
retirement retains metadata for identity/rollback without exposing it as an active
|
||||
Age. No catalog packet change, remote chunk load, automatic old-label migration
|
||||
or terrain change is introduced.
|
||||
|
||||
Build and isolated server checks passed actual Binder name assignment, metadata
|
||||
with case/punctuation, named Linking/automatic Return Link components, legacy
|
||||
vanilla fallback, unload/reload and full-restart index/book persistence. Temporary
|
||||
config restored; server stopped and world removed. Client GUI/tooltip/lectern
|
||||
presentation remains manual. New names require a newly created Age and newly
|
||||
bound book; old Ages retain their existing fallback label.
|
||||
|
||||
## Ordinary dropped books
|
||||
|
||||
`1.0.1.115-beta.1` implements the 26.3 half of #114. A server-only queued join
|
||||
conversion creates the custom entity before discarding an ordinary book drop,
|
||||
preserving toss state and item data through native restoreFrom. It gives the
|
||||
replacement a new UUID (the original is still registered), adopts open-book
|
||||
lifetime/decay rules and skips already-custom entities. Canceled replacements
|
||||
preserve the ordinary source. No force-loading, world scanning or per-tick retry.
|
||||
Loaded ordinary book entities are also eligible.
|
||||
|
||||
Build and isolated source/state checks passed, including cancellation recovery.
|
||||
The no-player fixture required one loaded entity chunk for inspection. The user
|
||||
requested to perform further checks; no additional automated restart test was run.
|
||||
Server stopped and temporary world removed. Manual checks: Q/inventory toss,
|
||||
death/Disarm, broken stand, full-inventory delivery/retrieval, open-book visual
|
||||
orientation, motion, ownership/pickup, component preservation and restart. Root
|
||||
1.21.1 remains unchanged and #114 stays open until both tracks are complete.
|
||||
+582
-3
@@ -2,14 +2,15 @@
|
||||
|
||||
This is an **experimental beta port** for issues #97–#104, not gameplay parity
|
||||
with the Minecraft 1.21.1 release. The 26.3 backend persists separate Ages and
|
||||
their seeds. Books, the Book Binder, and the Ink Mixer work, and the Binder can
|
||||
their seeds. Books, lectern book stands, the Book Binder, Ink Mixer, Writing Desk and page collections
|
||||
are ported, and the Binder can
|
||||
author Standard, Amplified, Island, Flat, Void, or Cave terrain with supported
|
||||
biome-distribution pages, selected material/vegetation/structure features, and
|
||||
independent Age weather and lighting, multiple authored or seeded celestial
|
||||
bodies and eclipses, biome/material completion, and random features/structures.
|
||||
Instability, crystal portals, discovery/stations, writing recovery and optional
|
||||
Instability, crystal portals, remaining discovery/stations, writing recovery and optional
|
||||
integration parity remain unfinished. The latest published beta is
|
||||
[1.0.1.93-beta.1](RELEASE_NOTES_1.0.1.93-beta.1.md).
|
||||
[1.0.1.115-beta.1](RELEASE_NOTES_1.0.1.115-beta.1.md).
|
||||
Use a test world and keep a backup; do not
|
||||
substitute this JAR for the 1.21.1 mod or import 1.21.1 saves.
|
||||
|
||||
@@ -1291,6 +1292,388 @@ and `age material_probe <name> [chunk_x chunk_z]`. The latter examines one
|
||||
terrain-stage chunk (default 0,0) without feature, lighting or entity generation. Nothing is committed
|
||||
or published without explicit request.
|
||||
|
||||
The local `1.0.1.94-beta.1` slice ports **structure-aware completion of explicit
|
||||
biome distributions** from 1.21.1. Single Biome without a supplied biome now
|
||||
prefers a biome compatible with every authored structure page, when a common
|
||||
candidate exists. Cave + Single + Nether Fortresses therefore selects an
|
||||
eligible Cave/Nether biome rather than an unrelated cave biome. If there is
|
||||
no common candidate, ordinary seeded filling and authored structure overrides
|
||||
remain available, as in root.
|
||||
|
||||
For multi-biome controllers, each uncovered structure requirement can add one
|
||||
compatible biome from the terrain's available pool. This is an intentional
|
||||
exception to ordinary minimum-only filling: an already-full pool may gain
|
||||
another biome for its structure pages. Supplied biomes, their order and
|
||||
duplicate weights remain intact. A supplied Single Biome is never replaced,
|
||||
Native remains unchanged, and Dungeons impose no biome requirement. The
|
||||
no-controller fallback remains unchanged in this slice.
|
||||
|
||||
Test Single Biome + Villages without biome pages, Cave + Single Biome + Nether
|
||||
Fortresses, and a multi-biome book with structures. Compare an explicit End
|
||||
Single Biome + Villages to confirm that authored override still works.
|
||||
`age create_completed_biomes <name> <seed> <terrain> <distribution> [structure-page-id]`
|
||||
now accepts an optional structure fixture. Existing Ages are not rerolled;
|
||||
restart/reseed retains completed pools. Nothing is committed or published
|
||||
without explicit request.
|
||||
|
||||
The local `1.0.1.95-beta.1` slice seed-completes **omitted biome controllers**
|
||||
in partly authored books, using 1.21.1's first AgeTraits distribution draw.
|
||||
Native, Single and Medium are common; the other supported distributions are
|
||||
rarer. Explicit controllers still win. Supplied biomes are never discarded:
|
||||
a Native roll with authored biomes falls back by count, as does a Single roll
|
||||
with multiple biome pages. Other seeded multi-biome controllers are retained,
|
||||
so a lone supplied biome may receive a second biome to meet the minimum.
|
||||
|
||||
Void without biome pages still uses The Void; supplied Void biome pages keep
|
||||
their existing count-based behavior. An omitted controller that rolls Native
|
||||
on Cave terrain uses the same seeded Single fallback already used by random
|
||||
Ages, since the port has no native multi-cave source. Explicit Native Cave
|
||||
behavior remains unchanged. Structure-aware completion from .94 applies to
|
||||
the resulting nonnative controller. Full Link-Panel-only random generation
|
||||
retains its existing selector and seeds.
|
||||
|
||||
Test new terrain-only books without biome controllers, one/two supplied biome
|
||||
pages without a controller, and Cave + Nether Fortresses without a biome
|
||||
controller. Compare an explicit Single Biome book and restart/revisit.
|
||||
`age create_completed_biomes <name> <seed> <terrain> auto [structure-page-id]`
|
||||
provides an omitted-controller fixture. Existing Ages are not rerolled.
|
||||
Nothing is committed or published without explicit request.
|
||||
|
||||
The local `1.0.1.96-beta.1` slice seed-completes **omitted terrain pages** in
|
||||
partly authored books. It starts with the same ranked terrain draw as random
|
||||
Ages, following 1.21.1's `selectForWriting` model. If that terrain cannot host
|
||||
the supplied features/materials, a deterministic fallback chooses a compatible
|
||||
terrain in stable profile order. The port also preflights its current biome
|
||||
restrictions, avoiding an automatic Cave choice for supplied surface biomes.
|
||||
|
||||
Explicit terrain still wins and incompatible explicit combinations remain
|
||||
rejected. Missing-terrain Fortress books can now bind using Cave terrain;
|
||||
unpaired fluids use supported surface noise terrain. No extra random features
|
||||
are added to partly authored books. The original book page list stays intact;
|
||||
the chosen terrain is appended only to internal generation input so material
|
||||
and color/controller indices do not shift. Sky/color/celestial compilation
|
||||
still reads the original pages. Fully random Link-Panel-only Ages retain their
|
||||
existing selector. Already-created Ages do not change terrain.
|
||||
|
||||
Test several new books with Sky or a feature but no terrain page, Nether
|
||||
Fortresses alone, and an unpaired Water page. Compare an explicit Flat book
|
||||
and a Plains biome page without terrain; the latter must not choose Cave.
|
||||
`age create_auto_terrain <name> <seed> <page-id>` exercises the production
|
||||
selector for one-page fixtures. Restart and revisit to confirm persistence.
|
||||
Nothing is committed or published without explicit request.
|
||||
|
||||
The local `1.0.1.97-beta.1` slice ports **Native Cave biome distribution** from
|
||||
1.21.1. A new Cave + Native book now freezes the full recognized Cave/Nether
|
||||
biome pool and distributes it with the existing seeded Tiny-region sampler,
|
||||
instead of forcing Lush Caves. An omitted controller or random Cave Age that
|
||||
rolls Native now uses that same pool rather than converting to Single.
|
||||
Random structures inspect the saved pool, so eligible Nether structures are
|
||||
no longer hidden behind an assumed Lush-only source.
|
||||
|
||||
Existing Single Cave Ages remain Single. Old empty Native Cave records keep
|
||||
their Lush fallback; they are not silently expanded on load. Other Native
|
||||
terrains remain unchanged, and explicit Native still rejects biome pages.
|
||||
Pools persist across restart/reseed; reseeding can still change future region
|
||||
layout. Modded biome surface-rule integration remains separate pending work.
|
||||
Back up before updating and do not downgrade worlds containing new Native
|
||||
Cave pools to earlier beta builds.
|
||||
|
||||
Test a **new** Cave + Native Biomes book without individual biome pages,
|
||||
explore multiple regions, then restart/revisit. Compare an explicit Single
|
||||
Cave book and an existing old Cave Age. The existing
|
||||
`age create_completed_biomes <name> <seed> cave native` fixture also checks
|
||||
the native source against Tiny's sampler without generating chunks.
|
||||
Nothing is committed or published without explicit request.
|
||||
|
||||
### Accelerated random block ticks
|
||||
|
||||
The local `1.0.1.98-beta.1` slice ports the **Accelerated** environment page
|
||||
from 1.21.1. Repeated pages stack: each adds three extra random positions per
|
||||
eligible block section per tick. Only already-loaded chunks within one chunk
|
||||
of players are candidates; no extra chunks are loaded. Defaults match 1.21.1:
|
||||
at most eight candidate chunks and 16,384 attempted positions per Age tick,
|
||||
shared across sections with rotating selection. These limits live in
|
||||
`config/rivencraft-local.toml` under `accelerated_age`; setting
|
||||
`max_random_tick_attempts_per_tick` to zero disables the extra ticks.
|
||||
|
||||
This affects random-ticking blocks such as crops and saplings, not entities,
|
||||
machines, scheduled block ticks, or the day/night clock. The authored page
|
||||
count persists with the Age across unload/reload, reseeding and restart.
|
||||
Existing Ages default to zero; pageless random Ages do not gain Accelerated
|
||||
automatically. Instability costs remain part of the pending instability port.
|
||||
|
||||
Test two otherwise comparable new Ages, one with a single Accelerated page
|
||||
and one with repeated pages. Plant crops/saplings with suitable light and
|
||||
space, remain nearby, and compare growth. Revisit after restart. The operator
|
||||
command `/rivencraft_port age acceleration <name>` reports the saved count
|
||||
and current budgets. Disposable-server fixture
|
||||
`age create_accelerated <name> <seed> <pages>` checks authoring and bounded
|
||||
attempt arithmetic and creates a Flat Age; it does not prove live growth.
|
||||
Build and disposable dedicated-server checks passed for counts 0, 1 and 32,
|
||||
overflow/disabled budget arithmetic, unload/reload, reseed and full restart.
|
||||
No player was connected for the server checks, so live growth remains an
|
||||
in-game acceptance check. The test server was stopped and its world removed.
|
||||
|
||||
### Non-collectible arrival platforms
|
||||
|
||||
The local `1.0.1.99-beta.1` slice ports 1.21.1's dedicated `cloud_platform`
|
||||
block for fallback Age landings. It reuses the existing opaque Crystal Block
|
||||
texture, has glass-like breaking strength (0.3), and produces no loot or
|
||||
pick-block item. It has no item, recipe, Creative entry or generated material
|
||||
Page. Ordinary Crystal Blocks retain their normal behavior.
|
||||
|
||||
New fallback platforms use this block. Existing platforms made from ordinary
|
||||
Crystal Blocks are not rewritten. This slice changes the fallback material;
|
||||
the port's existing landing search and 3-by-3 fallback footprint remain.
|
||||
|
||||
Test a new Void Age: arrive on the platform, check its appearance and footing,
|
||||
then break a block in Survival and confirm there is no drop. Creative
|
||||
pick-block must not supply an item. Compare a normal Crystal Block, and revisit
|
||||
the platform after restart. Operator fixtures
|
||||
`/rivencraft_port age create_platform <name> <seed>` and
|
||||
`/rivencraft_port age platform_probe <name>` validate a disposable Void
|
||||
platform's properties and saved blocks.
|
||||
The build and disposable dedicated-server checks passed for actual Void
|
||||
arrival generation, collision/opacity, strength, loot, pick-block exclusion,
|
||||
material-page exclusion, unload/reload and full restart. Shared model assets
|
||||
were verified in the JAR. The test server was stopped and its world removed;
|
||||
client appearance and Survival breaking remain the in-game check.
|
||||
|
||||
### Configurable automatic Return Links
|
||||
|
||||
The local `1.0.1.100-beta.1` slice ports the 1.21.1 automatic Return Link
|
||||
policy for successful handheld Descriptive Book arrivals. In
|
||||
`config/rivencraft-local.toml`, set `[descriptive_books] return_link_mode` to
|
||||
`DISABLED` (the default), `ON_CREATION`, or `ALWAYS`. ON_CREATION issues one
|
||||
Return Link when the Age is first reached with that mode enabled, shared
|
||||
across players in the world. ALWAYS issues one on each successful arrival.
|
||||
Disabled/Always arrivals do not consume the ON_CREATION marker, matching
|
||||
1.21.1. The marker persists across reload, reseeding and restart.
|
||||
|
||||
Return Links retain the exact departure dimension, position, yaw and pitch.
|
||||
They use ordinary Linking Book rules and have no automatic Link Panel
|
||||
modifiers. They are issued after Disarm and dropped at the destination if
|
||||
inventory space is unavailable. Failed travel, binding a book, ordinary
|
||||
Linking Book travel and operator teleport probes do not issue Return Links.
|
||||
Stands/portals remain pending and will need the same successful-travel hook.
|
||||
|
||||
For the in-game check, confirm the default gives no Return Link. Then enable
|
||||
ON_CREATION and visit a fresh Age twice: only the first arrival should grant
|
||||
one. Use that book to return and check position/facing; restart and revisit
|
||||
to confirm no duplicate. In ALWAYS, each arrival should grant a book. Test
|
||||
a Disarm Descriptive Book and a full inventory as well. Restore DISABLED
|
||||
after testing if automatic Return Links are unwanted.
|
||||
The build and disposable-server fixtures passed mode decisions, book target
|
||||
components, visit-record codec round trips, world persistence through
|
||||
unload/reload, reseeding and restart, and config loading with DISABLED and
|
||||
ON_CREATION. No player joined those fixtures; actual travel, Disarm and
|
||||
inventory-full delivery remain the in-game acceptance check. The server was
|
||||
stopped and its test world removed.
|
||||
|
||||
### Handheld books left at departure
|
||||
|
||||
The local `1.0.1.101-beta.1` slice ports the 1.21.1 dropped-book entity and
|
||||
handheld travel ownership. Successful handheld Linking and Descriptive Book
|
||||
travel removes the active book from its hand and leaves it at departure as
|
||||
an open-book entity. The existing Linking/Descriptive entity textures and
|
||||
skyward-facing model orientation are reused with 26.3's render-state API.
|
||||
The other hand and ordinary inventory remain unchanged unless Disarm applies.
|
||||
|
||||
Failed travel restores the book. A rejected dropped-entity spawn also restores
|
||||
it instead of consuming it. Return Links are issued after the active book is
|
||||
left behind. Dropped books have a one-second pickup delay, no ordinary five-
|
||||
minute expiry, slow damage every 10,000 loaded ticks, and water/rain damage,
|
||||
matching 1.21.1. Book components and the decay counter persist with the entity.
|
||||
26.3 pickup particles use the extracted entity render state; 1.21.1's custom
|
||||
`ItemEntity.copy()` workaround is not needed on this target.
|
||||
|
||||
Test travel from both hands with each book type, return to recover the open
|
||||
book, verify pages face upward, and check pickup animation and restart.
|
||||
Attempt a blocked/invalid link and confirm the book stays in hand. With
|
||||
Disarm, the active book should use its model while other items drop normally.
|
||||
With automatic Return Links enabled, the new Return Link should arrive with
|
||||
the player. Test rain/water damage with disposable books only.
|
||||
|
||||
Build and disposable dedicated-server checks passed both hands, failed travel,
|
||||
exception recovery, cancelled entity spawning, successful hand removal, saved
|
||||
book components/lifetime, and a full restart with both book entities restored.
|
||||
The fixture uses a fake player and does not prove client rendering or actual
|
||||
cross-dimension travel. Entity loading is asynchronous: querying in the same
|
||||
command batch as chunk loading can briefly see zero books. The server was
|
||||
stopped and its world removed after validation. Operator fixtures are
|
||||
`age create_dropped_fixture <name> <seed>` and `age dropped_probe <name>`.
|
||||
|
||||
### Notebook and Collation Folder
|
||||
|
||||
The local `1.0.1.102-beta.1` slice adapts the 1.21.1 Notebook and Collation
|
||||
Folder, their server menu, persistent page component and existing page editor.
|
||||
Notebook capacity is 252 positions; Folder capacity is 27. Sparse positions,
|
||||
dark empty pages through the last occupied position, search, scrolling,
|
||||
single-page movement, shift transfers and the held-item lock are retained.
|
||||
Search consumes inventory keys while focused. Large pages keep the native
|
||||
30-by-40 atlas proportions and use the existing glyph-only item renderer.
|
||||
Unsupported symbol pages receive the port's red tint and retain their items.
|
||||
|
||||
Both collections appear in Creative. Notebook crafting reuses the 1.21.1
|
||||
leather/string pattern. The Folder has no recipe in 1.21.1 and receives none
|
||||
here; discovery/library loot is still pending. Link Panels cannot be inserted
|
||||
through the collection menu. Unknown symbol pages remain stored and are
|
||||
counted in the tooltip. Writing Desk integration, sealed notebooks, Symbol
|
||||
Portfolios and bulk Binder interactions remain later work.
|
||||
|
||||
Test both collection types and hands. Insert and remove pages by normal and
|
||||
shift clicks, place a page near the final position, move it with left/right
|
||||
arrows after selection, scroll/search, and type E in the search field. Verify
|
||||
Link Panels are rejected and the held collection cannot be moved/swapped.
|
||||
Close/reopen, put the collection in a chest, restart and recover its contents.
|
||||
Compare large glyphs and page outlines with the 1.21.1 editor.
|
||||
Build and disposable-server checks passed both hands and capacities, sparse
|
||||
positions, page movement, shift transfers through the actual menu input path,
|
||||
Link Panel/non-Page rejection, held-item pickup locking, save/network codec
|
||||
round trips, and all four collections stored in a chest across full restart.
|
||||
The recipe and adapted recipe-unlock advancement loaded successfully. Shared
|
||||
assets were checked in the JAR. No client joined these fixtures, so appearance,
|
||||
mouse handling and search keys still need the in-game check. The test server
|
||||
was stopped and every disposable test world removed.
|
||||
|
||||
The local `1.0.1.103-beta.1` follow-up fixes the Notebook/Folder keyboard port:
|
||||
26.3 uses different key values from the old GLFW-based implementation. Escape
|
||||
now delegates to normal container closing even with search focused, while
|
||||
left/right page movement uses the current key helpers. Search still consumes
|
||||
ordinary inventory/hotbar keys. Check Escape with search focused and while
|
||||
holding a page on the cursor; closing should use normal carried-item recovery.
|
||||
|
||||
### Collection imports in the Book Binder
|
||||
|
||||
The local `1.0.1.104-beta.1` slice adapts 1.21.1's transactional collection
|
||||
import. Shift-click a Notebook or Collation Folder in the Binder inventory
|
||||
to append its pages. Carry one on the cursor and click the page strip to
|
||||
insert its pages before that position; clicking the panel position inserts
|
||||
at the beginning. Empty collection positions are skipped, preserving the
|
||||
relative order of occupied pages. Later Binder pages shift to make room.
|
||||
|
||||
The import validates every page and total capacity before changing anything.
|
||||
A full Binder, invalid insertion point, empty collection, or conflicting
|
||||
Link Panel rejects the transaction without consuming pages. Successful
|
||||
imports leave the empty collection on the cursor or in its inventory slot,
|
||||
retaining its other components. Ordinary collection menus still reject Link
|
||||
Panels; the Binder retains 1.21.1's handling for explicitly supplied collection
|
||||
data containing a panel. Dedicated stored-collection input/UI remains unported.
|
||||
|
||||
Test shift-click append and cursor insertion for both collection types, using
|
||||
distinct ordered pages and gaps. Check that the collection empties only after
|
||||
success and that rejected imports leave both sides unchanged. Bind a book
|
||||
with the imported pages, inspect its symbols, and restart with pages in the
|
||||
Binder and collections in a chest to check persistence.
|
||||
Build and disposable-server checks passed full-capacity and panel-conflict
|
||||
rejection, invalid positions, sparse cursor insertion, shift append, empty-cover
|
||||
retention, real Binder assembly and book page order. The empty collections and
|
||||
finished book survived full restart in a chest. No client joined this fixture;
|
||||
cursor/shift UI interaction remains the in-game check. The server was stopped
|
||||
and its world removed. Include collection imports in the future JEI guide port.
|
||||
|
||||
### Symbol Portfolios and Sealed Notebooks
|
||||
|
||||
The local `1.0.1.105-beta.1` slice ports the remaining collection items from
|
||||
1.21.1. The main Creative tab offers prefilled Symbol Portfolios for every
|
||||
symbol-page category, including registered biome/material/fluid pages.
|
||||
Categories exceeding 252 pages split into named volumes without truncation.
|
||||
Portfolios use the Notebook editor and Binder collection import and can be
|
||||
emptied/refilled like ordinary collections. No portfolio recipe is added.
|
||||
|
||||
A Sealed Notebook opens server-side into a Collation Folder with 16 random
|
||||
pages. As in 1.21.1, the candidate pool excludes IDs under `block/` and
|
||||
`biome/` and the corrupted category; repeated draws are allowed. This also
|
||||
excludes biome-controller IDs under `biome/`, matching the existing code.
|
||||
The pool is not restricted to already-ported effects, so unsupported pages
|
||||
retain their red tint. Survival consumes one sealed item; Creative retains
|
||||
it. The last item becomes the Folder in hand, otherwise the Folder goes into
|
||||
inventory or drops if there is no space. Sealed Notebooks appear in Creative;
|
||||
their discovery/library loot remains pending, and no crafting recipe is added.
|
||||
|
||||
Test portfolio names, volumes, contents, editor and Binder import. Open sealed
|
||||
stacks from both hands in Survival, including with full inventory, and confirm
|
||||
each Folder has 16 pages. Check Creative retains the sealed item. Put the
|
||||
resulting items in a chest, restart and revisit.
|
||||
Build and disposable-server checks passed the 252/253-page boundary, complete
|
||||
base-registry category coverage/order, seeded draws and exclusions, both-hand
|
||||
opening, Survival/Creative consumption, full-inventory delivery, and portfolio/
|
||||
Folder persistence in a chest across restart. Shared models/textures were
|
||||
verified in the JAR. The server was stopped and its world removed. No client
|
||||
joined those fixtures; Creative presentation, editor and Binder interaction
|
||||
remain the in-game check. Future JEI collection guides should cover these items.
|
||||
|
||||
### Writing Desk
|
||||
|
||||
The local `1.0.1.106-beta.1` slice adapts the 1.21.1 Writing Desk, its
|
||||
two-block model, inventory, server catalog and existing GUI. It reuses the
|
||||
Legacy desk/scrollbar/page atlases and current block textures. Selected pages
|
||||
and white paper stacks render on the desk using the existing offsets.
|
||||
|
||||
There are 88 collection tabs, cycled four at a time. Each holds a loose Page
|
||||
or a collection with its normal capacity. Search, AZ sorting, ALL view,
|
||||
category filtering, page selection, movement and extraction retain the
|
||||
1.21.1 menu rules. Link Panels cannot be stored or copied, and no Link Panel
|
||||
creation recipe is added to the desk. Unknown pages remain recoverable but
|
||||
cannot be copied while unresolved. Unsupported port effects remain red.
|
||||
|
||||
Supply paper and Ink Vials, select a known symbol, then take its virtual output
|
||||
to copy it. Previewing consumes nothing; taking a copy consumes one paper and
|
||||
one ink by default. `[writing] symbol_ink_cost` in `rivencraft-local.toml`
|
||||
retains the 1.21.1 configurable cost. Vials fill a 16-unit meter and return
|
||||
vanilla glass bottles. Refilling is event-driven and requires bottle space;
|
||||
there is no refill server ticker. Originals remain in their collection.
|
||||
|
||||
The desk recipe and its recipe-unlock advancement are adapted to 26.3.
|
||||
New Minecraft handles Container drops during block-entity removal, so the
|
||||
old onRemove drop call is replaced by partner cleanup without duplicate
|
||||
inventory drops. The GUI uses current input helpers, including Escape while
|
||||
search is focused. The same mouse-button correction fixes Notebook right-click
|
||||
and scrollbar dragging. Progression awards and JEI guides remain later work.
|
||||
|
||||
Test placement/orientation and opening from both halves, paper/selected-page
|
||||
rendering and neighboring block visibility. Check the atlas layout and tab
|
||||
numerals, cycling 0–87, searching/sorting/ALL, page dragging into inventory,
|
||||
and copying with paper/ink. Confirm costs occur only on pickup, bottles block
|
||||
refill when full, and removing bottle space allows refill. Restart with loaded
|
||||
collections and a selected page. Break either half and check drops once.
|
||||
|
||||
Build and disposable-server checks passed copy preview/pickup costs,
|
||||
inventory-full rejection, Link Panel rejection, sparse page movement/extraction,
|
||||
88-tab reachability, all four facings and both halves' removal/drop counts.
|
||||
Collections, active tab, ink, paper and display page survived full restart.
|
||||
Recipe/advancement data loaded; shared models and atlases were verified in
|
||||
the JAR. No client joined these fixtures: visuals, mouse and live catalog
|
||||
updates remain the in-game check. The server was stopped and world removed.
|
||||
|
||||
### Progression advancements
|
||||
|
||||
The local `1.0.1.107-beta.1` slice reuses the 1.21.1 progression tree and
|
||||
translations. The root, Page and Linking Book inventory milestones load along
|
||||
with action milestones for taking a Link Panel from the Ink Mixer, copying a
|
||||
Page at the Writing Desk, binding an Age in the Book Binder and successfully
|
||||
travelling by book into a registered Age. Both normal and shift output pickup
|
||||
paths award after the server consumes the inputs. Opening a menu, viewing a
|
||||
preview or rejected travel does not earn an action milestone. Awards remain
|
||||
per-player and use vanilla advancement persistence. No retroactive action
|
||||
awards are inferred from existing books. The portal advancement is deferred
|
||||
until the Receptacle/portal feature and its icon item are ported.
|
||||
|
||||
The build and disposable-server checks passed advancement decoding, empty and
|
||||
preview Mixer negative checks, normal/shift Panel pickup, actual Desk copying,
|
||||
Binder assembly and registered-Age entry eligibility. Fixtures use vanilla
|
||||
PlayerAdvancements because NeoForge FakePlayer deliberately discards awards.
|
||||
The temporary server was stopped and its world deleted. Client tree layout,
|
||||
toasts, inventory milestones and actual book-travel awards remain manual checks.
|
||||
|
||||
For the in-game check, open Advancements and find the Rivencraft tree. Obtain a
|
||||
Page and Linking Book; then take a Panel from the Mixer, copy a Page at the Desk,
|
||||
bind a Descriptive Book and enter its Age. Check that previews alone do not award
|
||||
progress, then reconnect/restart to confirm earned progress remains. If these
|
||||
milestones were already earned, revoke the Rivencraft tree with the vanilla
|
||||
operator advancement command before retesting. Include shift-click output pickup.
|
||||
Nothing has been committed, pushed or published for this slice.
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
@@ -1312,3 +1695,199 @@ The dedicated development server uses `build/run-server`, with its own world
|
||||
and properties. Set a nonconflicting `server-port` there before running if
|
||||
another Minecraft server is active. The prototype restores all indexed Ages
|
||||
during the Overworld load, before initial chunk preparation completes.
|
||||
|
||||
The `1.0.1.108-beta.1` visual correction adapts the advancement root background
|
||||
to 26.3's ClientAsset resource identifier (`minecraft:gui/advancements/backgrounds/stone`).
|
||||
The older full texture path decoded but resolved to a missing texture. The shared
|
||||
1.21.1 root keeps its original path. Check the stone background in the client.
|
||||
|
||||
### Crystal advancement background and recipe discovery
|
||||
|
||||
The local `1.0.1.109-beta.1` slice switches the progression background to the
|
||||
existing blue Crystal Block texture (`rivencraft:block/crystal_block`) using
|
||||
the 26.3 ClientAsset identifier format. No new artwork is generated.
|
||||
|
||||
The Ink Vial, Ink Mixer, Book Binder and Crystal Block now receive their 1.21.1
|
||||
recipe-book discovery advancements. Black Dye unlocks the vial, a Glass Bottle
|
||||
unlocks the mixer, an Iron Ingot unlocks the binder, and a Crystal Shard unlocks
|
||||
the block. As with the already-ported Notebook and Writing Desk, 26.3's
|
||||
`recipe_unlocked` condition uses a `recipes` list instead of the older `recipe`
|
||||
field. Recipe ingredients and outputs are unchanged; no Blank Page recipe is added.
|
||||
|
||||
Build and disposable-server checks passed all six loaded crafting unlocks:
|
||||
wrong material stays locked, obtaining the matching material unlocks the recipe,
|
||||
progress remains per-player, and granting the recipe directly completes its
|
||||
advancement. The test server was stopped and its temporary world removed.
|
||||
The player passed the preceding progression/background correction; this crystal
|
||||
background and client recipe-book discovery still need an in-game check. Use
|
||||
Survival and a fresh recipe book (or revoke the relevant recipe/advancement)
|
||||
when testing the four newly ported unlocks. Nothing is committed or published.
|
||||
|
||||
### Lectern book stands
|
||||
|
||||
The local `1.0.1.110-beta.1` slice ports the 1.21.1 lectern conversion and
|
||||
book/page display. Place a Rivencraft book or Page on an empty vanilla lectern;
|
||||
the stand keeps its facing and consumes one item in Survival (Creative retains
|
||||
its held stack). Vanilla occupied lecterns are not replaced. Shift-right-click
|
||||
with an empty hand retrieves the stored item and restores a vanilla lectern.
|
||||
Breaking drops one lectern and its stored item. There is no separate stand item
|
||||
or new crafting recipe.
|
||||
|
||||
The extracted renderer reuses vanilla lectern geometry, existing book textures
|
||||
and the final 1.21.1 Page orientation, including east/west yaw corrections. Books
|
||||
show their name above the stand; Pages have no hover label. Empty-hand use opens
|
||||
the current port book GUI. Clicking the destination panel travels while leaving
|
||||
the book on its stand. Disarm/Maintain Momentum, same-dimension Linking Book
|
||||
restrictions, successful-Age advancements and configured Return Links use the
|
||||
existing travel logic. Handheld travel still drops the active book at departure.
|
||||
Unbound books can be displayed; retrieve and bind them in hand for this slice.
|
||||
Live previews and exploration remain later port work.
|
||||
|
||||
The stand packet includes the source dimension, position and displayed ItemStack.
|
||||
The server checks distance, loaded source chunk, stand identity and exact current
|
||||
book contents before travel, rejecting removed/replaced books or stale screens.
|
||||
No chunk is loaded just to inspect a remote stand. Book storage uses 26.3
|
||||
ValueInput/ValueOutput, update tags and packets. The modern block-entity removal
|
||||
hook receives the old state, so the stored item is dropped there once; retrieval
|
||||
empties it first to prevent duplicate drops.
|
||||
|
||||
Build and disposable-server checks passed conversion/retrieval for all four
|
||||
facings, both hands and all three item kinds; occupied-lectern/unrelated-item
|
||||
rejection; distant/wrong-dimension/replaced/removed-source rejection; exact break
|
||||
drops; component serialization; and named-book persistence across full restart.
|
||||
The server was stopped and its world removed. Client checks: display/orientation
|
||||
in all facings, names above books only, empty-hand GUI opening, panel-click travel
|
||||
for both bound book types, retention at the departure stand, modifier effects,
|
||||
Shift retrieval and break drops. Also restart with a loaded stand. Nothing has
|
||||
been committed, pushed or published for this slice.
|
||||
|
||||
### Descriptive Book author and written pages
|
||||
|
||||
The local `1.0.1.111-beta.1` slice brings the authored-book layout closer to
|
||||
1.21.1: Legacy gilded cover, front name/author on the left and destination panel
|
||||
on the right, followed by one authored glyph page at a time on the right. It
|
||||
reuses `bookui_rpage_full.png` and the existing runtime glyph item pipeline;
|
||||
no new artwork or per-symbol PNGs are generated. Bottom corners and left/right
|
||||
arrow keys turn pages with the vanilla page-turn sound. Only the front page's
|
||||
panel activates travel, including when opened from a stand. Escape still closes
|
||||
the GUI, and focused naming fields retain text input behavior.
|
||||
|
||||
The server records the player's profile name in the persistent/networked
|
||||
`book_author` component when binding through the Binder or the temporary handheld
|
||||
binding flow. Existing books retain their saved ordered `book_symbols`; older
|
||||
books with no author show the shared unknown-author text. Page zero is the front
|
||||
spread, followed by each saved symbol in order (including repeated IDs). Books
|
||||
with no symbols retain the Legacy blank final spread. Inferred generation pages
|
||||
are not inserted into authored contents. The current destination placeholder
|
||||
remains; live snapshots and the pageless random-Age glyph description remain
|
||||
future port work.
|
||||
|
||||
Build and disposable-server checks passed real Binder author assignment, ordered
|
||||
symbol indexing, repeated-symbol presentation, navigation bounds, older/panel-only
|
||||
fallback and book author/page persistence across full restart. The server was
|
||||
stopped and temporary world deleted. Client checks: bind a new book with several
|
||||
distinct pages and verify your name, cover/layout, glyph size/no extra item outline,
|
||||
corner/arrow turns, sound, Escape, GUI resize and travel only from the front panel.
|
||||
Repeat opening the book from a lectern. No commit, push or publication was made.
|
||||
|
||||
### Built-in animated link panels
|
||||
|
||||
The local `1.0.1.112-beta.1` slice ports the 1.21.1 Legacy fallback animation to
|
||||
both book GUIs. Two noisy teal spirals expand into the panel using the shared
|
||||
GUI renderer, replacing the static placeholder and instructional text. Rendering
|
||||
and clicking share the same panel bounds. Separate GUI strata keep the cover,
|
||||
animation and Legacy frame in order. Descriptive Books display the animation only
|
||||
on their front page; clicking written pages does not travel. The animation keeps
|
||||
running while the book is open and does not pause the world, load destination
|
||||
chunks or render/audio-tick a remote world. Lectern books use the same animation.
|
||||
|
||||
This is the existing mod-independent fallback, not a live snapshot or a new shader.
|
||||
It currently uses the fallback's default teal tint; destination-specific tint and
|
||||
live renderer integrations remain unported. The GUI API and outline method were
|
||||
adapted; the root spiral/noise math, timing and reveal are unchanged.
|
||||
|
||||
The build passed. No server behavior or packet schema changes were made and no
|
||||
test server was needed. Check animation/reveal/frame in both books, at different
|
||||
GUI scales and on lecterns; panel clicks should still travel, Escape should close,
|
||||
and descriptive page turning should keep working. Nothing committed or published.
|
||||
|
||||
### Linking Book final spread and shared artwork
|
||||
|
||||
The local `1.0.1.113-beta.1` slice adapts the remaining Linking Book layout
|
||||
from 1.21.1. Its front spread uses the plain Legacy cover, animated right panel,
|
||||
and wrapped/shadow-free text on the left. It shows the bound title, destination
|
||||
dimension and floored block coordinates. Vanilla dimensions use translated names;
|
||||
other dimensions currently use their registry IDs until saved Age display-name
|
||||
metadata is ported. Automatic Return Links suppress the redundant title by its
|
||||
translation identity, matching root behavior.
|
||||
|
||||
The blank final spread, 0/1 counter, bottom-corner and arrow navigation and
|
||||
page-turn sound are now present. Binding widgets hide on the final spread and
|
||||
remain hidden after resizing. Escape closes even with naming focused; focused
|
||||
text inputs consume inventory/hotbar keys. Only the front panel can travel,
|
||||
including books opened from a stand.
|
||||
|
||||
Both book screens now share an extractor adaptation of root LegacyBookArtwork,
|
||||
plus wrapped-text, counter and corner-hit helpers. The descriptive gilded cover,
|
||||
author and symbol pages are retained. This removes duplicated atlas composition
|
||||
without changing packets, saved data, Age generation or destination coordinates.
|
||||
The build passed; no server run was needed for this client-only change. Check
|
||||
long titles, Overworld/Nether/End destination text and negative coordinates,
|
||||
front/final turning/sound, resize/Escape/naming, lectern opening and front-only
|
||||
travel. Recheck descriptive glyph pages after the shared-artwork refactor.
|
||||
Nothing committed, pushed or published.
|
||||
|
||||
### Saved Age and Linking Book destination names
|
||||
|
||||
The local `1.0.1.114-beta.1` slice ports the root destination-name snapshot.
|
||||
New Ages save the entered title, preserving case/spaces/punctuation, separately
|
||||
from their existing dimension ID, folder and seed. New Linking Books bound inside
|
||||
those Ages store that title in `link_destination_name`; the front GUI and tooltip
|
||||
share the same name resolution. Vanilla dimensions keep their familiar names.
|
||||
Automatic Return Links capture the departure dimension's saved name when available.
|
||||
|
||||
The optional display-name map defaults empty for older indexes. Older books/Ages
|
||||
without metadata retain translated vanilla names or raw dimension IDs; no names
|
||||
are inferred from inventory books or folder names. This does not rename folders,
|
||||
rewrite old book labels, change collision suffixes, or reroll any generation.
|
||||
Labels on existing bound books remain snapshots as in root. Names retained with
|
||||
retired identities remain unavailable through active-Age lookup and survive a
|
||||
failed-retirement rollback. Tooltip coordinates now use floor-based BlockPos
|
||||
conversion, matching the GUI for negative values.
|
||||
|
||||
Build and disposable-server checks passed real Binder creation/name assignment,
|
||||
case/punctuation preservation, Linking/automatic Return Link labels, older vanilla
|
||||
fallback, Age unload/reload and full-restart persistence of Age metadata and book
|
||||
components. The temporary Return Link test config was restored, and the server
|
||||
was stopped/world removed. Client check: create a new named Age, enter it, bind
|
||||
a Linking Book there, and inspect its destination in the GUI and tooltip (including
|
||||
on a stand). Recheck after restart; optional Return Links should show the departure
|
||||
name when enabled. No commit, push or publication was made.
|
||||
|
||||
### Universal open-book drops (26.3 half of issue #114)
|
||||
|
||||
The local `1.0.1.115-beta.1` slice converts ordinary dropped Linking and
|
||||
Descriptive Books into the existing open-book entity, including unwritten
|
||||
variants. A server-only join listener queues one conversion after entity/chunk
|
||||
registration; it does not scan worlds or force-load chunks. Existing custom
|
||||
book entities and non-book items are skipped. The source must actually be
|
||||
registered and still present before conversion, and is discarded only after
|
||||
replacement succeeds. Rejected replacements leave the ordinary drop intact.
|
||||
|
||||
The factory uses native entity-state copying and preserves ItemStack components,
|
||||
position, motion, facing, pickup delay, owner/thrower and ordinary entity metadata.
|
||||
It does not apply the handheld travel constructor's Y offset or zero velocity.
|
||||
The replacement receives a new UUID so both can coexist during the transaction;
|
||||
its lifetime switches to the existing open-book decay/weather rules. Existing
|
||||
ordinary books loaded from disk also use the join hook. No per-tick retry is used
|
||||
if another mod cancels conversion; those drops are eligible on a later reload.
|
||||
|
||||
Build and disposable-server checks passed state/component/owner copying and
|
||||
ordinary item, player, container and stand-drop conversion, custom-entity recursion
|
||||
avoidance, non-book exclusion and cancellation recovery. A no-player fixture's
|
||||
entity chunk became unavailable to the first query; temporarily loading that
|
||||
single fixture chunk made the checks pass. No storage stall occurred. The player
|
||||
offered to perform the remaining validation, so no further automated restart run
|
||||
was started. Server stopped and world removed. Shared issue #114 remains open:
|
||||
1.21.1 implementation and manual client validation are pending. Nothing committed
|
||||
or published.
|
||||
@@ -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,88 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.115-beta.1
|
||||
|
||||
Collects the player-validated port slices .107–.115 since 1.0.1.106-beta.1.
|
||||
Minecraft 26.3, NeoForge 26.3.0.99 and Java 25. This remains a beta with
|
||||
incomplete Minecraft 1.21.1 gameplay and optional-integration parity.
|
||||
|
||||
## Book stands and dropped books
|
||||
|
||||
- Place a Linking Book, Descriptive Book or Page on an empty vanilla lectern
|
||||
to convert it into a Rivencraft book stand, preserving facing. Occupied
|
||||
vanilla lecterns are left alone.
|
||||
- Books use the existing open-book model and textures and show their name
|
||||
above the stand. Pages use their item artwork without a hover label,
|
||||
retaining the 1.21.1 orientation corrections.
|
||||
- Open a bound book with an empty hand, then click its destination panel to
|
||||
travel while leaving the book on its stand. Existing link modifiers,
|
||||
same-dimension rules, Return Link configuration and Age-entry awards apply.
|
||||
- Shift-right-click with an empty hand retrieves the stored item and restores
|
||||
a vanilla lectern. Breaking returns one lectern and its contents. Unwritten
|
||||
books can be displayed; retrieve them to bind in hand in this beta.
|
||||
- Ordinary dropped Linking/Descriptive Books now become recoverable open-book
|
||||
entities, including unwritten books, inventory/player drops, container and
|
||||
stand drops, death/Disarm drops and ordinary book entities loaded from disk.
|
||||
- Conversion preserves the stack and components, position, motion, facing,
|
||||
pickup delay and ownership/thrower metadata. It uses the existing open-book
|
||||
lifetime, slow decay and water/rain behavior. Already-custom books and
|
||||
non-book items are skipped.
|
||||
- Conversion runs after registration without world scans or force-loading.
|
||||
The original is removed only after replacement succeeds; a canceled
|
||||
replacement leaves the ordinary item intact and eligible on a later reload.
|
||||
- Stand travel validates source dimension, distance, loaded chunk and the exact
|
||||
currently stored book, rejecting stale screens after removal/replacement.
|
||||
|
||||
## Book GUIs and names
|
||||
|
||||
- Descriptive Books reuse the Legacy gilded cover, name/author front spread,
|
||||
and one authored symbol at a time on following right-hand pages. Saved page
|
||||
order and repetitions are retained, with runtime glyphs and no extra item outline.
|
||||
- New Descriptive Books record their author server-side. Older books without
|
||||
that metadata use the unknown-author label.
|
||||
- Linking Books use the plain Legacy cover, wrapped title, destination name
|
||||
and block coordinates, plus the blank final spread. Return Links suppress
|
||||
the redundant title. Bottom corners or arrow keys turn pages with sound.
|
||||
- Both GUIs share the Legacy artwork/text/counter/corner helpers. Naming
|
||||
controls hide on later pages and remain correct after resizing. Escape closes
|
||||
even with naming focused; normal text input does not trigger inventory keys.
|
||||
- Ported the existing 1.21.1 fallback's animated teal spiral panel to both GUIs,
|
||||
including lecterns. Removed placeholder labels. The Legacy frame draws over
|
||||
the animation; travel is active only on the front panel.
|
||||
- The animated fallback is not a live destination view. Destination tint,
|
||||
snapshot renderer integrations and random-Age glyph descriptions remain pending.
|
||||
- New Ages save their entered display name independently of their ID, folder
|
||||
and seed. Newly bound Linking Books and automatic Return Links capture their
|
||||
destination/departure label for both GUI and tooltip. Older Ages/books retain
|
||||
vanilla-name or dimension-ID fallbacks; existing labels are not rewritten.
|
||||
- Corrected negative tooltip coordinates to floor consistently with the GUI.
|
||||
|
||||
## Progression and recipe discovery
|
||||
|
||||
- Ported the Rivencraft advancement tree: root, obtaining a Page/Linking Book,
|
||||
taking a Link Panel from the Ink Mixer, copying a Page, binding an Age and
|
||||
successfully travelling into an Age. Action awards occur after committed
|
||||
server actions, including normal and shift pickup; previews earn nothing.
|
||||
- The advancement background uses the existing blue Crystal Block texture,
|
||||
with the corrected 26.3 resource-path format. The portal node waits for the
|
||||
Receptacle/portal port.
|
||||
- Added recipe-book discovery for Ink Vials (Black Dye), Ink Mixers (Glass
|
||||
Bottle), Book Binders (Iron Ingot) and Crystal Blocks (Crystal Shard), alongside
|
||||
the Notebook and Writing Desk unlocks. Three-by-three recipes appear in a
|
||||
Crafting Table's recipe book, not the inventory's two-by-two book.
|
||||
- Ingredients and costs are unchanged. Blank Pages remain uncraftable.
|
||||
|
||||
## Validation and remaining work
|
||||
|
||||
- Player in-game checks passed each slice and the final dropped-book changes.
|
||||
- The 26.3 and independent 1.21.1 builds passed. Gradle test tasks have no
|
||||
standalone test sources; targeted disposable-server fixtures covered real
|
||||
station actions, advancements/recipe unlocks, stand ownership/drops and
|
||||
persistence, book author/page/name persistence, and drop conversion/state.
|
||||
- Temporary test servers were stopped and their worlds deleted. A no-player
|
||||
entity fixture required one loaded fixture chunk for inspection; further
|
||||
visual/restart validation was handed to the player.
|
||||
- 1.21.1 gameplay code is unchanged. Shared issue #114 remains open for its
|
||||
matching universal dropped-book fix; Writing Desk model placement stays in #113.
|
||||
- Instability, full writing recovery, discovery/library loot, crystal portals,
|
||||
remaining stations/progression and optional integrations still need port work.
|
||||
|
||||
Keep 26.3 and 1.21.1 instances/saves separate and back up beta worlds before updating.
|
||||
@@ -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).
|
||||
@@ -203,6 +203,26 @@ 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 'data/rivencraft/advancement/progression/*.json'
|
||||
exclude 'data/rivencraft/advancement/progression/open_portal.json'
|
||||
exclude 'data/rivencraft/advancement/progression/root.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'
|
||||
@@ -214,7 +234,10 @@ tasks.named('processResources', ProcessResources).configure {
|
||||
include 'assets/rivencraft/textures/gui/bookui_cover.png'
|
||||
include 'assets/rivencraft/textures/gui/notebook_pages.png'
|
||||
include 'assets/rivencraft/textures/gui/bookui_pager.png'
|
||||
include 'assets/rivencraft/textures/gui/bookui_rpage_full.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'
|
||||
@@ -230,9 +253,13 @@ tasks.named('processResources', ProcessResources).configure {
|
||||
include 'assets/rivencraft/models/block/book_binder.json'
|
||||
include 'assets/rivencraft/models/item/book_binder.json'
|
||||
include 'assets/rivencraft/blockstates/book_binder.json'
|
||||
include 'assets/rivencraft/blockstates/book_stand.json'
|
||||
include 'data/rivencraft/loot_table/blocks/book_stand.json'
|
||||
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.93-beta.1
|
||||
mod_version=1.0.1.115-beta.1
|
||||
@@ -85,10 +85,11 @@
|
||||
"symbol.rivencraft.feature.abandoned_camps": "Abandoned Camps",
|
||||
"symbol.rivencraft.feature.abandoned_camps.description": "Allow abandoned camps on surface noise terrain, including outside their usual biomes.",
|
||||
"symbol.rivencraft.block.generated.description": "Modify the next compatible feature. Without a following feature, choose a compatible feature from the Age seed.",
|
||||
"symbol.rivencraft.biome.native.description": "Use the terrain's native biome source; do not include Biome pages.",
|
||||
"symbol.rivencraft.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.",
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -57,6 +57,15 @@ 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");
|
||||
@@ -82,6 +91,12 @@ 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<String>> LINK_DESTINATION_NAME = COMPONENTS.registerComponentType(
|
||||
"link_destination_name", builder -> builder.persistent(Codec.STRING).networkSynchronized(ByteBufCodecs.STRING_UTF8));
|
||||
public static final Supplier<DataComponentType<LinkBookTarget>> LINK_BOOK_TARGET = COMPONENTS.registerComponentType(
|
||||
"link_book_target", builder -> builder.persistent(LinkBookTarget.CODEC)
|
||||
.networkSynchronized(LinkBookTarget.STREAM_CODEC));
|
||||
@@ -93,6 +108,8 @@ public final class Rivencraft {
|
||||
"symbol_page_id", builder -> builder.persistent(Identifier.CODEC)
|
||||
.networkSynchronized(Identifier.STREAM_CODEC));
|
||||
/** Ordered authored page IDs remain on the finished book for later port slices. */
|
||||
public static final Supplier<DataComponentType<String>> BOOK_AUTHOR = COMPONENTS.registerComponentType(
|
||||
"book_author", builder -> builder.persistent(Codec.STRING).networkSynchronized(ByteBufCodecs.STRING_UTF8));
|
||||
public static final Supplier<DataComponentType<java.util.List<Identifier>>> BOOK_SYMBOLS = COMPONENTS.registerComponentType(
|
||||
"book_symbols", builder -> builder.persistent(Identifier.CODEC.listOf())
|
||||
.networkSynchronized(ByteBufCodecs.<io.netty.buffer.ByteBuf, Identifier>list(252)
|
||||
@@ -110,12 +127,22 @@ 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<net.dnorberg.rivencraft.block.BookStandBlock> BOOK_STAND =
|
||||
BLOCKS.registerBlock("book_stand", net.dnorberg.rivencraft.block.BookStandBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.LECTERN));
|
||||
public static final Supplier<BlockEntityType<net.dnorberg.rivencraft.block.entity.BookStandBlockEntity>> BOOK_STAND_BLOCK_ENTITY =
|
||||
BLOCK_ENTITY_TYPES.register("book_stand", () -> new BlockEntityType<>(
|
||||
net.dnorberg.rivencraft.block.entity.BookStandBlockEntity::new, BOOK_STAND.get()));
|
||||
public static final DeferredBlock<PortBookBinderBlock> BOOK_BINDER = BLOCKS.registerBlock(
|
||||
"book_binder", PortBookBinderBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
||||
@@ -134,6 +161,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,
|
||||
@@ -166,10 +212,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);
|
||||
@@ -179,7 +227,9 @@ public final class Rivencraft {
|
||||
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(net.dnorberg.rivencraft.event.BookStandConversionHandler::onRightClickBlock);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogout);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onServerStopped);
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package net.dnorberg.rivencraft.advancement;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Awards action-only progression after the server has committed the action. */
|
||||
public final class ProgressionAdvancements {
|
||||
public static final String LINK_PANEL = "link_panel";
|
||||
public static final String COPY_PAGE = "copy_page";
|
||||
public static final String BIND_AGE = "bind_age";
|
||||
public static final String ENTER_AGE = "enter_age";
|
||||
|
||||
private ProgressionAdvancements() {}
|
||||
|
||||
public static void award(ServerPlayer player, String path) {
|
||||
var advancement = player.level().getServer().getAdvancements().get(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "progression/" + path));
|
||||
if (advancement != null) player.getAdvancements().award(advancement, "completed");
|
||||
}
|
||||
|
||||
public static void awardAgeTravel(ServerPlayer player) {
|
||||
if (RuntimeAgeBackend.savedAges(player.level().getServer()).containsKey(player.level().dimension().identifier()))
|
||||
award(player, ENTER_AGE);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,9 @@ final class AgeLevelIndex extends SavedData {
|
||||
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)
|
||||
.optionalFieldOf("day_cycles", Map.of()).forGetter(index -> index.dayCycles),
|
||||
Codec.unboundedMap(Identifier.CODEC, Codec.STRING)
|
||||
.optionalFieldOf("display_names", Map.of()).forGetter(index -> index.displayNames)
|
||||
).apply(instance, AgeLevelIndex::new));
|
||||
|
||||
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
||||
@@ -65,6 +67,7 @@ final class AgeLevelIndex extends SavedData {
|
||||
private final Map<Identifier, PortSkyStyle> sky;
|
||||
private final Map<Identifier, PortAgeColors> colors;
|
||||
private final Map<Identifier, PortDayCycle> dayCycles;
|
||||
private final Map<Identifier, String> displayNames;
|
||||
|
||||
private AgeLevelIndex() {
|
||||
this.ages = new LinkedHashMap<>();
|
||||
@@ -81,6 +84,7 @@ final class AgeLevelIndex extends SavedData {
|
||||
this.sky = new LinkedHashMap<>();
|
||||
this.colors = new LinkedHashMap<>();
|
||||
this.dayCycles = new LinkedHashMap<>();
|
||||
this.displayNames = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
|
||||
@@ -94,7 +98,7 @@ final class AgeLevelIndex extends SavedData {
|
||||
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures,
|
||||
Map<Identifier, PortLighting> lighting,
|
||||
Map<Identifier, PortSkyStyle> sky, Map<Identifier, PortAgeColors> colors,
|
||||
Map<Identifier, PortDayCycle> dayCycles) {
|
||||
Map<Identifier, PortDayCycle> dayCycles, Map<Identifier, String> displayNames) {
|
||||
this.ages = new LinkedHashMap<>(ages);
|
||||
this.retired = new LinkedHashSet<>(retired);
|
||||
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
||||
@@ -109,6 +113,16 @@ final class AgeLevelIndex extends SavedData {
|
||||
this.sky = new LinkedHashMap<>(sky);
|
||||
this.colors = new LinkedHashMap<>(colors);
|
||||
this.dayCycles = new LinkedHashMap<>(dayCycles);
|
||||
this.displayNames = new LinkedHashMap<>(displayNames);
|
||||
}
|
||||
|
||||
String displayName(Identifier id) { return ages.containsKey(id) ? displayNames.get(id) : null; }
|
||||
|
||||
void setDisplayName(Identifier id, String name) {
|
||||
if (!ages.containsKey(id)) throw new IllegalArgumentException("Age not indexed: " + id);
|
||||
if (name == null || name.isBlank() || name.length() > 32) throw new IllegalArgumentException("Invalid Age display name");
|
||||
displayNames.put(id, name.strip());
|
||||
setDirty();
|
||||
}
|
||||
|
||||
Map<Identifier, Long> entries() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,12 @@ public final class PortAgeCommands {
|
||||
public static void register(RegisterCommandsEvent event) {
|
||||
event.getDispatcher().register(Commands.literal("rivencraft_port")
|
||||
.requires(Commands.hasPermission(Commands.LEVEL_ADMINS))
|
||||
.then(Commands.literal("recipe_discovery_probe")
|
||||
.executes(context -> {
|
||||
PortProgressionProbe.recipeDiscovery(context.getSource().getServer().overworld());
|
||||
context.getSource().sendSuccess(() -> Component.literal("Recipe discovery checks passed for all six crafting unlocks"), false);
|
||||
return 1;
|
||||
}))
|
||||
.then(Commands.literal("compat")
|
||||
.executes(context -> {
|
||||
StringBuilder mods = new StringBuilder("Optional mods: ");
|
||||
@@ -69,6 +75,177 @@ public final class PortAgeCommands {
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("age")
|
||||
.then(Commands.literal("create_drop_conversion_fixture")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> {
|
||||
var level = RuntimeAgeBackend.create(context.getSource().getServer(), ageId(context),
|
||||
LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.VOID);
|
||||
PortBookDropConversionProbe.create(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Drop sources queued; run drop_conversion_probe after server tasks complete"), false);
|
||||
return 1;
|
||||
}))))
|
||||
.then(Commands.literal("drop_conversion_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) throw new IllegalArgumentException("Age not loaded");
|
||||
PortBookDropConversionProbe.verify(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Universal book drop conversion/ownership checks passed"), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("create_book_stand_fixture")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> {
|
||||
var level = RuntimeAgeBackend.create(context.getSource().getServer(), ageId(context),
|
||||
LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.VOID);
|
||||
PortBookStandProbe.create(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Book Stand conversion/ownership/source checks passed"), false);
|
||||
return 1;
|
||||
}))))
|
||||
.then(Commands.literal("book_stand_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) throw new IllegalArgumentException("Age not loaded");
|
||||
PortBookStandProbe.verify(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Book Stand persistence passed"), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("create_writing_desk_fixture")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> {
|
||||
var level = RuntimeAgeBackend.create(context.getSource().getServer(), ageId(context),
|
||||
LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.VOID);
|
||||
PortWritingDeskProbe.create(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Writing Desk transaction/removal checks passed"), false);
|
||||
return 1;
|
||||
}))))
|
||||
.then(Commands.literal("progression_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) throw new IllegalArgumentException("Age not loaded");
|
||||
PortProgressionProbe.run(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Progression data/action checks passed"), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("writing_desk_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) throw new IllegalArgumentException("Age not loaded");
|
||||
PortWritingDeskProbe.verify(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Writing Desk persistence passed"), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("create_collection_extras_fixture")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> {
|
||||
var level = RuntimeAgeBackend.create(context.getSource().getServer(), ageId(context),
|
||||
LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.VOID);
|
||||
PortCollectionExtrasProbe.create(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Portfolio/Sealed Notebook checks passed"), false);
|
||||
return 1;
|
||||
}))))
|
||||
.then(Commands.literal("collection_extras_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) throw new IllegalArgumentException("Age not loaded");
|
||||
PortCollectionExtrasProbe.verify(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Portfolio/Sealed Notebook persistence passed"), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("create_binder_collection_fixture")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> {
|
||||
var level = RuntimeAgeBackend.create(context.getSource().getServer(), ageId(context),
|
||||
LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.VOID);
|
||||
PortBinderCollectionProbe.create(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Binder collection transaction/assembly checks passed"), false);
|
||||
return 1;
|
||||
}))))
|
||||
.then(Commands.literal("binder_collection_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) throw new IllegalArgumentException("Age not loaded");
|
||||
PortBinderCollectionProbe.verify(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Binder collection persistence passed"), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("create_notebook_fixture")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> {
|
||||
var level = RuntimeAgeBackend.create(context.getSource().getServer(), ageId(context),
|
||||
LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.VOID);
|
||||
PortNotebookProbe.create(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Notebook menu/codec checks passed"), false);
|
||||
return 1;
|
||||
}))))
|
||||
.then(Commands.literal("notebook_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) throw new IllegalArgumentException("Age not loaded");
|
||||
PortNotebookProbe.verify(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Notebook chest persistence passed"), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("create_dropped_fixture")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> {
|
||||
var level = RuntimeAgeBackend.create(context.getSource().getServer(), ageId(context),
|
||||
LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.VOID);
|
||||
PortDroppedBookProbe.create(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Dropped book ownership/codec checks passed"), false);
|
||||
return 1;
|
||||
}))))
|
||||
.then(Commands.literal("dropped_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
var level = context.getSource().getServer().getLevel(
|
||||
ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) throw new IllegalArgumentException("Age not loaded");
|
||||
level.getChunk(0, 0);
|
||||
PortDroppedBookProbe.verify(level);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Dropped book persistence checks passed"), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("create_return_fixture")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(PortAgeCommands::createReturnFixture))))
|
||||
.then(Commands.literal("return_link_status")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::returnLinkStatus)))
|
||||
.then(Commands.literal("create_platform")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(PortAgeCommands::createPlatform))))
|
||||
.then(Commands.literal("platform_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probePlatform)))
|
||||
.then(Commands.literal("create_accelerated")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("pages", IntegerArgumentType.integer(0, 256))
|
||||
.executes(PortAgeCommands::createAccelerated)))))
|
||||
.then(Commands.literal("acceleration")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::acceleration)))
|
||||
.then(Commands.literal("create_auto_terrain")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("page", IdentifierArgument.id())
|
||||
.executes(PortAgeCommands::createAutomaticTerrain)))))
|
||||
.then(Commands.literal("create_biome_surface")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
@@ -108,7 +285,10 @@ public final class PortAgeCommands {
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.then(Commands.argument("distribution", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::createCompletedBiomes))))))
|
||||
.executes(context -> createCompletedBiomes(context, null))
|
||||
.then(Commands.argument("structure", IdentifierArgument.id())
|
||||
.executes(context -> createCompletedBiomes(context,
|
||||
IdentifierArgument.getId(context, "structure")))))))))
|
||||
.then(Commands.literal("biome_pool")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeBiomePool)))
|
||||
@@ -692,6 +872,63 @@ public final class PortAgeCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int createAutomaticTerrain(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
var server = context.getSource().getServer();
|
||||
verifyAutomaticTerrain(server);
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
var source = java.util.List.of(IdentifierArgument.getId(context, "page"));
|
||||
var selected = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, source, seed);
|
||||
var pages = selected.generationPages();
|
||||
int created = createAge(context, selected.terrain(), selected.biomes(),
|
||||
net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(pages, seed),
|
||||
net.dnorberg.rivencraft.writing.PortAuthoredWriting.vegetationPlan(pages),
|
||||
net.dnorberg.rivencraft.writing.PortAuthoredWriting.dungeonCount(pages),
|
||||
net.dnorberg.rivencraft.writing.PortAuthoredWriting.lakePlan(pages, seed),
|
||||
net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed));
|
||||
if (created != 0) context.getSource().sendSuccess(() -> Component.literal("Automatic terrain: " + selected), false);
|
||||
return created;
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyAutomaticTerrain(net.minecraft.server.MinecraftServer server) {
|
||||
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
var sky = java.util.List.of(symbol.apply("sky/overworld"));
|
||||
var water = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(Identifier.withDefaultNamespace("water"));
|
||||
var plains = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("plains"));
|
||||
var seen = java.util.EnumSet.noneOf(PortAgeTerrain.class);
|
||||
for (long seed = 0; seed < 64; seed++) {
|
||||
var selected = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, sky, seed);
|
||||
seen.add(selected.terrain());
|
||||
if (selected.terrain() != PortRandomAgeSelection.terrain(seed)
|
||||
|| !selected.generationPages().subList(0, sky.size()).equals(sky)
|
||||
|| !selected.equals(net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, sky, seed)))
|
||||
throw new IllegalStateException("Automatic terrain seed/determinism/page-prefix check failed");
|
||||
var fortress = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server,
|
||||
java.util.List.of(symbol.apply("feature/nether_fortresses")), seed);
|
||||
if (fortress.terrain() != PortAgeTerrain.CAVE)
|
||||
throw new IllegalStateException("Omitted terrain cannot host a Cave-only structure");
|
||||
var wet = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, java.util.List.of(water), seed);
|
||||
if (wet.terrain() == PortAgeTerrain.FLAT || wet.terrain() == PortAgeTerrain.VOID || wet.terrain() == PortAgeTerrain.CAVE)
|
||||
throw new IllegalStateException("Automatic terrain cannot host its implied lake");
|
||||
var biome = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, java.util.List.of(plains), seed);
|
||||
if (biome.terrain() == PortAgeTerrain.CAVE || !biome.biomes().biomes().contains(Identifier.withDefaultNamespace("plains")))
|
||||
throw new IllegalStateException("Automatic terrain discarded a supplied surface biome");
|
||||
var explicit = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server,
|
||||
java.util.List.of(symbol.apply("terrain/flat"), sky.getFirst()), seed);
|
||||
if (explicit.terrain() != PortAgeTerrain.FLAT || explicit.generationPages().size() != 2)
|
||||
throw new IllegalStateException("Explicit terrain lost precedence");
|
||||
}
|
||||
if (seen.size() != PortAgeTerrain.values().length
|
||||
|| !net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(java.util.List.of(symbol.apply("feature/nether_fortresses")))
|
||||
|| net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(java.util.List.of(
|
||||
symbol.apply("terrain/flat"), symbol.apply("feature/dungeons"))))
|
||||
throw new IllegalStateException("Automatic terrain availability/coverage failed");
|
||||
}
|
||||
|
||||
private static int createBiomeSurface(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
@@ -946,16 +1183,24 @@ public final class PortAgeCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int createCompletedBiomes(CommandContext<CommandSourceStack> context) {
|
||||
private static int createCompletedBiomes(CommandContext<CommandSourceStack> context, Identifier structure) {
|
||||
try {
|
||||
var server = context.getSource().getServer();
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
var distribution = PortBiomePlan.Distribution.valueOf(
|
||||
StringArgumentType.getString(context, "distribution").toUpperCase(Locale.ROOT));
|
||||
String distributionName = StringArgumentType.getString(context, "distribution");
|
||||
var distribution = distributionName.equalsIgnoreCase("auto") ? null
|
||||
: PortBiomePlan.Distribution.valueOf(distributionName.toUpperCase(Locale.ROOT));
|
||||
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
var pages = java.util.List.of(symbol.apply("terrain/" + terrain.name().toLowerCase(Locale.ROOT)),
|
||||
symbol.apply("biome/" + distribution.name().toLowerCase(Locale.ROOT)));
|
||||
var pages = new java.util.ArrayList<Identifier>();
|
||||
pages.add(symbol.apply("terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
|
||||
if (distribution != null) pages.add(symbol.apply("biome/" + distribution.name().toLowerCase(Locale.ROOT)));
|
||||
else verifyAutomaticBiomes(server);
|
||||
if (structure != null) {
|
||||
if (!PortStructurePages.supports(structure)) throw new IllegalArgumentException("Unsupported structure page " + structure);
|
||||
pages.add(structure);
|
||||
verifyStructureBiomes(server);
|
||||
}
|
||||
var plan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server, seed);
|
||||
// Controller-only and partial pools complete; already-sufficient pools never gain extras.
|
||||
var plains = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("plains"));
|
||||
@@ -980,7 +1225,10 @@ public final class PortAgeCommands {
|
||||
var encoded = PortBiomePlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, plan).getOrThrow();
|
||||
if (!plan.equals(PortBiomePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
|
||||
throw new IllegalStateException("Completed biome plan codec mismatch");
|
||||
int created = createAge(context, terrain, plan);
|
||||
var features = net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed);
|
||||
int created = createAge(context, terrain, plan, null, null, 0, null, features);
|
||||
if (created != 0 && terrain == PortAgeTerrain.CAVE && plan.distribution() == PortBiomePlan.Distribution.NATIVE)
|
||||
verifyNativeCave(server, ageId(context), seed, plan);
|
||||
if (created != 0) context.getSource().sendSuccess(() -> Component.literal("Completed biome plan: " + plan), false);
|
||||
return created == 0 ? 0 : probeBiomePool(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
@@ -989,6 +1237,121 @@ public final class PortAgeCommands {
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyAutomaticBiomes(net.minecraft.server.MinecraftServer server) {
|
||||
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
var standard = symbol.apply("terrain/standard");
|
||||
var end = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("the_end"));
|
||||
var forest = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("forest"));
|
||||
var seen = java.util.EnumSet.noneOf(PortBiomePlan.Distribution.class);
|
||||
var caveIds = java.util.Set.copyOf(PortRandomAgeSelection.eligibleBiomes(server, PortAgeTerrain.CAVE));
|
||||
for (long seed = 0; seed < 128; seed++) {
|
||||
var expected = PortRandomAgeSelection.distribution(seed);
|
||||
var selected = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(standard), server, seed);
|
||||
seen.add(selected.distribution());
|
||||
if (selected.distribution() != expected || !selected.equals(
|
||||
net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||
java.util.List.of(standard, symbol.apply("weather/off")), server, seed)))
|
||||
throw new IllegalStateException("Missing biome controller is not seeded/independent");
|
||||
var authored = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(standard, end, forest), server, seed);
|
||||
if (authored.biomes().size() != 2 || !authored.biomes().equals(java.util.List.of(
|
||||
Identifier.withDefaultNamespace("the_end"), Identifier.withDefaultNamespace("forest")))
|
||||
|| authored.distribution() != (expected == PortBiomePlan.Distribution.NATIVE
|
||||
|| expected == PortBiomePlan.Distribution.SINGLE ? PortBiomePlan.Distribution.TILED : expected))
|
||||
throw new IllegalStateException("Automatic controller discarded or extended authored biomes");
|
||||
var one = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(standard, end), server, seed);
|
||||
if (!one.biomes().getFirst().equals(Identifier.withDefaultNamespace("the_end"))
|
||||
|| one.distribution() != (expected == PortBiomePlan.Distribution.NATIVE ? PortBiomePlan.Distribution.SINGLE : expected)
|
||||
|| one.biomes().size() != (one.distribution() == PortBiomePlan.Distribution.SINGLE ? 1 : 2))
|
||||
throw new IllegalStateException("Automatic controller lost a supplied biome");
|
||||
var cave = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||
java.util.List.of(symbol.apply("terrain/cave")), server, seed);
|
||||
if (cave.distribution() != expected || !caveIds.containsAll(cave.biomes())
|
||||
|| expected == PortBiomePlan.Distribution.NATIVE && !java.util.Set.copyOf(cave.biomes()).equals(caveIds))
|
||||
throw new IllegalStateException("Automatic Cave controller/biome pool is invalid");
|
||||
var emptyVoid = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||
java.util.List.of(symbol.apply("terrain/void")), server, seed);
|
||||
var authoredVoid = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||
java.util.List.of(symbol.apply("terrain/void"), end), server, seed);
|
||||
if (emptyVoid.distribution() != PortBiomePlan.Distribution.SINGLE
|
||||
|| !emptyVoid.biomes().equals(java.util.List.of(Identifier.withDefaultNamespace("the_void")))
|
||||
|| !authoredVoid.biomes().equals(java.util.List.of(Identifier.withDefaultNamespace("the_end"))))
|
||||
throw new IllegalStateException("Automatic Void controller changed biome fallback");
|
||||
var village = symbol.apply("feature/villages");
|
||||
var withStructure = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(standard, village), server, seed);
|
||||
if (withStructure.distribution() != PortBiomePlan.Distribution.NATIVE
|
||||
&& withStructure.biomes().stream().noneMatch(PortStructurePages.compatibleBiomes(village, server.registryAccess())::contains))
|
||||
throw new IllegalStateException("Automatic controller skipped structure-aware completion");
|
||||
}
|
||||
if (seen.size() != PortBiomePlan.Distribution.values().length)
|
||||
throw new IllegalStateException("Automatic biome controller fixture lacks distribution coverage");
|
||||
}
|
||||
|
||||
private static void verifyNativeCave(net.minecraft.server.MinecraftServer server, Identifier age, long seed, PortBiomePlan plan) {
|
||||
var expected = PortRandomAgeSelection.caveBiomes(server.registryAccess());
|
||||
if (!plan.biomes().equals(expected)) throw new IllegalStateException("Native Cave did not freeze the recognized biome pool");
|
||||
var old = PortBiomePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
|
||||
com.google.gson.JsonParser.parseString("{\"distribution\":\"NATIVE\",\"biomes\":[]}")).getOrThrow();
|
||||
if (!old.biomes().isEmpty() || old.distribution() != PortBiomePlan.Distribution.NATIVE)
|
||||
throw new IllegalStateException("Old Native Cave plan was rewritten");
|
||||
var level = server.getLevel(ResourceKey.create(Registries.DIMENSION, age));
|
||||
var source = level.getChunkSource().getGenerator().getBiomeSource();
|
||||
if (expected.size() < 2) return;
|
||||
if (!(source instanceof PortScaledBiomeSource scaled)) throw new IllegalStateException("Native Cave is not using region distribution");
|
||||
var registry = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
var holders = plan.biomes().stream().map(id -> (net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome>)
|
||||
registry.getOrThrow(ResourceKey.create(Registries.BIOME, id))).toList();
|
||||
var tiny = new PortScaledBiomeSource(net.minecraft.core.HolderSet.direct(holders), seed, 0);
|
||||
var sampled = new java.util.HashSet<Identifier>();
|
||||
for (int x = -64; x <= 64; x += 4) for (int z = -64; z <= 64; z += 4) {
|
||||
var biome = scaled.getNoiseBiome(x, 0, z);
|
||||
if (!biome.equals(tiny.getNoiseBiome(x, 0, z)) || !biome.equals(scaled.getNoiseBiome(x, 16, z)))
|
||||
throw new IllegalStateException("Native Cave does not match root's Tiny-region sampler");
|
||||
sampled.add(biome.unwrapKey().orElseThrow().identifier());
|
||||
}
|
||||
if (!java.util.Set.copyOf(expected).containsAll(sampled) || sampled.size() < 2)
|
||||
throw new IllegalStateException("Native Cave sampler did not distribute its recognized biome pool");
|
||||
}
|
||||
|
||||
private static void verifyStructureBiomes(net.minecraft.server.MinecraftServer server) {
|
||||
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
java.util.function.Function<String, Identifier> biome = name ->
|
||||
net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace(name));
|
||||
var village = symbol.apply("feature/villages");
|
||||
var fortress = symbol.apply("feature/nether_fortresses");
|
||||
for (long seed = 0; seed < 32; seed++) {
|
||||
var singlePages = java.util.List.of(symbol.apply("biome/single"), village);
|
||||
var single = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(singlePages, server, seed);
|
||||
var cave = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||
java.util.List.of(symbol.apply("terrain/cave"), symbol.apply("biome/single"), fortress), server, seed);
|
||||
if (!PortStructurePages.compatibleBiomes(village, server.registryAccess()).contains(single.biomes().getFirst())
|
||||
|| !single.equals(net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(singlePages, server, seed))
|
||||
|| !PortStructurePages.compatibleBiomes(fortress, server.registryAccess()).contains(cave.biomes().getFirst()))
|
||||
throw new IllegalStateException("Structure-aware Single completion failed");
|
||||
var stronghold = symbol.apply("feature/strongholds");
|
||||
var shared = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(
|
||||
symbol.apply("biome/single"), village, stronghold), server, seed);
|
||||
if (!PortStructurePages.compatibleBiomes(village, server.registryAccess()).contains(shared.biomes().getFirst())
|
||||
|| !PortStructurePages.compatibleBiomes(stronghold, server.registryAccess()).contains(shared.biomes().getFirst()))
|
||||
throw new IllegalStateException("Single completion did not satisfy common structure requirements");
|
||||
var explicit = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||
java.util.List.of(symbol.apply("biome/single"), biome.apply("the_end"), village), server, seed);
|
||||
var nativePlan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||
java.util.List.of(symbol.apply("biome/native"), village), server, seed);
|
||||
if (!explicit.biomes().equals(java.util.List.of(Identifier.withDefaultNamespace("the_end")))
|
||||
|| nativePlan.distribution() != PortBiomePlan.Distribution.NATIVE || !nativePlan.biomes().isEmpty())
|
||||
throw new IllegalStateException("Explicit Single/Native biome precedence changed");
|
||||
var multiPages = java.util.List.of(symbol.apply("biome/tiny"), biome.apply("forest"), biome.apply("the_end"), village);
|
||||
var multi = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(multiPages, server, seed);
|
||||
if (multi.biomes().size() != 3 || !multi.biomes().subList(0, 2).equals(java.util.List.of(
|
||||
Identifier.withDefaultNamespace("forest"), Identifier.withDefaultNamespace("the_end")))
|
||||
|| !PortStructurePages.compatibleBiomes(village, server.registryAccess()).contains(multi.biomes().get(2)))
|
||||
throw new IllegalStateException("Structure biome completion discarded authored choices");
|
||||
var full = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(
|
||||
symbol.apply("biome/tiny"), biome.apply("plains"), biome.apply("forest"), village, village), server, seed);
|
||||
if (full.biomes().size() != 2) throw new IllegalStateException("Compatible/repeated structures added unnecessary biomes");
|
||||
}
|
||||
}
|
||||
|
||||
private static int probeBiomePool(CommandContext<CommandSourceStack> context) {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) {
|
||||
@@ -1947,6 +2310,119 @@ public final class PortAgeCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int createReturnFixture(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
var local = new PortAgeVisits(java.util.List.of());
|
||||
var disabled = net.dnorberg.rivencraft.PortConfig.ReturnLinkMode.DISABLED;
|
||||
var always = net.dnorberg.rivencraft.PortConfig.ReturnLinkMode.ALWAYS;
|
||||
var once = net.dnorberg.rivencraft.PortConfig.ReturnLinkMode.ON_CREATION;
|
||||
if (PortReturnLinks.shouldIssue(disabled, local, id) || local.hasVisited(id)
|
||||
|| !PortReturnLinks.shouldIssue(always, local, id)
|
||||
|| !PortReturnLinks.shouldIssue(always, local, id) || local.hasVisited(id)
|
||||
|| !PortReturnLinks.shouldIssue(once, local, id)
|
||||
|| PortReturnLinks.shouldIssue(once, local, id))
|
||||
throw new IllegalStateException("Return Link mode regression");
|
||||
var encoded = PortAgeVisits.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, local).getOrThrow();
|
||||
var decoded = PortAgeVisits.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow();
|
||||
if (!decoded.hasVisited(id) || PortReturnLinks.shouldIssue(once, decoded, id))
|
||||
throw new IllegalStateException("Return Link visit codec regression");
|
||||
var target = new LinkBookTarget(Identifier.fromNamespaceAndPath("minecraft", "overworld"),
|
||||
12.25, 80.5, -42.75, 135.0F, -20.0F);
|
||||
var book = PortReturnLinks.createBook(target);
|
||||
if (!book.is(Rivencraft.LINKING_BOOK.get())
|
||||
|| !target.equals(book.get(Rivencraft.LINK_BOOK_TARGET.get()))
|
||||
|| !book.getHoverName().getString().equals("Return Link")
|
||||
|| book.has(Rivencraft.LINK_PROPERTIES.get()))
|
||||
throw new IllegalStateException("Return Link book component regression");
|
||||
var server = context.getSource().getServer();
|
||||
RuntimeAgeBackend.create(server, id, LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.VOID);
|
||||
var visits = server.overworld().getDataStorage().computeIfAbsent(PortAgeVisits.TYPE);
|
||||
if (!PortReturnLinks.shouldIssue(once, visits, id) || PortReturnLinks.shouldIssue(once, visits, id))
|
||||
throw new IllegalStateException("Return Link world marker regression");
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
return returnLinkStatus(context);
|
||||
}
|
||||
|
||||
private static int returnLinkStatus(CommandContext<CommandSourceStack> context) {
|
||||
var visits = context.getSource().getServer().overworld().getDataStorage().computeIfAbsent(PortAgeVisits.TYPE);
|
||||
context.getSource().sendSuccess(() -> Component.literal(ageId(context) + " Return Link mode="
|
||||
+ net.dnorberg.rivencraft.PortConfig.RETURN_LINK_MODE.get()
|
||||
+ "; ON_CREATION issued=" + visits.hasVisited(ageId(context))), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int createPlatform(CommandContext<CommandSourceStack> context) {
|
||||
ServerLevel level = RuntimeAgeBackend.create(context.getSource().getServer(), ageId(context),
|
||||
LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.VOID);
|
||||
var landing = PortAgeTravel.findLanding(level);
|
||||
if (!new net.minecraft.core.BlockPos(0, 64, 0).equals(landing))
|
||||
throw new IllegalStateException("Unexpected Void landing: " + landing);
|
||||
return probePlatform(context);
|
||||
}
|
||||
|
||||
/** Inspect the disposable Void fixture without regenerating or repairing its platform. */
|
||||
private static int probePlatform(CommandContext<CommandSourceStack> context) {
|
||||
ServerLevel level = context.getSource().getServer().getLevel(
|
||||
ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + ageId(context)));
|
||||
return 0;
|
||||
}
|
||||
for (int x = -1; x <= 1; x++) for (int z = -1; z <= 1; z++) {
|
||||
var pos = new net.minecraft.core.BlockPos(x, 63, z);
|
||||
var state = level.getBlockState(pos);
|
||||
if (!state.is(Rivencraft.CLOUD_PLATFORM.get())
|
||||
|| state.getDestroySpeed(level, pos) != 0.3F
|
||||
|| !state.isSolidRender() || !state.isCollisionShapeFullBlock(level, pos)
|
||||
|| !state.getCloneItemStack(level, pos, false).isEmpty()
|
||||
|| !net.minecraft.world.level.block.Block.getDrops(state, level, pos, null).isEmpty())
|
||||
throw new IllegalStateException("Platform properties mismatch at " + pos);
|
||||
}
|
||||
Identifier id = Identifier.fromNamespaceAndPath("rivencraft", "cloud_platform");
|
||||
if (net.dnorberg.rivencraft.writing.PortRegistryPages.solidMaterialIds().contains(id)
|
||||
|| net.dnorberg.rivencraft.writing.PortRegistryPages.find(
|
||||
net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(id)) != null
|
||||
|| !Rivencraft.CLOUD_PLATFORM.get().asItem().equals(net.minecraft.world.item.Items.AIR))
|
||||
throw new IllegalStateException("Platform is obtainable as an item or material Page");
|
||||
context.getSource().sendSuccess(() -> Component.literal(ageId(context)
|
||||
+ " platform passed: solid/opaque, strength 0.3, no loot/pick item/material Page"), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int createAccelerated(CommandContext<CommandSourceStack> context) {
|
||||
int count = IntegerArgumentType.getInteger(context, "pages");
|
||||
var pages = new java.util.ArrayList<Identifier>();
|
||||
pages.add(Identifier.fromNamespaceAndPath("rivencraft", "terrain/flat"));
|
||||
for (int i = 0; i < count; i++) pages.add(PortAcceleratedTicks.PAGE);
|
||||
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages)
|
||||
|| !net.dnorberg.rivencraft.writing.PortAuthoredWriting.isPageImplemented(PortAcceleratedTicks.PAGE)
|
||||
|| PortAcceleratedTicks.pageCount(pages) != count
|
||||
|| PortAcceleratedTicks.attemptBudget(1, 8, 16384) != 24
|
||||
|| PortAcceleratedTicks.attemptBudget(32, 8, 16384) != 768
|
||||
|| PortAcceleratedTicks.attemptBudget(Integer.MAX_VALUE, Integer.MAX_VALUE, 16384) != 16384
|
||||
|| PortAcceleratedTicks.attemptBudget(32, 8, 0) != 0)
|
||||
throw new IllegalStateException("Accelerated writing/budget regression");
|
||||
ServerLevel level = RuntimeAgeBackend.create(context.getSource().getServer(), ageId(context),
|
||||
LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.FLAT);
|
||||
PortAcceleratedTicks.configure(level, count);
|
||||
return acceleration(context);
|
||||
}
|
||||
|
||||
private static int acceleration(CommandContext<CommandSourceStack> context) {
|
||||
ServerLevel level = context.getSource().getServer().getLevel(
|
||||
ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + ageId(context)));
|
||||
return 0;
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal(ageId(context) + " Accelerated pages="
|
||||
+ PortAcceleratedTicks.pages(level) + "; extra attempts/eligible section="
|
||||
+ 3L * PortAcceleratedTicks.pages(level)
|
||||
+ "; max loaded chunks=" + net.dnorberg.rivencraft.PortConfig.ACCELERATED_MAX_CHUNKS.get()
|
||||
+ "; max attempts/tick=" + net.dnorberg.rivencraft.PortConfig.ACCELERATED_MAX_ATTEMPTS.get()), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int sampleBiome(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
|
||||
@@ -37,7 +37,7 @@ public final class PortAgeTravel {
|
||||
return moved ? null : "Teleportation failed; remaining in source dimension";
|
||||
}
|
||||
|
||||
private static BlockPos findLanding(ServerLevel target) {
|
||||
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
|
||||
@@ -112,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);
|
||||
|
||||
@@ -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,157 @@
|
||||
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 = new PortProgressionPlayer(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");
|
||||
if (!PortProgressionProbe.isComplete(player, "bind_age"))
|
||||
throw new IllegalStateException("Missing bind_age award");
|
||||
if (!player.getGameProfile().name().equals(book.get(Rivencraft.BOOK_AUTHOR.get())))
|
||||
throw new IllegalStateException("Binder author was not server assigned");
|
||||
var pages = net.dnorberg.rivencraft.writing.DescriptiveBookPages.from(book);
|
||||
if (pages.lastPage() != EXPECTED.size() || pages.symbolAt(0).isPresent()
|
||||
|| !pages.symbolAt(1).orElseThrow().equals(EXPECTED.getFirst())
|
||||
|| !pages.symbolAt(EXPECTED.size()).orElseThrow().equals(EXPECTED.getLast())
|
||||
|| pages.symbolAt(EXPECTED.size() + 1).isPresent()
|
||||
|| pages.turn(0, -1) != 0 || pages.turn(pages.lastPage(), 1) != pages.lastPage())
|
||||
throw new IllegalStateException("Authored book page indexing failed");
|
||||
var old = net.dnorberg.rivencraft.writing.DescriptiveBookPages.from(new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get()));
|
||||
if (old.lastPage() != 1 || !old.author().isEmpty() || old.symbolAt(1).isPresent())
|
||||
throw new IllegalStateException("Older panel-only book fallback failed");
|
||||
var repeated = new net.dnorberg.rivencraft.writing.DescriptiveBookPages(
|
||||
List.of(EXPECTED.getFirst(), EXPECTED.getFirst()), "Test");
|
||||
if (repeated.lastPage() != 2 || !repeated.symbolAt(1).equals(repeated.symbolAt(2)))
|
||||
throw new IllegalStateException("Repeated symbol display was collapsed");
|
||||
var ageId = book.get(Rivencraft.AGE_BOOK_ID.get());
|
||||
var server = level.getServer();
|
||||
if (!RuntimeAgeBackend.savedAgeName(server, ageId).orElseThrow().equals("Collection Fixture104"))
|
||||
throw new IllegalStateException("Created Age lost its display name");
|
||||
var namedLevel = server.getLevel(net.minecraft.resources.ResourceKey.create(
|
||||
net.minecraft.core.registries.Registries.DIMENSION, ageId));
|
||||
if (!net.dnorberg.rivencraft.item.LinkingBookItem.destinationName(namedLevel).equals("Collection Fixture104"))
|
||||
throw new IllegalStateException("Link binding name lookup did not use Age title");
|
||||
RuntimeAgeBackend.setAgeDisplayName(server, ageId, "Crystal Garden's Dawn");
|
||||
var target = new net.dnorberg.rivencraft.item.LinkBookTarget(ageId, -0.5, 64, -1.25, 90, 0);
|
||||
var link = new ItemStack(Rivencraft.LINKING_BOOK.get());
|
||||
link.set(Rivencraft.LINK_BOOK_TARGET.get(), target);
|
||||
link.set(Rivencraft.LINK_DESTINATION_NAME.get(), net.dnorberg.rivencraft.item.LinkingBookItem.destinationName(namedLevel));
|
||||
if (!net.dnorberg.rivencraft.item.LinkingBookItem.destinationName(link, target).getString().equals("Crystal Garden's Dawn"))
|
||||
throw new IllegalStateException("Link destination label ignored saved name");
|
||||
var mode = net.dnorberg.rivencraft.PortConfig.RETURN_LINK_MODE.get();
|
||||
player.getInventory().clearContent();
|
||||
try {
|
||||
net.dnorberg.rivencraft.PortConfig.RETURN_LINK_MODE.set(net.dnorberg.rivencraft.PortConfig.ReturnLinkMode.ALWAYS);
|
||||
PortReturnLinks.afterSuccessfulTravel(player, ageId, target);
|
||||
var returned = java.util.stream.IntStream.range(0, player.getInventory().getContainerSize())
|
||||
.mapToObj(i -> player.getInventory().getItem(i)).filter(i -> i.is(Rivencraft.LINKING_BOOK.get()))
|
||||
.findFirst().orElseThrow();
|
||||
if (!"Crystal Garden's Dawn".equals(returned.get(Rivencraft.LINK_DESTINATION_NAME.get())))
|
||||
throw new IllegalStateException("Automatic Return Link lost departure name");
|
||||
chest.setItem(4, returned.copy());
|
||||
} finally { net.dnorberg.rivencraft.PortConfig.RETURN_LINK_MODE.set(mode); }
|
||||
chest.setItem(3, link);
|
||||
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 (!"RivenBinderProbe".equals(chest.getItem(2).get(Rivencraft.BOOK_AUTHOR.get())))
|
||||
throw new IllegalStateException("Authored book lost its author");
|
||||
var ageId = chest.getItem(2).get(Rivencraft.AGE_BOOK_ID.get());
|
||||
if (!RuntimeAgeBackend.savedAgeName(level.getServer(), ageId).orElseThrow().equals("Crystal Garden's Dawn")
|
||||
|| !"Crystal Garden's Dawn".equals(chest.getItem(3).get(Rivencraft.LINK_DESTINATION_NAME.get()))
|
||||
|| !"Crystal Garden's Dawn".equals(chest.getItem(4).get(Rivencraft.LINK_DESTINATION_NAME.get())))
|
||||
throw new IllegalStateException("Age/Linking/Return destination names did not survive persistence");
|
||||
var old = new ItemStack(Rivencraft.LINKING_BOOK.get());
|
||||
var overworld = new net.dnorberg.rivencraft.item.LinkBookTarget(net.minecraft.world.level.Level.OVERWORLD.identifier(), 0, 64, 0, 0, 0);
|
||||
if (!net.dnorberg.rivencraft.item.LinkingBookItem.destinationName(old, overworld).getString()
|
||||
.equals(net.minecraft.network.chat.Component.translatable("dimension.minecraft.overworld").getString()))
|
||||
throw new IllegalStateException("Older book vanilla name fallback changed");
|
||||
if (!EXPECTED.equals(chest.getItem(2).get(Rivencraft.BOOK_SYMBOLS.get())))
|
||||
throw new IllegalStateException("Authored book page order was not preserved");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.entity.DroppedBookEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.TickTask;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.TagValueOutput;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Join conversion runs on queued server tasks: create and inspect on separate commands. */
|
||||
final class PortBookDropConversionProbe {
|
||||
private static final UUID OWNER = UUID.fromString("cc5909e5-aafa-43d6-b841-a917ec9c0115");
|
||||
private static final AABB AREA = new AABB(-16, -64, -16, 32, 320, 32);
|
||||
private static ItemStack book(boolean descriptive, String label) {
|
||||
var stack = new ItemStack(descriptive ? Rivencraft.DESCRIPTIVE_BOOK.get() : Rivencraft.LINKING_BOOK.get());
|
||||
stack.set(DataComponents.CUSTOM_NAME, Component.literal(label));
|
||||
return stack;
|
||||
}
|
||||
static void create(ServerLevel level) {
|
||||
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++) level.setBlock(new BlockPos(x, 63, z), Blocks.STONE.defaultBlockState(), 2);
|
||||
var player = net.neoforged.neoforge.common.util.FakePlayerFactory.get(level,
|
||||
new com.mojang.authlib.GameProfile(UUID.fromString("84f38604-48fe-48d3-a4fb-f858a7200115"), "RivenDropProbe"));
|
||||
player.setPos(8.5, 64, 8.5);
|
||||
// Test exact factory state before physics can advance position or pickup timers.
|
||||
var sample = new ItemEntity(level, 8.5, 66, 8.5, book(false, "state_copy"));
|
||||
sample.getItem().set(Rivencraft.LINK_BOOK_TARGET.get(), new net.dnorberg.rivencraft.item.LinkBookTarget(
|
||||
level.dimension().identifier(), -0.5, 64, -1.25, 135, -20));
|
||||
sample.getItem().set(Rivencraft.LINK_DESTINATION_NAME.get(), "Named destination");
|
||||
sample.getItem().set(Rivencraft.LINK_PROPERTIES.get(), new net.dnorberg.rivencraft.item.LinkProperties(net.dnorberg.rivencraft.item.LinkProperties.DISARM
|
||||
| net.dnorberg.rivencraft.item.LinkProperties.MAINTAIN_MOMENTUM));
|
||||
sample.setTarget(OWNER);
|
||||
sample.setThrower(player);
|
||||
sample.setPickUpDelay(80);
|
||||
sample.setDeltaMovement(new Vec3(0.2, 0.1, -0.3));
|
||||
sample.setYRot(37);
|
||||
sample.setXRot(12);
|
||||
sample.setNoGravity(true);
|
||||
var copy = DroppedBookEntity.fromDrop(sample);
|
||||
if (!copy.position().equals(sample.position()) || !copy.getDeltaMovement().equals(sample.getDeltaMovement())
|
||||
|| copy.getYRot() != 37 || copy.getXRot() != 12 || !copy.isNoGravity()
|
||||
|| !ItemStack.matches(copy.getItem(), sample.getItem()) || !OWNER.equals(copy.getTarget())
|
||||
|| copy.getUUID().equals(sample.getUUID()) || copy.getAge() != -32768)
|
||||
throw new IllegalStateException("Drop conversion lost placement, ownership or item components");
|
||||
var originalTag = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, level.registryAccess());
|
||||
var copiedTag = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, level.registryAccess());
|
||||
sample.saveWithoutId(originalTag); copy.saveWithoutId(copiedTag);
|
||||
for (String key : List.of("PickupDelay", "Owner", "Thrower", "Item"))
|
||||
if (!java.util.Objects.equals(originalTag.buildResult().get(key), copiedTag.buildResult().get(key)))
|
||||
throw new IllegalStateException("Drop conversion lost metadata: " + key);
|
||||
// Four ordinary book variants must all convert through the actual event route.
|
||||
for (String name : List.of("manual_link", "manual_description", "unwritten_link", "unwritten_description")) {
|
||||
var stack = book(name.endsWith("description"), name);
|
||||
if (name.equals("manual_link")) {
|
||||
stack.set(Rivencraft.LINK_BOOK_TARGET.get(), sample.getItem().get(Rivencraft.LINK_BOOK_TARGET.get()));
|
||||
stack.set(Rivencraft.LINK_DESTINATION_NAME.get(), "Named destination");
|
||||
}
|
||||
if (name.equals("manual_description")) stack.set(Rivencraft.AGE_BOOK_ID.get(), level.dimension().identifier());
|
||||
var item = new ItemEntity(level, 8.5, 66, 8.5, stack);
|
||||
item.setNoGravity(true); item.setDeltaMovement(Vec3.ZERO); item.setTarget(OWNER);
|
||||
level.addFreshEntity(item);
|
||||
}
|
||||
player.drop(book(false, "player_toss"), false, net.minecraft.util.Prediction.SERVER_ONLY);
|
||||
net.minecraft.world.Containers.dropItemStack(level, 8.5, 65, 8.5, book(true, "container"));
|
||||
var standPos = new BlockPos(10, 64, 8);
|
||||
level.setBlock(standPos, Rivencraft.BOOK_STAND.get().defaultBlockState(), 3);
|
||||
((net.dnorberg.rivencraft.block.entity.BookStandBlockEntity) level.getBlockEntity(standPos)).setBook(book(false, "stand"));
|
||||
level.destroyBlock(standPos, true);
|
||||
var custom = new DroppedBookEntity(level, new Vec3(8.5, 66, 8.5), book(false, "already_custom"));
|
||||
custom.setNoGravity(true);
|
||||
level.addFreshEntity(custom);
|
||||
level.addFreshEntity(new ItemEntity(level, 8.5, 66, 8.5, net.dnorberg.rivencraft.item.PageItem.createLinkPanelPage()));
|
||||
java.util.function.Consumer<net.neoforged.neoforge.event.entity.EntityJoinLevelEvent> cancel = event -> {
|
||||
if (event.getLevel() == level && event.getEntity() instanceof DroppedBookEntity dropped
|
||||
&& dropped.getItem().getHoverName().getString().equals("cancel_preserve")) event.setCanceled(true);
|
||||
};
|
||||
net.neoforged.neoforge.common.NeoForge.EVENT_BUS.addListener(cancel);
|
||||
try {
|
||||
var item = new ItemEntity(level, 8.5, 66, 8.5, book(false, "cancel_preserve"));
|
||||
item.setNoGravity(true); item.setDeltaMovement(Vec3.ZERO);
|
||||
level.addFreshEntity(item);
|
||||
} finally {
|
||||
// Queued after that source's conversion task; no fixture listener remains installed.
|
||||
level.getServer().schedule(new TickTask(0, () -> net.neoforged.neoforge.common.NeoForge.EVENT_BUS.unregister(cancel)));
|
||||
}
|
||||
}
|
||||
static void verify(ServerLevel level) {
|
||||
var drops = level.getEntitiesOfClass(ItemEntity.class, AREA);
|
||||
for (String name : List.of("manual_link", "manual_description", "unwritten_link", "unwritten_description",
|
||||
"player_toss", "container", "stand", "already_custom", "cancel_preserve")) {
|
||||
var found = drops.stream().filter(e -> e.getItem().getHoverName().getString().equals(name)).toList();
|
||||
if (found.size() != 1 || !name.equals("cancel_preserve") && !(found.getFirst() instanceof DroppedBookEntity))
|
||||
throw new IllegalStateException("Expected one open book for " + name + ", found " + found.size());
|
||||
}
|
||||
var named = drops.stream().filter(e -> e.getItem().getHoverName().getString().equals("manual_link")).findFirst().orElseThrow();
|
||||
if (!OWNER.equals(named.getTarget()) || !"Named destination".equals(named.getItem().get(Rivencraft.LINK_DESTINATION_NAME.get())))
|
||||
throw new IllegalStateException("Converted book lost owner or destination");
|
||||
if (drops.stream().filter(e -> e.getItem().is(Rivencraft.PAGE.get())).anyMatch(e -> e instanceof DroppedBookEntity))
|
||||
throw new IllegalStateException("Non-book item was converted");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.BookStandBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.dnorberg.rivencraft.event.BookStandConversionHandler;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||
import net.dnorberg.rivencraft.network.UseBookStandPayload;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Isolated conversion, ownership, stale-screen and persistence checks. */
|
||||
final class PortBookStandProbe {
|
||||
private static final BlockPos POS = new BlockPos(0, 64, 0);
|
||||
static void create(ServerLevel level) {
|
||||
var player = net.neoforged.neoforge.common.util.FakePlayerFactory.get(level,
|
||||
new com.mojang.authlib.GameProfile(java.util.UUID.fromString("a5c323a1-3b58-4dd6-927f-e08000800110"), "RivenStandProbe"));
|
||||
player.setPos(0.5, 64, 2.5);
|
||||
try {
|
||||
for (Direction facing : Direction.Plane.HORIZONTAL) for (InteractionHand hand : InteractionHand.values())
|
||||
for (var sample : java.util.List.of(new ItemStack(Rivencraft.LINKING_BOOK.get()),
|
||||
new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get()), PageItem.createLinkPanelPage().copyWithCount(3))) {
|
||||
level.setBlock(POS, Blocks.LECTERN.defaultBlockState().setValue(LecternBlock.FACING, facing), 3);
|
||||
player.setItemInHand(hand, sample.copy());
|
||||
if (!BookStandConversionHandler.convert(player, hand, POS)) throw new IllegalStateException("Conversion failed");
|
||||
var stand = (BookStandBlockEntity) level.getBlockEntity(POS);
|
||||
var stored = sample.copyWithCount(1);
|
||||
if (!ItemStack.matches(stand.getBook(), stored) || player.getItemInHand(hand).getCount() != sample.getCount() - 1
|
||||
|| stand.getBlockState().getValue(BookStandBlock.FACING) != facing)
|
||||
throw new IllegalStateException("Conversion lost ownership or facing");
|
||||
var packet = new UseBookStandPayload(level.dimension().identifier(), POS, stored.copy());
|
||||
if (PortBookNetworking.validatedStandBook(player, packet).isEmpty()) throw new IllegalStateException("Valid source rejected");
|
||||
player.setPos(100, 64, 100);
|
||||
if (!PortBookNetworking.validatedStandBook(player, packet).isEmpty()) throw new IllegalStateException("Distant source accepted");
|
||||
player.setPos(0.5, 64, 2.5);
|
||||
if (!PortBookNetworking.validatedStandBook(player, new UseBookStandPayload(
|
||||
net.minecraft.resources.Identifier.fromNamespaceAndPath("minecraft", "overworld"), POS, stored)).isEmpty())
|
||||
throw new IllegalStateException("Wrong dimension accepted");
|
||||
if (!PortBookNetworking.validatedStandBook(player, new UseBookStandPayload(packet.dimension(), POS, new ItemStack(Items.DIRT))).isEmpty())
|
||||
throw new IllegalStateException("Replaced book accepted");
|
||||
var loaded = net.minecraft.world.level.block.entity.BlockEntity.loadStatic(POS, stand.getBlockState(),
|
||||
stand.saveWithFullMetadata(level.registryAccess()), level.registryAccess());
|
||||
if (!(loaded instanceof BookStandBlockEntity copy) || !ItemStack.matches(copy.getBook(), stored))
|
||||
throw new IllegalStateException("Stand serialization lost content");
|
||||
player.getInventory().clearContent();
|
||||
player.setShiftKeyDown(true);
|
||||
stand.getBlockState().useWithoutItem(level, player, new BlockHitResult(Vec3.atCenterOf(POS), Direction.UP, POS, false));
|
||||
player.setShiftKeyDown(false);
|
||||
if (!level.getBlockState(POS).is(Blocks.LECTERN)
|
||||
|| level.getBlockState(POS).getValue(LecternBlock.FACING) != facing
|
||||
|| player.getInventory().countItem(stored.getItem()) != 1
|
||||
|| !PortBookNetworking.validatedStandBook(player, packet).isEmpty())
|
||||
throw new IllegalStateException("Retrieval did not restore lectern or recover item exactly once");
|
||||
player.getInventory().clearContent();
|
||||
player.setItemInHand(hand, stored.copy());
|
||||
BookStandConversionHandler.convert(player, hand, POS);
|
||||
level.destroyBlock(POS, true);
|
||||
var drops = level.getEntitiesOfClass(net.minecraft.world.entity.item.ItemEntity.class,
|
||||
new net.minecraft.world.phys.AABB(-2, 62, -2, 3, 68, 3));
|
||||
int items = drops.stream().filter(e -> e.getItem().is(stored.getItem())).mapToInt(e -> e.getItem().getCount()).sum();
|
||||
int lecterns = drops.stream().filter(e -> e.getItem().is(Items.LECTERN)).mapToInt(e -> e.getItem().getCount()).sum();
|
||||
if (items != 1 || lecterns != 1) throw new IllegalStateException("Break drops duplicated/lost: " + items + "/" + lecterns);
|
||||
drops.forEach(net.minecraft.world.entity.Entity::discard);
|
||||
}
|
||||
level.setBlock(POS, Blocks.LECTERN.defaultBlockState().setValue(LecternBlock.HAS_BOOK, true), 3);
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, PageItem.createLinkPanelPage());
|
||||
if (BookStandConversionHandler.convert(player, InteractionHand.MAIN_HAND, POS)) throw new IllegalStateException("Occupied lectern converted");
|
||||
level.setBlock(POS, Blocks.LECTERN.defaultBlockState(), 3);
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.DIRT));
|
||||
if (BookStandConversionHandler.convert(player, InteractionHand.MAIN_HAND, POS)) throw new IllegalStateException("Unrelated item accepted");
|
||||
var named = new ItemStack(Rivencraft.LINKING_BOOK.get());
|
||||
named.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.literal("Stand persistence"));
|
||||
player.setItemInHand(InteractionHand.MAIN_HAND, named);
|
||||
if (!BookStandConversionHandler.convert(player, InteractionHand.MAIN_HAND, POS)) throw new IllegalStateException("Final stand missing");
|
||||
} finally { player.setShiftKeyDown(false); player.getInventory().clearContent(); }
|
||||
verify(level);
|
||||
}
|
||||
static void verify(ServerLevel level) {
|
||||
if (!(level.getBlockEntity(POS) instanceof BookStandBlockEntity stand)
|
||||
|| !stand.getBook().is(Rivencraft.LINKING_BOOK.get())
|
||||
|| !stand.getBook().getHoverName().getString().equals("Stand persistence"))
|
||||
throw new IllegalStateException("Stand restart persistence failed");
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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,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,25 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import net.minecraft.server.PlayerAdvancements;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.datafix.DataFixers;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.neoforged.neoforge.common.util.FakePlayer;
|
||||
|
||||
/** FakePlayer normally discards all advancement progress; fixtures need vanilla tracking. */
|
||||
final class PortProgressionPlayer extends FakePlayer {
|
||||
private PlayerAdvancements progress;
|
||||
|
||||
PortProgressionPlayer(ServerLevel level, GameProfile profile) { super(level, profile); }
|
||||
|
||||
@Override public PlayerAdvancements getAdvancements() {
|
||||
if (progress == null) {
|
||||
var server = level().getServer();
|
||||
progress = new PlayerAdvancements(DataFixers.getDataFixer(), server.getPlayerList(),
|
||||
server.getAdvancements(), server.getWorldPath(LevelResource.PLAYER_ADVANCEMENTS_DIR)
|
||||
.resolve(getUUID() + ".json"), this);
|
||||
}
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.advancement.ProgressionAdvancements;
|
||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||
import net.minecraft.core.BlockPos;
|
||||
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 operator fixture for loaded advancement data and committed action awards. */
|
||||
final class PortProgressionProbe {
|
||||
/** Operator-fixture assertion; deliberately requires the real loaded advancement. */
|
||||
static boolean isComplete(net.minecraft.server.level.ServerPlayer player, String path) {
|
||||
var advancement = player.level().getServer().getAdvancements().get(
|
||||
net.minecraft.resources.Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "progression/" + path));
|
||||
if (advancement == null) throw new IllegalStateException("Missing progression advancement: " + path);
|
||||
return player.getAdvancements().getOrStartProgress(advancement).isDone();
|
||||
}
|
||||
|
||||
|
||||
static void recipeDiscovery(ServerLevel level) {
|
||||
var materials = java.util.Map.of(
|
||||
"ink_vial", Items.DYE.black(),
|
||||
"ink_mixer", Items.GLASS_BOTTLE,
|
||||
"book_binder", Items.IRON_INGOT,
|
||||
"crystal_block", Rivencraft.CRYSTAL_SHARD.get(),
|
||||
"notebook", Items.LEATHER,
|
||||
"writing_desk", Items.FEATHER);
|
||||
for (var entry : materials.entrySet()) {
|
||||
var id = net.minecraft.resources.Identifier.fromNamespaceAndPath("rivencraft", entry.getKey());
|
||||
var key = net.minecraft.resources.ResourceKey.create(net.minecraft.core.registries.Registries.RECIPE, id);
|
||||
var player = new PortProgressionPlayer(level, new com.mojang.authlib.GameProfile(
|
||||
java.util.UUID.nameUUIDFromBytes(id.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8)),
|
||||
"RivenRecipe"));
|
||||
var advancement = level.getServer().getAdvancements().get(
|
||||
net.minecraft.resources.Identifier.fromNamespaceAndPath("rivencraft", "recipes/misc/" + entry.getKey()));
|
||||
if (advancement == null || level.getServer().getRecipeManager().byKey(key).isEmpty())
|
||||
throw new IllegalStateException("Missing recipe/discovery data: " + id);
|
||||
var progress = player.getAdvancements();
|
||||
net.minecraft.advancements.triggers.CriteriaTriggers.INVENTORY_CHANGED.trigger(player,
|
||||
player.getInventory(), new ItemStack(Items.COBBLESTONE));
|
||||
if (player.getRecipeBook().contains(key))
|
||||
throw new IllegalStateException("Wrong material unlocked " + id);
|
||||
var material = new ItemStack(entry.getValue());
|
||||
player.getInventory().setItem(0, material);
|
||||
net.minecraft.advancements.triggers.CriteriaTriggers.INVENTORY_CHANGED.trigger(player, player.getInventory(), material);
|
||||
if (!player.getRecipeBook().contains(key) || !progress.getOrStartProgress(advancement).isDone())
|
||||
throw new IllegalStateException("Material did not unlock " + id);
|
||||
// The second criterion supports /recipe give without first obtaining its material.
|
||||
var other = new PortProgressionPlayer(level, new com.mojang.authlib.GameProfile(
|
||||
java.util.UUID.randomUUID(), "RivenRecipeGive"));
|
||||
if (other.getRecipeBook().contains(key)) throw new IllegalStateException("Recipe leaked between players");
|
||||
other.awardRecipes(java.util.List.of(level.getServer().getRecipeManager().byKey(key).orElseThrow()));
|
||||
if (!other.getAdvancements().getOrStartProgress(advancement).isDone())
|
||||
throw new IllegalStateException("Recipe unlock criterion did not complete " + id);
|
||||
}
|
||||
}
|
||||
|
||||
static void run(ServerLevel level) {
|
||||
var player = new PortProgressionPlayer(level,
|
||||
new com.mojang.authlib.GameProfile(java.util.UUID.fromString("b428c3ef-57a6-43dc-95e8-441b6a171107"), "RivenProgression"));
|
||||
var pos = new BlockPos(4, 64, 0);
|
||||
player.setPos(4.5, 64, 2.5);
|
||||
level.setBlock(pos, Rivencraft.INK_MIXER.get().defaultBlockState(), 3);
|
||||
var mixer = (InkMixerBlockEntity) level.getBlockEntity(pos);
|
||||
try {
|
||||
// Every tree node must decode; portal advancement waits for its missing item/feature port.
|
||||
for (String path : new String[]{"root", "page", "linking_book", "link_panel", "copy_page", "bind_age", "enter_age"})
|
||||
if (isComplete(player, path))
|
||||
throw new IllegalStateException("Fixture requires fresh player advancement data");
|
||||
var menu = new InkMixerMenu(1, player.getInventory(), mixer);
|
||||
menu.clicked(InkMixerMenu.RESULT, 0, ContainerInput.PICKUP, player);
|
||||
if (isComplete(player, "link_panel"))
|
||||
throw new IllegalStateException("Empty Mixer awarded progression");
|
||||
mixer.setItem(InkMixerBlockEntity.PAPER, new ItemStack(Items.PAPER, 2));
|
||||
mixer.setItem(InkMixerBlockEntity.INK, new ItemStack(Rivencraft.INK_VIAL.get()));
|
||||
menu.broadcastChanges();
|
||||
if (isComplete(player, "link_panel"))
|
||||
throw new IllegalStateException("Mixer preview awarded progression");
|
||||
menu.clicked(InkMixerMenu.RESULT, 0, ContainerInput.PICKUP, player);
|
||||
if (!isComplete(player, "link_panel"))
|
||||
throw new IllegalStateException("Mixer pickup did not award progression");
|
||||
menu.setCarried(ItemStack.EMPTY);
|
||||
var advancement = level.getServer().getAdvancements().get(
|
||||
net.minecraft.resources.Identifier.fromNamespaceAndPath("rivencraft", "progression/link_panel"));
|
||||
player.getAdvancements().revoke(advancement, "completed");
|
||||
menu.quickMoveStack(player, InkMixerMenu.RESULT);
|
||||
if (!isComplete(player, "link_panel"))
|
||||
throw new IllegalStateException("Mixer shift pickup did not award progression");
|
||||
ProgressionAdvancements.awardAgeTravel(player);
|
||||
if (!isComplete(player, "enter_age"))
|
||||
throw new IllegalStateException("Registered Age did not award entry");
|
||||
} finally {
|
||||
player.getInventory().clearContent();
|
||||
level.removeBlock(pos, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,30 +19,18 @@ public final class PortRandomAgeSelection {
|
||||
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 = weighted(new Random(seed ^ 0x5445525241494E4CL),
|
||||
List.of(PortAgeTerrain.AMPLIFIED, PortAgeTerrain.CAVE, PortAgeTerrain.FLAT,
|
||||
PortAgeTerrain.ISLAND, PortAgeTerrain.STANDARD, PortAgeTerrain.VOID),
|
||||
new int[]{1, 1, 5, 1, 6, 1});
|
||||
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 = 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});
|
||||
PortBiomePlan.Distribution distribution = distribution(seed);
|
||||
List<Identifier> eligible = eligibleBiomes(server, terrain);
|
||||
if (eligible.size() < 2 && distribution != PortBiomePlan.Distribution.NATIVE)
|
||||
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||
// 26.3 has no native multi-cave biome source; choose a seeded Cave
|
||||
// biome rather than silently using Lush Caves for every random Age.
|
||||
if (terrain == PortAgeTerrain.CAVE && distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
return new Selection(terrain, new PortBiomePlan(distribution, List.of()));
|
||||
return new Selection(terrain, new PortBiomePlan(distribution,
|
||||
terrain == PortAgeTerrain.CAVE ? eligible : List.of()));
|
||||
|
||||
int count = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||
if (count > 1) {
|
||||
@@ -54,14 +42,30 @@ public final class PortRandomAgeSelection {
|
||||
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) {
|
||||
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
|
||||
.filter(PortRegistryPages::isCaveBiome)
|
||||
.map(holder -> holder.key().identifier())
|
||||
.sorted().toList();
|
||||
return caves.isEmpty() ? List.of(Identifier.withDefaultNamespace("lush_caves")) : caves;
|
||||
return caveBiomes(server.registryAccess());
|
||||
}
|
||||
List<Identifier> surface = server.overworld().getChunkSource().getGenerator().getBiomeSource()
|
||||
.possibleBiomes().stream().flatMap(holder -> holder.unwrapKey().stream())
|
||||
@@ -69,6 +73,12 @@ public final class PortRandomAgeSelection {
|
||||
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;
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ public final class PortRandomStructureSelection {
|
||||
}
|
||||
|
||||
private static Set<Identifier> actualBiomes(MinecraftServer server, PortAgeTerrain terrain, PortBiomePlan plan) {
|
||||
if (plan != null && plan.distribution() != PortBiomePlan.Distribution.NATIVE)
|
||||
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"));
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
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);
|
||||
var source = player.level().getServer().getLevel(net.minecraft.resources.ResourceKey.create(
|
||||
net.minecraft.core.registries.Registries.DIMENSION, departure.dimension()));
|
||||
if (source != null) book.set(Rivencraft.LINK_DESTINATION_NAME.get(),
|
||||
net.dnorberg.rivencraft.item.LinkingBookItem.destinationName(source));
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -95,11 +95,22 @@ public final class PortStructurePages {
|
||||
var set = registries.lookupOrThrow(Registries.STRUCTURE_SET)
|
||||
.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)));
|
||||
if (set.isEmpty()) return false;
|
||||
var compatible = set.get().value().structures().stream()
|
||||
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.toSet());
|
||||
return compatible.isEmpty() || biomes.stream().anyMatch(compatible::contains);
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
private static Map<Identifier, Integer> enabled(List<Identifier> pages) {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
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 = new PortProgressionPlayer(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");
|
||||
if (!PortProgressionProbe.isComplete(player, "copy_page"))
|
||||
throw new IllegalStateException("Missing copy_page award");
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -270,6 +270,18 @@ public final class RuntimeAgeBackend {
|
||||
Rivencraft.LOGGER.info("Retired 26.3 prototype Age {} (archived at {})", id, paths.archive());
|
||||
}
|
||||
|
||||
/** Display metadata never changes the dimension identity, seed or filesystem path. */
|
||||
public static java.util.Optional<String> savedAgeName(MinecraftServer server, Identifier id) {
|
||||
checkServerThread(server);
|
||||
return java.util.Optional.ofNullable(index(server).displayName(id));
|
||||
}
|
||||
|
||||
public static void setAgeDisplayName(MinecraftServer server, Identifier id, String name) {
|
||||
checkServerThread(server);
|
||||
index(server).setDisplayName(id, name);
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
}
|
||||
|
||||
public static Map<Identifier, Long> savedAges(MinecraftServer server) {
|
||||
checkServerThread(server);
|
||||
return index(server).entries();
|
||||
@@ -366,15 +378,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);
|
||||
@@ -399,9 +410,12 @@ public final class RuntimeAgeBackend {
|
||||
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);
|
||||
@@ -453,7 +467,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) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.util.Prediction;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
/** Vanilla lectern geometry with Rivencraft storage instead of a vanilla book menu. */
|
||||
public final class BookStandBlock extends LecternBlock {
|
||||
public BookStandBlock(Properties properties) { super(properties); }
|
||||
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return new BookStandBlockEntity(pos, state); }
|
||||
@Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) {
|
||||
if (!(level.getBlockEntity(pos) instanceof BookStandBlockEntity stand)) return InteractionResult.PASS;
|
||||
if (player.isShiftKeyDown() && !level.isClientSide()) {
|
||||
var book = stand.takeBook();
|
||||
level.setBlock(pos, Blocks.LECTERN.defaultBlockState().setValue(FACING, state.getValue(FACING)), 3);
|
||||
if (!player.getInventory().add(book)) player.drop(book, false, Prediction.SERVER_ONLY);
|
||||
level.playSound(null, pos, SoundEvents.BOOK_PAGE_TURN, SoundSource.BLOCKS, 1, 1);
|
||||
}
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
@Override protected net.minecraft.world.MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos) { return null; }
|
||||
@Override protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, net.minecraft.core.Direction direction) { return 0; }
|
||||
}
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
|
||||
/** One stored book or page, synchronized independently of the player's inventory. */
|
||||
public final class BookStandBlockEntity extends BlockEntity {
|
||||
private ItemStack book = ItemStack.EMPTY;
|
||||
public BookStandBlockEntity(BlockPos pos, BlockState state) { super(Rivencraft.BOOK_STAND_BLOCK_ENTITY.get(), pos, state); }
|
||||
public ItemStack getBook() { return book; }
|
||||
public void setBook(ItemStack stack) { book = stack; sync(); }
|
||||
public ItemStack takeBook() { ItemStack result = book; book = ItemStack.EMPTY; sync(); return result; }
|
||||
private void sync() {
|
||||
setChanged();
|
||||
if (level != null && !level.isClientSide()) level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 2);
|
||||
}
|
||||
@Override protected void loadAdditional(ValueInput input) {
|
||||
super.loadAdditional(input);
|
||||
book = input.read("Book", ItemStack.CODEC).orElse(ItemStack.EMPTY);
|
||||
}
|
||||
@Override protected void saveAdditional(ValueOutput output) {
|
||||
super.saveAdditional(output);
|
||||
if (!book.isEmpty()) output.store("Book", ItemStack.CODEC, book);
|
||||
}
|
||||
@Override public CompoundTag getUpdateTag(HolderLookup.Provider registries) { return saveWithoutMetadata(registries); }
|
||||
@Override public ClientboundBlockEntityDataPacket getUpdatePacket() { return ClientboundBlockEntityDataPacket.create(this); }
|
||||
@Override public void preRemoveSideEffects(BlockPos pos, BlockState oldState) {
|
||||
if (level != null && !level.isClientSide())
|
||||
net.minecraft.world.Containers.dropItemStack(level, pos.getX() + 0.5, pos.getY() + 1, pos.getZ() + 0.5, takeBook());
|
||||
super.preRemoveSideEffects(pos, oldState);
|
||||
}
|
||||
}
|
||||
+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;
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ public final class AgeCatalogClient {
|
||||
|
||||
@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());
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
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.block.BookStandBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.minecraft.client.gui.Font;
|
||||
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.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.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Extracted version of the 1.21.1 lectern display, retaining its final page orientation. */
|
||||
public final class BookStandRenderer implements BlockEntityRenderer<BookStandBlockEntity, BookStandRenderer.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;
|
||||
private final ItemModelResolver resolver;
|
||||
private final Font font;
|
||||
public BookStandRenderer(BlockEntityRendererProvider.Context context) {
|
||||
model = new BookModel(context.bakeLayer(ModelLayers.BOOK));
|
||||
resolver = context.itemModelResolver();
|
||||
font = context.font();
|
||||
}
|
||||
public static final class State extends BlockEntityRenderState {
|
||||
final ItemStackRenderState page = new ItemStackRenderState();
|
||||
boolean present, isPage, descriptive;
|
||||
float rotation;
|
||||
Component name;
|
||||
}
|
||||
@Override public State createRenderState() { return new State(); }
|
||||
@Override public void extractRenderState(BookStandBlockEntity stand, State state, float partialTick,
|
||||
Vec3 camera, ModelFeatureRenderer.CrumblingOverlay overlay) {
|
||||
BlockEntityRenderer.super.extractRenderState(stand, state, partialTick, camera, overlay);
|
||||
var book = stand.getBook();
|
||||
state.present = !book.isEmpty();
|
||||
state.isPage = book.is(Rivencraft.PAGE.get());
|
||||
state.descriptive = book.is(Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
state.name = state.present && !state.isPage ? book.getHoverName().copy() : null;
|
||||
Direction facing = stand.getBlockState().getValue(BookStandBlock.FACING);
|
||||
state.rotation = state.isPage ? facing.toYRot() - 180 + (facing.getAxis() == Direction.Axis.X ? 180 : 0)
|
||||
: -facing.getClockWise().toYRot();
|
||||
resolver.updateForTopItem(state.page, state.isPage ? book : net.minecraft.world.item.ItemStack.EMPTY,
|
||||
ItemDisplayContext.FIXED, stand.getLevel(), null, 0);
|
||||
}
|
||||
@Override public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) {
|
||||
if (!state.present) return;
|
||||
pose.pushPose();
|
||||
pose.translate(0.5F, 1.0625F, 0.5F);
|
||||
pose.rotateDegrees(Axis.YP, state.rotation);
|
||||
if (state.isPage) {
|
||||
pose.rotateDegrees(Axis.XP, 67.5F);
|
||||
pose.scale(0.7F, 0.7F, 0.7F);
|
||||
state.page.submit(pose, collector, state.lightCoords, OverlayTexture.NO_OVERLAY, 0);
|
||||
} else {
|
||||
pose.rotateDegrees(Axis.ZP, 67.5F);
|
||||
pose.translate(0, -0.125F, 0);
|
||||
collector.submitModel(model, BookModel.State.forAnimation(0, 0.1F, 0.9F, 1.2F), pose,
|
||||
state.descriptive ? DESCRIPTIVE : LINKING, state.lightCoords, OverlayTexture.NO_OVERLAY, 0);
|
||||
}
|
||||
pose.popPose();
|
||||
if (state.name != null) {
|
||||
pose.pushPose();
|
||||
pose.translate(0.5F, 1.6F, 0.5F);
|
||||
pose.rotate(camera.orientation);
|
||||
pose.scale(0.025F, -0.025F, 0.025F);
|
||||
collector.submitText(pose, -font.width(state.name) / 2.0F, 0, state.name.getVisualOrderText(),
|
||||
false, Font.DisplayMode.NORMAL, state.lightCoords, 0xFFFFFFFF, 0, 0);
|
||||
pose.popPose();
|
||||
}
|
||||
}
|
||||
}
|
||||
+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);
|
||||
}
|
||||
}
|
||||
+5
@@ -18,12 +18,17 @@ public final class PortBookBinderClientEvents {
|
||||
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);
|
||||
event.registerBlockEntityRenderer(Rivencraft.BOOK_STAND_BLOCK_ENTITY.get(), BookStandRenderer::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
|
||||
|
||||
+15
@@ -26,4 +26,19 @@ public final class PortBookInteractionHandler {
|
||||
? new PortLinkingBookScreen(event.getHand(), event.getItemStack().copy())
|
||||
: new PortDescriptiveBookScreen(event.getHand(), event.getItemStack().copy()));
|
||||
}
|
||||
@SubscribeEvent
|
||||
public static void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) {
|
||||
if (!event.getLevel().isClientSide() || event.getEntity().isShiftKeyDown()
|
||||
|| !event.getItemStack().isEmpty()
|
||||
|| !(event.getLevel().getBlockEntity(event.getPos()) instanceof net.dnorberg.rivencraft.block.entity.BookStandBlockEntity stand)) return;
|
||||
var book = stand.getBook();
|
||||
if (!book.is(Rivencraft.LINKING_BOOK.get()) && !book.is(Rivencraft.DESCRIPTIVE_BOOK.get())) return;
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(InteractionResult.SUCCESS);
|
||||
var source = new net.dnorberg.rivencraft.network.UseBookStandPayload(
|
||||
event.getLevel().dimension().identifier(), event.getPos(), book.copy());
|
||||
Minecraft.getInstance().setScreenAndShow(book.is(Rivencraft.LINKING_BOOK.get())
|
||||
? new PortLinkingBookScreen(source) : new PortDescriptiveBookScreen(source));
|
||||
}
|
||||
|
||||
}
|
||||
+88
-36
@@ -10,81 +10,111 @@ import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.neoforge.client.network.ClientPacketDistributor;
|
||||
|
||||
/** Temporary 26.3 descriptive-book interaction; authored pages and live previews remain unported. */
|
||||
/** Legacy book layout and authored glyph pages; destination rendering remains a port placeholder. */
|
||||
public final class PortDescriptiveBookScreen extends Screen {
|
||||
private static final int WIDTH = 327;
|
||||
private static final int HEIGHT = 199;
|
||||
private static final int TEXT_COLOR = 0xFF3B281B;
|
||||
private static final Identifier COVER = texture("bookui_cover.png");
|
||||
private static final Identifier LEFT_PAGE = texture("notebook_pages.png");
|
||||
private static final Identifier RIGHT_PANEL = texture("bookui_pager.png");
|
||||
|
||||
|
||||
private final InteractionHand hand;
|
||||
private final boolean bound;
|
||||
private final PortLegacyLinkPanel linkPanel = new PortLegacyLinkPanel();
|
||||
private final net.dnorberg.rivencraft.writing.DescriptiveBookPages pages;
|
||||
private int pageIndex;
|
||||
private net.dnorberg.rivencraft.network.UseBookStandPayload standSource;
|
||||
private EditBox titleInput;
|
||||
|
||||
public PortDescriptiveBookScreen(InteractionHand hand, ItemStack book) {
|
||||
super(book.getHoverName());
|
||||
this.hand = hand;
|
||||
pages = net.dnorberg.rivencraft.writing.DescriptiveBookPages.from(book);
|
||||
bound = book.has(Rivencraft.AGE_BOOK_ID.get());
|
||||
}
|
||||
|
||||
public PortDescriptiveBookScreen(net.dnorberg.rivencraft.network.UseBookStandPayload source) {
|
||||
this(InteractionHand.MAIN_HAND, source.book());
|
||||
standSource = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
if (bound) return;
|
||||
if (bound || standSource != null) return;
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
titleInput = new EditBox(font, left + 40, top + 69, 110, 20,
|
||||
Component.literal("Age name"));
|
||||
titleInput.setMaxLength(PortBookNetworking.MAX_BOOK_TITLE_LENGTH);
|
||||
titleInput.setHint(Component.literal("Name this Age"));
|
||||
titleInput.setHint(Component.literal(standSource == null ? "Name this Age" : "Remove book to bind"));
|
||||
addRenderableWidget(titleInput);
|
||||
setInitialFocus(titleInput);
|
||||
addRenderableWidget(Button.builder(Component.literal("Bind"), button -> bind())
|
||||
.bounds(left + 40, top + 100, 53, 20).build());
|
||||
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> onClose())
|
||||
.bounds(left + 97, top + 100, 53, 20).build());
|
||||
updatePageControls();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
blit(graphics, COVER, left, top + 7, 34, 192, 152, 0);
|
||||
blit(graphics, COVER, left + 34, top + 7, 103, 192, 49, 0);
|
||||
blit(graphics, COVER, left + 137, top + 7, 4, 192, 45, 0);
|
||||
blit(graphics, COVER, left + 141, top + 7, 186, 192, 0, 0);
|
||||
blit(graphics, LEFT_PAGE, left + 7, top, 156, 195, 0, 0);
|
||||
blit(graphics, RIGHT_PANEL, left + 163, top, 156, 195, 0, 0);
|
||||
graphics.text(font, title, left + 42, top + 29, TEXT_COLOR, false);
|
||||
if (bound) {
|
||||
graphics.fill(left + 173, top + 20, left + 305, top + 103, 0xFF15141D);
|
||||
graphics.text(font, Component.literal("Age preview unavailable"), left + 181, top + 47,
|
||||
0xFFE9DFCB, false);
|
||||
graphics.text(font, Component.literal("Click to enter"), left + 205, top + 65,
|
||||
0xFFE9DFCB, false);
|
||||
} else {
|
||||
graphics.text(font, Component.literal("Bind a prototype Age"), left + 183, top + 39,
|
||||
TEXT_COLOR, false);
|
||||
PortLegacyBookArtwork.drawCover(graphics, left, top, true);
|
||||
if (pageIndex > 0) PortLegacyBookArtwork.drawLeftPage(graphics, left, top);
|
||||
if (pageIndex == 0) {
|
||||
if (bound) {
|
||||
graphics.nextStratum();
|
||||
linkPanel.render(graphics, left, top, partialTick);
|
||||
graphics.nextStratum();
|
||||
}
|
||||
PortLegacyBookArtwork.drawPanelPage(graphics, left, top);
|
||||
if (bound) {
|
||||
graphics.enableScissor(left + 38, top + 38, left + 154, top + 174);
|
||||
int textY = PortLegacyBookArtwork.drawText(graphics, font, title, left + 40, top + 40, 110, TEXT_COLOR);
|
||||
Component author = pages.author().isBlank()
|
||||
? Component.translatable("screen.rivencraft.age.unknown_author") : Component.literal(pages.author());
|
||||
PortLegacyBookArtwork.drawText(graphics, font, author, left + 50, textY + 2, 100, TEXT_COLOR);
|
||||
graphics.disableScissor();
|
||||
} else {
|
||||
graphics.text(font, title, left + 42, top + 29, TEXT_COLOR, false);
|
||||
graphics.text(font, Component.literal(standSource == null ? "Bind a prototype Age" : "Remove book to bind"),
|
||||
left + 183, top + 39, TEXT_COLOR, false);
|
||||
}
|
||||
} else if (pages.symbolAt(pageIndex).isPresent()) {
|
||||
PortLegacyBookArtwork.drawFullPage(graphics, left, top);
|
||||
graphics.nextStratum();
|
||||
var symbol = pages.symbolAt(pageIndex).orElseThrow();
|
||||
ItemStack glyph = new ItemStack(Rivencraft.PAGE.get());
|
||||
glyph.set(Rivencraft.SYMBOL_PAGE_ID.get(), symbol);
|
||||
glyph.set(Rivencraft.PAGE_GLYPH_ONLY.get(), true);
|
||||
graphics.enableScissor(left + 174, top + 26, left + 306, top + 172);
|
||||
graphics.pose().pushMatrix();
|
||||
graphics.pose().translate(left + 177, top + 29);
|
||||
graphics.pose().scale(8.0F, 8.0F);
|
||||
graphics.fakeItem(glyph, 0, 0);
|
||||
graphics.pose().popMatrix();
|
||||
graphics.disableScissor();
|
||||
}
|
||||
graphics.nextStratum();
|
||||
PortLegacyBookArtwork.drawCounter(graphics, font, left, top, pageIndex, pages.lastPage());
|
||||
super.extractRenderState(graphics, mouseX, mouseY, partialTick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
if (bound && event.button() == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
if (event.button() == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
if (event.x() >= left + 173 && event.x() < left + 305
|
||||
&& event.y() >= top + 20 && event.y() < top + 103) {
|
||||
ClientPacketDistributor.sendToServer(new UseDescriptiveBookPayload(hand == InteractionHand.OFF_HAND));
|
||||
int direction = PortLegacyBookArtwork.turnDirection(event.x() - left, event.y() - top, pageIndex, pages.lastPage());
|
||||
if (direction != 0) { turnPage(direction); return true; }
|
||||
if (bound && pageIndex == 0 && PortLegacyLinkPanel.contains(event.x() - left, event.y() - top)) {
|
||||
if (standSource != null) ClientPacketDistributor.sendToServer(standSource);
|
||||
else ClientPacketDistributor.sendToServer(new UseDescriptiveBookPayload(hand == InteractionHand.OFF_HAND));
|
||||
onClose();
|
||||
return true;
|
||||
}
|
||||
@@ -92,6 +122,36 @@ public final class PortDescriptiveBookScreen extends Screen {
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
private void turnPage(int direction) {
|
||||
int next = pages.turn(pageIndex, direction);
|
||||
if (next == pageIndex) return;
|
||||
pageIndex = next;
|
||||
minecraft.getSoundManager().play(net.minecraft.client.resources.sounds.SimpleSoundInstance.forUI(
|
||||
net.minecraft.sounds.SoundEvents.BOOK_PAGE_TURN, 1.0F, 0.65F));
|
||||
updatePageControls();
|
||||
}
|
||||
|
||||
private void updatePageControls() {
|
||||
for (var widget : renderables) if (widget instanceof net.minecraft.client.gui.components.AbstractWidget control)
|
||||
control.visible = pageIndex == 0;
|
||||
}
|
||||
|
||||
@Override public boolean keyPressed(net.minecraft.client.input.KeyEvent event) {
|
||||
if (event.isEscape()) return super.keyPressed(event);
|
||||
if (titleInput == null || !titleInput.isFocused() || !titleInput.visible) {
|
||||
if (event.isLeft()) { turnPage(-1); return true; }
|
||||
if (event.isRight()) { turnPage(1); return true; }
|
||||
} else {
|
||||
// Keep inventory/hotbar actions out of a focused name field.
|
||||
return titleInput.keyPressed(event);
|
||||
}
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
|
||||
@Override public void tick() {
|
||||
if (bound) linkPanel.tick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPauseScreen() {
|
||||
return false;
|
||||
@@ -105,12 +165,4 @@ public final class PortDescriptiveBookScreen extends Screen {
|
||||
onClose();
|
||||
}
|
||||
|
||||
private static Identifier texture(String filename) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/gui/" + filename);
|
||||
}
|
||||
|
||||
private static void blit(GuiGraphicsExtractor graphics, Identifier texture, int x, int y,
|
||||
int width, int height, int u, int v) {
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, x, y, u, v, width, height, 256, 256);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Original Mystcraft Legacy book atlases and page composition (LGPL-3.0,
|
||||
* copyright XCompWiz, revision 9bc8ddc061845df0cd5ea47f4fdef773cff786ae). */
|
||||
final class PortLegacyBookArtwork {
|
||||
private static final int ATLAS_SIZE = 256;
|
||||
private static final Identifier COVER = texture("bookui_cover.png");
|
||||
private static final Identifier LEFT_PAGE = texture("notebook_pages.png");
|
||||
private static final Identifier RIGHT_PANEL = texture("bookui_pager.png");
|
||||
private static final Identifier RIGHT_FULL = texture("bookui_rpage_full.png");
|
||||
|
||||
private PortLegacyBookArtwork() {}
|
||||
|
||||
static void drawCover(GuiGraphicsExtractor graphics, int left, int top, boolean gilded) {
|
||||
blit(graphics, COVER, left, top + 7, 34, 192, 152, 0);
|
||||
blit(graphics, COVER, left + 34, top + 7, 103, 192, 49, 0);
|
||||
blit(graphics, COVER, left + 137, top + 7, 4, 192, 45, 0);
|
||||
blit(graphics, COVER, left + 141, top + 7, 186, 192, 0, 0);
|
||||
if (gilded) {
|
||||
blit(graphics, COVER, left, top + 7, 34, 192, 186, 0);
|
||||
blit(graphics, COVER, left + 293, top + 7, 34, 192, 186, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void drawLeftPage(GuiGraphicsExtractor graphics, int left, int top) {
|
||||
blit(graphics, LEFT_PAGE, left + 7, top, 156, 195, 0, 0);
|
||||
}
|
||||
|
||||
static void drawPanelPage(GuiGraphicsExtractor graphics, int left, int top) {
|
||||
blit(graphics, RIGHT_PANEL, left + 163, top, 156, 195, 0, 0);
|
||||
}
|
||||
|
||||
static void drawFullPage(GuiGraphicsExtractor graphics, int left, int top) {
|
||||
blit(graphics, RIGHT_FULL, left + 163, top, 156, 195, 0, 0);
|
||||
}
|
||||
|
||||
static int drawText(GuiGraphicsExtractor graphics, net.minecraft.client.gui.Font font,
|
||||
net.minecraft.network.chat.Component text, int x, int y, int width, int color) {
|
||||
for (var line : font.split(text, width)) {
|
||||
graphics.text(font, line, x, y, color, false);
|
||||
y += font.lineHeight;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
static void drawCounter(GuiGraphicsExtractor graphics, net.minecraft.client.gui.Font font,
|
||||
int left, int top, int page, int last) {
|
||||
String counter = page + "/" + last;
|
||||
graphics.text(font, counter, left + 165 - font.width(counter) / 2, top + 185, 0xFF000000, false);
|
||||
}
|
||||
|
||||
static int turnDirection(double x, double y, int page, int last) {
|
||||
if (y < 164 || y >= 195) return 0;
|
||||
if (page > 0 && x >= 7 && x < 35) return -1;
|
||||
return page < last && x >= 293 && x < 320 ? 1 : 0;
|
||||
}
|
||||
|
||||
private static Identifier texture(String filename) {
|
||||
return Identifier.fromNamespaceAndPath("rivencraft", "textures/gui/" + filename);
|
||||
}
|
||||
|
||||
private static void blit(GuiGraphicsExtractor graphics, Identifier texture, int x, int y,
|
||||
int width, int height, int u, int v) {
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, x, y, u, v, width, height, ATLAS_SIZE, ATLAS_SIZE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
|
||||
/**
|
||||
* Built-in linking-panel animation inspired by Mystcraft Legacy's portal bloom.
|
||||
*
|
||||
* <p>The original effect used a GLSL noise shader over an optional live world
|
||||
* view. This port retains the 1.21.1 fallback's two noisy spirals and default teal
|
||||
* tint, using the normal GUI API without a renderer mod.</p>
|
||||
*/
|
||||
final class PortLegacyLinkPanel {
|
||||
static final int X = 173, Y = 20, WIDTH = 132, HEIGHT = 83;
|
||||
private static final int CELL_SIZE = 3;
|
||||
private static final int BORDER_COLOR = 0xFF7B6AA8;
|
||||
private int ticks;
|
||||
private static final int LINK_COLOR = 0xFF006666;
|
||||
|
||||
static boolean contains(double x, double y) {
|
||||
return x >= X && x < X + WIDTH && y >= Y && y < Y + HEIGHT;
|
||||
}
|
||||
|
||||
void render(GuiGraphicsExtractor graphics, int bookLeft, int bookTop, float partialTick) {
|
||||
render(graphics, bookLeft + X, bookTop + Y, WIDTH, HEIGHT, partialTick);
|
||||
}
|
||||
|
||||
void tick() {
|
||||
ticks++;
|
||||
}
|
||||
|
||||
private void render(GuiGraphicsExtractor graphics, int left, int top, int width, int height, float partialTick) {
|
||||
graphics.fill(left, top, left + width, top + height, 0xFF000011);
|
||||
|
||||
float time = (ticks + partialTick) * 0.035F;
|
||||
float reveal = Math.min(1.0F, time / 1.8F);
|
||||
for (int y = 1; y < height - 1; y += CELL_SIZE) {
|
||||
for (int x = 1; x < width - 1; x += CELL_SIZE) {
|
||||
float normalizedX = (x - width * 0.5F) / height;
|
||||
float normalizedY = (y - height * 0.5F) / height;
|
||||
float first = spiral(normalizedX, normalizedY, time, 0.0F);
|
||||
float second = spiral(normalizedX, normalizedY, time, 2.35F);
|
||||
float intensity = Math.max(first, second) * reveal;
|
||||
if (intensity <= 0.035F) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int color = scaledColor(LINK_COLOR, Math.min(1.0F, 0.12F + intensity));
|
||||
graphics.fill(left + x, top + y,
|
||||
left + Math.min(width - 1, x + CELL_SIZE),
|
||||
top + Math.min(height - 1, y + CELL_SIZE), color);
|
||||
}
|
||||
}
|
||||
graphics.outline(left, top, width, height, BORDER_COLOR);
|
||||
}
|
||||
|
||||
private static float spiral(float x, float y, float time, float phase) {
|
||||
float distance = (float)Math.sqrt(x * x + y * y);
|
||||
float angle = (float)Math.atan2(y, x);
|
||||
float arm = 0.5F + 0.5F * (float)Math.cos(angle * 3.0F - distance * 25.0F + time * 4.0F + phase);
|
||||
float pulse = 0.72F + 0.28F * noise(x * 31.0F, y * 31.0F, time + phase);
|
||||
float core = Math.max(0.0F, 1.0F - distance * 2.15F);
|
||||
return core * (0.28F + arm * 0.72F) * pulse;
|
||||
}
|
||||
|
||||
private static float noise(float x, float y, float time) {
|
||||
double value = Math.sin(x * 12.9898D + y * 78.233D + time * 19.19D) * 43758.5453D;
|
||||
return (float)(value - Math.floor(value));
|
||||
}
|
||||
|
||||
private static int scaledColor(int color, float scale) {
|
||||
int red = Math.min(255, Math.round(((color >> 16) & 0xFF) * scale + 18.0F * scale));
|
||||
int green = Math.min(255, Math.round(((color >> 8) & 0xFF) * scale + 24.0F * scale));
|
||||
int blue = Math.min(255, Math.round((color & 0xFF) * scale + 42.0F * scale));
|
||||
return 0xFF000000 | red << 16 | green << 8 | blue;
|
||||
}
|
||||
}
|
||||
+82
-33
@@ -10,35 +10,48 @@ import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.neoforge.client.network.ClientPacketDistributor;
|
||||
|
||||
/** Minimal 26.3 book interaction using the Legacy artwork; live preview is not ported yet. */
|
||||
/** Legacy Linking Book front and final spreads with the shared fallback panel. */
|
||||
public final class PortLinkingBookScreen extends Screen {
|
||||
private static final int WIDTH = 327;
|
||||
private static final int HEIGHT = 199;
|
||||
private static final int TEXT_COLOR = 0xFF3B281B;
|
||||
private static final Identifier COVER = texture("bookui_cover.png");
|
||||
private static final Identifier LEFT_PAGE = texture("notebook_pages.png");
|
||||
private static final Identifier RIGHT_PANEL = texture("bookui_pager.png");
|
||||
|
||||
|
||||
private final InteractionHand hand;
|
||||
private final boolean bound;
|
||||
private final net.dnorberg.rivencraft.item.LinkBookTarget target;
|
||||
private final boolean returnLink;
|
||||
private final Component destination;
|
||||
private int pageIndex;
|
||||
private final PortLegacyLinkPanel linkPanel = new PortLegacyLinkPanel();
|
||||
private net.dnorberg.rivencraft.network.UseBookStandPayload standSource;
|
||||
private EditBox titleInput;
|
||||
|
||||
public PortLinkingBookScreen(InteractionHand hand, ItemStack book) {
|
||||
super(book.getHoverName());
|
||||
this.hand = hand;
|
||||
bound = book.has(Rivencraft.LINK_BOOK_TARGET.get());
|
||||
target = book.get(Rivencraft.LINK_BOOK_TARGET.get());
|
||||
bound = target != null;
|
||||
destination = bound ? net.dnorberg.rivencraft.item.LinkingBookItem.destinationName(book, target) : Component.empty();
|
||||
Component customName = book.get(net.minecraft.core.component.DataComponents.CUSTOM_NAME);
|
||||
returnLink = customName != null
|
||||
&& customName.getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents translated
|
||||
&& translated.getKey().equals("item.rivencraft.linking_book.return");
|
||||
}
|
||||
|
||||
public PortLinkingBookScreen(net.dnorberg.rivencraft.network.UseBookStandPayload source) {
|
||||
this(InteractionHand.MAIN_HAND, source.book());
|
||||
standSource = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
if (bound) return;
|
||||
if (bound || standSource != null) return;
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
titleInput = new EditBox(font, left + 40, top + 69, 110, 20,
|
||||
@@ -51,38 +64,55 @@ public final class PortLinkingBookScreen extends Screen {
|
||||
.bounds(left + 40, top + 100, 53, 20).build());
|
||||
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> onClose())
|
||||
.bounds(left + 97, top + 100, 53, 20).build());
|
||||
updatePageControls();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
blit(graphics, COVER, left, top + 7, 34, 192, 152, 0);
|
||||
blit(graphics, COVER, left + 34, top + 7, 103, 192, 49, 0);
|
||||
blit(graphics, COVER, left + 137, top + 7, 4, 192, 45, 0);
|
||||
blit(graphics, COVER, left + 141, top + 7, 186, 192, 0, 0);
|
||||
if (bound) blit(graphics, LEFT_PAGE, left + 7, top, 156, 195, 0, 0);
|
||||
blit(graphics, RIGHT_PANEL, left + 163, top, 156, 195, 0, 0);
|
||||
graphics.text(font, title, left + 42, top + 29, TEXT_COLOR, false);
|
||||
if (bound) {
|
||||
graphics.fill(left + 173, top + 20, left + 305, top + 103, 0xFF15141D);
|
||||
graphics.text(font, Component.literal("Destination preview"), left + 185, top + 47, 0xFFE9DFCB, false);
|
||||
graphics.text(font, Component.literal("Click to link"), left + 205, top + 65, 0xFFE9DFCB, false);
|
||||
PortLegacyBookArtwork.drawCover(graphics, left, top, false);
|
||||
if (pageIndex > 0) {
|
||||
PortLegacyBookArtwork.drawLeftPage(graphics, left, top);
|
||||
} else {
|
||||
graphics.text(font, Component.literal("Name this Linking Book"), left + 183, top + 39,
|
||||
TEXT_COLOR, false);
|
||||
if (bound) {
|
||||
graphics.nextStratum();
|
||||
linkPanel.render(graphics, left, top, partialTick);
|
||||
graphics.nextStratum();
|
||||
}
|
||||
PortLegacyBookArtwork.drawPanelPage(graphics, left, top);
|
||||
graphics.enableScissor(left + 38, top + 38, left + 154, top + 174);
|
||||
if (bound) {
|
||||
int y = top + 40;
|
||||
if (!returnLink) y = PortLegacyBookArtwork.drawText(graphics, font, title,
|
||||
left + 40, y, 110, TEXT_COLOR) + 4;
|
||||
y = PortLegacyBookArtwork.drawText(graphics, font, destination, left + 50, y, 100, TEXT_COLOR) + 10;
|
||||
var pos = net.minecraft.core.BlockPos.containing(target.x(), target.y(), target.z());
|
||||
PortLegacyBookArtwork.drawText(graphics, font,
|
||||
Component.literal(pos.getX() + ", " + pos.getY() + ", " + pos.getZ()),
|
||||
left + 40, y, 110, TEXT_COLOR);
|
||||
} else {
|
||||
PortLegacyBookArtwork.drawText(graphics, font, Component.literal(
|
||||
standSource == null ? "Name this Linking Book" : "Remove book to bind"),
|
||||
left + 40, top + 43, 110, TEXT_COLOR);
|
||||
}
|
||||
graphics.disableScissor();
|
||||
}
|
||||
graphics.nextStratum();
|
||||
PortLegacyBookArtwork.drawCounter(graphics, font, left, top, pageIndex, 1);
|
||||
super.extractRenderState(graphics, mouseX, mouseY, partialTick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
if (bound && event.button() == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
if (event.button() == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
if (event.x() >= left + 173 && event.x() < left + 305
|
||||
&& event.y() >= top + 20 && event.y() < top + 103) {
|
||||
ClientPacketDistributor.sendToServer(new UseLinkingBookPayload(hand == InteractionHand.OFF_HAND));
|
||||
int direction = PortLegacyBookArtwork.turnDirection(event.x() - left, event.y() - top, pageIndex, 1);
|
||||
if (direction != 0) { turnPage(direction); return true; }
|
||||
if (bound && pageIndex == 0 && PortLegacyLinkPanel.contains(event.x() - left, event.y() - top)) {
|
||||
if (standSource != null) ClientPacketDistributor.sendToServer(standSource);
|
||||
else ClientPacketDistributor.sendToServer(new UseLinkingBookPayload(hand == InteractionHand.OFF_HAND));
|
||||
onClose();
|
||||
return true;
|
||||
}
|
||||
@@ -90,6 +120,33 @@ public final class PortLinkingBookScreen extends Screen {
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
private void turnPage(int direction) {
|
||||
int next = Math.clamp(pageIndex + direction, 0, 1);
|
||||
if (next == pageIndex) return;
|
||||
pageIndex = next;
|
||||
minecraft.getSoundManager().play(net.minecraft.client.resources.sounds.SimpleSoundInstance.forUI(
|
||||
net.minecraft.sounds.SoundEvents.BOOK_PAGE_TURN, 1.0F, 0.65F));
|
||||
updatePageControls();
|
||||
}
|
||||
|
||||
private void updatePageControls() {
|
||||
for (var widget : renderables) if (widget instanceof net.minecraft.client.gui.components.AbstractWidget control)
|
||||
control.visible = pageIndex == 0;
|
||||
}
|
||||
|
||||
@Override public boolean keyPressed(net.minecraft.client.input.KeyEvent event) {
|
||||
if (event.isEscape()) return super.keyPressed(event);
|
||||
if (titleInput == null || !titleInput.isFocused() || !titleInput.visible) {
|
||||
if (event.isLeft()) { turnPage(-1); return true; }
|
||||
if (event.isRight()) { turnPage(1); return true; }
|
||||
} else return titleInput.keyPressed(event);
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
|
||||
@Override public void tick() {
|
||||
if (bound) linkPanel.tick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPauseScreen() {
|
||||
return false;
|
||||
@@ -103,12 +160,4 @@ public final class PortLinkingBookScreen extends Screen {
|
||||
onClose();
|
||||
}
|
||||
|
||||
private static Identifier texture(String filename) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/gui/" + filename);
|
||||
}
|
||||
|
||||
private static void blit(GuiGraphicsExtractor graphics, Identifier texture, int x, int y,
|
||||
int width, int height, int u, int v) {
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, x, y, u, v, width, height, 256, 256);
|
||||
}
|
||||
}
|
||||
@@ -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,62 @@
|
||||
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);
|
||||
}
|
||||
|
||||
/** Conversion preserves ordinary toss/drop state; the travel constructor has different placement rules. */
|
||||
public static DroppedBookEntity fromDrop(ItemEntity source) {
|
||||
var book = new DroppedBookEntity(Rivencraft.DROPPED_BOOK.get(), source.level());
|
||||
book.restoreFrom(source);
|
||||
// The original remains registered until replacement succeeds, so this needs a new identity.
|
||||
book.setUUID(java.util.UUID.randomUUID());
|
||||
book.setItem(source.getItem().copy());
|
||||
book.setUnlimitedLifetime();
|
||||
return book;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package net.dnorberg.rivencraft.event;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.BookStandBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
|
||||
|
||||
public final class BookStandConversionHandler {
|
||||
private BookStandConversionHandler() {}
|
||||
public static void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) {
|
||||
var book = event.getItemStack();
|
||||
if (!book.is(Rivencraft.LINKING_BOOK.get()) && !book.is(Rivencraft.DESCRIPTIVE_BOOK.get()) && !book.is(Rivencraft.PAGE.get())) return;
|
||||
var state = event.getLevel().getBlockState(event.getPos());
|
||||
if (!state.is(Blocks.LECTERN) || state.getValue(LecternBlock.HAS_BOOK)) return;
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(InteractionResult.SUCCESS);
|
||||
if (!event.getLevel().isClientSide()) convert(event.getEntity(), event.getHand(), event.getPos());
|
||||
}
|
||||
public static boolean convert(Player player, InteractionHand hand, BlockPos pos) {
|
||||
var level = player.level();
|
||||
var state = level.getBlockState(pos);
|
||||
var book = player.getItemInHand(hand);
|
||||
if (level.isClientSide() || !state.is(Blocks.LECTERN) || state.getValue(LecternBlock.HAS_BOOK)
|
||||
|| (!book.is(Rivencraft.LINKING_BOOK.get()) && !book.is(Rivencraft.DESCRIPTIVE_BOOK.get()) && !book.is(Rivencraft.PAGE.get()))) return false;
|
||||
if (!level.setBlock(pos, Rivencraft.BOOK_STAND.get().defaultBlockState()
|
||||
.setValue(BookStandBlock.FACING, state.getValue(LecternBlock.FACING)).setValue(BookStandBlock.HAS_BOOK, true), 3)) return false;
|
||||
if (!(level.getBlockEntity(pos) instanceof BookStandBlockEntity stand)) return false;
|
||||
stand.setBook(book.consumeAndReturn(1, player));
|
||||
level.playSound(null, pos, SoundEvents.BOOK_PUT, SoundSource.BLOCKS, 1, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package net.dnorberg.rivencraft.event;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.entity.DroppedBookEntity;
|
||||
import net.minecraft.server.TickTask;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.bus.api.EventPriority;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent;
|
||||
|
||||
/** Converts ordinary book drops from every source, including entities restored from disk. */
|
||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID)
|
||||
public final class DroppedBookConversionHandler {
|
||||
private DroppedBookConversionHandler() {}
|
||||
|
||||
@SubscribeEvent(priority = EventPriority.LOWEST)
|
||||
public static void onJoin(EntityJoinLevelEvent event) {
|
||||
if (!(event.getLevel() instanceof ServerLevel level)
|
||||
|| !(event.getEntity() instanceof ItemEntity item) || item instanceof DroppedBookEntity
|
||||
|| !isBook(item.getItem())) return;
|
||||
// Join events can run before chunk promotion. Do not add another entity inside that callback.
|
||||
// Keep the original until the queued replacement has actually been accepted.
|
||||
level.getServer().schedule(new TickTask(0, () -> {
|
||||
if (level.getServer().getLevel(level.dimension()) != level || item.isRemoved()
|
||||
|| !item.isAddedToLevel() || item.level() != level || !isBook(item.getItem())) return;
|
||||
var replacement = DroppedBookEntity.fromDrop(item);
|
||||
if (level.addFreshEntity(replacement)) item.discard();
|
||||
}));
|
||||
}
|
||||
|
||||
private static boolean isBook(ItemStack stack) {
|
||||
return stack.is(Rivencraft.LINKING_BOOK.get()) || stack.is(Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -102,13 +102,37 @@ public final class LinkingBookItem extends Item {
|
||||
}
|
||||
}
|
||||
|
||||
public static String destinationName(ServerLevel level) {
|
||||
var id = level.dimension().identifier();
|
||||
return net.dnorberg.rivencraft.age.RuntimeAgeBackend.savedAgeName(level.getServer(), id)
|
||||
.orElseGet(() -> {
|
||||
if (id.equals(Level.OVERWORLD.identifier())) return "Overworld";
|
||||
if (id.equals(Level.NETHER.identifier())) return "The Nether";
|
||||
if (id.equals(Level.END.identifier())) return "The End";
|
||||
return id.toString();
|
||||
});
|
||||
}
|
||||
|
||||
public static Component destinationName(ItemStack book, LinkBookTarget target) {
|
||||
String name = book.get(Rivencraft.LINK_DESTINATION_NAME.get());
|
||||
if (name != null && !name.isBlank()) return Component.literal(name);
|
||||
var id = target.dimension();
|
||||
if (id.equals(Level.OVERWORLD.identifier())) return Component.translatable("dimension.minecraft.overworld");
|
||||
if (id.equals(Level.NETHER.identifier())) return Component.translatable("dimension.minecraft.the_nether");
|
||||
if (id.equals(Level.END.identifier())) return Component.translatable("dimension.minecraft.the_end");
|
||||
return Component.literal(id.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
LinkBookTarget target = stack.get(Rivencraft.LINK_BOOK_TARGET.get());
|
||||
tooltip.accept(Component.literal(target == null ? "Unbound (26.3 port preview)"
|
||||
: "Destination: " + target.dimension() + " (" + (int) target.x() + ", "
|
||||
+ (int) target.y() + ", " + (int) target.z() + ")"));
|
||||
if (target == null) tooltip.accept(Component.literal("Unbound (26.3 port preview)"));
|
||||
else {
|
||||
BlockPos pos = BlockPos.containing(target.x(), target.y(), target.z());
|
||||
tooltip.accept(Component.translatable("item.rivencraft.linking_book.destination",
|
||||
destinationName(stack, target).getString(), pos.getX(), pos.getY(), pos.getZ()));
|
||||
}
|
||||
LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||
if (properties.has(LinkProperties.DISARM))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.disarm"));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -42,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);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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;
|
||||
net.dnorberg.rivencraft.advancement.ProgressionAdvancements.awardAgeTravel(player);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package net.dnorberg.rivencraft.menu;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.advancement.ProgressionAdvancements;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.world.Container;
|
||||
@@ -110,6 +112,8 @@ public final class InkMixerMenu extends AbstractContainerMenu {
|
||||
if (!moveItemStackTo(remainder, PLAYER_START, slots.size(), true) || !remainder.isEmpty())
|
||||
return ItemStack.EMPTY;
|
||||
if (!mixer.consumeLinkPanelMaterials()) return ItemStack.EMPTY;
|
||||
if (player instanceof ServerPlayer serverPlayer)
|
||||
ProgressionAdvancements.award(serverPlayer, ProgressionAdvancements.LINK_PANEL);
|
||||
refreshResult();
|
||||
return output;
|
||||
}
|
||||
@@ -150,6 +154,8 @@ public final class InkMixerMenu extends AbstractContainerMenu {
|
||||
}
|
||||
@Override public void onTake(Player player, ItemStack stack) {
|
||||
if (!clientSide && mixer.consumeLinkPanelMaterials()) {
|
||||
if (player instanceof ServerPlayer serverPlayer)
|
||||
ProgressionAdvancements.award(serverPlayer, ProgressionAdvancements.LINK_PANEL);
|
||||
refreshResult();
|
||||
}
|
||||
super.onTake(player, stack);
|
||||
|
||||
@@ -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)
|
||||
@@ -110,6 +118,7 @@ public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
var id = PortBookNetworking.createAge(player, title, blockEntity.symbolPages());
|
||||
if (id == null) return;
|
||||
book.set(Rivencraft.AGE_BOOK_ID.get(), id);
|
||||
book.set(Rivencraft.BOOK_AUTHOR.get(), player.getGameProfile().name());
|
||||
book.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
if (blockEntity.hasSymbolPages())
|
||||
book.set(Rivencraft.BOOK_SYMBOLS.get(), blockEntity.symbolPages());
|
||||
@@ -121,6 +130,8 @@ public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
binder.setItem(PortBookBinderBlockEntity.PANEL, panel);
|
||||
blockEntity.clearSymbolPages();
|
||||
setCarried(book);
|
||||
net.dnorberg.rivencraft.advancement.ProgressionAdvancements.award(player,
|
||||
net.dnorberg.rivencraft.advancement.ProgressionAdvancements.BIND_AGE);
|
||||
broadcastChanges();
|
||||
}
|
||||
|
||||
@@ -145,6 +156,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,469 @@
|
||||
package net.dnorberg.rivencraft.menu;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.advancement.ProgressionAdvancements;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
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;
|
||||
if (player instanceof ServerPlayer serverPlayer)
|
||||
ProgressionAdvancements.award(serverPlayer, ProgressionAdvancements.COPY_PAGE);
|
||||
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()) {
|
||||
if (player instanceof ServerPlayer serverPlayer)
|
||||
ProgressionAdvancements.award(serverPlayer, ProgressionAdvancements.COPY_PAGE);
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,15 @@ public final class PortBookNetworking {
|
||||
|
||||
public static void registerPayloads(RegisterPayloadHandlersEvent event) {
|
||||
var registrar = event.registrar("1");
|
||||
registrar.playToClient(WritingDeskCatalogPayload.TYPE, WritingDeskCatalogPayload.STREAM_CODEC);
|
||||
registrar.playToServer(BindLinkingBookPayload.TYPE, BindLinkingBookPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player) bind(player, payload);
|
||||
});
|
||||
registrar.playToServer(UseBookStandPayload.TYPE, UseBookStandPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player) useStand(player, payload);
|
||||
});
|
||||
registrar.playToServer(UseLinkingBookPayload.TYPE, UseLinkingBookPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player) use(player, payload);
|
||||
@@ -72,6 +77,7 @@ public final class PortBookNetworking {
|
||||
String title = payload.title().strip();
|
||||
if (title.isEmpty() || title.length() > MAX_BOOK_TITLE_LENGTH) return;
|
||||
stack.set(Rivencraft.LINK_BOOK_TARGET.get(), LinkBookTarget.at(player));
|
||||
stack.set(Rivencraft.LINK_DESTINATION_NAME.get(), LinkingBookItem.destinationName(player.level()));
|
||||
stack.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN,
|
||||
SoundSource.PLAYERS, 1.0F, 1.0F);
|
||||
@@ -82,7 +88,8 @@ public final class PortBookNetworking {
|
||||
private static void use(ServerPlayer player, UseLinkingBookPayload payload) {
|
||||
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
|
||||
if (!stack.is(Rivencraft.LINKING_BOOK.get())) return;
|
||||
String failure = LinkingBookItem.travel(player, stack);
|
||||
String failure = net.dnorberg.rivencraft.item.PortHandheldTravel.travel(player,
|
||||
hand(payload.offhand()), stack, () -> LinkingBookItem.travel(player, stack));
|
||||
if (failure != null) player.sendOverlayMessage(Component.literal(failure));
|
||||
}
|
||||
|
||||
@@ -94,6 +101,7 @@ public final class PortBookNetworking {
|
||||
Identifier id = createAge(player, title);
|
||||
if (id == null) return;
|
||||
stack.set(Rivencraft.AGE_BOOK_ID.get(), id);
|
||||
stack.set(Rivencraft.BOOK_AUTHOR.get(), player.getGameProfile().name());
|
||||
stack.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN,
|
||||
SoundSource.PLAYERS, 1.0F, 1.0F);
|
||||
@@ -110,8 +118,20 @@ public final class PortBookNetworking {
|
||||
if (title == null || title.isBlank() || title.length() > MAX_BOOK_TITLE_LENGTH) return null;
|
||||
long seed = player.getRandom().nextLong();
|
||||
net.dnorberg.rivencraft.age.PortBiomePlan biomePlan;
|
||||
PortAgeTerrain terrain;
|
||||
List<Identifier> generationPages;
|
||||
try {
|
||||
biomePlan = PortAuthoredWriting.biomePlan(pages, player.level().getServer(), seed);
|
||||
if (pages.isEmpty()) {
|
||||
var random = PortRandomAgeSelection.select(player.level().getServer(), seed);
|
||||
terrain = random.terrain();
|
||||
biomePlan = random.biomes();
|
||||
generationPages = pages;
|
||||
} else {
|
||||
var selected = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(player.level().getServer(), pages, seed);
|
||||
terrain = selected.terrain();
|
||||
biomePlan = selected.biomes();
|
||||
generationPages = selected.generationPages();
|
||||
}
|
||||
} catch (IllegalArgumentException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age writing unavailable: " + exception.getMessage()));
|
||||
return null;
|
||||
@@ -128,25 +148,22 @@ public final class PortBookNetworking {
|
||||
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||
}
|
||||
try {
|
||||
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
|
||||
if (pages.isEmpty()) {
|
||||
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
|
||||
terrain = random.terrain();
|
||||
biomePlan = random.biomes();
|
||||
}
|
||||
var environment = net.dnorberg.rivencraft.age.PortEnvironmentSelection.resolve(pages, seed, terrain);
|
||||
var randomFeatures = pages.isEmpty()
|
||||
? net.dnorberg.rivencraft.age.PortRandomFeatureSelection.select(server, seed, terrain, biomePlan) : null;
|
||||
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
||||
randomFeatures == null ? PortAuthoredWriting.rockPlan(pages, seed) : randomFeatures.rocks(),
|
||||
PortAuthoredWriting.vegetationPlan(pages),
|
||||
randomFeatures == null ? PortAuthoredWriting.dungeonCount(pages) : randomFeatures.dungeons(),
|
||||
randomFeatures == null ? PortAuthoredWriting.lakePlan(pages, seed) : randomFeatures.lakes(),
|
||||
randomFeatures == null ? PortAuthoredWriting.terrainFeaturePlan(pages, seed) : randomFeatures.terrain(),
|
||||
var level = RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
||||
randomFeatures == null ? PortAuthoredWriting.rockPlan(generationPages, seed) : randomFeatures.rocks(),
|
||||
PortAuthoredWriting.vegetationPlan(generationPages),
|
||||
randomFeatures == null ? PortAuthoredWriting.dungeonCount(generationPages) : randomFeatures.dungeons(),
|
||||
randomFeatures == null ? PortAuthoredWriting.lakePlan(generationPages, seed) : randomFeatures.lakes(),
|
||||
randomFeatures == null ? PortAuthoredWriting.terrainFeaturePlan(generationPages, seed) : randomFeatures.terrain(),
|
||||
environment.weather(), environment.lighting(), environment.sky(),
|
||||
net.dnorberg.rivencraft.age.PortAgeColors.resolve(pages, seed),
|
||||
net.dnorberg.rivencraft.age.PortDayCycle.resolve(pages, seed,
|
||||
net.dnorberg.rivencraft.age.PortDayCycle.nativeTicks(server.overworld())));
|
||||
net.dnorberg.rivencraft.age.PortAcceleratedTicks.configure(level,
|
||||
net.dnorberg.rivencraft.age.PortAcceleratedTicks.pageCount(pages));
|
||||
RuntimeAgeBackend.setAgeDisplayName(server, id, title);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
||||
return null;
|
||||
@@ -157,16 +174,51 @@ public final class PortBookNetworking {
|
||||
private static void enterAge(ServerPlayer player, UseDescriptiveBookPayload payload) {
|
||||
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
|
||||
if (!stack.is(Rivencraft.DESCRIPTIVE_BOOK.get())) return;
|
||||
travelDescriptive(player, stack, hand(payload.offhand()));
|
||||
}
|
||||
|
||||
/** Validation is shared with operator fixtures; no remote chunk is loaded to inspect a stand. */
|
||||
public static ItemStack validatedStandBook(ServerPlayer player, UseBookStandPayload payload) {
|
||||
var level = player.level();
|
||||
if (!level.dimension().identifier().equals(payload.dimension())
|
||||
|| player.distanceToSqr(Vec3.atCenterOf(payload.pos())) > 64
|
||||
|| !level.hasChunkAt(payload.pos())
|
||||
|| !level.getBlockState(payload.pos()).is(Rivencraft.BOOK_STAND.get())
|
||||
|| !(level.getBlockEntity(payload.pos()) instanceof net.dnorberg.rivencraft.block.entity.BookStandBlockEntity stand)
|
||||
|| !ItemStack.matches(stand.getBook(), payload.book())) return ItemStack.EMPTY;
|
||||
return stand.getBook();
|
||||
}
|
||||
|
||||
private static void useStand(ServerPlayer player, UseBookStandPayload payload) {
|
||||
var book = validatedStandBook(player, payload);
|
||||
if (book.is(Rivencraft.LINKING_BOOK.get())) {
|
||||
String failure = LinkingBookItem.travel(player, book);
|
||||
if (failure != null) player.sendOverlayMessage(Component.literal(failure));
|
||||
else net.dnorberg.rivencraft.advancement.ProgressionAdvancements.awardAgeTravel(player);
|
||||
} else if (book.is(Rivencraft.DESCRIPTIVE_BOOK.get())) travelDescriptive(player, book, null);
|
||||
}
|
||||
|
||||
private static void travelDescriptive(ServerPlayer player, ItemStack stack, InteractionHand heldHand) {
|
||||
Identifier id = stack.get(Rivencraft.AGE_BOOK_ID.get());
|
||||
if (id == null) return;
|
||||
ServerLevel source = player.level();
|
||||
LinkBookTarget returnTarget = LinkBookTarget.at(player);
|
||||
Vec3 departure = player.position();
|
||||
Vec3 incoming = player.getDeltaMovement();
|
||||
String failure = PortAgeTravel.enter(player, id);
|
||||
java.util.function.Supplier<String> travel = () -> {
|
||||
String result = PortAgeTravel.enter(player, id);
|
||||
if (result == null) LinkingBookItem.applySuccessfulLink(player,
|
||||
stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE),
|
||||
source, departure, incoming, player.getYRot());
|
||||
return result;
|
||||
};
|
||||
String failure = heldHand == null ? travel.get()
|
||||
: net.dnorberg.rivencraft.item.PortHandheldTravel.travel(player, heldHand, stack, travel);
|
||||
if (failure != null) player.sendOverlayMessage(Component.literal(failure));
|
||||
else LinkingBookItem.applySuccessfulLink(player,
|
||||
stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE),
|
||||
source, departure, incoming, player.getYRot());
|
||||
else {
|
||||
if (heldHand == null) net.dnorberg.rivencraft.advancement.ProgressionAdvancements.awardAgeTravel(player);
|
||||
net.dnorberg.rivencraft.age.PortReturnLinks.afterSuccessfulTravel(player, id, returnTarget);
|
||||
}
|
||||
}
|
||||
|
||||
private static InteractionHand hand(boolean offhand) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Carries the displayed source so a removed/replaced book cannot activate a stale screen. */
|
||||
public record UseBookStandPayload(Identifier dimension, BlockPos pos, ItemStack book) implements CustomPacketPayload {
|
||||
public static final Type<UseBookStandPayload> TYPE = new Type<>(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "use_book_stand"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, UseBookStandPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
Identifier.STREAM_CODEC, UseBookStandPayload::dimension, BlockPos.STREAM_CODEC, UseBookStandPayload::pos,
|
||||
ItemStack.STREAM_CODEC, UseBookStandPayload::book, UseBookStandPayload::new);
|
||||
@Override public Type<UseBookStandPayload> type() { return TYPE; }
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
|
||||
import net.dnorberg.rivencraft.writing.WritingDeskCatalog;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Current server-approved symbols from this open Writing Desk menu. */
|
||||
public record WritingDeskCatalogPayload(int containerId, List<SymbolDisplayMetadata> symbols)
|
||||
implements CustomPacketPayload {
|
||||
public static final Type<WritingDeskCatalogPayload> TYPE = new Type<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "writing_desk_catalog"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, WritingDeskCatalogPayload> STREAM_CODEC =
|
||||
StreamCodec.of(
|
||||
(buffer, payload) -> {
|
||||
buffer.writeVarInt(payload.containerId());
|
||||
WritingDeskCatalog.write(buffer, payload.symbols());
|
||||
},
|
||||
buffer -> new WritingDeskCatalogPayload(buffer.readVarInt(), WritingDeskCatalog.read(buffer)));
|
||||
|
||||
@Override public Type<? extends CustomPacketPayload> type() { return TYPE; }
|
||||
}
|
||||
@@ -32,6 +32,11 @@ public final class ModCreativeTabs {
|
||||
output.accept(Rivencraft.INK_VIAL.get());
|
||||
output.accept(Rivencraft.BOOK_BINDER_ITEM.get());
|
||||
output.accept(Rivencraft.INK_MIXER_ITEM.get());
|
||||
output.accept(Rivencraft.WRITING_DESK_ITEM.get());
|
||||
output.accept(Rivencraft.NOTEBOOK.get());
|
||||
output.accept(Rivencraft.COLLATION_FOLDER.get());
|
||||
output.accept(Rivencraft.SEALED_NOTEBOOK.get());
|
||||
creativeSymbolPortfolios(parameters.holders()).forEach(output::accept);
|
||||
})
|
||||
.build());
|
||||
TABS.register("rivencraft_link_pages", () -> CreativeModeTab.builder()
|
||||
@@ -65,6 +70,15 @@ public final class ModCreativeTabs {
|
||||
private ModCreativeTabs() {
|
||||
}
|
||||
|
||||
public static java.util.List<net.minecraft.world.item.ItemStack> creativeSymbolPortfolios(
|
||||
net.minecraft.core.HolderLookup.Provider registries) {
|
||||
var portfolios = new java.util.ArrayList<net.minecraft.world.item.ItemStack>();
|
||||
for (String category : PortSymbolPages.categories()) portfolios.addAll(
|
||||
net.dnorberg.rivencraft.item.SymbolPortfolioItem.createVolumes(
|
||||
net.minecraft.resources.Identifier.parse(category), PortSymbolPages.pages(category, registries)));
|
||||
return java.util.List.copyOf(portfolios);
|
||||
}
|
||||
|
||||
public static void register(IEventBus modEventBus) {
|
||||
TABS.register(modEventBus);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Client-readable snapshot of authored pages; index zero is the front/link-panel spread. */
|
||||
public record DescriptiveBookPages(List<Identifier> symbols, String author) {
|
||||
public DescriptiveBookPages { symbols = List.copyOf(symbols); }
|
||||
public static DescriptiveBookPages from(ItemStack book) {
|
||||
return new DescriptiveBookPages(book.getOrDefault(Rivencraft.BOOK_SYMBOLS.get(), List.of()),
|
||||
book.getOrDefault(Rivencraft.BOOK_AUTHOR.get(), ""));
|
||||
}
|
||||
public int lastPage() { return Math.max(1, symbols.size()); }
|
||||
public int turn(int page, int direction) { return Math.clamp(page + direction, 0, lastPage()); }
|
||||
public Optional<Identifier> symbolAt(int page) {
|
||||
return page >= 1 && page <= symbols.size() ? Optional.of(symbols.get(page - 1)) : Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.DataResult;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** A bounded, position-preserving collection of Page stacks stored on a Notebook item. */
|
||||
public record NotebookContents(List<ItemStack> pages) {
|
||||
// Legacy did not impose a book-page cap. Our fixed-slot binder persists slot IDs
|
||||
// as unsigned bytes; 252 page positions (after its three input slots) fit safely.
|
||||
public static final int MAX_PAGES = 252;
|
||||
public static final NotebookContents EMPTY = new NotebookContents(List.of());
|
||||
public static final Codec<NotebookContents> CODEC = ItemStack.OPTIONAL_CODEC.listOf()
|
||||
.validate(NotebookContents::validate)
|
||||
.xmap(NotebookContents::new, NotebookContents::pages);
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, NotebookContents> STREAM_CODEC = StreamCodec.of(
|
||||
NotebookContents::encode, NotebookContents::decode);
|
||||
|
||||
public NotebookContents {
|
||||
if (pages.size() > MAX_PAGES) {
|
||||
throw new IllegalArgumentException("Notebook exceeds " + MAX_PAGES + " page positions");
|
||||
}
|
||||
List<ItemStack> copies = new ArrayList<>(pages.size());
|
||||
for (ItemStack page : pages) {
|
||||
validatePage(page).getOrThrow();
|
||||
copies.add(page.copy());
|
||||
}
|
||||
pages = List.copyOf(copies);
|
||||
}
|
||||
|
||||
public int pageCount() {
|
||||
return (int)pages.stream().filter(page -> !page.isEmpty()).count();
|
||||
}
|
||||
|
||||
public int unresolvedCount() {
|
||||
return (int)pages.stream()
|
||||
.filter(page -> !page.isEmpty()
|
||||
&& page.has(Rivencraft.SYMBOL_PAGE_ID.get())
|
||||
&& PortSymbolPages.find(page.get(Rivencraft.SYMBOL_PAGE_ID.get())) == null)
|
||||
.count();
|
||||
}
|
||||
|
||||
private static DataResult<List<ItemStack>> validate(List<ItemStack> pages) {
|
||||
if (pages.size() > MAX_PAGES) {
|
||||
return DataResult.error(() -> "Notebook exceeds " + MAX_PAGES + " page positions");
|
||||
}
|
||||
for (ItemStack page : pages) {
|
||||
DataResult<ItemStack> result = validatePage(page);
|
||||
if (result.error().isPresent()) {
|
||||
return DataResult.error(() -> result.error().orElseThrow().message());
|
||||
}
|
||||
}
|
||||
return DataResult.success(pages);
|
||||
}
|
||||
|
||||
private static DataResult<ItemStack> validatePage(ItemStack page) {
|
||||
if (page.isEmpty()) {
|
||||
return DataResult.success(page);
|
||||
}
|
||||
if (!page.is(Rivencraft.PAGE.get())) {
|
||||
return DataResult.error(() -> "Notebook contains a non-Page item");
|
||||
}
|
||||
return page.getCount() == 1
|
||||
? DataResult.success(page)
|
||||
: DataResult.error(() -> "Notebook Page stacks must contain exactly one item");
|
||||
}
|
||||
|
||||
private static void encode(RegistryFriendlyByteBuf buffer, NotebookContents contents) {
|
||||
ByteBufCodecs.VAR_INT.encode(buffer, contents.pages.size());
|
||||
contents.pages.forEach(page -> ItemStack.OPTIONAL_STREAM_CODEC.encode(buffer, page));
|
||||
}
|
||||
|
||||
private static NotebookContents decode(RegistryFriendlyByteBuf buffer) {
|
||||
int size = ByteBufCodecs.VAR_INT.decode(buffer);
|
||||
if (size < 0 || size > MAX_PAGES) {
|
||||
throw new IllegalArgumentException("Invalid Notebook page count " + size);
|
||||
}
|
||||
List<ItemStack> pages = new ArrayList<>(size);
|
||||
for (int index = 0; index < size; index++) {
|
||||
pages.add(ItemStack.OPTIONAL_STREAM_CODEC.decode(buffer));
|
||||
}
|
||||
return new NotebookContents(pages);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.DataResult;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
|
||||
/** Persistent contents of a Page item. Unknown symbols remain explicit and recoverable. */
|
||||
public record PageContent(Kind kind, Optional<SymbolReference> symbol) {
|
||||
public static final PageContent BLANK = new PageContent(Kind.BLANK, Optional.empty());
|
||||
public static final PageContent LINK_PANEL = new PageContent(Kind.LINK_PANEL, Optional.empty());
|
||||
private static final Codec<Kind> KIND_CODEC = Codec.STRING.comapFlatMap(
|
||||
value -> {
|
||||
try {
|
||||
return DataResult.success(Kind.valueOf(value.toUpperCase(Locale.ROOT)));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return DataResult.error(() -> "Unknown page content kind: " + value);
|
||||
}
|
||||
},
|
||||
value -> value.name().toLowerCase(Locale.ROOT)
|
||||
);
|
||||
private static final Codec<PageContent> RECORD_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
KIND_CODEC.fieldOf("kind").forGetter(PageContent::kind),
|
||||
SymbolReference.CODEC.optionalFieldOf("symbol").forGetter(PageContent::symbol)
|
||||
).apply(instance, PageContent::new));
|
||||
public static final Codec<PageContent> CODEC = RECORD_CODEC.flatXmap(PageContent::validate, PageContent::validate);
|
||||
public static final StreamCodec<ByteBuf, PageContent> STREAM_CODEC = StreamCodec.of(
|
||||
PageContent::encode, PageContent::decode);
|
||||
|
||||
public PageContent {
|
||||
Objects.requireNonNull(kind, "kind");
|
||||
symbol = Objects.requireNonNull(symbol, "symbol");
|
||||
boolean needsSymbol = kind == Kind.SYMBOL || kind == Kind.UNRESOLVED_SYMBOL;
|
||||
if (needsSymbol != symbol.isPresent()) {
|
||||
throw new IllegalArgumentException("Page contents do not match kind " + kind);
|
||||
}
|
||||
}
|
||||
|
||||
public static PageContent symbol(SymbolReference symbol) {
|
||||
return new PageContent(Kind.SYMBOL, Optional.of(symbol));
|
||||
}
|
||||
|
||||
public static PageContent unresolved(SymbolReference symbol) {
|
||||
return new PageContent(Kind.UNRESOLVED_SYMBOL, Optional.of(symbol));
|
||||
}
|
||||
|
||||
/** Re-resolves persisted data against the authoritative current server catalog. */
|
||||
public PageContent resolve() {
|
||||
if (kind != Kind.SYMBOL && kind != Kind.UNRESOLVED_SYMBOL) {
|
||||
return this;
|
||||
}
|
||||
SymbolReference reference = symbol.orElseThrow();
|
||||
return PortSymbolPages.find(reference.id()) != null ? symbol(reference) : unresolved(reference);
|
||||
}
|
||||
|
||||
public boolean isResolvedSymbol() {
|
||||
return kind == Kind.SYMBOL;
|
||||
}
|
||||
|
||||
private static DataResult<PageContent> validate(PageContent content) {
|
||||
boolean needsSymbol = content.kind == Kind.SYMBOL || content.kind == Kind.UNRESOLVED_SYMBOL;
|
||||
return needsSymbol == content.symbol.isPresent()
|
||||
? DataResult.success(content)
|
||||
: DataResult.error(() -> "Page contents do not match kind " + content.kind);
|
||||
}
|
||||
|
||||
private static void encode(ByteBuf buffer, PageContent content) {
|
||||
ByteBufCodecs.VAR_INT.encode(buffer, content.kind.ordinal());
|
||||
content.symbol.ifPresent(reference -> SymbolReference.STREAM_CODEC.encode(buffer, reference));
|
||||
}
|
||||
|
||||
private static PageContent decode(ByteBuf buffer) {
|
||||
int ordinal = ByteBufCodecs.VAR_INT.decode(buffer);
|
||||
if (ordinal < 0 || ordinal >= Kind.values().length) {
|
||||
throw new IllegalArgumentException("Unknown page content kind ordinal " + ordinal);
|
||||
}
|
||||
Kind kind = Kind.values()[ordinal];
|
||||
Optional<SymbolReference> symbol = kind == Kind.SYMBOL || kind == Kind.UNRESOLVED_SYMBOL
|
||||
? Optional.of(SymbolReference.STREAM_CODEC.decode(buffer))
|
||||
: Optional.empty();
|
||||
return new PageContent(kind, symbol);
|
||||
}
|
||||
|
||||
public enum Kind {
|
||||
BLANK,
|
||||
SYMBOL,
|
||||
UNRESOLVED_SYMBOL,
|
||||
LINK_PANEL
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
||||
import net.dnorberg.rivencraft.age.PortBiomePlan;
|
||||
import net.dnorberg.rivencraft.age.PortRandomAgeSelection;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
/** Root AgeRuntimeProfiles.selectForWriting, limited to currently supported port combinations. */
|
||||
public final class PortAuthoredTerrainSelection {
|
||||
private PortAuthoredTerrainSelection() {}
|
||||
|
||||
public record Selection(PortAgeTerrain terrain, List<Identifier> generationPages, PortBiomePlan biomes) {
|
||||
public Selection { generationPages = List.copyOf(generationPages); }
|
||||
}
|
||||
|
||||
public static Selection select(MinecraftServer server, List<Identifier> pages, long seed) {
|
||||
var explicit = PortAuthoredWriting.explicitTerrain(pages);
|
||||
if (explicit != null) return compile(server, pages, seed, explicit);
|
||||
var preferred = PortRandomAgeSelection.terrain(seed);
|
||||
try {
|
||||
return compile(server, pages, seed, preferred);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Match root's stable profile-ID fallback rather than rerolling the seed.
|
||||
}
|
||||
var compatible = new ArrayList<Selection>();
|
||||
for (var terrain : PortRandomAgeSelection.terrains()) {
|
||||
if (terrain == preferred) continue;
|
||||
try { compatible.add(compile(server, pages, seed, terrain)); }
|
||||
catch (IllegalArgumentException ignored) { }
|
||||
}
|
||||
if (compatible.isEmpty()) throw new IllegalArgumentException("No supported terrain can host this book's pages");
|
||||
return compatible.get(Math.floorMod(Long.hashCode(seed), compatible.size()));
|
||||
}
|
||||
|
||||
private static Selection compile(MinecraftServer server, List<Identifier> pages, long seed, PortAgeTerrain terrain) {
|
||||
if (!PortAuthoredWriting.supportsOnTerrain(pages, terrain, seed))
|
||||
throw new IllegalArgumentException("Unsupported pages for " + terrain + " terrain");
|
||||
var resolved = PortAuthoredWriting.withTerrain(pages, terrain);
|
||||
// Also preflight registry-backed biome restrictions, so an automatic Cave
|
||||
// choice cannot turn supplied surface biome pages into an unusable book.
|
||||
var biomes = PortAuthoredWriting.biomePlan(resolved, server, seed);
|
||||
return new Selection(terrain, resolved, biomes);
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ public final class PortAuthoredWriting {
|
||||
if (entry == null) return false;
|
||||
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|
||||
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) return true;
|
||||
if (page.equals(net.dnorberg.rivencraft.age.PortAcceleratedTicks.PAGE)) return true;
|
||||
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null
|
||||
|| PortLighting.forPage(page) != null || PortSkyStyle.forPage(page) != null) return true;
|
||||
if (entry.category().equals("rivencraft:biome") && PortRegistryPages.biomeId(page) != null) return true;
|
||||
@@ -72,11 +73,35 @@ public final class PortAuthoredWriting {
|
||||
|
||||
/** A page never disappears into an Age unless this port can apply its effect. */
|
||||
public static boolean supports(List<Identifier> pages) {
|
||||
if (pages.stream().noneMatch(PortAuthoredWriting::isTerrain))
|
||||
return net.dnorberg.rivencraft.age.PortRandomAgeSelection.terrains().stream()
|
||||
.anyMatch(terrain -> supportsOnTerrain(pages, terrain, 0));
|
||||
try {
|
||||
return supportsExplicit(PortMaterialCompletion.complete(pages, 0));
|
||||
} catch (IllegalArgumentException exception) { return false; }
|
||||
}
|
||||
|
||||
static boolean supportsOnTerrain(List<Identifier> pages, PortAgeTerrain terrain, long seed) {
|
||||
try {
|
||||
return supportsExplicit(PortMaterialCompletion.complete(withTerrain(pages, terrain), seed));
|
||||
} catch (IllegalArgumentException exception) { return false; }
|
||||
}
|
||||
|
||||
static List<Identifier> withTerrain(List<Identifier> pages, PortAgeTerrain terrain) {
|
||||
if (pages.stream().anyMatch(PortAuthoredWriting::isTerrain)) return List.copyOf(pages);
|
||||
var resolved = new ArrayList<>(pages);
|
||||
// Append internally so original material/color/controller indices stay stable.
|
||||
resolved.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||
"terrain/" + terrain.name().toLowerCase(java.util.Locale.ROOT)));
|
||||
return List.copyOf(resolved);
|
||||
}
|
||||
|
||||
static PortAgeTerrain explicitTerrain(List<Identifier> pages) {
|
||||
return pages.stream().filter(PortAuthoredWriting::isTerrain).findFirst()
|
||||
.map(page -> PortAgeTerrain.valueOf(page.getPath().substring(8).toUpperCase(java.util.Locale.ROOT)))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static boolean supportsExplicit(List<Identifier> pages) {
|
||||
if (!net.dnorberg.rivencraft.age.PortAgeColors.validNumbers(pages)) return false;
|
||||
if (!net.dnorberg.rivencraft.age.PortDayCycle.validWriting(pages)) return false;
|
||||
@@ -104,6 +129,8 @@ public final class PortAuthoredWriting {
|
||||
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|
||||
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) {
|
||||
// Shared colors target Sky, Ambient Light Color or Normal Time; trailing colors tint the sky.
|
||||
} else if (page.equals(net.dnorberg.rivencraft.age.PortAcceleratedTicks.PAGE)) {
|
||||
// Every Accelerated page adds random-tick attempts; it is not a controller.
|
||||
} else if (isTerrain(page) && !terrain) {
|
||||
terrain = true;
|
||||
} else if (PortSkyStyle.forPage(page) != null && sky++ == 0) {
|
||||
@@ -321,17 +348,18 @@ public final class PortAuthoredWriting {
|
||||
|
||||
/** Resolves all authored biomes from the active registry before creating a level. */
|
||||
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||
return biomePlan(pages, registries, 0, null);
|
||||
return biomePlan(pages, registries, 0, null, null);
|
||||
}
|
||||
|
||||
/** Creation-only completion; the saved plan never rerolls on load or reseed. */
|
||||
public static PortBiomePlan biomePlan(List<Identifier> pages, net.minecraft.server.MinecraftServer server, long seed) {
|
||||
return biomePlan(pages, server.registryAccess(), seed,
|
||||
net.dnorberg.rivencraft.age.PortRandomAgeSelection.eligibleBiomes(server, terrain(pages)));
|
||||
net.dnorberg.rivencraft.age.PortRandomAgeSelection.eligibleBiomes(server, terrain(pages)),
|
||||
net.dnorberg.rivencraft.age.PortRandomAgeSelection.distribution(seed));
|
||||
}
|
||||
|
||||
private static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries,
|
||||
long seed, List<Identifier> available) {
|
||||
long seed, List<Identifier> available, PortBiomePlan.Distribution fallback) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException(
|
||||
"Unsupported terrain or biome-page combination in this port slice");
|
||||
List<Identifier> biomes = new ArrayList<>();
|
||||
@@ -364,8 +392,22 @@ public final class PortAuthoredWriting {
|
||||
throw new IllegalArgumentException("Vegetation pages need at least one surface Overworld biome");
|
||||
if (pages.contains(PortTerrainFeaturePlan.DENSE_ORES) && !biomes.isEmpty() && !hasOreBiome)
|
||||
throw new IllegalArgumentException("Dense Ores needs at least one biome with registered ore placements");
|
||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
return new PortBiomePlan(distribution, List.of());
|
||||
if (distribution == null && fallback != null) {
|
||||
distribution = fallback;
|
||||
// Root's authored-pool fallback never discards supplied biome pages.
|
||||
if (pages.contains(VOID_TERRAIN) && biomes.isEmpty()) distribution = PortBiomePlan.Distribution.SINGLE;
|
||||
if (!biomes.isEmpty() && (pages.contains(VOID_TERRAIN)
|
||||
|| distribution == PortBiomePlan.Distribution.NATIVE
|
||||
|| distribution == PortBiomePlan.Distribution.SINGLE && biomes.size() > 1))
|
||||
distribution = biomes.size() == 1 ? PortBiomePlan.Distribution.SINGLE : biomes.size() == 2
|
||||
? PortBiomePlan.Distribution.TILED : PortBiomePlan.Distribution.MEDIUM;
|
||||
}
|
||||
if (distribution == PortBiomePlan.Distribution.NATIVE) {
|
||||
var nativePool = caveTerrain
|
||||
? (available != null ? available : net.dnorberg.rivencraft.age.PortRandomAgeSelection.caveBiomes(registries))
|
||||
: List.<Identifier>of();
|
||||
return new PortBiomePlan(distribution, nativePool);
|
||||
}
|
||||
if (distribution != null && available != null) {
|
||||
int minimum = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||
int target = minimum;
|
||||
@@ -374,6 +416,9 @@ public final class PortAuthoredWriting {
|
||||
while (target < available.size() && random.nextInt(3) != 0) target++;
|
||||
}
|
||||
var distinct = new java.util.HashSet<>(biomes);
|
||||
PortStructureBiomeCompletion.complete(pages, registries, available, !biomes.isEmpty(),
|
||||
distribution, biomes, distinct, seed);
|
||||
target = Math.max(target, distinct.size());
|
||||
// Void may retain authored weather/season biomes, but must not gain
|
||||
// an unrequested The Void biome just to fill a multi-biome controller.
|
||||
var filler = new ArrayList<>(pages.contains(VOID_TERRAIN) && !biomes.isEmpty() ? List.<Identifier>of() : available);
|
||||
|
||||
@@ -103,6 +103,7 @@ public final class PortRegistryPages {
|
||||
|
||||
private static String materialCategory(Block block) {
|
||||
if (block == null || block == Blocks.AIR || block == Blocks.CAVE_AIR || block == Blocks.VOID_AIR
|
||||
|| block == Rivencraft.CLOUD_PLATFORM.get()
|
||||
|| block instanceof EntityBlock || block.defaultBlockState().hasBlockEntity()) return null;
|
||||
var state = block.defaultBlockState();
|
||||
var fluid = state.getFluidState();
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import net.dnorberg.rivencraft.age.PortBiomePlan;
|
||||
import net.dnorberg.rivencraft.age.PortStructurePages;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Root AgeBiomeSelection.completeStructureBiomes, before ordinary minimum filling. */
|
||||
final class PortStructureBiomeCompletion {
|
||||
private PortStructureBiomeCompletion() {}
|
||||
|
||||
static void complete(List<Identifier> pages, HolderLookup.Provider registries, List<Identifier> available,
|
||||
boolean hasAuthoredBiomes, PortBiomePlan.Distribution distribution,
|
||||
List<Identifier> selected, Set<Identifier> distinct, long seed) {
|
||||
var requirements = pages.stream().filter(PortStructurePages::supports).distinct()
|
||||
.map(page -> PortStructurePages.compatibleBiomes(page, registries))
|
||||
.filter(requirement -> !requirement.isEmpty()).toList();
|
||||
if (requirements.isEmpty()) return;
|
||||
if (distribution == PortBiomePlan.Distribution.SINGLE) {
|
||||
if (hasAuthoredBiomes) return;
|
||||
var candidates = available.stream()
|
||||
.filter(candidate -> requirements.stream().allMatch(requirement -> requirement.contains(candidate)))
|
||||
.toList();
|
||||
if (candidates.isEmpty()) return;
|
||||
Identifier biome = choose(candidates, seed, 0x5354525543544249L);
|
||||
selected.clear();
|
||||
distinct.clear();
|
||||
selected.add(biome);
|
||||
distinct.add(biome);
|
||||
return;
|
||||
}
|
||||
for (int index = 0; index < requirements.size(); index++) {
|
||||
var requirement = requirements.get(index);
|
||||
if (selected.stream().anyMatch(requirement::contains)) continue;
|
||||
var candidates = available.stream().filter(requirement::contains)
|
||||
.filter(candidate -> !distinct.contains(candidate)).toList();
|
||||
if (candidates.isEmpty()) continue;
|
||||
Identifier biome = choose(candidates, seed, 0x5354525543544249L + index);
|
||||
selected.add(biome);
|
||||
distinct.add(biome);
|
||||
}
|
||||
}
|
||||
|
||||
private static Identifier choose(List<Identifier> candidates, long seed, long salt) {
|
||||
var shuffled = new ArrayList<>(candidates);
|
||||
Collections.shuffle(shuffled, new Random(seed ^ salt));
|
||||
return shuffled.getFirst();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,17 @@ package net.dnorberg.rivencraft.writing;
|
||||
public final class PortSymbolNumerals {
|
||||
private PortSymbolNumerals() {}
|
||||
|
||||
public static int[] digitsFor(int number) {
|
||||
if (number < 0) throw new IllegalArgumentException("Numeral must be non-negative");
|
||||
if (number < 25) return new int[] {number};
|
||||
int count = 0;
|
||||
for (int remaining = number; remaining > 0; remaining /= 25) count++;
|
||||
int[] digits = new int[count];
|
||||
for (int remaining = number, index = count - 1; index >= 0; index--, remaining /= 25)
|
||||
digits[index] = remaining % 25;
|
||||
return digits;
|
||||
}
|
||||
|
||||
public static int[] componentsForDigit(int digit) {
|
||||
if (digit < 0 || digit >= 25) throw new IllegalArgumentException("Base-25 digit must be in [0, 24]");
|
||||
if (digit == 0) return new int[] {1};
|
||||
|
||||
@@ -37,6 +37,15 @@ public final class PortSymbolPages {
|
||||
return List.copyOf(categories);
|
||||
}
|
||||
|
||||
/** Same bundled, non-biome/material/non-corrupted pool as 1.21.1's booster. */
|
||||
public static List<Identifier> sealedCandidateIds() {
|
||||
return ENTRIES.values().stream().filter(entry -> entry.id().getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& !entry.id().getPath().startsWith("block/")
|
||||
&& !entry.id().getPath().startsWith("biome/")
|
||||
&& !entry.category().equals("rivencraft:corrupted"))
|
||||
.map(Entry::id).sorted().toList();
|
||||
}
|
||||
|
||||
public static List<ItemStack> pages(String category, HolderLookup.Provider registries) {
|
||||
List<ItemStack> pages = new ArrayList<>();
|
||||
ENTRIES.values().stream().filter(entry -> entry.category().equals(category))
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Client-safe symbol presentation selected by the authoritative server catalog. */
|
||||
public record SymbolDisplayMetadata(
|
||||
Identifier id,
|
||||
String nameTranslationKey,
|
||||
String descriptionTranslationKey,
|
||||
Identifier category,
|
||||
boolean resolved,
|
||||
String poem
|
||||
) {
|
||||
public static final Codec<SymbolDisplayMetadata> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.fieldOf("id").forGetter(SymbolDisplayMetadata::id),
|
||||
Codec.STRING.fieldOf("name").forGetter(SymbolDisplayMetadata::nameTranslationKey),
|
||||
Codec.STRING.fieldOf("description").forGetter(SymbolDisplayMetadata::descriptionTranslationKey),
|
||||
Identifier.CODEC.fieldOf("category").forGetter(SymbolDisplayMetadata::category),
|
||||
Codec.BOOL.fieldOf("resolved").forGetter(SymbolDisplayMetadata::resolved),
|
||||
Codec.STRING.optionalFieldOf("poem", "").forGetter(SymbolDisplayMetadata::poem)
|
||||
).apply(instance, SymbolDisplayMetadata::new));
|
||||
public static final StreamCodec<ByteBuf, SymbolDisplayMetadata> STREAM_CODEC = StreamCodec.composite(
|
||||
Identifier.STREAM_CODEC, SymbolDisplayMetadata::id,
|
||||
ByteBufCodecs.STRING_UTF8, SymbolDisplayMetadata::nameTranslationKey,
|
||||
ByteBufCodecs.STRING_UTF8, SymbolDisplayMetadata::descriptionTranslationKey,
|
||||
Identifier.STREAM_CODEC, SymbolDisplayMetadata::category,
|
||||
ByteBufCodecs.BOOL, SymbolDisplayMetadata::resolved,
|
||||
ByteBufCodecs.STRING_UTF8, SymbolDisplayMetadata::poem,
|
||||
SymbolDisplayMetadata::new
|
||||
);
|
||||
|
||||
public static SymbolDisplayMetadata unresolved(SymbolReference reference) {
|
||||
return new SymbolDisplayMetadata(
|
||||
reference.id(),
|
||||
"symbol.rivencraft.unresolved",
|
||||
"symbol.rivencraft.unresolved.description",
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "unresolved"),
|
||||
false,
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import java.util.Objects;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Stable namespaced identity for one writing symbol. */
|
||||
public record SymbolReference(Identifier id) {
|
||||
public static final Codec<SymbolReference> CODEC = Identifier.CODEC.xmap(
|
||||
SymbolReference::new, SymbolReference::id);
|
||||
public static final StreamCodec<ByteBuf, SymbolReference> STREAM_CODEC = Identifier.STREAM_CODEC.map(
|
||||
SymbolReference::new, SymbolReference::id);
|
||||
|
||||
public SymbolReference {
|
||||
Objects.requireNonNull(id, "id");
|
||||
}
|
||||
|
||||
/** Remains useful when the defining data pack has been removed. */
|
||||
public String unresolvedName() {
|
||||
return id.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.item.CollationFolderItem;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** A bounded, server-derived view of symbols physically present in the desk. */
|
||||
public final class WritingDeskCatalog {
|
||||
private static final int MAX_ENTRIES = WritingDeskBlockEntity.COLLECTIONS * NotebookContents.MAX_PAGES;
|
||||
|
||||
private WritingDeskCatalog() {}
|
||||
|
||||
public static List<SymbolDisplayMetadata> available(WritingDeskBlockEntity desk) {
|
||||
Map<Identifier, SymbolDisplayMetadata> unique = new HashMap<>();
|
||||
for (int slot = 0; slot < WritingDeskBlockEntity.COLLECTIONS; slot++) {
|
||||
ItemStack collection = desk.getItem(slot);
|
||||
if (collection.is(Rivencraft.PAGE.get())) add(unique, collection);
|
||||
if (CollationFolderItem.isCollection(collection)) {
|
||||
List<ItemStack> pages = collection.getOrDefault(
|
||||
Rivencraft.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
|
||||
pages.forEach(page -> add(unique, page));
|
||||
}
|
||||
}
|
||||
return unique.values().stream()
|
||||
.sorted(Comparator.comparing((SymbolDisplayMetadata symbol) -> symbol.category().toString())
|
||||
.thenComparing(symbol -> symbol.id().toString()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static void add(Map<Identifier, SymbolDisplayMetadata> unique, ItemStack page) {
|
||||
if (!page.is(Rivencraft.PAGE.get())) return;
|
||||
PageContent content = PageItem.content(page).resolve();
|
||||
if (content.kind() != PageContent.Kind.SYMBOL) return;
|
||||
SymbolReference reference = content.symbol().orElseThrow();
|
||||
var entry = PortSymbolPages.find(reference.id());
|
||||
if (entry != null) unique.put(reference.id(), new SymbolDisplayMetadata(entry.id(),
|
||||
entry.nameKey(), entry.descriptionKey(), Identifier.parse(entry.category()), true, entry.poem()));
|
||||
}
|
||||
|
||||
public static void write(RegistryFriendlyByteBuf buffer, List<SymbolDisplayMetadata> entries) {
|
||||
if (entries.size() > MAX_ENTRIES) throw new IllegalArgumentException("Desk symbol catalog exceeds limit");
|
||||
buffer.writeVarInt(entries.size());
|
||||
entries.forEach(entry -> SymbolDisplayMetadata.STREAM_CODEC.encode(buffer, entry));
|
||||
}
|
||||
|
||||
public static List<SymbolDisplayMetadata> read(RegistryFriendlyByteBuf buffer) {
|
||||
int count = buffer.readVarInt();
|
||||
if (count < 0 || count > MAX_ENTRIES) throw new IllegalArgumentException("Invalid desk symbol count " + count);
|
||||
List<SymbolDisplayMetadata> entries = new ArrayList<>(count);
|
||||
for (int index = 0; index < count; index++) {
|
||||
entries.add(SymbolDisplayMetadata.STREAM_CODEC.decode(buffer));
|
||||
}
|
||||
return List.copyOf(entries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"model":{"type":"minecraft:model","model":"rivencraft:item/collation_folder"}}
|
||||
@@ -0,0 +1 @@
|
||||
{"model":{"type":"minecraft:model","model":"rivencraft:item/notebook"}}
|
||||
@@ -0,0 +1 @@
|
||||
{"model":{"type":"minecraft:model","model":"rivencraft:item/sealed_notebook"}}
|
||||
@@ -0,0 +1 @@
|
||||
{"model":{"type":"minecraft:model","model":"rivencraft:item/symbol_portfolio"}}
|
||||
@@ -0,0 +1 @@
|
||||
{"model":{"type":"minecraft:model","model":"rivencraft:item/writing_desk"}}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"display": {
|
||||
"icon": {
|
||||
"id": "rivencraft:descriptive_book"
|
||||
},
|
||||
"title": {
|
||||
"translate": "advancement.rivencraft.root.title"
|
||||
},
|
||||
"description": {
|
||||
"translate": "advancement.rivencraft.root.description"
|
||||
},
|
||||
"background": "rivencraft:block/crystal_block",
|
||||
"show_toast": false,
|
||||
"announce_to_chat": false
|
||||
},
|
||||
"criteria": {
|
||||
"completed": {
|
||||
"trigger": "minecraft:tick"
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"parent": "minecraft:recipes/root",
|
||||
"criteria": {
|
||||
"has_material": {
|
||||
"trigger": "minecraft:inventory_changed",
|
||||
"conditions": {
|
||||
"items": [
|
||||
{
|
||||
"items": "minecraft:iron_ingot"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"has_the_recipe": {
|
||||
"trigger": "minecraft:recipe_unlocked",
|
||||
"conditions": {
|
||||
"recipes": [
|
||||
"rivencraft:book_binder"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"requirements": [
|
||||
[
|
||||
"has_material",
|
||||
"has_the_recipe"
|
||||
]
|
||||
],
|
||||
"rewards": {
|
||||
"recipes": [
|
||||
"rivencraft:book_binder"
|
||||
]
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"parent": "minecraft:recipes/root",
|
||||
"criteria": {
|
||||
"has_crystal_shard": {
|
||||
"trigger": "minecraft:inventory_changed",
|
||||
"conditions": {
|
||||
"items": [
|
||||
{
|
||||
"items": "rivencraft:crystal_shard"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"has_the_recipe": {
|
||||
"trigger": "minecraft:recipe_unlocked",
|
||||
"conditions": {
|
||||
"recipes": [
|
||||
"rivencraft:crystal_block"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"requirements": [
|
||||
[
|
||||
"has_crystal_shard",
|
||||
"has_the_recipe"
|
||||
]
|
||||
],
|
||||
"rewards": {
|
||||
"recipes": [
|
||||
"rivencraft:crystal_block"
|
||||
]
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"parent": "minecraft:recipes/root",
|
||||
"criteria": {
|
||||
"has_material": {
|
||||
"trigger": "minecraft:inventory_changed",
|
||||
"conditions": {
|
||||
"items": [
|
||||
{
|
||||
"items": "minecraft:glass_bottle"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"has_the_recipe": {
|
||||
"trigger": "minecraft:recipe_unlocked",
|
||||
"conditions": {
|
||||
"recipes": [
|
||||
"rivencraft:ink_mixer"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"requirements": [
|
||||
[
|
||||
"has_material",
|
||||
"has_the_recipe"
|
||||
]
|
||||
],
|
||||
"rewards": {
|
||||
"recipes": [
|
||||
"rivencraft:ink_mixer"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"parent": "minecraft:recipes/root",
|
||||
"criteria": {
|
||||
"has_black_dye": {
|
||||
"trigger": "minecraft:inventory_changed",
|
||||
"conditions": {
|
||||
"items": [
|
||||
{
|
||||
"items": "minecraft:black_dye"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"has_the_recipe": {
|
||||
"trigger": "minecraft:recipe_unlocked",
|
||||
"conditions": {
|
||||
"recipes": [
|
||||
"rivencraft:ink_vial"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"requirements": [
|
||||
[
|
||||
"has_black_dye",
|
||||
"has_the_recipe"
|
||||
]
|
||||
],
|
||||
"rewards": {
|
||||
"recipes": [
|
||||
"rivencraft:ink_vial"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"parent": "minecraft:recipes/root",
|
||||
"criteria": {
|
||||
"has_material": {"trigger":"minecraft:inventory_changed","conditions":{"items":[{"items":"minecraft:leather"}]}},
|
||||
"has_the_recipe": {"trigger":"minecraft:recipe_unlocked","conditions":{"recipes":["rivencraft:notebook"]}}
|
||||
},
|
||||
"requirements": [["has_material", "has_the_recipe"]],
|
||||
"rewards": {"recipes":["rivencraft:notebook"]}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"parent":"minecraft:recipes/root",
|
||||
"criteria":{
|
||||
"has_material":{"trigger":"minecraft:inventory_changed","conditions":{"items":[{"items":"minecraft:feather"}]}},
|
||||
"has_the_recipe":{"trigger":"minecraft:recipe_unlocked","conditions":{"recipes":["rivencraft:writing_desk"]}}
|
||||
},
|
||||
"requirements":[["has_material","has_the_recipe"]],
|
||||
"rewards":{"recipes":["rivencraft:writing_desk"]}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"category": "misc",
|
||||
"key": {"L": "minecraft:leather", "S": "minecraft:string"},
|
||||
"pattern": [" L ", "S ", " L "],
|
||||
"result": {"count": 1, "id": "rivencraft:notebook"}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type":"minecraft:crafting_shaped", "category":"misc",
|
||||
"pattern":["V F", "PPP", "P P"],
|
||||
"key":{"V":"minecraft:glass_bottle", "F":"minecraft:feather", "P":"#minecraft:planks"},
|
||||
"result":{"id":"rivencraft:writing_desk", "count":1}
|
||||
}
|
||||
Reference in new issue
Block a user