Public Access
Port lectern stands, book GUIs and progression to 26.3 beta
This commit is contained in:
1 parent
be8440ddb5
commit
20cfc1160e
43 files changed
+1844
-84
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,
|
Age weather/lighting, seeded and colorable Overworld/Nether/End/Static skies,
|
||||||
independent Sun/Moon controls, multiple bodies and eclipses, seeded biome and
|
independent Sun/Moon controls, multiple bodies and eclipses, seeded biome and
|
||||||
feature completion, and random structure selection. Nether/End noise-biome
|
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.
|
1.21.1 gameplay or optional-integration parity.
|
||||||
See the [26.3 beta guide](ports/mc263/README.md) and
|
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.
|
Keep the builds and their saves separate, and back up worlds before updating.
|
||||||
|
|
||||||
## Getting started
|
## 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
|
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;
|
required tests passed, including the desk test. Issue #97 was then closed;
|
||||||
remaining 26.3 parity work stays under #98–#104.
|
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
|
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
|
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
|
are ported, and the Binder can
|
||||||
author Standard, Amplified, Island, Flat, Void, or Cave terrain with supported
|
author Standard, Amplified, Island, Flat, Void, or Cave terrain with supported
|
||||||
biome-distribution pages, selected material/vegetation/structure features, and
|
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.
|
bodies and eclipses, biome/material completion, and random features/structures.
|
||||||
Instability, crystal portals, remaining discovery/stations, writing recovery and optional
|
Instability, crystal portals, remaining discovery/stations, writing recovery and optional
|
||||||
integration parity remain unfinished. The latest published beta is
|
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
|
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.
|
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
|
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.
|
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:
|
From the repository root:
|
||||||
|
|
||||||
```sh
|
```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
|
and properties. Set a nonconflicting `server-port` there before running if
|
||||||
another Minecraft server is active. The prototype restores all indexed Ages
|
another Minecraft server is active. The prototype restores all indexed Ages
|
||||||
during the Overworld load, before initial chunk preparation completes.
|
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/models/block/writing_desk_extension.json'
|
||||||
include 'assets/rivencraft/blockstates/writing_desk.json'
|
include 'assets/rivencraft/blockstates/writing_desk.json'
|
||||||
include 'data/rivencraft/loot_table/blocks/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/textures/item/ink_vial.png.mcmeta'
|
||||||
include 'assets/rivencraft/models/item/ink_vial.json'
|
include 'assets/rivencraft/models/item/ink_vial.json'
|
||||||
include 'assets/rivencraft/models/item/crystal_shard.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/bookui_cover.png'
|
||||||
include 'assets/rivencraft/textures/gui/notebook_pages.png'
|
include 'assets/rivencraft/textures/gui/notebook_pages.png'
|
||||||
include 'assets/rivencraft/textures/gui/bookui_pager.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/block/crystal_block.png'
|
||||||
include 'assets/rivencraft/textures/entity/linking_book.png'
|
include 'assets/rivencraft/textures/entity/linking_book.png'
|
||||||
include 'assets/rivencraft/textures/entity/descriptive_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/block/book_binder.json'
|
||||||
include 'assets/rivencraft/models/item/book_binder.json'
|
include 'assets/rivencraft/models/item/book_binder.json'
|
||||||
include 'assets/rivencraft/blockstates/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 'data/rivencraft/loot_table/blocks/book_binder.json'
|
||||||
include 'assets/rivencraft/models/block/crystal_block.json'
|
include 'assets/rivencraft/models/block/crystal_block.json'
|
||||||
include 'assets/rivencraft/blockstates/crystal_block.json'
|
include 'assets/rivencraft/blockstates/crystal_block.json'
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ neo_version=26.3.0.99
|
|||||||
mod_id=rivencraft
|
mod_id=rivencraft
|
||||||
mod_name=Rivencraft
|
mod_name=Rivencraft
|
||||||
mod_license=LGPL-3.0-or-later
|
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
|
COMPONENTS.registerComponentType("notebook_contents", builder -> builder
|
||||||
.persistent(net.dnorberg.rivencraft.writing.NotebookContents.CODEC)
|
.persistent(net.dnorberg.rivencraft.writing.NotebookContents.CODEC)
|
||||||
.networkSynchronized(net.dnorberg.rivencraft.writing.NotebookContents.STREAM_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(
|
public static final Supplier<DataComponentType<LinkBookTarget>> LINK_BOOK_TARGET = COMPONENTS.registerComponentType(
|
||||||
"link_book_target", builder -> builder.persistent(LinkBookTarget.CODEC)
|
"link_book_target", builder -> builder.persistent(LinkBookTarget.CODEC)
|
||||||
.networkSynchronized(LinkBookTarget.STREAM_CODEC));
|
.networkSynchronized(LinkBookTarget.STREAM_CODEC));
|
||||||
@@ -106,6 +108,8 @@ public final class Rivencraft {
|
|||||||
"symbol_page_id", builder -> builder.persistent(Identifier.CODEC)
|
"symbol_page_id", builder -> builder.persistent(Identifier.CODEC)
|
||||||
.networkSynchronized(Identifier.STREAM_CODEC));
|
.networkSynchronized(Identifier.STREAM_CODEC));
|
||||||
/** Ordered authored page IDs remain on the finished book for later port slices. */
|
/** 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(
|
public static final Supplier<DataComponentType<java.util.List<Identifier>>> BOOK_SYMBOLS = COMPONENTS.registerComponentType(
|
||||||
"book_symbols", builder -> builder.persistent(Identifier.CODEC.listOf())
|
"book_symbols", builder -> builder.persistent(Identifier.CODEC.listOf())
|
||||||
.networkSynchronized(ByteBufCodecs.<io.netty.buffer.ByteBuf, Identifier>list(252)
|
.networkSynchronized(ByteBufCodecs.<io.netty.buffer.ByteBuf, Identifier>list(252)
|
||||||
@@ -133,6 +137,12 @@ public final class Rivencraft {
|
|||||||
public static final Supplier<BlockEntityType<StarFissureBlockEntity>>
|
public static final Supplier<BlockEntityType<StarFissureBlockEntity>>
|
||||||
STAR_FISSURE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("star_fissure", () -> new BlockEntityType<>(
|
STAR_FISSURE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("star_fissure", () -> new BlockEntityType<>(
|
||||||
StarFissureBlockEntity::new, STAR_FISSURE.get()));
|
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(
|
public static final DeferredBlock<PortBookBinderBlock> BOOK_BINDER = BLOCKS.registerBlock(
|
||||||
"book_binder", PortBookBinderBlock::new,
|
"book_binder", PortBookBinderBlock::new,
|
||||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
() -> 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.PortAgeWeather::onLevelTickPre);
|
||||||
NeoForge.EVENT_BUS.addListener(net.dnorberg.rivencraft.age.PortAcceleratedTicks::onLevelTickPost);
|
NeoForge.EVENT_BUS.addListener(net.dnorberg.rivencraft.age.PortAcceleratedTicks::onLevelTickPost);
|
||||||
NeoForge.EVENT_BUS.addListener(PortAgeCommands::register);
|
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::onLogin);
|
||||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogout);
|
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogout);
|
||||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onServerStopped);
|
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)
|
Codec.unboundedMap(Identifier.CODEC, PortAgeColors.CODEC)
|
||||||
.optionalFieldOf("colors", Map.of()).forGetter(index -> index.colors),
|
.optionalFieldOf("colors", Map.of()).forGetter(index -> index.colors),
|
||||||
Codec.unboundedMap(Identifier.CODEC, PortDayCycle.CODEC)
|
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));
|
).apply(instance, AgeLevelIndex::new));
|
||||||
|
|
||||||
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
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, PortSkyStyle> sky;
|
||||||
private final Map<Identifier, PortAgeColors> colors;
|
private final Map<Identifier, PortAgeColors> colors;
|
||||||
private final Map<Identifier, PortDayCycle> dayCycles;
|
private final Map<Identifier, PortDayCycle> dayCycles;
|
||||||
|
private final Map<Identifier, String> displayNames;
|
||||||
|
|
||||||
private AgeLevelIndex() {
|
private AgeLevelIndex() {
|
||||||
this.ages = new LinkedHashMap<>();
|
this.ages = new LinkedHashMap<>();
|
||||||
@@ -81,6 +84,7 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
this.sky = new LinkedHashMap<>();
|
this.sky = new LinkedHashMap<>();
|
||||||
this.colors = new LinkedHashMap<>();
|
this.colors = new LinkedHashMap<>();
|
||||||
this.dayCycles = new LinkedHashMap<>();
|
this.dayCycles = new LinkedHashMap<>();
|
||||||
|
this.displayNames = new LinkedHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
|
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
|
||||||
@@ -94,7 +98,7 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures,
|
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures,
|
||||||
Map<Identifier, PortLighting> lighting,
|
Map<Identifier, PortLighting> lighting,
|
||||||
Map<Identifier, PortSkyStyle> sky, Map<Identifier, PortAgeColors> colors,
|
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.ages = new LinkedHashMap<>(ages);
|
||||||
this.retired = new LinkedHashSet<>(retired);
|
this.retired = new LinkedHashSet<>(retired);
|
||||||
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
||||||
@@ -109,6 +113,16 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
this.sky = new LinkedHashMap<>(sky);
|
this.sky = new LinkedHashMap<>(sky);
|
||||||
this.colors = new LinkedHashMap<>(colors);
|
this.colors = new LinkedHashMap<>(colors);
|
||||||
this.dayCycles = new LinkedHashMap<>(dayCycles);
|
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() {
|
Map<Identifier, Long> entries() {
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ public final class PortAgeCommands {
|
|||||||
public static void register(RegisterCommandsEvent event) {
|
public static void register(RegisterCommandsEvent event) {
|
||||||
event.getDispatcher().register(Commands.literal("rivencraft_port")
|
event.getDispatcher().register(Commands.literal("rivencraft_port")
|
||||||
.requires(Commands.hasPermission(Commands.LEVEL_ADMINS))
|
.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")
|
.then(Commands.literal("compat")
|
||||||
.executes(context -> {
|
.executes(context -> {
|
||||||
StringBuilder mods = new StringBuilder("Optional mods: ");
|
StringBuilder mods = new StringBuilder("Optional mods: ");
|
||||||
@@ -69,6 +75,44 @@ public final class PortAgeCommands {
|
|||||||
return 1;
|
return 1;
|
||||||
})))
|
})))
|
||||||
.then(Commands.literal("age")
|
.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.literal("create_writing_desk_fixture")
|
||||||
.then(Commands.argument("name", StringArgumentType.word())
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
.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);
|
context.getSource().sendSuccess(() -> Component.literal("Writing Desk transaction/removal checks passed"), false);
|
||||||
return 1;
|
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.literal("writing_desk_probe")
|
||||||
.then(Commands.argument("name", StringArgumentType.word())
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
.executes(context -> {
|
.executes(context -> {
|
||||||
|
|||||||
+59
-1
@@ -52,7 +52,7 @@ final class PortBinderCollectionProbe {
|
|||||||
if (!binder.importCollection(folder, -1).isEmpty() || !binder.importCollection(folder, 1).isEmpty())
|
if (!binder.importCollection(folder, -1).isEmpty() || !binder.importCollection(folder, 1).isEmpty())
|
||||||
throw new IllegalStateException("Invalid collection insertion point accepted");
|
throw new IllegalStateException("Invalid collection insertion point accepted");
|
||||||
binder.insertPage(0, page("terrain/flat"));
|
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"));
|
new com.mojang.authlib.GameProfile(java.util.UUID.fromString("5225a1ed-5ead-44b5-a63c-c78e7dca86f5"), "RivenBinderProbe"));
|
||||||
player.setPos(0.5, 64, 2.5);
|
player.setPos(0.5, 64, 2.5);
|
||||||
var menu = new PortBookBinderMenu(1, player.getInventory(), binder);
|
var menu = new PortBookBinderMenu(1, player.getInventory(), binder);
|
||||||
@@ -81,6 +81,52 @@ final class PortBinderCollectionProbe {
|
|||||||
|| !binder.getItem(PortBookBinderBlockEntity.COVER).isEmpty()
|
|| !binder.getItem(PortBookBinderBlockEntity.COVER).isEmpty()
|
||||||
|| !binder.getItem(PortBookBinderBlockEntity.PANEL).isEmpty())
|
|| !binder.getItem(PortBookBinderBlockEntity.PANEL).isEmpty())
|
||||||
throw new IllegalStateException("Collection-backed Binder assembly regression");
|
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());
|
chest.setItem(2, book.copy());
|
||||||
menu.setCarried(ItemStack.EMPTY);
|
menu.setCarried(ItemStack.EMPTY);
|
||||||
} finally { player.getInventory().clearContent(); }
|
} finally { player.getInventory().clearContent(); }
|
||||||
@@ -93,6 +139,18 @@ final class PortBinderCollectionProbe {
|
|||||||
throw new IllegalStateException("Empty collection covers were not preserved");
|
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)
|
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");
|
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())))
|
if (!EXPECTED.equals(chest.getItem(2).get(Rivencraft.BOOK_SYMBOLS.get())))
|
||||||
throw new IllegalStateException("Authored book page order was not preserved");
|
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);
|
boolean issue = shouldIssue(mode, storage.computeIfAbsent(PortAgeVisits.TYPE), age);
|
||||||
if (!issue) return;
|
if (!issue) return;
|
||||||
ItemStack book = createBook(departure);
|
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);
|
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.
|
// Persist the once-per-Age marker before the next arrival can issue another.
|
||||||
if (mode == PortConfig.ReturnLinkMode.ON_CREATION)
|
if (mode == PortConfig.ReturnLinkMode.ON_CREATION)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ final class PortWritingDeskProbe {
|
|||||||
|
|
||||||
static void create(ServerLevel level) {
|
static void create(ServerLevel level) {
|
||||||
PortAgeTravel.findLanding(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"));
|
new com.mojang.authlib.GameProfile(java.util.UUID.fromString("f92ad9ee-1ff0-4fe2-bcd3-994d4c035933"), "RivenDeskProbe"));
|
||||||
player.setPos(0.5, 64, 2.5);
|
player.setPos(0.5, 64, 2.5);
|
||||||
// New Minecraft drops Container block-entity contents during removal.
|
// New Minecraft drops Container block-entity contents during removal.
|
||||||
@@ -69,6 +69,8 @@ final class PortWritingDeskProbe {
|
|||||||
if (menu.getCarried().isEmpty() || desk.inkRemaining() != 15
|
if (menu.getCarried().isEmpty() || desk.inkRemaining() != 15
|
||||||
|| desk.getItem(WritingDeskBlockEntity.PAPER).getCount() != 1 || desk.getItem(0).isEmpty())
|
|| desk.getItem(WritingDeskBlockEntity.PAPER).getCount() != 1 || desk.getItem(0).isEmpty())
|
||||||
throw new IllegalStateException("Desk copy did not consume exactly one paper/ink");
|
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);
|
menu.setCarried(ItemStack.EMPTY);
|
||||||
for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++)
|
for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++)
|
||||||
player.getInventory().setItem(slot, new ItemStack(Items.COBBLESTONE, 64));
|
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());
|
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) {
|
public static Map<Identifier, Long> savedAges(MinecraftServer server) {
|
||||||
checkServerThread(server);
|
checkServerThread(server);
|
||||||
return index(server).entries();
|
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.registerBlockEntityRenderer(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), context -> new StarFissureRenderer());
|
||||||
event.registerEntityRenderer(Rivencraft.DROPPED_BOOK.get(), DroppedBookRenderer::new);
|
event.registerEntityRenderer(Rivencraft.DROPPED_BOOK.get(), DroppedBookRenderer::new);
|
||||||
event.registerBlockEntityRenderer(Rivencraft.WRITING_DESK_BLOCK_ENTITY.get(), WritingDeskRenderer::new);
|
event.registerBlockEntityRenderer(Rivencraft.WRITING_DESK_BLOCK_ENTITY.get(), WritingDeskRenderer::new);
|
||||||
|
event.registerBlockEntityRenderer(Rivencraft.BOOK_STAND_BLOCK_ENTITY.get(), BookStandRenderer::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
|
|||||||
+15
@@ -26,4 +26,19 @@ public final class PortBookInteractionHandler {
|
|||||||
? new PortLinkingBookScreen(event.getHand(), event.getItemStack().copy())
|
? new PortLinkingBookScreen(event.getHand(), event.getItemStack().copy())
|
||||||
: new PortDescriptiveBookScreen(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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+87
-35
@@ -10,81 +10,111 @@ import net.minecraft.client.gui.components.Button;
|
|||||||
import net.minecraft.client.gui.components.EditBox;
|
import net.minecraft.client.gui.components.EditBox;
|
||||||
import net.minecraft.client.gui.screens.Screen;
|
import net.minecraft.client.gui.screens.Screen;
|
||||||
import net.minecraft.client.input.MouseButtonEvent;
|
import net.minecraft.client.input.MouseButtonEvent;
|
||||||
import net.minecraft.client.renderer.RenderPipelines;
|
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.resources.Identifier;
|
|
||||||
import net.minecraft.world.InteractionHand;
|
import net.minecraft.world.InteractionHand;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.neoforged.neoforge.client.network.ClientPacketDistributor;
|
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 {
|
public final class PortDescriptiveBookScreen extends Screen {
|
||||||
private static final int WIDTH = 327;
|
private static final int WIDTH = 327;
|
||||||
private static final int HEIGHT = 199;
|
private static final int HEIGHT = 199;
|
||||||
private static final int TEXT_COLOR = 0xFF3B281B;
|
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 InteractionHand hand;
|
||||||
private final boolean bound;
|
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;
|
private EditBox titleInput;
|
||||||
|
|
||||||
public PortDescriptiveBookScreen(InteractionHand hand, ItemStack book) {
|
public PortDescriptiveBookScreen(InteractionHand hand, ItemStack book) {
|
||||||
super(book.getHoverName());
|
super(book.getHoverName());
|
||||||
this.hand = hand;
|
this.hand = hand;
|
||||||
|
pages = net.dnorberg.rivencraft.writing.DescriptiveBookPages.from(book);
|
||||||
bound = book.has(Rivencraft.AGE_BOOK_ID.get());
|
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
|
@Override
|
||||||
protected void init() {
|
protected void init() {
|
||||||
if (bound) return;
|
if (bound || standSource != null) return;
|
||||||
int left = (width - WIDTH) / 2;
|
int left = (width - WIDTH) / 2;
|
||||||
int top = (height - HEIGHT) / 2;
|
int top = (height - HEIGHT) / 2;
|
||||||
titleInput = new EditBox(font, left + 40, top + 69, 110, 20,
|
titleInput = new EditBox(font, left + 40, top + 69, 110, 20,
|
||||||
Component.literal("Age name"));
|
Component.literal("Age name"));
|
||||||
titleInput.setMaxLength(PortBookNetworking.MAX_BOOK_TITLE_LENGTH);
|
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);
|
addRenderableWidget(titleInput);
|
||||||
setInitialFocus(titleInput);
|
setInitialFocus(titleInput);
|
||||||
addRenderableWidget(Button.builder(Component.literal("Bind"), button -> bind())
|
addRenderableWidget(Button.builder(Component.literal("Bind"), button -> bind())
|
||||||
.bounds(left + 40, top + 100, 53, 20).build());
|
.bounds(left + 40, top + 100, 53, 20).build());
|
||||||
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> onClose())
|
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> onClose())
|
||||||
.bounds(left + 97, top + 100, 53, 20).build());
|
.bounds(left + 97, top + 100, 53, 20).build());
|
||||||
|
updatePageControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||||
int left = (width - WIDTH) / 2;
|
int left = (width - WIDTH) / 2;
|
||||||
int top = (height - HEIGHT) / 2;
|
int top = (height - HEIGHT) / 2;
|
||||||
blit(graphics, COVER, left, top + 7, 34, 192, 152, 0);
|
PortLegacyBookArtwork.drawCover(graphics, left, top, true);
|
||||||
blit(graphics, COVER, left + 34, top + 7, 103, 192, 49, 0);
|
if (pageIndex > 0) PortLegacyBookArtwork.drawLeftPage(graphics, left, top);
|
||||||
blit(graphics, COVER, left + 137, top + 7, 4, 192, 45, 0);
|
if (pageIndex == 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) {
|
if (bound) {
|
||||||
graphics.fill(left + 173, top + 20, left + 305, top + 103, 0xFF15141D);
|
graphics.nextStratum();
|
||||||
graphics.text(font, Component.literal("Age preview unavailable"), left + 181, top + 47,
|
linkPanel.render(graphics, left, top, partialTick);
|
||||||
0xFFE9DFCB, false);
|
graphics.nextStratum();
|
||||||
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.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);
|
super.extractRenderState(graphics, mouseX, mouseY, partialTick);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
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 left = (width - WIDTH) / 2;
|
||||||
int top = (height - HEIGHT) / 2;
|
int top = (height - HEIGHT) / 2;
|
||||||
if (event.x() >= left + 173 && event.x() < left + 305
|
int direction = PortLegacyBookArtwork.turnDirection(event.x() - left, event.y() - top, pageIndex, pages.lastPage());
|
||||||
&& event.y() >= top + 20 && event.y() < top + 103) {
|
if (direction != 0) { turnPage(direction); return true; }
|
||||||
ClientPacketDistributor.sendToServer(new UseDescriptiveBookPayload(hand == InteractionHand.OFF_HAND));
|
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();
|
onClose();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -92,6 +122,36 @@ public final class PortDescriptiveBookScreen extends Screen {
|
|||||||
return super.mouseClicked(event, doubleClick);
|
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
|
@Override
|
||||||
public boolean isPauseScreen() {
|
public boolean isPauseScreen() {
|
||||||
return false;
|
return false;
|
||||||
@@ -105,12 +165,4 @@ public final class PortDescriptiveBookScreen extends Screen {
|
|||||||
onClose();
|
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.components.EditBox;
|
||||||
import net.minecraft.client.gui.screens.Screen;
|
import net.minecraft.client.gui.screens.Screen;
|
||||||
import net.minecraft.client.input.MouseButtonEvent;
|
import net.minecraft.client.input.MouseButtonEvent;
|
||||||
import net.minecraft.client.renderer.RenderPipelines;
|
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.resources.Identifier;
|
|
||||||
import net.minecraft.world.InteractionHand;
|
import net.minecraft.world.InteractionHand;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.neoforged.neoforge.client.network.ClientPacketDistributor;
|
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 {
|
public final class PortLinkingBookScreen extends Screen {
|
||||||
private static final int WIDTH = 327;
|
private static final int WIDTH = 327;
|
||||||
private static final int HEIGHT = 199;
|
private static final int HEIGHT = 199;
|
||||||
private static final int TEXT_COLOR = 0xFF3B281B;
|
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 InteractionHand hand;
|
||||||
private final boolean bound;
|
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;
|
private EditBox titleInput;
|
||||||
|
|
||||||
public PortLinkingBookScreen(InteractionHand hand, ItemStack book) {
|
public PortLinkingBookScreen(InteractionHand hand, ItemStack book) {
|
||||||
super(book.getHoverName());
|
super(book.getHoverName());
|
||||||
this.hand = hand;
|
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
|
@Override
|
||||||
protected void init() {
|
protected void init() {
|
||||||
if (bound) return;
|
if (bound || standSource != null) return;
|
||||||
int left = (width - WIDTH) / 2;
|
int left = (width - WIDTH) / 2;
|
||||||
int top = (height - HEIGHT) / 2;
|
int top = (height - HEIGHT) / 2;
|
||||||
titleInput = new EditBox(font, left + 40, top + 69, 110, 20,
|
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());
|
.bounds(left + 40, top + 100, 53, 20).build());
|
||||||
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> onClose())
|
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> onClose())
|
||||||
.bounds(left + 97, top + 100, 53, 20).build());
|
.bounds(left + 97, top + 100, 53, 20).build());
|
||||||
|
updatePageControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||||
int left = (width - WIDTH) / 2;
|
int left = (width - WIDTH) / 2;
|
||||||
int top = (height - HEIGHT) / 2;
|
int top = (height - HEIGHT) / 2;
|
||||||
blit(graphics, COVER, left, top + 7, 34, 192, 152, 0);
|
PortLegacyBookArtwork.drawCover(graphics, left, top, false);
|
||||||
blit(graphics, COVER, left + 34, top + 7, 103, 192, 49, 0);
|
if (pageIndex > 0) {
|
||||||
blit(graphics, COVER, left + 137, top + 7, 4, 192, 45, 0);
|
PortLegacyBookArtwork.drawLeftPage(graphics, left, top);
|
||||||
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);
|
|
||||||
} else {
|
} else {
|
||||||
graphics.text(font, Component.literal("Name this Linking Book"), left + 183, top + 39,
|
if (bound) {
|
||||||
TEXT_COLOR, false);
|
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);
|
super.extractRenderState(graphics, mouseX, mouseY, partialTick);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
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 left = (width - WIDTH) / 2;
|
||||||
int top = (height - HEIGHT) / 2;
|
int top = (height - HEIGHT) / 2;
|
||||||
if (event.x() >= left + 173 && event.x() < left + 305
|
int direction = PortLegacyBookArtwork.turnDirection(event.x() - left, event.y() - top, pageIndex, 1);
|
||||||
&& event.y() >= top + 20 && event.y() < top + 103) {
|
if (direction != 0) { turnPage(direction); return true; }
|
||||||
ClientPacketDistributor.sendToServer(new UseLinkingBookPayload(hand == InteractionHand.OFF_HAND));
|
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();
|
onClose();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -90,6 +120,33 @@ public final class PortLinkingBookScreen extends Screen {
|
|||||||
return super.mouseClicked(event, doubleClick);
|
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
|
@Override
|
||||||
public boolean isPauseScreen() {
|
public boolean isPauseScreen() {
|
||||||
return false;
|
return false;
|
||||||
@@ -103,12 +160,4 @@ public final class PortLinkingBookScreen extends Screen {
|
|||||||
onClose();
|
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);
|
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
|
@Override
|
||||||
public void tick() {
|
public void tick() {
|
||||||
super.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
|
@Override
|
||||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||||
LinkBookTarget target = stack.get(Rivencraft.LINK_BOOK_TARGET.get());
|
LinkBookTarget target = stack.get(Rivencraft.LINK_BOOK_TARGET.get());
|
||||||
tooltip.accept(Component.literal(target == null ? "Unbound (26.3 port preview)"
|
if (target == null) tooltip.accept(Component.literal("Unbound (26.3 port preview)"));
|
||||||
: "Destination: " + target.dimension() + " (" + (int) target.x() + ", "
|
else {
|
||||||
+ (int) target.y() + ", " + (int) target.z() + ")"));
|
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);
|
LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||||
if (properties.has(LinkProperties.DISARM))
|
if (properties.has(LinkProperties.DISARM))
|
||||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.disarm"));
|
tooltip.accept(Component.translatable("item.rivencraft.link_property.disarm"));
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public final class PortHandheldTravel {
|
|||||||
try {
|
try {
|
||||||
String failure = travel.get();
|
String failure = travel.get();
|
||||||
if (failure != null) return failure;
|
if (failure != null) return failure;
|
||||||
|
net.dnorberg.rivencraft.advancement.ProgressionAdvancements.awardAgeTravel(player);
|
||||||
dropped = source.addFreshEntity(new DroppedBookEntity(source, departure, book));
|
dropped = source.addFreshEntity(new DroppedBookEntity(source, departure, book));
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package net.dnorberg.rivencraft.menu;
|
package net.dnorberg.rivencraft.menu;
|
||||||
|
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
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.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
import net.minecraft.world.Container;
|
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())
|
if (!moveItemStackTo(remainder, PLAYER_START, slots.size(), true) || !remainder.isEmpty())
|
||||||
return ItemStack.EMPTY;
|
return ItemStack.EMPTY;
|
||||||
if (!mixer.consumeLinkPanelMaterials()) return ItemStack.EMPTY;
|
if (!mixer.consumeLinkPanelMaterials()) return ItemStack.EMPTY;
|
||||||
|
if (player instanceof ServerPlayer serverPlayer)
|
||||||
|
ProgressionAdvancements.award(serverPlayer, ProgressionAdvancements.LINK_PANEL);
|
||||||
refreshResult();
|
refreshResult();
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
@@ -150,6 +154,8 @@ public final class InkMixerMenu extends AbstractContainerMenu {
|
|||||||
}
|
}
|
||||||
@Override public void onTake(Player player, ItemStack stack) {
|
@Override public void onTake(Player player, ItemStack stack) {
|
||||||
if (!clientSide && mixer.consumeLinkPanelMaterials()) {
|
if (!clientSide && mixer.consumeLinkPanelMaterials()) {
|
||||||
|
if (player instanceof ServerPlayer serverPlayer)
|
||||||
|
ProgressionAdvancements.award(serverPlayer, ProgressionAdvancements.LINK_PANEL);
|
||||||
refreshResult();
|
refreshResult();
|
||||||
}
|
}
|
||||||
super.onTake(player, stack);
|
super.onTake(player, stack);
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ public final class PortBookBinderMenu extends AbstractContainerMenu {
|
|||||||
var id = PortBookNetworking.createAge(player, title, blockEntity.symbolPages());
|
var id = PortBookNetworking.createAge(player, title, blockEntity.symbolPages());
|
||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
book.set(Rivencraft.AGE_BOOK_ID.get(), id);
|
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));
|
book.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||||
if (blockEntity.hasSymbolPages())
|
if (blockEntity.hasSymbolPages())
|
||||||
book.set(Rivencraft.BOOK_SYMBOLS.get(), blockEntity.symbolPages());
|
book.set(Rivencraft.BOOK_SYMBOLS.get(), blockEntity.symbolPages());
|
||||||
@@ -129,6 +130,8 @@ public final class PortBookBinderMenu extends AbstractContainerMenu {
|
|||||||
binder.setItem(PortBookBinderBlockEntity.PANEL, panel);
|
binder.setItem(PortBookBinderBlockEntity.PANEL, panel);
|
||||||
blockEntity.clearSymbolPages();
|
blockEntity.clearSymbolPages();
|
||||||
setCarried(book);
|
setCarried(book);
|
||||||
|
net.dnorberg.rivencraft.advancement.ProgressionAdvancements.award(player,
|
||||||
|
net.dnorberg.rivencraft.advancement.ProgressionAdvancements.BIND_AGE);
|
||||||
broadcastChanges();
|
broadcastChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package net.dnorberg.rivencraft.menu;
|
package net.dnorberg.rivencraft.menu;
|
||||||
|
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
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.block.entity.WritingDeskBlockEntity;
|
||||||
import net.dnorberg.rivencraft.item.PageItem;
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||||
@@ -321,6 +323,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
|||||||
return ItemStack.EMPTY;
|
return ItemStack.EMPTY;
|
||||||
}
|
}
|
||||||
if (!blockEntity.consumeCopyMaterials()) return ItemStack.EMPTY;
|
if (!blockEntity.consumeCopyMaterials()) return ItemStack.EMPTY;
|
||||||
|
if (player instanceof ServerPlayer serverPlayer)
|
||||||
|
ProgressionAdvancements.award(serverPlayer, ProgressionAdvancements.COPY_PAGE);
|
||||||
refreshOutput();
|
refreshOutput();
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
@@ -439,6 +443,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
|||||||
}
|
}
|
||||||
@Override public void onTake(Player player, ItemStack stack) {
|
@Override public void onTake(Player player, ItemStack stack) {
|
||||||
if (!clientSide && blockEntity.consumeCopyMaterials()) {
|
if (!clientSide && blockEntity.consumeCopyMaterials()) {
|
||||||
|
if (player instanceof ServerPlayer serverPlayer)
|
||||||
|
ProgressionAdvancements.award(serverPlayer, ProgressionAdvancements.COPY_PAGE);
|
||||||
refreshOutput();
|
refreshOutput();
|
||||||
}
|
}
|
||||||
super.onTake(player, stack);
|
super.onTake(player, stack);
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ public final class PortBookNetworking {
|
|||||||
(payload, context) -> {
|
(payload, context) -> {
|
||||||
if (context.player() instanceof ServerPlayer player) bind(player, payload);
|
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,
|
registrar.playToServer(UseLinkingBookPayload.TYPE, UseLinkingBookPayload.STREAM_CODEC,
|
||||||
(payload, context) -> {
|
(payload, context) -> {
|
||||||
if (context.player() instanceof ServerPlayer player) use(player, payload);
|
if (context.player() instanceof ServerPlayer player) use(player, payload);
|
||||||
@@ -73,6 +77,7 @@ public final class PortBookNetworking {
|
|||||||
String title = payload.title().strip();
|
String title = payload.title().strip();
|
||||||
if (title.isEmpty() || title.length() > MAX_BOOK_TITLE_LENGTH) return;
|
if (title.isEmpty() || title.length() > MAX_BOOK_TITLE_LENGTH) return;
|
||||||
stack.set(Rivencraft.LINK_BOOK_TARGET.get(), LinkBookTarget.at(player));
|
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));
|
stack.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||||
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN,
|
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN,
|
||||||
SoundSource.PLAYERS, 1.0F, 1.0F);
|
SoundSource.PLAYERS, 1.0F, 1.0F);
|
||||||
@@ -96,6 +101,7 @@ public final class PortBookNetworking {
|
|||||||
Identifier id = createAge(player, title);
|
Identifier id = createAge(player, title);
|
||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
stack.set(Rivencraft.AGE_BOOK_ID.get(), id);
|
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));
|
stack.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||||
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN,
|
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN,
|
||||||
SoundSource.PLAYERS, 1.0F, 1.0F);
|
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.PortDayCycle.nativeTicks(server.overworld())));
|
||||||
net.dnorberg.rivencraft.age.PortAcceleratedTicks.configure(level,
|
net.dnorberg.rivencraft.age.PortAcceleratedTicks.configure(level,
|
||||||
net.dnorberg.rivencraft.age.PortAcceleratedTicks.pageCount(pages));
|
net.dnorberg.rivencraft.age.PortAcceleratedTicks.pageCount(pages));
|
||||||
|
RuntimeAgeBackend.setAgeDisplayName(server, id, title);
|
||||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||||
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
||||||
return null;
|
return null;
|
||||||
@@ -167,22 +174,51 @@ public final class PortBookNetworking {
|
|||||||
private static void enterAge(ServerPlayer player, UseDescriptiveBookPayload payload) {
|
private static void enterAge(ServerPlayer player, UseDescriptiveBookPayload payload) {
|
||||||
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
|
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
|
||||||
if (!stack.is(Rivencraft.DESCRIPTIVE_BOOK.get())) return;
|
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());
|
Identifier id = stack.get(Rivencraft.AGE_BOOK_ID.get());
|
||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
ServerLevel source = player.level();
|
ServerLevel source = player.level();
|
||||||
LinkBookTarget returnTarget = LinkBookTarget.at(player);
|
LinkBookTarget returnTarget = LinkBookTarget.at(player);
|
||||||
Vec3 departure = player.position();
|
Vec3 departure = player.position();
|
||||||
Vec3 incoming = player.getDeltaMovement();
|
Vec3 incoming = player.getDeltaMovement();
|
||||||
String failure = net.dnorberg.rivencraft.item.PortHandheldTravel.travel(player,
|
java.util.function.Supplier<String> travel = () -> {
|
||||||
hand(payload.offhand()), stack, () -> {
|
|
||||||
String result = PortAgeTravel.enter(player, id);
|
String result = PortAgeTravel.enter(player, id);
|
||||||
if (result == null) LinkingBookItem.applySuccessfulLink(player,
|
if (result == null) LinkingBookItem.applySuccessfulLink(player,
|
||||||
stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE),
|
stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE),
|
||||||
source, departure, incoming, player.getYRot());
|
source, departure, incoming, player.getYRot());
|
||||||
return result;
|
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));
|
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) {
|
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