Public Access
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35980338e1 |
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.106-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
|
||||
|
||||
@@ -2021,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.
|
||||
+226
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
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, Ink Mixer, Writing Desk and page collections
|
||||
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
|
||||
@@ -10,7 +10,7 @@ independent Age weather and lighting, multiple authored or seeded celestial
|
||||
bodies and eclipses, biome/material completion, and random features/structures.
|
||||
Instability, crystal portals, remaining discovery/stations, writing recovery and optional
|
||||
integration parity remain unfinished. The latest published beta is
|
||||
[1.0.1.106-beta.1](RELEASE_NOTES_1.0.1.106-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.
|
||||
|
||||
@@ -1646,6 +1646,34 @@ 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
|
||||
@@ -1667,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,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.
|
||||
@@ -220,6 +220,9 @@ tasks.named('processResources', ProcessResources).configure {
|
||||
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'
|
||||
@@ -231,6 +234,7 @@ 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'
|
||||
@@ -249,6 +253,8 @@ 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'
|
||||
|
||||
@@ -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.106-beta.1
|
||||
mod_version=1.0.1.115-beta.1
|
||||
@@ -95,6 +95,8 @@ public final class Rivencraft {
|
||||
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));
|
||||
@@ -106,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)
|
||||
@@ -133,6 +137,12 @@ public final class Rivencraft {
|
||||
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());
|
||||
@@ -219,6 +229,7 @@ public final class Rivencraft {
|
||||
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() {
|
||||
|
||||
@@ -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,44 @@ 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())
|
||||
@@ -79,6 +123,15 @@ public final class PortAgeCommands {
|
||||
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 -> {
|
||||
|
||||
+59
-1
@@ -52,7 +52,7 @@ final class PortBinderCollectionProbe {
|
||||
if (!binder.importCollection(folder, -1).isEmpty() || !binder.importCollection(folder, 1).isEmpty())
|
||||
throw new IllegalStateException("Invalid collection insertion point accepted");
|
||||
binder.insertPage(0, page("terrain/flat"));
|
||||
var player = net.neoforged.neoforge.common.util.FakePlayerFactory.get(level,
|
||||
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);
|
||||
@@ -81,6 +81,52 @@ final class PortBinderCollectionProbe {
|
||||
|| !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(); }
|
||||
@@ -93,6 +139,18 @@ final class PortBinderCollectionProbe {
|
||||
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");
|
||||
}
|
||||
|
||||
+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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ public final class PortReturnLinks {
|
||||
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)
|
||||
|
||||
@@ -23,7 +23,7 @@ final class PortWritingDeskProbe {
|
||||
|
||||
static void create(ServerLevel level) {
|
||||
PortAgeTravel.findLanding(level);
|
||||
var player = net.neoforged.neoforge.common.util.FakePlayerFactory.get(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.
|
||||
@@ -69,6 +69,8 @@ final class PortWritingDeskProbe {
|
||||
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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
+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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -20,6 +20,7 @@ public final class PortBookBinderClientEvents {
|
||||
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
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,17 @@ public final class DroppedBookEntity extends ItemEntity {
|
||||
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();
|
||||
|
||||
+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());
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
|
||||
@@ -21,6 +21,7 @@ public final class PortHandheldTravel {
|
||||
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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -118,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());
|
||||
@@ -129,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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.WritingDeskBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||
@@ -321,6 +323,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
if (!blockEntity.consumeCopyMaterials()) return ItemStack.EMPTY;
|
||||
if (player instanceof ServerPlayer serverPlayer)
|
||||
ProgressionAdvancements.award(serverPlayer, ProgressionAdvancements.COPY_PAGE);
|
||||
refreshOutput();
|
||||
return copy;
|
||||
}
|
||||
@@ -439,6 +443,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
||||
}
|
||||
@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);
|
||||
|
||||
@@ -38,6 +38,10 @@ public final class PortBookNetworking {
|
||||
(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);
|
||||
@@ -73,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);
|
||||
@@ -96,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);
|
||||
@@ -157,6 +163,7 @@ public final class PortBookNetworking {
|
||||
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;
|
||||
@@ -167,22 +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 = net.dnorberg.rivencraft.item.PortHandheldTravel.travel(player,
|
||||
hand(payload.offhand()), stack, () -> {
|
||||
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;
|
||||
});
|
||||
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 net.dnorberg.rivencraft.age.PortReturnLinks.afterSuccessfulTravel(player, id, returnTarget);
|
||||
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; }
|
||||
}
|
||||
@@ -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,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"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user