Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8246640f49 | ||
|
|
ba2faace15 | ||
|
|
88355ee276 |
No files matched your search
@@ -16,6 +16,9 @@ The Writing Desk block texture is copied from Legacy's
|
|||||||
assets/mystcraft/textures/entity/desk.png under the same license.
|
assets/mystcraft/textures/entity/desk.png under the same license.
|
||||||
The Writing Desk GUI atlas is copied from Legacy's assets/mystcraft/gui/writingdesk.png
|
The Writing Desk GUI atlas is copied from Legacy's assets/mystcraft/gui/writingdesk.png
|
||||||
under the same license; desk wood and inlay textures are cropped from desk.png.
|
under the same license; desk wood and inlay textures are cropped from desk.png.
|
||||||
|
The Ink Mixer GUI atlas and block textures are copied from Legacy's
|
||||||
|
assets/mystcraft/gui/inkmixer.png and textures/blocks/inkmixer_*.png under
|
||||||
|
the same license. Rivencraft's block model is original and uses those textures.
|
||||||
|
|
||||||
The initial build layout is derived from the official NeoForge 1.21.1
|
The initial build layout is derived from the official NeoForge 1.21.1
|
||||||
ModDevGradle MDK: https://github.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle
|
ModDevGradle MDK: https://github.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle
|
||||||
@@ -4,8 +4,8 @@ Rivencraft is a clean, incremental port of the ideas and gameplay of
|
|||||||
[Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to Minecraft
|
[Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to Minecraft
|
||||||
1.21.1 on NeoForge.
|
1.21.1 on NeoForge.
|
||||||
|
|
||||||
The bootstrap, books/linking, and Ages milestones are complete. Milestone 4 is
|
The bootstrap, books/linking, Ages, and writing-system milestones are complete.
|
||||||
building the writing system on a data-pack symbol catalog and persistent
|
The writing system uses a data-pack symbol catalog and persistent
|
||||||
ordered-writing model. The project currently provides functional Linking Books
|
ordered-writing model. The project currently provides functional Linking Books
|
||||||
and randomly seeded, persistent vanilla-like Ages created from page-free
|
and randomly seeded, persistent vanilla-like Ages created from page-free
|
||||||
Descriptive Books. Instability and full symbol-to-world grammar interpretation
|
Descriptive Books. Instability and full symbol-to-world grammar interpretation
|
||||||
@@ -19,6 +19,11 @@ hand and use the Ink Vial in the other to prepare a baseline Link Panel Page.
|
|||||||
Each vial holds 16 ink units, and the default link-panel cost is two units.
|
Each vial holds 16 ink units, and the default link-panel cost is two units.
|
||||||
The spent vial returns a glass bottle. Server owners can adjust the symbol-copy
|
The spent vial returns a glass bottle. Server owners can adjust the symbol-copy
|
||||||
and link-panel costs in the `writing` section of `rivencraft-server.toml`.
|
and link-panel costs in the `writing` section of `rivencraft-server.toml`.
|
||||||
|
The Ink Mixer now offers a Legacy-style station for producing Link Panels:
|
||||||
|
insert paper and Ink Vials, then take a Link Panel from the result slot. Each
|
||||||
|
vial fills its 16-unit basin and returns a glass bottle; no server tick is
|
||||||
|
needed. The earlier paper-in-one-hand method remains available. Ink modifier
|
||||||
|
ingredients and link properties are planned for Milestone 5.
|
||||||
The Writing Desk can copy a selected written page from its collection tabs.
|
The Writing Desk can copy a selected written page from its collection tabs.
|
||||||
The Writing Desk is now craftable from planks, a glass bottle, and a feather.
|
The Writing Desk is now craftable from planks, a glass bottle, and a feather.
|
||||||
It occupies two adjacent blocks, with one shared inventory; loaded paper and
|
It occupies two adjacent blocks, with one shared inventory; loaded paper and
|
||||||
@@ -61,6 +66,21 @@ portrait preview and name above the player inventory.
|
|||||||
|
|
||||||
## Book Binder
|
## Book Binder
|
||||||
|
|
||||||
|
For a new survival Age: craft an Ink Vial, an Ink Mixer, a Writing Desk, and a Book Binder.
|
||||||
|
Put vanilla paper and an Ink Vial in the Ink Mixer to make a Link Panel
|
||||||
|
Page. Place leather (or an empty Notebook) and the Link Panel in the Book Binder,
|
||||||
|
enter an Age name, and take the finished Descriptive Book. Written symbol Pages
|
||||||
|
are optional; without them, the book creates a random Age. To duplicate a Page
|
||||||
|
you already own, store it in a Writing Desk tab, select it, supply paper and ink,
|
||||||
|
and take the copied Page from the output. A Notebook can hold and transfer a
|
||||||
|
larger ordered collection of Pages between desks and the binder.
|
||||||
|
|
||||||
|
With JEI installed, Rivencraft displays the Ink Mixer operation in its own
|
||||||
|
category and the hand-use, copying, binding, and Linking Book crafting flows in a Writing category.
|
||||||
|
JEI is optional; vanilla crafting
|
||||||
|
recipes and all writing interactions work without it. For a development client
|
||||||
|
with JEI, run `./gradlew runClient -PwithJei=true`.
|
||||||
|
|
||||||
Craft a Book Binder from three iron ingots and five planks. Put leather or an
|
Craft a Book Binder from three iron ingots and five planks. Put leather or an
|
||||||
empty Notebook in the left cover slot. Enter a title, then place a Link Panel
|
empty Notebook in the left cover slot. Enter a title, then place a Link Panel
|
||||||
Page first in the horizontal page strip. Click a page in the strip with a
|
Page first in the horizontal page strip. Click a page in the strip with a
|
||||||
@@ -84,8 +104,8 @@ font.
|
|||||||
|
|
||||||
## Linking Book
|
## Linking Book
|
||||||
|
|
||||||
Craft a Linking Book from a vanilla book and an ender pearl, then use it in the
|
Craft an unwritten Linking Book from one Link Panel Page and one leather in any
|
||||||
air. An unbound book opens a screen where it can be named and bound to the
|
two crafting-grid positions. An unbound book opens a screen where it can be named and bound to the
|
||||||
current location. A bound book opens its destination screen and travels when
|
current location. A bound book opens its destination screen and travels when
|
||||||
its preview panel is clicked. The destination is stored on the item and
|
its preview panel is clicked. The destination is stored on the item and
|
||||||
survives inventory and world saves. Travel only succeeds if the target
|
survives inventory and world saves. Travel only succeeds if the target
|
||||||
@@ -93,6 +113,7 @@ dimension is loaded and a non-fluid, passable landing position can be found
|
|||||||
nearby.
|
nearby.
|
||||||
The Linking Book uses the same Legacy page layout and bottom-corner page turns
|
The Linking Book uses the same Legacy page layout and bottom-corner page turns
|
||||||
as the Descriptive Book, but without the Descriptive Book's gold cover edges.
|
as the Descriptive Book, but without the Descriptive Book's gold cover edges.
|
||||||
|
Both screens play Minecraft's book-page-turn sound when a page is turned.
|
||||||
|
|
||||||
Successful book travel plays Mystcraft Legacy's original linking sound at both
|
Successful book travel plays Mystcraft Legacy's original linking sound at both
|
||||||
the departure and arrival locations.
|
the departure and arrival locations.
|
||||||
|
|||||||
+14
-6
@@ -27,6 +27,14 @@ sourceSets.main.resources {
|
|||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
maven {
|
||||||
|
name = 'JaredsMaven'
|
||||||
|
url = 'https://maven.blamejared.com'
|
||||||
|
content {
|
||||||
|
includeGroup 'mezz.jei'
|
||||||
|
includeGroup 'net.mezzdev.config'
|
||||||
|
}
|
||||||
|
}
|
||||||
maven { url = 'https://maven.createmod.net' }
|
maven { url = 'https://maven.createmod.net' }
|
||||||
maven {
|
maven {
|
||||||
url = 'https://www.cursemaven.com'
|
url = 'https://www.cursemaven.com'
|
||||||
@@ -172,12 +180,12 @@ dependencies {
|
|||||||
compileOnly "maven.modrinth:immersive-portals-neoforge:${immersive_portals_version}"
|
compileOnly "maven.modrinth:immersive-portals-neoforge:${immersive_portals_version}"
|
||||||
implementation "curse.maven:dynamicdimension-ar-edition-1469680:${dynamic_dimensions_file_id}"
|
implementation "curse.maven:dynamicdimension-ar-edition-1469680:${dynamic_dimensions_file_id}"
|
||||||
implementation "lol.bai:badpackets:neo-${badpackets_version}"
|
implementation "lol.bai:badpackets:neo-${badpackets_version}"
|
||||||
// Example optional mod dependency with JEI
|
// JEI integration is isolated in its plugin and absent from the published runtime dependencies.
|
||||||
// The JEI API is declared for compile time use, while the full JEI artifact is used at runtime
|
compileOnly "mezz.jei:jei-${minecraft_version}-neoforge-api:${jei_version}"
|
||||||
// compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}"
|
// Opt in to JEI for a dev client with -PwithJei=true.
|
||||||
// compileOnly "mezz.jei:jei-${mc_version}-neoforge-api:${jei_version}"
|
if (providers.gradleProperty('withJei').map { it.toBoolean() }.orElse(false).get()) {
|
||||||
// We add the full version to localRuntime, not runtimeOnly, so that we do not publish a dependency on it
|
localRuntime "mezz.jei:jei-${minecraft_version}-neoforge:${jei_version}"
|
||||||
// localRuntime "mezz.jei:jei-${mc_version}-neoforge:${jei_version}"
|
}
|
||||||
|
|
||||||
// Example mod dependency using a mod jar from ./libs with a flat dir repository
|
// Example mod dependency using a mod jar from ./libs with a flat dir repository
|
||||||
// This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar
|
// This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ project's GNU LGPL v3 license. Rivencraft itself uses the same license.
|
|||||||
| `assets/rivencraft/textures/block/book_binder.png` | `assets/mystcraft/textures/entity/bookbinder/bookbinder.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Book Binder model uses this body texture |
|
| `assets/rivencraft/textures/block/book_binder.png` | `assets/mystcraft/textures/entity/bookbinder/bookbinder.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Book Binder model uses this body texture |
|
||||||
| `assets/rivencraft/textures/block/book_binder_press.png` | `assets/mystcraft/textures/entity/bookbinder/press_part.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Book Binder model uses this press texture |
|
| `assets/rivencraft/textures/block/book_binder_press.png` | `assets/mystcraft/textures/entity/bookbinder/press_part.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Book Binder model uses this press texture |
|
||||||
| `assets/rivencraft/textures/gui/book_binder_legacy.png` | `assets/mystcraft/gui/pagebinder.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Book Binder GUI uses the original 176×181 panel |
|
| `assets/rivencraft/textures/gui/book_binder_legacy.png` | `assets/mystcraft/gui/pagebinder.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Book Binder GUI uses the original 176×181 panel |
|
||||||
|
| `assets/rivencraft/textures/gui/ink_mixer_legacy.png` | `assets/mystcraft/gui/inkmixer.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Ink Mixer GUI uses the original 176×181 panel |
|
||||||
|
| `assets/rivencraft/textures/block/ink_mixer_top.png`, `ink_mixer_side.png`, `ink_mixer_bottom.png` | `assets/mystcraft/textures/blocks/inkmixer_top.png`, `inkmixer_side.png`, `inkmixer_bottom.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Rivencraft supplies its own block model |
|
||||||
| `assets/rivencraft/textures/item/linking_book_unwritten.png` | `assets/mystcraft/textures/items/unlinked.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only |
|
| `assets/rivencraft/textures/item/linking_book_unwritten.png` | `assets/mystcraft/textures/items/unlinked.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only |
|
||||||
| `assets/rivencraft/textures/item/page_link_panel.png` | `page_background.png` and `PageBuilder.BasicPageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Reproduces the legacy 160 px scale, black panel at `(25,30)`–`(135,75)`, and 128 px resample |
|
| `assets/rivencraft/textures/item/page_link_panel.png` | `page_background.png` and `PageBuilder.BasicPageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Reproduces the legacy 160 px scale, black panel at `(25,30)`–`(135,75)`, and 128 px resample |
|
||||||
| `assets/rivencraft/textures/item/page_symbol*.png` and `page_unresolved.png` | `page_background.png`, `textures/symbolcomponents.png`, `WordData`, `ModSymbols`, and `PageBuilder.PageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Static 1.21.1 renditions of the legacy runtime-composed four-word symbol pages; `normal_time` uses the legacy Full Length poem |
|
| `assets/rivencraft/textures/item/page_symbol*.png` and `page_unresolved.png` | `page_background.png`, `textures/symbolcomponents.png`, `WordData`, `ModSymbols`, and `PageBuilder.PageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Static 1.21.1 renditions of the legacy runtime-composed four-word symbol pages; `normal_time` uses the legacy Full Length poem |
|
||||||
|
|||||||
+8
-3
@@ -33,8 +33,10 @@ acceptance issue and all required implementation issues are complete.
|
|||||||
lifecycle management, data-pack integration, and optional destination
|
lifecycle management, data-pack integration, and optional destination
|
||||||
renderer compatibility. **Complete.** See
|
renderer compatibility. **Complete.** See
|
||||||
[Milestone 3](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/milestone/8).
|
[Milestone 3](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/milestone/8).
|
||||||
4. **Writing system** — pages, symbols, notebooks, desks, and book assembly.
|
4. **Writing system** — pages, notebooks, desks, book assembly, and the baseline
|
||||||
**In progress.**
|
Ink Mixer for Link Panels. Symbol effects are deferred until Milestone 5.
|
||||||
|
**Complete.** See [Milestone 4](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/milestone/9)
|
||||||
|
and [writing-system data and migration](WRITING_DATA.md).
|
||||||
5. **Age grammar** — translate symbol combinations into modern biome, noise,
|
5. **Age grammar** — translate symbol combinations into modern biome, noise,
|
||||||
dimension-type, and world-generation configuration. Optional integrations
|
dimension-type, and world-generation configuration. Optional integrations
|
||||||
may add composable biome regions and seasonal behavior.
|
may add composable biome regions and seasonal behavior.
|
||||||
@@ -62,10 +64,13 @@ The writing system and Age grammar will build on the existing page-free Age
|
|||||||
path. Page-free books remain a useful deterministic fallback and test fixture
|
path. Page-free books remain a useful deterministic fallback and test fixture
|
||||||
after written symbol combinations become available.
|
after written symbol combinations become available.
|
||||||
|
|
||||||
Milestone 4 begins with a versioned, data-pack-defined symbol catalog and an
|
Milestone 4 delivered a versioned, data-pack-defined symbol catalog and an
|
||||||
immutable ordered-writing model. See [WRITING_DATA.md](WRITING_DATA.md). These
|
immutable ordered-writing model. See [WRITING_DATA.md](WRITING_DATA.md). These
|
||||||
contracts preserve unknown symbol identifiers and store author intent; they do
|
contracts preserve unknown symbol identifiers and store author intent; they do
|
||||||
not interpret symbols into Age generation, which remains Milestone 5 work.
|
not interpret symbols into Age generation, which remains Milestone 5 work.
|
||||||
|
The Ink Mixer is included in Milestone 4 after book assembly; it
|
||||||
|
uses paper and Ink Vials to make baseline Link Panels. Adding modifier
|
||||||
|
ingredients and symbol-driven link properties belongs to Milestone 5.
|
||||||
|
|
||||||
## Provenance rules
|
## Provenance rules
|
||||||
|
|
||||||
|
|||||||
+56
-5
@@ -65,11 +65,11 @@ unresolved symbol, and baseline link panel. Symbol and unresolved-symbol pages
|
|||||||
retain their namespaced symbol reference; unresolved pages can be inspected and
|
retain their namespaced symbol reference; unresolved pages can be inspected and
|
||||||
removed but are rejected as usable writing until their data pack returns.
|
removed but are rejected as usable writing until their data pack returns.
|
||||||
|
|
||||||
Only server-side writing operations may create a known symbol
|
Only server-side writing operations may create a known symbol page or link
|
||||||
page or link panel from vanilla paper. The shared `PageItem` creation and validation gates reject
|
panel from vanilla paper. The shared `PageItem` creation and validation gates
|
||||||
new pages for identifiers absent from the server catalog. Later Writing Desk
|
reject new pages for identifiers absent from the server catalog. The Writing
|
||||||
and Book Binder packets must call these gates rather than accepting page
|
Desk refuses to copy an unresolved page, and the Book Binder keeps such a page
|
||||||
contents supplied by a client.
|
removable but will not assemble a book from it until its symbol resolves again.
|
||||||
|
|
||||||
The item model uses server-assigned custom-model data derived from the symbol
|
The item model uses server-assigned custom-model data derived from the symbol
|
||||||
identifier. Built-in pages reproduce Mystcraft Legacy's four-word poem
|
identifier. Built-in pages reproduce Mystcraft Legacy's four-word poem
|
||||||
@@ -84,6 +84,29 @@ placement. The surface draws the native 30×40 portrait page region from
|
|||||||
Legacy's book UI atlas; symbols are layered at a uniform scale, while the
|
Legacy's book UI atlas; symbols are layered at a uniform scale, while the
|
||||||
square item models remain unchanged in inventories.
|
square item models remain unchanged in inventories.
|
||||||
|
|
||||||
|
A Notebook holds 27 ordered positions. Its pages, names, gaps, and unresolved
|
||||||
|
IDs are saved on the item, so moving the Notebook between an inventory, Desk,
|
||||||
|
and Binder does not rearrange them. A loose Page in a Desk tab exposes just one
|
||||||
|
position; a Notebook exposes all 27. The Desk has 88 persistent collection
|
||||||
|
tabs, shown four at a time. Use the up/down tab controls to move by four, put
|
||||||
|
paper and Ink Vials in their input slots, select a stored page, and take the
|
||||||
|
copy from the virtual output. Taking it spends one paper and the configured
|
||||||
|
symbol-copy ink cost. The `AZ`, `ALL`, search, and category controls only change
|
||||||
|
which stored pages are displayed; the server rechecks the actual source before
|
||||||
|
copying. Moving a page within the Desk or dragging it into the inventory removes
|
||||||
|
the original only when the destination accepts it.
|
||||||
|
|
||||||
|
The Book Binder takes a leather or empty-Notebook cover, an Age title, one Link
|
||||||
|
Panel, and optionally up to 27 ordered written Pages. A filled Notebook can be
|
||||||
|
imported at a chosen strip position; the empty Notebook remains recoverable.
|
||||||
|
The preview result is virtual. Taking the completed Descriptive Book commits the
|
||||||
|
inputs once, creates its Age identity, and saves schema-versioned
|
||||||
|
`OrderedWriting` plus the Age reference on the book. Link-Panel-only books
|
||||||
|
create random Ages. A written symbol does **not** yet change that generation.
|
||||||
|
Inputs, title, and pending Age identity survive Binder save/load; breaking the
|
||||||
|
Binder returns stored inputs. The completed book retains both its writing and
|
||||||
|
Age identity through item save/load and copying.
|
||||||
|
|
||||||
## Ink and baseline link panels
|
## Ink and baseline link panels
|
||||||
|
|
||||||
An Ink Vial is crafted from two black dye, a glass bottle, and a water bucket.
|
An Ink Vial is crafted from two black dye, a glass bottle, and a water bucket.
|
||||||
@@ -103,3 +126,31 @@ resolved page, then spends paper and ink only when that copy is taken. The
|
|||||||
desk's 16-unit ink meter refills from an Ink Vial when needed and returns a
|
desk's 16-unit ink meter refills from an Ink Vial when needed and returns a
|
||||||
vanilla glass bottle. Link-property modifiers are
|
vanilla glass bottle. Link-property modifiers are
|
||||||
deferred beyond this baseline flow.
|
deferred beyond this baseline flow.
|
||||||
|
|
||||||
|
The Ink Mixer is the station version of Link Panel preparation. Put paper and
|
||||||
|
Ink Vials in its inputs and take the Link Panel from its virtual output. Its
|
||||||
|
16-unit basin refills only when needed and only if the glass-bottle output has
|
||||||
|
space. The basin and inputs persist through save/load; breaking the Mixer
|
||||||
|
returns stored items. A Link Panel plus leather also crafts an unwritten
|
||||||
|
Linking Book. JEI, when installed, displays the Mixer operation and the
|
||||||
|
writing, binding, and Linking Book crafting flows; it is not required to play.
|
||||||
|
|
||||||
|
## Migration and validation
|
||||||
|
|
||||||
|
The 0.3.7.2 bare-list `OrderedWriting` form reads as schema version 1 without
|
||||||
|
changing entry order. Descriptive Books with no writing component keep their
|
||||||
|
existing Age behavior. Old eight-slot Desks migrate their four collections,
|
||||||
|
ink supply, paper, bottle return, and legacy output into the 88-tab layout;
|
||||||
|
saved inputs are never silently discarded. Missing symbol IDs survive page,
|
||||||
|
Notebook, Binder, and assembled-book persistence instead of being substituted
|
||||||
|
or erased.
|
||||||
|
|
||||||
|
Dedicated-server GameTests exercise codec and item round-trips, legacy data,
|
||||||
|
workstation NBT reload, break drops, full-inventory refusal, forged menu IDs,
|
||||||
|
and replayed output requests. World saves and chunk unloads use the same block
|
||||||
|
entity NBT persistence path as those reload tests. This does not replace a
|
||||||
|
manual smoke test of loading an older world before updating a production save.
|
||||||
|
|
||||||
|
Milestone 4 stops at preserving author intent and offering a playable random
|
||||||
|
Age fallback. Milestone 5 will define symbol grammar and validation, translate
|
||||||
|
valid writing into actual Age generation, and introduce symbol and ink modifiers.
|
||||||
+2
-1
@@ -22,6 +22,7 @@ neo_version=21.1.251
|
|||||||
ponder_version=1.0.82
|
ponder_version=1.0.82
|
||||||
dynamic_dimensions_file_id=7707789
|
dynamic_dimensions_file_id=7707789
|
||||||
badpackets_version=0.8.2
|
badpackets_version=0.8.2
|
||||||
|
jei_version=19.57.0.449
|
||||||
# The loader version range can only use the major version of FML as bounds
|
# The loader version range can only use the major version of FML as bounds
|
||||||
loader_version_range=[1,)
|
loader_version_range=[1,)
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ mod_name=Rivencraft
|
|||||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||||
mod_license=LGPL-3.0-or-later
|
mod_license=LGPL-3.0-or-later
|
||||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||||
mod_version=0.3.8.2
|
mod_version=0.4.0.0
|
||||||
immersive_portals_version=6.0.7
|
immersive_portals_version=6.0.7
|
||||||
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
||||||
# This should match the base package used for the mod sources.
|
# This should match the base package used for the mod sources.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.registry.ModCreativeTabs;
|
|||||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
import net.dnorberg.rivencraft.registry.ModItems;
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
import net.dnorberg.rivencraft.registry.ModMenus;
|
import net.dnorberg.rivencraft.registry.ModMenus;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModRecipeSerializers;
|
||||||
import net.dnorberg.rivencraft.registry.ModSounds;
|
import net.dnorberg.rivencraft.registry.ModSounds;
|
||||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||||
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||||
@@ -43,6 +44,7 @@ public final class Rivencraft {
|
|||||||
ModBlockEntities.register(modEventBus);
|
ModBlockEntities.register(modEventBus);
|
||||||
ModItems.register(modEventBus);
|
ModItems.register(modEventBus);
|
||||||
ModMenus.register(modEventBus);
|
ModMenus.register(modEventBus);
|
||||||
|
ModRecipeSerializers.register(modEventBus);
|
||||||
ModSounds.register(modEventBus);
|
ModSounds.register(modEventBus);
|
||||||
ModCreativeTabs.register(modEventBus);
|
ModCreativeTabs.register(modEventBus);
|
||||||
NeoForge.EVENT_BUS.addListener(BookStandConversionHandler::onRightClickBlock);
|
NeoForge.EVENT_BUS.addListener(BookStandConversionHandler::onRightClickBlock);
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package net.dnorberg.rivencraft.block;
|
||||||
|
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||||
|
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.Containers;
|
||||||
|
import net.minecraft.world.InteractionResult;
|
||||||
|
import net.minecraft.world.SimpleMenuProvider;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||||
|
import net.minecraft.world.level.block.RenderShape;
|
||||||
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.phys.BlockHitResult;
|
||||||
|
|
||||||
|
public final class InkMixerBlock extends BaseEntityBlock {
|
||||||
|
public static final MapCodec<InkMixerBlock> CODEC = simpleCodec(InkMixerBlock::new);
|
||||||
|
|
||||||
|
public InkMixerBlock(Properties properties) { super(properties); }
|
||||||
|
|
||||||
|
@Override protected MapCodec<? extends BaseEntityBlock> codec() { return CODEC; }
|
||||||
|
@Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; }
|
||||||
|
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||||
|
return new InkMixerBlockEntity(pos, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
|
||||||
|
Player player, BlockHitResult hit) {
|
||||||
|
if (!level.isClientSide && player instanceof ServerPlayer serverPlayer
|
||||||
|
&& level.getBlockEntity(pos) instanceof InkMixerBlockEntity mixer) {
|
||||||
|
serverPlayer.openMenu(new SimpleMenuProvider(
|
||||||
|
(id, inventory, ignored) -> new InkMixerMenu(id, inventory, mixer),
|
||||||
|
Component.translatable("container.rivencraft.ink_mixer")),
|
||||||
|
buffer -> buffer.writeBlockPos(pos));
|
||||||
|
}
|
||||||
|
return InteractionResult.sidedSuccess(level.isClientSide);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onRemove(BlockState state, Level level, BlockPos pos,
|
||||||
|
BlockState replacement, boolean moving) {
|
||||||
|
if (!state.is(replacement.getBlock()) && !level.isClientSide
|
||||||
|
&& level.getBlockEntity(pos) instanceof InkMixerBlockEntity mixer) {
|
||||||
|
Containers.dropContents(level, pos, mixer);
|
||||||
|
level.updateNeighbourForOutputSignal(pos, this);
|
||||||
|
}
|
||||||
|
super.onRemove(state, level, pos, replacement, moving);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,7 +105,7 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe
|
|||||||
|| items.get(LINK_PANEL).getCount() != 1) return false;
|
|| items.get(LINK_PANEL).getCount() != 1) return false;
|
||||||
for (int slot = FIRST_PAGE; slot < SIZE; slot++) {
|
for (int slot = FIRST_PAGE; slot < SIZE; slot++) {
|
||||||
ItemStack page = items.get(slot);
|
ItemStack page = items.get(slot);
|
||||||
if (!page.isEmpty() && (!validPage(page) || page.getCount() != 1
|
if (!page.isEmpty() && (!validPage(page) || !PageItem.isUsableWriting(page) || page.getCount() != 1
|
||||||
|| PageItem.content(page).kind() == PageContent.Kind.BLANK)) return false;
|
|| PageItem.content(page).kind() == PageContent.Kind.BLANK)) return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package net.dnorberg.rivencraft.block.entity;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
|
import net.dnorberg.rivencraft.writing.WritingInkService;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.core.NonNullList;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.world.Container;
|
||||||
|
import net.minecraft.world.ContainerHelper;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.Items;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
|
||||||
|
/** Baseline Link Panel mixer. Modifier ingredients belong to the symbol/ink-effects milestone. */
|
||||||
|
public final class InkMixerBlockEntity extends BlockEntity implements Container {
|
||||||
|
public static final int INK = 0;
|
||||||
|
public static final int PAPER = 1;
|
||||||
|
public static final int BOTTLES = 2;
|
||||||
|
public static final int SIZE = 3;
|
||||||
|
|
||||||
|
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||||
|
private int inkRemaining;
|
||||||
|
|
||||||
|
public InkMixerBlockEntity(BlockPos pos, BlockState state) {
|
||||||
|
super(ModBlockEntities.INK_MIXER.get(), pos, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int inkRemaining() { return inkRemaining; }
|
||||||
|
|
||||||
|
@Override public int getContainerSize() { return SIZE; }
|
||||||
|
@Override public boolean isEmpty() { return items.stream().allMatch(ItemStack::isEmpty); }
|
||||||
|
@Override public ItemStack getItem(int slot) { return items.get(slot); }
|
||||||
|
@Override public ItemStack removeItem(int slot, int amount) {
|
||||||
|
ItemStack removed = ContainerHelper.removeItem(items, slot, amount);
|
||||||
|
if (!removed.isEmpty()) {
|
||||||
|
setChanged();
|
||||||
|
if (slot == BOTTLES) refillEmptyBasin();
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
@Override public ItemStack removeItemNoUpdate(int slot) { return ContainerHelper.takeItem(items, slot); }
|
||||||
|
@Override public void setItem(int slot, ItemStack stack) {
|
||||||
|
items.set(slot, stack);
|
||||||
|
if (stack.getCount() > getMaxStackSize()) stack.setCount(getMaxStackSize());
|
||||||
|
setChanged();
|
||||||
|
if (slot == INK || slot == BOTTLES) refillEmptyBasin();
|
||||||
|
}
|
||||||
|
@Override public boolean canPlaceItem(int slot, ItemStack stack) {
|
||||||
|
return switch (slot) {
|
||||||
|
case INK -> stack.is(ModItems.INK_VIAL.get());
|
||||||
|
case PAPER -> stack.is(Items.PAPER);
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@Override public boolean stillValid(Player player) {
|
||||||
|
return level != null && level.isLoaded(worldPosition) && level.getBlockEntity(worldPosition) == this
|
||||||
|
&& player.distanceToSqr(worldPosition.getX() + 0.5, worldPosition.getY() + 0.5,
|
||||||
|
worldPosition.getZ() + 0.5) <= 64.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasBottleSpace() {
|
||||||
|
ItemStack bottles = items.get(BOTTLES);
|
||||||
|
return bottles.isEmpty() || bottles.is(Items.GLASS_BOTTLE)
|
||||||
|
&& bottles.getCount() < bottles.getMaxStackSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** No server tick: refill only when ink, bottle space, or consumption changes. */
|
||||||
|
private void refillEmptyBasin() {
|
||||||
|
if (level == null || level.isClientSide || inkRemaining != 0 || !hasBottleSpace()) return;
|
||||||
|
ItemStack vial = items.get(INK);
|
||||||
|
if (!vial.is(ModItems.INK_VIAL.get())) return;
|
||||||
|
int amount = Math.clamp(InkVialItem.remaining(vial), 0, InkVialItem.CAPACITY);
|
||||||
|
if (amount == 0) return;
|
||||||
|
inkRemaining = amount;
|
||||||
|
vial.shrink(1);
|
||||||
|
if (vial.isEmpty()) items.set(INK, ItemStack.EMPTY);
|
||||||
|
ItemStack bottles = items.get(BOTTLES);
|
||||||
|
if (bottles.isEmpty()) items.set(BOTTLES, new ItemStack(Items.GLASS_BOTTLE));
|
||||||
|
else bottles.grow(1);
|
||||||
|
setChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canCreateLinkPanel() {
|
||||||
|
if (!items.get(PAPER).is(Items.PAPER)) return false;
|
||||||
|
int cost = WritingInkService.linkPanelCost();
|
||||||
|
if (inkRemaining >= cost) return true;
|
||||||
|
ItemStack vial = items.get(INK);
|
||||||
|
return vial.is(ModItems.INK_VIAL.get()) && hasBottleSpace()
|
||||||
|
&& inkRemaining + InkVialItem.remaining(vial) >= cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Commit inputs only when a virtual output is actually taken. */
|
||||||
|
public boolean consumeLinkPanelMaterials() {
|
||||||
|
if (!canCreateLinkPanel()) return false;
|
||||||
|
int cost = WritingInkService.linkPanelCost();
|
||||||
|
if (inkRemaining < cost) {
|
||||||
|
ItemStack vial = items.get(INK);
|
||||||
|
inkRemaining += Math.clamp(InkVialItem.remaining(vial), 0, InkVialItem.CAPACITY);
|
||||||
|
vial.shrink(1);
|
||||||
|
if (vial.isEmpty()) items.set(INK, ItemStack.EMPTY);
|
||||||
|
ItemStack bottles = items.get(BOTTLES);
|
||||||
|
if (bottles.isEmpty()) items.set(BOTTLES, new ItemStack(Items.GLASS_BOTTLE));
|
||||||
|
else bottles.grow(1);
|
||||||
|
}
|
||||||
|
inkRemaining -= cost;
|
||||||
|
items.get(PAPER).shrink(1);
|
||||||
|
if (items.get(PAPER).isEmpty()) items.set(PAPER, ItemStack.EMPTY);
|
||||||
|
setChanged();
|
||||||
|
if (inkRemaining == 0) refillEmptyBasin();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void setLevel(Level level) {
|
||||||
|
super.setLevel(level);
|
||||||
|
refillEmptyBasin();
|
||||||
|
}
|
||||||
|
@Override public void clearContent() {
|
||||||
|
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||||
|
inkRemaining = 0;
|
||||||
|
setChanged();
|
||||||
|
}
|
||||||
|
@Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
|
||||||
|
super.loadAdditional(tag, registries);
|
||||||
|
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||||
|
ContainerHelper.loadAllItems(tag, items, registries);
|
||||||
|
inkRemaining = Math.clamp(tag.getInt("InkRemaining"), 0, InkVialItem.CAPACITY);
|
||||||
|
refillEmptyBasin();
|
||||||
|
}
|
||||||
|
@Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
|
||||||
|
super.saveAdditional(tag, registries);
|
||||||
|
ContainerHelper.saveAllItems(tag, items, registries);
|
||||||
|
tag.putInt("InkRemaining", inkRemaining);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
|
|||||||
import net.dnorberg.rivencraft.client.screen.NotebookScreen;
|
import net.dnorberg.rivencraft.client.screen.NotebookScreen;
|
||||||
import net.dnorberg.rivencraft.client.screen.WritingDeskScreen;
|
import net.dnorberg.rivencraft.client.screen.WritingDeskScreen;
|
||||||
import net.dnorberg.rivencraft.client.screen.BookBinderScreen;
|
import net.dnorberg.rivencraft.client.screen.BookBinderScreen;
|
||||||
|
import net.dnorberg.rivencraft.client.screen.InkMixerScreen;
|
||||||
import net.dnorberg.rivencraft.registry.ModMenus;
|
import net.dnorberg.rivencraft.registry.ModMenus;
|
||||||
|
|
||||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
|
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
|
||||||
@@ -34,6 +35,7 @@ public final class ClientEvents {
|
|||||||
event.register(ModMenus.NOTEBOOK.get(), NotebookScreen::new);
|
event.register(ModMenus.NOTEBOOK.get(), NotebookScreen::new);
|
||||||
event.register(ModMenus.WRITING_DESK.get(), WritingDeskScreen::new);
|
event.register(ModMenus.WRITING_DESK.get(), WritingDeskScreen::new);
|
||||||
event.register(ModMenus.BOOK_BINDER.get(), BookBinderScreen::new);
|
event.register(ModMenus.BOOK_BINDER.get(), BookBinderScreen::new);
|
||||||
|
event.register(ModMenus.INK_MIXER.get(), InkMixerScreen::new);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import net.minecraft.resources.ResourceLocation;
|
|||||||
import net.minecraft.world.entity.player.Inventory;
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.neoforged.neoforge.network.PacketDistributor;
|
import net.neoforged.neoforge.network.PacketDistributor;
|
||||||
|
import org.lwjgl.glfw.GLFW;
|
||||||
|
|
||||||
/** Legacy-sized binder: cover and result flank a scrolling strip of full page thumbnails. */
|
/** Legacy-sized binder: cover and result flank a scrolling strip of full page thumbnails. */
|
||||||
public final class BookBinderScreen extends AbstractContainerScreen<BookBinderMenu> {
|
public final class BookBinderScreen extends AbstractContainerScreen<BookBinderMenu> {
|
||||||
@@ -41,6 +42,8 @@ public final class BookBinderScreen extends AbstractContainerScreen<BookBinderMe
|
|||||||
inventoryLabelY = 87;
|
inventoryLabelY = 87;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override public boolean isPauseScreen() { return false; }
|
||||||
|
|
||||||
@Override protected void init() {
|
@Override protected void init() {
|
||||||
super.init();
|
super.init();
|
||||||
titleInput = new EditBox(font, leftPos + 9, topPos + 10, 112, 12,
|
titleInput = new EditBox(font, leftPos + 9, topPos + 10, 112, 12,
|
||||||
@@ -94,6 +97,13 @@ public final class BookBinderScreen extends AbstractContainerScreen<BookBinderMe
|
|||||||
}
|
}
|
||||||
graphics.disableScissor();
|
graphics.disableScissor();
|
||||||
drawScrollEnds(graphics);
|
drawScrollEnds(graphics);
|
||||||
|
if (mouseY >= topPos + STRIP_Y && mouseY < topPos + STRIP_Y + STRIP_HEIGHT) {
|
||||||
|
if (firstVisible > 0 && mouseX >= leftPos + STRIP_X && mouseX < leftPos + STRIP_X + 8)
|
||||||
|
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.book_binder.previous_pages"), mouseX, mouseY);
|
||||||
|
else if (firstVisible < maxFirstVisible() && mouseX >= leftPos + STRIP_X + STRIP_WIDTH - 8
|
||||||
|
&& mouseX < leftPos + STRIP_X + STRIP_WIDTH)
|
||||||
|
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.book_binder.next_pages"), mouseX, mouseY);
|
||||||
|
}
|
||||||
if (pageAtIndex(0).isEmpty() && mouseX >= leftPos + 27 && mouseX < leftPos + 45
|
if (pageAtIndex(0).isEmpty() && mouseX >= leftPos + 27 && mouseX < leftPos + 45
|
||||||
&& mouseY >= topPos + 26 && mouseY < topPos + 44) {
|
&& mouseY >= topPos + 26 && mouseY < topPos + 44) {
|
||||||
graphics.renderTooltip(font,
|
graphics.renderTooltip(font,
|
||||||
@@ -210,6 +220,14 @@ public final class BookBinderScreen extends AbstractContainerScreen<BookBinderMe
|
|||||||
} else titleInput.keyPressed(keyCode, scanCode, modifiers);
|
} else titleInput.keyPressed(keyCode, scanCode, modifiers);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (keyCode == GLFW.GLFW_KEY_LEFT && firstVisible > 0) {
|
||||||
|
firstVisible--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (keyCode == GLFW.GLFW_KEY_RIGHT && firstVisible < maxFirstVisible()) {
|
||||||
|
firstVisible++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return super.keyPressed(keyCode, scanCode, modifiers);
|
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,14 +20,18 @@ import net.dnorberg.rivencraft.client.preview.DestinationPreviewRenderers;
|
|||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.client.gui.GuiGraphics;
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.client.gui.screens.Screen;
|
import net.minecraft.client.gui.screens.Screen;
|
||||||
|
import net.minecraft.client.resources.sounds.SimpleSoundInstance;
|
||||||
import net.minecraft.client.gui.components.Button;
|
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.components.Tooltip;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.sounds.SoundEvents;
|
||||||
import net.minecraft.util.FormattedCharSequence;
|
import net.minecraft.util.FormattedCharSequence;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.InteractionHand;
|
import net.minecraft.world.InteractionHand;
|
||||||
import net.neoforged.neoforge.network.PacketDistributor;
|
import net.neoforged.neoforge.network.PacketDistributor;
|
||||||
|
import org.lwjgl.glfw.GLFW;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
public final class DescriptiveBookScreen extends Screen implements ExplorableBookScreen {
|
public final class DescriptiveBookScreen extends Screen implements ExplorableBookScreen {
|
||||||
@@ -113,7 +117,8 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
|
|||||||
if (preview.supportsExploration()) {
|
if (preview.supportsExploration()) {
|
||||||
exploreButton = addRenderableWidget(Button.builder(
|
exploreButton = addRenderableWidget(Button.builder(
|
||||||
EXPLORE_ICON, button -> enterExploreOverlay())
|
EXPLORE_ICON, button -> enterExploreOverlay())
|
||||||
.bounds(left + 229, top + 145, 20, 20).build());
|
.bounds(left + 229, top + 145, 20, 20)
|
||||||
|
.tooltip(Tooltip.create(Component.translatable("screen.rivencraft.book.explore"))).build());
|
||||||
exploreButton.active = bookStandPos == null;
|
exploreButton.active = bookStandPos == null;
|
||||||
exploreButton.visible = pageIndex == 0;
|
exploreButton.visible = pageIndex == 0;
|
||||||
}
|
}
|
||||||
@@ -150,6 +155,14 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
|
|||||||
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
|
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
|
||||||
renderBook(graphics, partialTick);
|
renderBook(graphics, partialTick);
|
||||||
super.render(graphics, mouseX, mouseY, partialTick);
|
super.render(graphics, mouseX, mouseY, partialTick);
|
||||||
|
int left = (width - BOOK_WIDTH) / 2;
|
||||||
|
int top = (height - BOOK_HEIGHT) / 2;
|
||||||
|
if (mouseY >= top + 164 && mouseY < top + 195) {
|
||||||
|
if (pageIndex > 0 && mouseX >= left + 7 && mouseX < left + 35)
|
||||||
|
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.book.previous_page"), mouseX, mouseY);
|
||||||
|
else if (pageIndex < pageCount() && mouseX >= left + 293 && mouseX < left + 320)
|
||||||
|
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.book.next_page"), mouseX, mouseY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderBook(GuiGraphics graphics, float partialTick) {
|
private void renderBook(GuiGraphics graphics, float partialTick) {
|
||||||
@@ -239,7 +252,10 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void turnPage(int direction) {
|
private void turnPage(int direction) {
|
||||||
pageIndex = Math.max(0, Math.min(pageCount(), pageIndex + direction));
|
int nextPage = Math.max(0, Math.min(pageCount(), pageIndex + direction));
|
||||||
|
if (nextPage == pageIndex) return;
|
||||||
|
pageIndex = nextPage;
|
||||||
|
minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.BOOK_PAGE_TURN, 1.0F, 0.65F));
|
||||||
if (titleInput != null) titleInput.visible = pageIndex == 0;
|
if (titleInput != null) titleInput.visible = pageIndex == 0;
|
||||||
if (createButton != null) createButton.visible = pageIndex == 0;
|
if (createButton != null) createButton.visible = pageIndex == 0;
|
||||||
if (cancelButton != null) cancelButton.visible = pageIndex == 0;
|
if (cancelButton != null) cancelButton.visible = pageIndex == 0;
|
||||||
@@ -303,6 +319,10 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||||
|
if (titleInput == null || !titleInput.isFocused()) {
|
||||||
|
if (keyCode == GLFW.GLFW_KEY_LEFT && pageIndex > 0) { turnPage(-1); return true; }
|
||||||
|
if (keyCode == GLFW.GLFW_KEY_RIGHT && pageIndex < pageCount()) { turnPage(1); return true; }
|
||||||
|
}
|
||||||
return preview != null && preview.keyPressed(keyCode, scanCode)
|
return preview != null && preview.keyPressed(keyCode, scanCode)
|
||||||
|| super.keyPressed(keyCode, scanCode, modifiers);
|
|| super.keyPressed(keyCode, scanCode, modifiers);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package net.dnorberg.rivencraft.client.screen;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||||
|
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
|
|
||||||
|
/** Legacy Ink Mixer atlas with a server-synced, sixteen-use basin. */
|
||||||
|
public final class InkMixerScreen extends AbstractContainerScreen<InkMixerMenu> {
|
||||||
|
private static final ResourceLocation BACKGROUND = ResourceLocation.fromNamespaceAndPath(
|
||||||
|
Rivencraft.MOD_ID, "textures/gui/ink_mixer_legacy.png");
|
||||||
|
|
||||||
|
public InkMixerScreen(InkMixerMenu menu, Inventory inventory, Component title) {
|
||||||
|
super(menu, inventory, title);
|
||||||
|
imageWidth = 176;
|
||||||
|
imageHeight = 181;
|
||||||
|
inventoryLabelY = 87;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public boolean isPauseScreen() { return false; }
|
||||||
|
@Override public void renderBackground(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
|
||||||
|
renderBg(graphics, partialTick, mouseX, mouseY);
|
||||||
|
}
|
||||||
|
@Override protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) {
|
||||||
|
graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
|
||||||
|
int centerX = leftPos + 88;
|
||||||
|
int centerY = topPos + 49;
|
||||||
|
int fillTop = centerY + 26 - Math.round(menu.inkRemaining() / (float) InkVialItem.CAPACITY * 52);
|
||||||
|
if (menu.inkRemaining() > 0) {
|
||||||
|
for (int y = Math.max(centerY - 26, fillTop); y <= centerY + 26; y++) {
|
||||||
|
int halfWidth = (int) Math.sqrt(26 * 26 - (y - centerY) * (y - centerY));
|
||||||
|
graphics.fill(centerX - halfWidth, y, centerX + halfWidth + 1, y + 1, 0xFF241228);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {
|
||||||
|
graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0x404040, false);
|
||||||
|
}
|
||||||
|
@Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
|
||||||
|
super.render(graphics, mouseX, mouseY, partialTick);
|
||||||
|
renderTooltip(graphics, mouseX, mouseY);
|
||||||
|
int x = mouseX - (leftPos + 88);
|
||||||
|
int y = mouseY - (topPos + 49);
|
||||||
|
if (x * x + y * y <= 26 * 26)
|
||||||
|
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.ink_mixer.ink",
|
||||||
|
menu.inkRemaining(), InkVialItem.CAPACITY), mouseX, mouseY);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,15 +16,19 @@ import net.dnorberg.rivencraft.item.LinkingBookItem;
|
|||||||
import net.minecraft.client.gui.GuiGraphics;
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.client.gui.components.Button;
|
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.components.Tooltip;
|
||||||
import net.minecraft.client.gui.screens.Screen;
|
import net.minecraft.client.gui.screens.Screen;
|
||||||
|
import net.minecraft.client.resources.sounds.SimpleSoundInstance;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.core.component.DataComponents;
|
import net.minecraft.core.component.DataComponents;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||||
|
import net.minecraft.sounds.SoundEvents;
|
||||||
import net.minecraft.util.FormattedCharSequence;
|
import net.minecraft.util.FormattedCharSequence;
|
||||||
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.network.PacketDistributor;
|
import net.neoforged.neoforge.network.PacketDistributor;
|
||||||
|
import org.lwjgl.glfw.GLFW;
|
||||||
|
|
||||||
public final class LinkingBookScreen extends Screen implements ExplorableBookScreen {
|
public final class LinkingBookScreen extends Screen implements ExplorableBookScreen {
|
||||||
private static final int BOOK_TEXT_COLOR = 0xFF3B281B;
|
private static final int BOOK_TEXT_COLOR = 0xFF3B281B;
|
||||||
@@ -93,7 +97,8 @@ public final class LinkingBookScreen extends Screen implements ExplorableBookScr
|
|||||||
if (preview.supportsExploration()) {
|
if (preview.supportsExploration()) {
|
||||||
exploreButton = addRenderableWidget(Button.builder(
|
exploreButton = addRenderableWidget(Button.builder(
|
||||||
EXPLORE_ICON, button -> enterExploreOverlay())
|
EXPLORE_ICON, button -> enterExploreOverlay())
|
||||||
.bounds(left + 229, top + 145, 20, 20).build());
|
.bounds(left + 229, top + 145, 20, 20)
|
||||||
|
.tooltip(Tooltip.create(Component.translatable("screen.rivencraft.book.explore"))).build());
|
||||||
exploreButton.active = bookStandPos == null;
|
exploreButton.active = bookStandPos == null;
|
||||||
exploreButton.visible = pageIndex == 0;
|
exploreButton.visible = pageIndex == 0;
|
||||||
}
|
}
|
||||||
@@ -131,6 +136,14 @@ public final class LinkingBookScreen extends Screen implements ExplorableBookScr
|
|||||||
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
|
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
|
||||||
renderBook(graphics, partialTick);
|
renderBook(graphics, partialTick);
|
||||||
super.render(graphics, mouseX, mouseY, partialTick);
|
super.render(graphics, mouseX, mouseY, partialTick);
|
||||||
|
int left = (width - BOOK_WIDTH) / 2;
|
||||||
|
int top = (height - BOOK_HEIGHT) / 2;
|
||||||
|
if (mouseY >= top + 164 && mouseY < top + 195) {
|
||||||
|
if (pageIndex > 0 && mouseX >= left + 7 && mouseX < left + 35)
|
||||||
|
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.book.previous_page"), mouseX, mouseY);
|
||||||
|
else if (pageIndex == 0 && mouseX >= left + 293 && mouseX < left + 320)
|
||||||
|
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.book.next_page"), mouseX, mouseY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderBook(GuiGraphics graphics, float partialTick) {
|
private void renderBook(GuiGraphics graphics, float partialTick) {
|
||||||
@@ -204,7 +217,9 @@ public final class LinkingBookScreen extends Screen implements ExplorableBookScr
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void turnPage(int page) {
|
private void turnPage(int page) {
|
||||||
|
if (pageIndex == page) return;
|
||||||
pageIndex = page;
|
pageIndex = page;
|
||||||
|
minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.BOOK_PAGE_TURN, 1.0F, 0.65F));
|
||||||
if (titleInput != null) titleInput.visible = pageIndex == 0;
|
if (titleInput != null) titleInput.visible = pageIndex == 0;
|
||||||
if (bindButton != null) bindButton.visible = pageIndex == 0;
|
if (bindButton != null) bindButton.visible = pageIndex == 0;
|
||||||
if (cancelButton != null) cancelButton.visible = pageIndex == 0;
|
if (cancelButton != null) cancelButton.visible = pageIndex == 0;
|
||||||
@@ -233,6 +248,10 @@ public final class LinkingBookScreen extends Screen implements ExplorableBookScr
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||||
|
if (titleInput == null || !titleInput.isFocused()) {
|
||||||
|
if (keyCode == GLFW.GLFW_KEY_LEFT && pageIndex > 0) { turnPage(0); return true; }
|
||||||
|
if (keyCode == GLFW.GLFW_KEY_RIGHT && pageIndex == 0) { turnPage(1); return true; }
|
||||||
|
}
|
||||||
return preview != null && preview.keyPressed(keyCode, scanCode)
|
return preview != null && preview.keyPressed(keyCode, scanCode)
|
||||||
|| super.keyPressed(keyCode, scanCode, modifiers);
|
|| super.keyPressed(keyCode, scanCode, modifiers);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
|
|||||||
inventoryLabelY = 123;
|
inventoryLabelY = 123;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override public boolean isPauseScreen() { return false; }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void init() {
|
protected void init() {
|
||||||
super.init();
|
super.init();
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
|
|||||||
import net.minecraft.client.gui.GuiGraphics;
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.client.gui.components.Button;
|
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.components.Tooltip;
|
||||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.world.entity.player.Inventory;
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
import net.minecraft.world.inventory.Slot;
|
import net.minecraft.world.inventory.Slot;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import org.lwjgl.glfw.GLFW;
|
||||||
|
|
||||||
/** Legacy desk atlas layout, with four visible tabs out of the desk's 88 collections. */
|
/** Legacy desk atlas layout, with four visible tabs out of the desk's 88 collections. */
|
||||||
public final class WritingDeskScreen extends AbstractContainerScreen<WritingDeskMenu> {
|
public final class WritingDeskScreen extends AbstractContainerScreen<WritingDeskMenu> {
|
||||||
@@ -60,12 +62,15 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
|
|||||||
titleLabelX = 222;
|
titleLabelX = 222;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override public boolean isPauseScreen() { return false; }
|
||||||
|
|
||||||
@Override protected void init() {
|
@Override protected void init() {
|
||||||
super.init();
|
super.init();
|
||||||
sortButton = addRenderableWidget(Button.builder(Component.literal("AZ"), button -> {
|
sortButton = addRenderableWidget(Button.builder(Component.literal("AZ"), button -> {
|
||||||
sortAlphabetically = !sortAlphabetically;
|
sortAlphabetically = !sortAlphabetically;
|
||||||
scrollRows = 0;
|
scrollRows = 0;
|
||||||
}).bounds(leftPos + 58, topPos, 20, 18).build());
|
}).bounds(leftPos + 58, topPos, 20, 18)
|
||||||
|
.tooltip(Tooltip.create(Component.translatable("screen.rivencraft.writing_desk.sort"))).build());
|
||||||
allButton = addRenderableWidget(Button.builder(Component.literal("ALL"), button -> {
|
allButton = addRenderableWidget(Button.builder(Component.literal("ALL"), button -> {
|
||||||
allMode = !allMode;
|
allMode = !allMode;
|
||||||
scrollRows = 0;
|
scrollRows = 0;
|
||||||
@@ -75,7 +80,8 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
|
|||||||
if (minecraft != null && minecraft.gameMode != null)
|
if (minecraft != null && minecraft.gameMode != null)
|
||||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, WritingDeskMenu.PAGE_BUTTON_BASE);
|
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, WritingDeskMenu.PAGE_BUTTON_BASE);
|
||||||
}
|
}
|
||||||
}).bounds(leftPos + 80, topPos, 28, 18).build());
|
}).bounds(leftPos + 80, topPos, 28, 18)
|
||||||
|
.tooltip(Tooltip.create(Component.translatable("screen.rivencraft.writing_desk.all"))).build());
|
||||||
search = new EditBox(font, leftPos + 110, topPos, 101, 18,
|
search = new EditBox(font, leftPos + 110, topPos, 101, 18,
|
||||||
Component.translatable("screen.rivencraft.writing_desk.search"));
|
Component.translatable("screen.rivencraft.writing_desk.search"));
|
||||||
search.setMaxLength(64);
|
search.setMaxLength(64);
|
||||||
@@ -203,8 +209,17 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
|
|||||||
}
|
}
|
||||||
if (mouseX >= leftPos + 213 && mouseX < leftPos + 231
|
if (mouseX >= leftPos + 213 && mouseX < leftPos + 231
|
||||||
&& mouseY >= topPos && mouseY < topPos + 18) {
|
&& mouseY >= topPos && mouseY < topPos + 18) {
|
||||||
graphics.renderTooltip(font, Component.literal(selectedCategory == null
|
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.writing_desk.category",
|
||||||
? "All categories" : selectedCategory.toString()), mouseX, mouseY);
|
selectedCategory == null
|
||||||
|
? Component.translatable("screen.rivencraft.writing_desk.all_categories")
|
||||||
|
: Component.literal(selectedCategory.toString())), mouseX, mouseY);
|
||||||
|
}
|
||||||
|
if (mouseX >= leftPos && mouseX < leftPos + 58) {
|
||||||
|
int localY = mouseY - topPos;
|
||||||
|
if (localY >= 20 && localY < 29)
|
||||||
|
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.writing_desk.previous_tabs"), mouseX, mouseY);
|
||||||
|
else if (localY >= 177 && localY < 186)
|
||||||
|
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.writing_desk.next_tabs"), mouseX, mouseY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,6 +460,16 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (minecraft != null && minecraft.gameMode != null) {
|
||||||
|
if (keyCode == GLFW.GLFW_KEY_PAGE_UP) {
|
||||||
|
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, WritingDeskMenu.PREVIOUS_TABS);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (keyCode == GLFW.GLFW_KEY_PAGE_DOWN) {
|
||||||
|
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, WritingDeskMenu.NEXT_TABS);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return super.keyPressed(keyCode, scanCode, modifiers);
|
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package net.dnorberg.rivencraft.compat.jei;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import mezz.jei.api.IModPlugin;
|
||||||
|
import mezz.jei.api.JeiPlugin;
|
||||||
|
import mezz.jei.api.constants.VanillaTypes;
|
||||||
|
import mezz.jei.api.gui.builder.IRecipeLayoutBuilder;
|
||||||
|
import mezz.jei.api.gui.ingredient.IRecipeSlotsView;
|
||||||
|
import mezz.jei.api.helpers.IGuiHelper;
|
||||||
|
import mezz.jei.api.recipe.IFocusGroup;
|
||||||
|
import mezz.jei.api.recipe.RecipeType;
|
||||||
|
import mezz.jei.api.recipe.category.AbstractRecipeCategory;
|
||||||
|
import mezz.jei.api.registration.IRecipeCatalystRegistration;
|
||||||
|
import mezz.jei.api.registration.IRecipeCategoryRegistration;
|
||||||
|
import mezz.jei.api.registration.IRecipeRegistration;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.Items;
|
||||||
|
|
||||||
|
/** Loaded by JEI's plugin scanner only when JEI is installed. No common mod class references this type. */
|
||||||
|
@JeiPlugin
|
||||||
|
public final class RivencraftJeiPlugin implements IModPlugin {
|
||||||
|
private static final RecipeType<WritingGuide> WRITING =
|
||||||
|
RecipeType.create(Rivencraft.MOD_ID, "writing", WritingGuide.class);
|
||||||
|
private static final RecipeType<InkMixerGuide> INK_MIXER =
|
||||||
|
RecipeType.create(Rivencraft.MOD_ID, "ink_mixer", InkMixerGuide.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation getPluginUid() {
|
||||||
|
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "jei_writing");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerCategories(IRecipeCategoryRegistration registration) {
|
||||||
|
IGuiHelper guiHelper = registration.getJeiHelpers().getGuiHelper();
|
||||||
|
registration.addRecipeCategories(new WritingCategory(guiHelper), new InkMixerCategory(guiHelper));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerRecipes(IRecipeRegistration registration) {
|
||||||
|
ItemStack panel = PageItem.createLinkPanelPage();
|
||||||
|
registration.addRecipes(WRITING, List.of(
|
||||||
|
new WritingGuide(List.of(new ItemStack(Items.PAPER), new ItemStack(ModItems.INK_VIAL.get())),
|
||||||
|
panel, "jei.rivencraft.writing.link_panel_hand"),
|
||||||
|
new WritingGuide(List.of(panel, new ItemStack(Items.PAPER), new ItemStack(ModItems.INK_VIAL.get())),
|
||||||
|
panel, "jei.rivencraft.writing.copy"),
|
||||||
|
new WritingGuide(List.of(new ItemStack(Items.LEATHER), panel),
|
||||||
|
new ItemStack(ModItems.DESCRIPTIVE_BOOK.get()), "jei.rivencraft.writing.bind"),
|
||||||
|
new WritingGuide(List.of(panel, new ItemStack(Items.LEATHER)),
|
||||||
|
new ItemStack(ModItems.LINKING_BOOK.get()), "jei.rivencraft.writing.linking_book")
|
||||||
|
));
|
||||||
|
registration.addRecipes(INK_MIXER, List.of(new InkMixerGuide(panel)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerRecipeCatalysts(IRecipeCatalystRegistration registration) {
|
||||||
|
registration.addRecipeCatalysts(WRITING, ModItems.INK_VIAL.get(),
|
||||||
|
ModItems.WRITING_DESK.get(), ModItems.BOOK_BINDER.get());
|
||||||
|
registration.addRecipeCatalysts(INK_MIXER, ModItems.INK_MIXER.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private record WritingGuide(List<ItemStack> inputs, ItemStack output, String instructionKey) {}
|
||||||
|
private record InkMixerGuide(ItemStack panel) {}
|
||||||
|
|
||||||
|
private static final class InkMixerCategory extends AbstractRecipeCategory<InkMixerGuide> {
|
||||||
|
InkMixerCategory(IGuiHelper guiHelper) {
|
||||||
|
super(INK_MIXER, Component.translatable("jei.rivencraft.ink_mixer"),
|
||||||
|
guiHelper.createDrawableIngredient(VanillaTypes.ITEM_STACK,
|
||||||
|
new ItemStack(ModItems.INK_MIXER.get())), 138, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRecipe(IRecipeLayoutBuilder builder, InkMixerGuide recipe, IFocusGroup focuses) {
|
||||||
|
builder.addInputSlot(2, 3).setStandardSlotBackground().addItemStack(new ItemStack(Items.PAPER));
|
||||||
|
builder.addInputSlot(23, 3).setStandardSlotBackground()
|
||||||
|
.addItemStack(new ItemStack(ModItems.INK_VIAL.get()));
|
||||||
|
builder.addOutputSlot(112, 3).setOutputSlotBackground().addItemStack(recipe.panel());
|
||||||
|
builder.addOutputSlot(112, 46).setStandardSlotBackground()
|
||||||
|
.addItemStack(new ItemStack(Items.GLASS_BOTTLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(InkMixerGuide recipe, IRecipeSlotsView slots, GuiGraphics graphics,
|
||||||
|
double mouseX, double mouseY) {
|
||||||
|
graphics.drawWordWrap(Minecraft.getInstance().font,
|
||||||
|
Component.translatable("jei.rivencraft.ink_mixer.usage"), 2, 27, 104, 0x404040);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class WritingCategory extends AbstractRecipeCategory<WritingGuide> {
|
||||||
|
WritingCategory(IGuiHelper guiHelper) {
|
||||||
|
super(WRITING, Component.translatable("jei.rivencraft.writing"),
|
||||||
|
guiHelper.createDrawableIngredient(VanillaTypes.ITEM_STACK,
|
||||||
|
new ItemStack(ModItems.WRITING_DESK.get())), 138, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRecipe(IRecipeLayoutBuilder builder, WritingGuide recipe, IFocusGroup focuses) {
|
||||||
|
for (int index = 0; index < recipe.inputs().size(); index++) {
|
||||||
|
builder.addInputSlot(2 + index * 21, 3).setStandardSlotBackground()
|
||||||
|
.addItemStack(recipe.inputs().get(index));
|
||||||
|
}
|
||||||
|
builder.addOutputSlot(112, 3).setOutputSlotBackground().addItemStack(recipe.output());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(WritingGuide recipe, IRecipeSlotsView slots, GuiGraphics graphics,
|
||||||
|
double mouseX, double mouseY) {
|
||||||
|
graphics.drawWordWrap(Minecraft.getInstance().font,
|
||||||
|
Component.translatable(recipe.instructionKey()), 2, 27, 134, 0x404040);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,6 +74,25 @@ public final class BookBinderGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void missingSymbolPageSurvivesReloadButCannotBeAssembled(GameTestHelper helper) {
|
||||||
|
BookBinderBlockEntity binder = place(helper);
|
||||||
|
binder.setTitle("Recoverable Writing");
|
||||||
|
binder.setItem(BookBinderBlockEntity.COVER, new ItemStack(Items.LEATHER));
|
||||||
|
binder.setItem(BookBinderBlockEntity.LINK_PANEL, PageItem.createLinkPanelPage());
|
||||||
|
SymbolReference missing = new SymbolReference(ResourceLocation.parse("example:removed_symbol"));
|
||||||
|
binder.setItem(BookBinderBlockEntity.FIRST_PAGE, PageItem.createRecoveredPage(missing));
|
||||||
|
var saved = binder.saveWithoutMetadata(helper.getLevel().registryAccess());
|
||||||
|
var restored = new BookBinderBlockEntity(binder.getBlockPos(), binder.getBlockState());
|
||||||
|
restored.loadWithComponents(saved, helper.getLevel().registryAccess());
|
||||||
|
helper.assertValueEqual(PageItem.content(restored.getItem(BookBinderBlockEntity.FIRST_PAGE))
|
||||||
|
.symbol().orElseThrow(), missing, "Reload must preserve the removed data-pack symbol ID");
|
||||||
|
helper.assertFalse(restored.canAssemble(), "Missing symbol cannot create a new written Age");
|
||||||
|
helper.assertTrue(restored.getItem(BookBinderBlockEntity.FIRST_PAGE).is(ModItems.PAGE.get()),
|
||||||
|
"Rejected writing must remain recoverable as a Page");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
@GameTest(template = "empty")
|
||||||
public static void assemblyIsAtomicAndRetainsPageOrder(GameTestHelper helper) {
|
public static void assemblyIsAtomicAndRetainsPageOrder(GameTestHelper helper) {
|
||||||
BookBinderBlockEntity binder = place(helper);
|
BookBinderBlockEntity binder = place(helper);
|
||||||
@@ -237,6 +256,39 @@ public final class BookBinderGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void forgedBinderActionsAndReplayedOutputCannotDuplicateBook(GameTestHelper helper) {
|
||||||
|
BookBinderBlockEntity binder = place(helper);
|
||||||
|
binder.setTitle("Single Assembly");
|
||||||
|
binder.setItem(BookBinderBlockEntity.COVER, new ItemStack(Items.LEATHER));
|
||||||
|
binder.setItem(BookBinderBlockEntity.LINK_PANEL, PageItem.createLinkPanelPage());
|
||||||
|
var first = helper.makeMockServerPlayerInLevel();
|
||||||
|
var second = helper.makeMockServerPlayerInLevel();
|
||||||
|
for (var player : List.of(first, second)) player.moveTo(binder.getBlockPos().getX() + 0.5,
|
||||||
|
binder.getBlockPos().getY() + 0.5, binder.getBlockPos().getZ() + 0.5);
|
||||||
|
BookBinderMenu firstMenu = new BookBinderMenu(1, first.getInventory(), binder);
|
||||||
|
BookBinderMenu secondMenu = new BookBinderMenu(2, second.getInventory(), binder);
|
||||||
|
helper.assertFalse(firstMenu.clickMenuButton(first, BookBinderMenu.SELECT_BASE + BookBinderBlockEntity.PAGE_COUNT),
|
||||||
|
"A fabricated page selection must be rejected");
|
||||||
|
helper.assertFalse(firstMenu.clickMenuButton(first, BookBinderMenu.STRIP_CLICK_BASE
|
||||||
|
+ BookBinderBlockEntity.PAGE_COUNT + 1), "A fabricated strip address must be rejected");
|
||||||
|
helper.assertTrue(firstMenu.quickMoveStack(first, BookBinderMenu.RESULT).is(ModItems.DESCRIPTIVE_BOOK.get()),
|
||||||
|
"The first viewer must receive the single assembled book");
|
||||||
|
helper.assertTrue(secondMenu.quickMoveStack(second, BookBinderMenu.RESULT).isEmpty(),
|
||||||
|
"Another viewer must not take a stale result");
|
||||||
|
helper.assertTrue(firstMenu.quickMoveStack(first, BookBinderMenu.RESULT).isEmpty(),
|
||||||
|
"Replaying the output request must not create a second book");
|
||||||
|
int books = first.getInventory().items.stream().filter(stack -> stack.is(ModItems.DESCRIPTIVE_BOOK.get()))
|
||||||
|
.mapToInt(ItemStack::getCount).sum();
|
||||||
|
helper.assertValueEqual(books, 1, "Only one completed book may be emitted");
|
||||||
|
helper.assertTrue(binder.getItem(BookBinderBlockEntity.COVER).isEmpty(), "Cover must be consumed once");
|
||||||
|
helper.assertTrue(binder.getItem(BookBinderBlockEntity.LINK_PANEL).isEmpty(), "Panel must be consumed once");
|
||||||
|
helper.getLevel().setBlock(binder.getBlockPos(), Blocks.AIR.defaultBlockState(), 3);
|
||||||
|
helper.assertFalse(firstMenu.clickMenuButton(first, BookBinderMenu.IMPORT_NOTEBOOK),
|
||||||
|
"A stale binder menu must reject packets after block removal");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
@GameTest(template = "empty")
|
||||||
public static void legacyStripInsertsAndRemovesPagesInOrder(GameTestHelper helper) {
|
public static void legacyStripInsertsAndRemovesPagesInOrder(GameTestHelper helper) {
|
||||||
BookBinderBlockEntity binder = place(helper);
|
BookBinderBlockEntity binder = place(helper);
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package net.dnorberg.rivencraft.gametest;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||||
|
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
|
import net.dnorberg.rivencraft.writing.WritingInkService;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.gametest.framework.GameTest;
|
||||||
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
|
import net.minecraft.world.inventory.ClickType;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.Items;
|
||||||
|
import net.minecraft.world.entity.item.ItemEntity;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.phys.AABB;
|
||||||
|
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||||
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
|
@GameTestHolder(Rivencraft.MOD_ID)
|
||||||
|
@PrefixGameTestTemplate(false)
|
||||||
|
public final class InkMixerGameTests {
|
||||||
|
private InkMixerGameTests() {}
|
||||||
|
|
||||||
|
private static InkMixerBlockEntity place(GameTestHelper helper) {
|
||||||
|
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||||
|
helper.getLevel().setBlock(pos, ModBlocks.INK_MIXER.get().defaultBlockState(), 3);
|
||||||
|
return (InkMixerBlockEntity) helper.getLevel().getBlockEntity(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void linkPanelRequiresTakingOutputAndPersistsInk(GameTestHelper helper) {
|
||||||
|
InkMixerBlockEntity mixer = place(helper);
|
||||||
|
mixer.setItem(InkMixerBlockEntity.PAPER, new ItemStack(Items.PAPER, 2));
|
||||||
|
mixer.setItem(InkMixerBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
|
||||||
|
var player = helper.makeMockServerPlayerInLevel();
|
||||||
|
player.moveTo(mixer.getBlockPos().getX() + 0.5, mixer.getBlockPos().getY() + 0.5,
|
||||||
|
mixer.getBlockPos().getZ() + 0.5);
|
||||||
|
InkMixerMenu menu = new InkMixerMenu(1, player.getInventory(), mixer);
|
||||||
|
menu.broadcastChanges();
|
||||||
|
helper.assertTrue(menu.getSlot(InkMixerMenu.RESULT).getItem().is(ModItems.PAGE.get()),
|
||||||
|
"Paper and filled basin must preview a Link Panel");
|
||||||
|
helper.assertValueEqual(mixer.getItem(InkMixerBlockEntity.PAPER).getCount(), 2,
|
||||||
|
"Preview must not consume paper");
|
||||||
|
helper.assertValueEqual(mixer.inkRemaining(), 16, "Vial must fill the basin");
|
||||||
|
helper.assertTrue(mixer.getItem(InkMixerBlockEntity.BOTTLES).is(Items.GLASS_BOTTLE),
|
||||||
|
"Consumed vial must return a vanilla glass bottle");
|
||||||
|
menu.clicked(InkMixerMenu.RESULT, 0, ClickType.PICKUP, player);
|
||||||
|
helper.assertTrue(menu.getCarried().is(ModItems.PAGE.get()), "Taking result must give a Link Panel");
|
||||||
|
helper.assertValueEqual(mixer.getItem(InkMixerBlockEntity.PAPER).getCount(), 1,
|
||||||
|
"Taking result must use exactly one paper");
|
||||||
|
helper.assertValueEqual(mixer.inkRemaining(), 16 - WritingInkService.linkPanelCost(),
|
||||||
|
"Taking result must spend the configured ink cost");
|
||||||
|
var saved = mixer.saveWithoutMetadata(helper.getLevel().registryAccess());
|
||||||
|
var restored = new InkMixerBlockEntity(mixer.getBlockPos(), ModBlocks.INK_MIXER.get().defaultBlockState());
|
||||||
|
restored.loadWithComponents(saved, helper.getLevel().registryAccess());
|
||||||
|
helper.assertValueEqual(restored.inkRemaining(), mixer.inkRemaining(), "Ink basin must persist");
|
||||||
|
helper.assertValueEqual(restored.getItem(InkMixerBlockEntity.PAPER).getCount(), 1,
|
||||||
|
"Paper input must persist");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void bottleCapacityBlocksAndThenPermitsRefill(GameTestHelper helper) {
|
||||||
|
InkMixerBlockEntity mixer = place(helper);
|
||||||
|
mixer.setItem(InkMixerBlockEntity.PAPER, new ItemStack(Items.PAPER));
|
||||||
|
mixer.setItem(InkMixerBlockEntity.BOTTLES, new ItemStack(Items.GLASS_BOTTLE, 64));
|
||||||
|
mixer.setItem(InkMixerBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
|
||||||
|
helper.assertValueEqual(mixer.inkRemaining(), 0, "Full bottle output must block refill");
|
||||||
|
helper.assertTrue(mixer.getItem(InkMixerBlockEntity.INK).is(ModItems.INK_VIAL.get()),
|
||||||
|
"Blocked refill must preserve vial");
|
||||||
|
helper.assertFalse(mixer.canCreateLinkPanel(), "Blocked refill must suppress output");
|
||||||
|
mixer.removeItem(InkMixerBlockEntity.BOTTLES, 1);
|
||||||
|
helper.assertValueEqual(mixer.inkRemaining(), 16, "Making bottle space must immediately refill");
|
||||||
|
helper.assertValueEqual(mixer.getItem(InkMixerBlockEntity.BOTTLES).getCount(), 64,
|
||||||
|
"One bottle must be returned after refill");
|
||||||
|
helper.assertTrue(mixer.canCreateLinkPanel(), "Refill must enable Link Panel output");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void shiftClickNeedsInventorySpaceAndBreakingReturnsInputs(GameTestHelper helper) {
|
||||||
|
InkMixerBlockEntity mixer = place(helper);
|
||||||
|
mixer.setItem(InkMixerBlockEntity.PAPER, new ItemStack(Items.PAPER, 2));
|
||||||
|
mixer.setItem(InkMixerBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
|
||||||
|
var player = helper.makeMockServerPlayerInLevel();
|
||||||
|
player.moveTo(mixer.getBlockPos().getX() + 0.5, mixer.getBlockPos().getY() + 0.5,
|
||||||
|
mixer.getBlockPos().getZ() + 0.5);
|
||||||
|
InkMixerMenu menu = new InkMixerMenu(1, player.getInventory(), mixer);
|
||||||
|
for (int slot = 0; slot < 36; slot++) player.getInventory().setItem(slot, new ItemStack(Items.STONE, 64));
|
||||||
|
helper.assertTrue(menu.quickMoveStack(player, InkMixerMenu.RESULT).isEmpty(),
|
||||||
|
"Full inventory must not consume mixer output");
|
||||||
|
helper.assertValueEqual(mixer.getItem(InkMixerBlockEntity.PAPER).getCount(), 2,
|
||||||
|
"Full inventory must preserve paper");
|
||||||
|
player.getInventory().setItem(0, ItemStack.EMPTY);
|
||||||
|
helper.assertTrue(menu.quickMoveStack(player, InkMixerMenu.RESULT).is(ModItems.PAGE.get()),
|
||||||
|
"Free inventory space must accept the Link Panel");
|
||||||
|
helper.assertValueEqual(mixer.getItem(InkMixerBlockEntity.PAPER).getCount(), 1,
|
||||||
|
"Shift-click must consume only one paper");
|
||||||
|
helper.getLevel().setBlock(mixer.getBlockPos(), Blocks.AIR.defaultBlockState(), 3);
|
||||||
|
helper.assertFalse(menu.stillValid(player), "Breaking the mixer must invalidate its menu");
|
||||||
|
var drops = helper.getLevel().getEntitiesOfClass(ItemEntity.class,
|
||||||
|
new AABB(mixer.getBlockPos()).inflate(2));
|
||||||
|
int paper = drops.stream().filter(entity -> entity.getItem().is(Items.PAPER))
|
||||||
|
.mapToInt(entity -> entity.getItem().getCount()).sum();
|
||||||
|
int bottles = drops.stream().filter(entity -> entity.getItem().is(Items.GLASS_BOTTLE))
|
||||||
|
.mapToInt(entity -> entity.getItem().getCount()).sum();
|
||||||
|
helper.assertValueEqual(paper, 1, "Breaking the mixer must return unused paper once");
|
||||||
|
helper.assertValueEqual(bottles, 1, "Breaking the mixer must return the consumed vial's bottle once");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package net.dnorberg.rivencraft.gametest;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
|
import net.dnorberg.rivencraft.writing.LinkingBookRecipe;
|
||||||
|
import net.minecraft.core.component.DataComponents;
|
||||||
|
import net.minecraft.gametest.framework.GameTest;
|
||||||
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.Items;
|
||||||
|
import net.minecraft.world.item.crafting.CraftingBookCategory;
|
||||||
|
import net.minecraft.world.item.crafting.CraftingInput;
|
||||||
|
import net.minecraft.world.item.crafting.RecipeType;
|
||||||
|
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||||
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
|
@GameTestHolder(Rivencraft.MOD_ID)
|
||||||
|
@PrefixGameTestTemplate(false)
|
||||||
|
public final class LinkingBookRecipeGameTests {
|
||||||
|
private LinkingBookRecipeGameTests() {}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void legacyRecipeRequiresExactlyLeatherAndLinkPanel(GameTestHelper helper) {
|
||||||
|
LinkingBookRecipe recipe = new LinkingBookRecipe(CraftingBookCategory.MISC);
|
||||||
|
ItemStack panel = PageItem.createLinkPanelPage();
|
||||||
|
CraftingInput valid = CraftingInput.of(2, 1, List.of(panel, new ItemStack(Items.LEATHER)));
|
||||||
|
helper.assertTrue(recipe.matches(valid, helper.getLevel()), "Link Panel and leather must match");
|
||||||
|
helper.assertTrue(recipe.matches(CraftingInput.of(2, 1,
|
||||||
|
List.of(new ItemStack(Items.LEATHER), panel)), helper.getLevel()),
|
||||||
|
"The Legacy recipe must be shapeless");
|
||||||
|
helper.assertFalse(recipe.matches(CraftingInput.of(2, 1,
|
||||||
|
List.of(new ItemStack(Items.BOOK), new ItemStack(Items.ENDER_PEARL))), helper.getLevel()),
|
||||||
|
"The old placeholder recipe must no longer match");
|
||||||
|
helper.assertFalse(recipe.matches(CraftingInput.of(2, 1,
|
||||||
|
List.of(new ItemStack(ModItems.PAGE.get()), new ItemStack(Items.LEATHER))), helper.getLevel()),
|
||||||
|
"A blank Page must not substitute for a Link Panel");
|
||||||
|
helper.assertFalse(recipe.matches(CraftingInput.of(3, 1,
|
||||||
|
List.of(panel, new ItemStack(Items.LEATHER), new ItemStack(Items.PAPER))), helper.getLevel()),
|
||||||
|
"An extra ingredient must invalidate the recipe");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void outputIsUnwrittenAndRegisteredRecipeLoads(GameTestHelper helper) {
|
||||||
|
ItemStack panel = PageItem.createLinkPanelPage();
|
||||||
|
panel.set(DataComponents.CUSTOM_NAME, Component.literal("Home Panel"));
|
||||||
|
CraftingInput input = CraftingInput.of(2, 1, List.of(panel, new ItemStack(Items.LEATHER)));
|
||||||
|
var holder = helper.getLevel().getRecipeManager()
|
||||||
|
.getRecipeFor(RecipeType.CRAFTING, input, helper.getLevel()).orElseThrow();
|
||||||
|
helper.assertValueEqual(holder.id(), ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "linking_book"),
|
||||||
|
"The registered recipe must replace the old placeholder");
|
||||||
|
ItemStack result = holder.value().assemble(input, helper.getLevel().registryAccess());
|
||||||
|
helper.assertTrue(result.is(ModItems.LINKING_BOOK.get()), "Crafting must produce a Linking Book");
|
||||||
|
helper.assertValueEqual(result.getCount(), 1, "Crafting must produce one book");
|
||||||
|
helper.assertFalse(result.has(ModDataComponents.LINK_TARGET.get()), "The book must be unwritten/unbound");
|
||||||
|
helper.assertFalse(result.has(ModDataComponents.PAGE_CONTENT.get()), "Page identity must not leak onto the book");
|
||||||
|
helper.assertValueEqual(result.get(DataComponents.CUSTOM_NAME), Component.literal("Home Panel"),
|
||||||
|
"The panel's custom name must survive crafting");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ package net.dnorberg.rivencraft.gametest;
|
|||||||
import com.mojang.serialization.JsonOps;
|
import com.mojang.serialization.JsonOps;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.book.AgeReference;
|
||||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||||
import net.dnorberg.rivencraft.writing.PageContent;
|
import net.dnorberg.rivencraft.writing.PageContent;
|
||||||
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||||
@@ -71,8 +73,12 @@ public final class WritingDataGameTests {
|
|||||||
"0.3.7.2 bare-list writing must load into schema version 1 intact");
|
"0.3.7.2 bare-list writing must load into schema version 1 intact");
|
||||||
|
|
||||||
ItemStack oldPageFreeBook = new ItemStack(ModItems.DESCRIPTIVE_BOOK.get());
|
ItemStack oldPageFreeBook = new ItemStack(ModItems.DESCRIPTIVE_BOOK.get());
|
||||||
|
AgeReference originalAge = new AgeReference(UUID.randomUUID(), "Legacy Page-Free Age");
|
||||||
|
oldPageFreeBook.set(ModDataComponents.AGE_REFERENCE.get(), originalAge);
|
||||||
CompoundTag saved = (CompoundTag) oldPageFreeBook.saveOptional(helper.getLevel().registryAccess());
|
CompoundTag saved = (CompoundTag) oldPageFreeBook.saveOptional(helper.getLevel().registryAccess());
|
||||||
ItemStack restored = ItemStack.parseOptional(helper.getLevel().registryAccess(), saved);
|
ItemStack restored = ItemStack.parseOptional(helper.getLevel().registryAccess(), saved);
|
||||||
|
helper.assertValueEqual(restored.get(ModDataComponents.AGE_REFERENCE.get()), originalAge,
|
||||||
|
"A page-free book must keep its existing Age identity");
|
||||||
helper.assertValueEqual(restored.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY),
|
helper.assertValueEqual(restored.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY),
|
||||||
OrderedWriting.EMPTY, "Older page-free books must not require writing migration");
|
OrderedWriting.EMPTY, "Older page-free books must not require writing migration");
|
||||||
helper.succeed();
|
helper.succeed();
|
||||||
@@ -136,6 +142,10 @@ public final class WritingDataGameTests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
ItemStack recovered = PageItem.createRecoveredPage(unknown);
|
ItemStack recovered = PageItem.createRecoveredPage(unknown);
|
||||||
|
CompoundTag recoveredTag = (CompoundTag) recovered.saveOptional(helper.getLevel().registryAccess());
|
||||||
|
ItemStack restoredRecovered = ItemStack.parseOptional(helper.getLevel().registryAccess(), recoveredTag);
|
||||||
|
helper.assertValueEqual(PageItem.content(restoredRecovered).symbol().orElseThrow(), unknown,
|
||||||
|
"A missing symbol must survive a full Page item reload");
|
||||||
helper.assertFalse(PageItem.isUsableWriting(recovered),
|
helper.assertFalse(PageItem.isUsableWriting(recovered),
|
||||||
"An unresolved page must remain recoverable without becoming valid writing");
|
"An unresolved page must remain recoverable without becoming valid writing");
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -249,11 +249,40 @@ public final class WritingDeskGameTests {
|
|||||||
"First viewer should take the last available copy");
|
"First viewer should take the last available copy");
|
||||||
helper.assertTrue(secondMenu.quickMoveStack(second, WritingDeskMenu.COPY_RESULT).isEmpty(),
|
helper.assertTrue(secondMenu.quickMoveStack(second, WritingDeskMenu.COPY_RESULT).isEmpty(),
|
||||||
"Second viewer must not copy the already-consumed paper");
|
"Second viewer must not copy the already-consumed paper");
|
||||||
|
helper.assertTrue(firstMenu.quickMoveStack(first, WritingDeskMenu.COPY_RESULT).isEmpty(),
|
||||||
|
"Replaying the first viewer's output request must not make another page");
|
||||||
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.PAPER).isEmpty(),
|
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.PAPER).isEmpty(),
|
||||||
"Shared desk must spend paper only once");
|
"Shared desk must spend paper only once");
|
||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void forgedDeskSelectionsAndStaleMenuCannotCopy(GameTestHelper helper) {
|
||||||
|
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||||
|
helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3);
|
||||||
|
WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos);
|
||||||
|
desk.setItem(0, PageItem.createLinkPanelPage());
|
||||||
|
desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER));
|
||||||
|
desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
|
||||||
|
var player = helper.makeMockServerPlayerInLevel();
|
||||||
|
player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
|
||||||
|
WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk);
|
||||||
|
helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.CATALOG_BUTTON_BASE + 999),
|
||||||
|
"A fabricated catalog index must be rejected");
|
||||||
|
helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.ALL_PAGE_BUTTON_BASE + 27),
|
||||||
|
"A fabricated page address must be rejected");
|
||||||
|
helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.extractPageButtonId(0, 0,
|
||||||
|
WritingDeskBlockEntity.PAPER)), "A page cannot be extracted into a material slot");
|
||||||
|
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 1,
|
||||||
|
"Forged actions must not spend paper");
|
||||||
|
helper.assertTrue(desk.getItem(0).is(ModItems.PAGE.get()), "Forged actions must not remove the source");
|
||||||
|
helper.getLevel().setBlock(pos, Blocks.AIR.defaultBlockState(), 3);
|
||||||
|
helper.assertFalse(menu.clickMenuButton(player, 0), "A stale menu must reject selection packets");
|
||||||
|
helper.assertTrue(menu.quickMoveStack(player, WritingDeskMenu.COPY_RESULT).isEmpty(),
|
||||||
|
"A stale menu must reject output packets");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
@GameTest(template = "empty")
|
||||||
public static void catalogAggregatesKnownSymbolsAcrossCollections(GameTestHelper helper) {
|
public static void catalogAggregatesKnownSymbolsAcrossCollections(GameTestHelper helper) {
|
||||||
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
|
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.dnorberg.rivencraft.item;
|
package net.dnorberg.rivencraft.item;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
import net.dnorberg.rivencraft.registry.ModItems;
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
import net.dnorberg.rivencraft.writing.PageContent;
|
import net.dnorberg.rivencraft.writing.PageContent;
|
||||||
@@ -28,6 +29,23 @@ public final class PageItem extends Item {
|
|||||||
return create(PageContent.symbol(reference));
|
return create(PageContent.symbol(reference));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fixed starter examples for the creative tab; the server still validates catalog membership on use. */
|
||||||
|
public static ItemStack createCreativeStarterPage(String path) {
|
||||||
|
if (!List.of("terrain/standard", "biome/native", "lighting/normal", "weather/normal",
|
||||||
|
"celestial/normal_time", "feature/caves").contains(path)) {
|
||||||
|
throw new IllegalArgumentException("Not a starter symbol: " + path);
|
||||||
|
}
|
||||||
|
SymbolReference reference = new SymbolReference(
|
||||||
|
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, path));
|
||||||
|
ItemStack page = create(PageContent.symbol(reference));
|
||||||
|
String key = "symbol.rivencraft." + path.replace('/', '.');
|
||||||
|
page.set(ModDataComponents.PAGE_SYMBOL_DISPLAY.get(), new SymbolDisplayMetadata(
|
||||||
|
reference.id(), key, key + ".description",
|
||||||
|
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, path.split("/")[0]),
|
||||||
|
true));
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
public static ItemStack createLinkPanelPage() {
|
public static ItemStack createLinkPanelPage() {
|
||||||
return create(PageContent.LINK_PANEL);
|
return create(PageContent.LINK_PANEL);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package net.dnorberg.rivencraft.menu;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||||
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModMenus;
|
||||||
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
|
import net.minecraft.world.Container;
|
||||||
|
import net.minecraft.world.SimpleContainer;
|
||||||
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||||
|
import net.minecraft.world.inventory.DataSlot;
|
||||||
|
import net.minecraft.world.inventory.Slot;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.Items;
|
||||||
|
|
||||||
|
/** Server-owned virtual output; taking it spends paper and the configured ink cost once. */
|
||||||
|
public final class InkMixerMenu extends AbstractContainerMenu {
|
||||||
|
public static final int RESULT = InkMixerBlockEntity.SIZE;
|
||||||
|
private static final int PLAYER_START = RESULT + 1;
|
||||||
|
private final InkMixerBlockEntity mixer;
|
||||||
|
private final Container inputs;
|
||||||
|
private final SimpleContainer result = new SimpleContainer(1);
|
||||||
|
private final boolean clientSide;
|
||||||
|
private int inkRemaining;
|
||||||
|
|
||||||
|
public InkMixerMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) {
|
||||||
|
this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos())
|
||||||
|
instanceof InkMixerBlockEntity found ? found : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InkMixerMenu(int id, Inventory inventory, InkMixerBlockEntity mixer) {
|
||||||
|
super(ModMenus.INK_MIXER.get(), id);
|
||||||
|
this.mixer = mixer;
|
||||||
|
inputs = mixer == null ? new SimpleContainer(InkMixerBlockEntity.SIZE) : mixer;
|
||||||
|
clientSide = inventory.player.level().isClientSide;
|
||||||
|
addDataSlot(new DataSlot() {
|
||||||
|
@Override public int get() { return clientSide ? inkRemaining : mixer.inkRemaining(); }
|
||||||
|
@Override public void set(int value) { inkRemaining = value; }
|
||||||
|
});
|
||||||
|
addSlot(new MixerSlot(InkMixerBlockEntity.INK, 8, 27));
|
||||||
|
addSlot(new MixerSlot(InkMixerBlockEntity.PAPER, 8, 48));
|
||||||
|
addSlot(new MixerSlot(InkMixerBlockEntity.BOTTLES, 152, 27));
|
||||||
|
addSlot(new ResultSlot(152, 48));
|
||||||
|
for (int row = 0; row < 3; row++)
|
||||||
|
for (int column = 0; column < 9; column++)
|
||||||
|
addSlot(new Slot(inventory, column + row * 9 + 9, 8 + column * 18, 99 + row * 18));
|
||||||
|
for (int column = 0; column < 9; column++)
|
||||||
|
addSlot(new Slot(inventory, column, 8 + column * 18, 157));
|
||||||
|
refreshResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int inkRemaining() { return inkRemaining; }
|
||||||
|
|
||||||
|
@Override public boolean stillValid(Player player) {
|
||||||
|
return mixer != null && mixer.stillValid(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void broadcastChanges() {
|
||||||
|
refreshResult();
|
||||||
|
super.broadcastChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshResult() {
|
||||||
|
if (clientSide) return;
|
||||||
|
ItemStack preview = mixer != null && mixer.canCreateLinkPanel()
|
||||||
|
? PageItem.createLinkPanelPage() : ItemStack.EMPTY;
|
||||||
|
if (!ItemStack.matches(result.getItem(0), preview)) result.setItem(0, preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public ItemStack quickMoveStack(Player player, int index) {
|
||||||
|
if (!stillValid(player) || index < 0 || index >= slots.size()) return ItemStack.EMPTY;
|
||||||
|
if (index == RESULT) {
|
||||||
|
refreshResult();
|
||||||
|
if (!mixer.canCreateLinkPanel()) return ItemStack.EMPTY;
|
||||||
|
ItemStack output = result.getItem(0).copy();
|
||||||
|
ItemStack remainder = output.copy();
|
||||||
|
if (!moveItemStackTo(remainder, PLAYER_START, slots.size(), true) || !remainder.isEmpty())
|
||||||
|
return ItemStack.EMPTY;
|
||||||
|
if (!mixer.consumeLinkPanelMaterials()) return ItemStack.EMPTY;
|
||||||
|
refreshResult();
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
Slot slot = slots.get(index);
|
||||||
|
if (!slot.hasItem()) return ItemStack.EMPTY;
|
||||||
|
ItemStack source = slot.getItem();
|
||||||
|
ItemStack original = source.copy();
|
||||||
|
boolean moved;
|
||||||
|
if (index < RESULT) moved = moveItemStackTo(source, PLAYER_START, slots.size(), true);
|
||||||
|
else if (source.is(ModItems.INK_VIAL.get()))
|
||||||
|
moved = moveItemStackTo(source, InkMixerBlockEntity.INK, InkMixerBlockEntity.INK + 1, false);
|
||||||
|
else if (source.is(Items.PAPER))
|
||||||
|
moved = moveItemStackTo(source, InkMixerBlockEntity.PAPER, InkMixerBlockEntity.PAPER + 1, false);
|
||||||
|
else moved = false;
|
||||||
|
if (!moved) return ItemStack.EMPTY;
|
||||||
|
if (source.isEmpty()) slot.setByPlayer(ItemStack.EMPTY);
|
||||||
|
else slot.setChanged();
|
||||||
|
refreshResult();
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class MixerSlot extends Slot {
|
||||||
|
MixerSlot(int index, int x, int y) { super(inputs, index, x, y); }
|
||||||
|
@Override public boolean mayPlace(ItemStack stack) { return inputs.canPlaceItem(getContainerSlot(), stack); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ResultSlot extends Slot {
|
||||||
|
ResultSlot(int x, int y) { super(result, 0, x, y); }
|
||||||
|
@Override public boolean mayPlace(ItemStack stack) { return false; }
|
||||||
|
@Override public boolean mayPickup(Player player) {
|
||||||
|
return clientSide ? hasItem() : stillValid(player) && mixer.canCreateLinkPanel();
|
||||||
|
}
|
||||||
|
@Override public void onTake(Player player, ItemStack stack) {
|
||||||
|
if (!clientSide && mixer.consumeLinkPanelMaterials()) refreshResult();
|
||||||
|
super.onTake(player, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import net.dnorberg.rivencraft.Rivencraft;
|
|||||||
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
|
||||||
import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity;
|
import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity;
|
||||||
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||||
|
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
@@ -24,6 +25,9 @@ public final class ModBlockEntities {
|
|||||||
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<BookBinderBlockEntity>> BOOK_BINDER =
|
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<BookBinderBlockEntity>> BOOK_BINDER =
|
||||||
BLOCK_ENTITIES.register("book_binder", () -> BlockEntityType.Builder
|
BLOCK_ENTITIES.register("book_binder", () -> BlockEntityType.Builder
|
||||||
.of(BookBinderBlockEntity::new, ModBlocks.BOOK_BINDER.get()).build(null));
|
.of(BookBinderBlockEntity::new, ModBlocks.BOOK_BINDER.get()).build(null));
|
||||||
|
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<InkMixerBlockEntity>> INK_MIXER =
|
||||||
|
BLOCK_ENTITIES.register("ink_mixer", () -> BlockEntityType.Builder
|
||||||
|
.of(InkMixerBlockEntity::new, ModBlocks.INK_MIXER.get()).build(null));
|
||||||
|
|
||||||
private ModBlockEntities() {
|
private ModBlockEntities() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import net.dnorberg.rivencraft.Rivencraft;
|
|||||||
import net.dnorberg.rivencraft.block.BookStandBlock;
|
import net.dnorberg.rivencraft.block.BookStandBlock;
|
||||||
import net.dnorberg.rivencraft.block.BookBinderBlock;
|
import net.dnorberg.rivencraft.block.BookBinderBlock;
|
||||||
import net.dnorberg.rivencraft.block.WritingDeskBlock;
|
import net.dnorberg.rivencraft.block.WritingDeskBlock;
|
||||||
|
import net.dnorberg.rivencraft.block.InkMixerBlock;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
@@ -26,6 +27,10 @@ public final class ModBlocks {
|
|||||||
"book_binder", BookBinderBlock::new,
|
"book_binder", BookBinderBlock::new,
|
||||||
BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion()
|
BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion()
|
||||||
);
|
);
|
||||||
|
public static final DeferredBlock<InkMixerBlock> INK_MIXER = BLOCKS.registerBlock(
|
||||||
|
"ink_mixer", InkMixerBlock::new,
|
||||||
|
BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion()
|
||||||
|
);
|
||||||
|
|
||||||
private ModBlocks() {
|
private ModBlocks() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public final class ModCreativeTabs {
|
|||||||
output.accept(ModItems.NOTEBOOK.get());
|
output.accept(ModItems.NOTEBOOK.get());
|
||||||
output.accept(ModItems.INK_VIAL.get());
|
output.accept(ModItems.INK_VIAL.get());
|
||||||
output.accept(ModItems.WRITING_DESK.get());
|
output.accept(ModItems.WRITING_DESK.get());
|
||||||
|
output.accept(ModItems.INK_MIXER.get());
|
||||||
output.accept(ModItems.BOOK_BINDER.get());
|
output.accept(ModItems.BOOK_BINDER.get());
|
||||||
})
|
})
|
||||||
.build());
|
.build());
|
||||||
@@ -32,7 +33,12 @@ public final class ModCreativeTabs {
|
|||||||
.title(Component.translatable("itemGroup.rivencraft.pages"))
|
.title(Component.translatable("itemGroup.rivencraft.pages"))
|
||||||
.icon(PageItem::createLinkPanelPage)
|
.icon(PageItem::createLinkPanelPage)
|
||||||
.displayItems((parameters, output) -> {
|
.displayItems((parameters, output) -> {
|
||||||
|
output.accept(ModItems.PAGE.get());
|
||||||
output.accept(PageItem.createLinkPanelPage());
|
output.accept(PageItem.createLinkPanelPage());
|
||||||
|
for (String id : new String[] {"terrain/standard", "biome/native", "lighting/normal",
|
||||||
|
"weather/normal", "celestial/normal_time", "feature/caves"}) {
|
||||||
|
output.accept(PageItem.createCreativeStarterPage(id));
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ public final class ModItems {
|
|||||||
);
|
);
|
||||||
public static final DeferredItem<BlockItem> WRITING_DESK = ITEMS.registerSimpleBlockItem(ModBlocks.WRITING_DESK);
|
public static final DeferredItem<BlockItem> WRITING_DESK = ITEMS.registerSimpleBlockItem(ModBlocks.WRITING_DESK);
|
||||||
public static final DeferredItem<BlockItem> BOOK_BINDER = ITEMS.registerSimpleBlockItem(ModBlocks.BOOK_BINDER);
|
public static final DeferredItem<BlockItem> BOOK_BINDER = ITEMS.registerSimpleBlockItem(ModBlocks.BOOK_BINDER);
|
||||||
|
public static final DeferredItem<BlockItem> INK_MIXER = ITEMS.registerSimpleBlockItem(ModBlocks.INK_MIXER);
|
||||||
|
|
||||||
private ModItems() {
|
private ModItems() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import net.dnorberg.rivencraft.Rivencraft;
|
|||||||
import net.dnorberg.rivencraft.menu.NotebookMenu;
|
import net.dnorberg.rivencraft.menu.NotebookMenu;
|
||||||
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
|
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
|
||||||
import net.dnorberg.rivencraft.menu.BookBinderMenu;
|
import net.dnorberg.rivencraft.menu.BookBinderMenu;
|
||||||
|
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.world.inventory.MenuType;
|
import net.minecraft.world.inventory.MenuType;
|
||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
@@ -21,6 +22,8 @@ public final class ModMenus {
|
|||||||
MENUS.register("writing_desk", () -> IMenuTypeExtension.create(WritingDeskMenu::new));
|
MENUS.register("writing_desk", () -> IMenuTypeExtension.create(WritingDeskMenu::new));
|
||||||
public static final DeferredHolder<MenuType<?>, MenuType<BookBinderMenu>> BOOK_BINDER =
|
public static final DeferredHolder<MenuType<?>, MenuType<BookBinderMenu>> BOOK_BINDER =
|
||||||
MENUS.register("book_binder", () -> IMenuTypeExtension.create(BookBinderMenu::new));
|
MENUS.register("book_binder", () -> IMenuTypeExtension.create(BookBinderMenu::new));
|
||||||
|
public static final DeferredHolder<MenuType<?>, MenuType<InkMixerMenu>> INK_MIXER =
|
||||||
|
MENUS.register("ink_mixer", () -> IMenuTypeExtension.create(InkMixerMenu::new));
|
||||||
|
|
||||||
private ModMenus() {
|
private ModMenus() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package net.dnorberg.rivencraft.registry;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.writing.LinkingBookRecipe;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||||
|
import net.minecraft.world.item.crafting.SimpleCraftingRecipeSerializer;
|
||||||
|
import net.neoforged.bus.api.IEventBus;
|
||||||
|
import net.neoforged.neoforge.registries.DeferredHolder;
|
||||||
|
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||||
|
|
||||||
|
public final class ModRecipeSerializers {
|
||||||
|
private static final DeferredRegister<RecipeSerializer<?>> SERIALIZERS =
|
||||||
|
DeferredRegister.create(Registries.RECIPE_SERIALIZER, Rivencraft.MOD_ID);
|
||||||
|
|
||||||
|
public static final DeferredHolder<RecipeSerializer<?>, RecipeSerializer<LinkingBookRecipe>> LINKING_BOOK =
|
||||||
|
SERIALIZERS.register("linking_book", () -> new SimpleCraftingRecipeSerializer<>(LinkingBookRecipe::new));
|
||||||
|
|
||||||
|
private ModRecipeSerializers() {}
|
||||||
|
|
||||||
|
public static void register(IEventBus modEventBus) {
|
||||||
|
SERIALIZERS.register(modEventBus);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package net.dnorberg.rivencraft.writing;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModRecipeSerializers;
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.core.NonNullList;
|
||||||
|
import net.minecraft.core.component.DataComponents;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.Items;
|
||||||
|
import net.minecraft.world.item.crafting.CraftingBookCategory;
|
||||||
|
import net.minecraft.world.item.crafting.CraftingInput;
|
||||||
|
import net.minecraft.world.item.crafting.CustomRecipe;
|
||||||
|
import net.minecraft.world.item.crafting.Ingredient;
|
||||||
|
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.neoforged.neoforge.common.crafting.DataComponentIngredient;
|
||||||
|
|
||||||
|
/** Legacy's shapeless leather + Link Panel recipe, adapted to page components. */
|
||||||
|
public final class LinkingBookRecipe extends CustomRecipe {
|
||||||
|
public LinkingBookRecipe(CraftingBookCategory category) {
|
||||||
|
super(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ItemStack findPanel(CraftingInput input) {
|
||||||
|
if (input.ingredientCount() != 2) return ItemStack.EMPTY;
|
||||||
|
ItemStack panel = ItemStack.EMPTY;
|
||||||
|
boolean leather = false;
|
||||||
|
for (ItemStack stack : input.items()) {
|
||||||
|
if (stack.isEmpty()) continue;
|
||||||
|
if (stack.is(ModItems.PAGE.get()) && PageItem.content(stack).kind() == PageContent.Kind.LINK_PANEL
|
||||||
|
&& panel.isEmpty()) {
|
||||||
|
panel = stack;
|
||||||
|
} else if (stack.is(Items.LEATHER) && !leather) {
|
||||||
|
leather = true;
|
||||||
|
} else {
|
||||||
|
return ItemStack.EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return leather ? panel : ItemStack.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(CraftingInput input, Level level) {
|
||||||
|
return !findPanel(input).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ItemStack assemble(CraftingInput input, HolderLookup.Provider registries) {
|
||||||
|
ItemStack panel = findPanel(input);
|
||||||
|
if (panel.isEmpty()) return ItemStack.EMPTY;
|
||||||
|
ItemStack book = new ItemStack(ModItems.LINKING_BOOK.get());
|
||||||
|
// Legacy copied the panel's data. Only its custom name is meaningful to a
|
||||||
|
// 1.21.1 Linking Book today; page-only components must not reach the book.
|
||||||
|
if (panel.has(DataComponents.CUSTOM_NAME)) {
|
||||||
|
book.set(DataComponents.CUSTOM_NAME, panel.get(DataComponents.CUSTOM_NAME));
|
||||||
|
}
|
||||||
|
return book;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canCraftInDimensions(int width, int height) {
|
||||||
|
return width * height >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NonNullList<Ingredient> getIngredients() {
|
||||||
|
NonNullList<Ingredient> ingredients = NonNullList.create();
|
||||||
|
ingredients.add(DataComponentIngredient.of(false,
|
||||||
|
net.dnorberg.rivencraft.registry.ModDataComponents.PAGE_CONTENT.get(),
|
||||||
|
PageContent.LINK_PANEL, ModItems.PAGE.get()));
|
||||||
|
ingredients.add(Ingredient.of(Items.LEATHER));
|
||||||
|
return ingredients;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecipeSerializer<?> getSerializer() {
|
||||||
|
return ModRecipeSerializers.LINKING_BOOK.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"variants": {
|
||||||
|
"": { "model": "rivencraft:block/ink_mixer" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,9 @@
|
|||||||
"container.rivencraft.writing_desk": "Writing Desk",
|
"container.rivencraft.writing_desk": "Writing Desk",
|
||||||
"block.rivencraft.book_binder": "Book Binder",
|
"block.rivencraft.book_binder": "Book Binder",
|
||||||
"container.rivencraft.book_binder": "Book Binder",
|
"container.rivencraft.book_binder": "Book Binder",
|
||||||
|
"block.rivencraft.ink_mixer": "Ink Mixer",
|
||||||
|
"container.rivencraft.ink_mixer": "Ink Mixer",
|
||||||
|
"screen.rivencraft.ink_mixer.ink": "Ink: %s/%s",
|
||||||
"screen.rivencraft.book_binder.title": "Book title",
|
"screen.rivencraft.book_binder.title": "Book title",
|
||||||
"screen.rivencraft.book_binder.title_hint": "Name the book",
|
"screen.rivencraft.book_binder.title_hint": "Name the book",
|
||||||
"screen.rivencraft.book_binder.missing_panel": "Link Panel required as the first page",
|
"screen.rivencraft.book_binder.missing_panel": "Link Panel required as the first page",
|
||||||
@@ -28,9 +31,17 @@
|
|||||||
"screen.rivencraft.book_binder.import.tooltip": "Move all Notebook pages into the binder in order",
|
"screen.rivencraft.book_binder.import.tooltip": "Move all Notebook pages into the binder in order",
|
||||||
"screen.rivencraft.book_binder.move_previous": "Move selected page left",
|
"screen.rivencraft.book_binder.move_previous": "Move selected page left",
|
||||||
"screen.rivencraft.book_binder.move_next": "Move selected page right",
|
"screen.rivencraft.book_binder.move_next": "Move selected page right",
|
||||||
|
"screen.rivencraft.book_binder.previous_pages": "Scroll pages left (Left Arrow)",
|
||||||
|
"screen.rivencraft.book_binder.next_pages": "Scroll pages right (Right Arrow)",
|
||||||
"screen.rivencraft.book_binder.select_hint": "Right-click page to select",
|
"screen.rivencraft.book_binder.select_hint": "Right-click page to select",
|
||||||
"screen.rivencraft.writing_desk.ink": "Ink: %s/%s",
|
"screen.rivencraft.writing_desk.ink": "Ink: %s/%s",
|
||||||
"screen.rivencraft.writing_desk.search": "Search",
|
"screen.rivencraft.writing_desk.search": "Search",
|
||||||
|
"screen.rivencraft.writing_desk.sort": "Sort pages by name (A–Z)",
|
||||||
|
"screen.rivencraft.writing_desk.all": "Show pages from all tabs",
|
||||||
|
"screen.rivencraft.writing_desk.category": "Cycle page category: %s",
|
||||||
|
"screen.rivencraft.writing_desk.all_categories": "All categories",
|
||||||
|
"screen.rivencraft.writing_desk.previous_tabs": "Previous four tabs (Page Up)",
|
||||||
|
"screen.rivencraft.writing_desk.next_tabs": "Next four tabs (Page Down)",
|
||||||
"screen.rivencraft.writing_desk.no_symbols": "No available symbols",
|
"screen.rivencraft.writing_desk.no_symbols": "No available symbols",
|
||||||
"item.rivencraft.ink_vial.remaining": "Ink: %s/%s",
|
"item.rivencraft.ink_vial.remaining": "Ink: %s/%s",
|
||||||
"item.rivencraft.ink_vial.usage": "Use with paper in the other hand to prepare a Link Panel.",
|
"item.rivencraft.ink_vial.usage": "Use with paper in the other hand to prepare a Link Panel.",
|
||||||
@@ -117,6 +128,15 @@
|
|||||||
"screen.rivencraft.book.explore": "Explore",
|
"screen.rivencraft.book.explore": "Explore",
|
||||||
"screen.rivencraft.book.exploring": "Exploring",
|
"screen.rivencraft.book.exploring": "Exploring",
|
||||||
"screen.rivencraft.book.return_from_explore": "Right-click to return",
|
"screen.rivencraft.book.return_from_explore": "Right-click to return",
|
||||||
|
"screen.rivencraft.book.previous_page": "Previous page (Left Arrow)",
|
||||||
|
"screen.rivencraft.book.next_page": "Next page (Right Arrow)",
|
||||||
|
"jei.rivencraft.writing": "Rivencraft Writing",
|
||||||
|
"jei.rivencraft.writing.link_panel_hand": "Hold paper and an Ink Vial in opposite hands; use the vial to make a Link Panel.",
|
||||||
|
"jei.rivencraft.ink_mixer": "Ink Mixer",
|
||||||
|
"jei.rivencraft.ink_mixer.usage": "Paper + ink makes a Link Panel. One vial fills 16 ink; each panel uses the configured ink cost. The empty vial returns as a glass bottle.",
|
||||||
|
"jei.rivencraft.writing.copy": "At a Writing Desk, select a stored Page, add paper and ink, then take the copy. The source Page stays.",
|
||||||
|
"jei.rivencraft.writing.bind": "At a Book Binder, name the Age and add a cover plus Link Panel. Optional symbol Pages follow the panel.",
|
||||||
|
"jei.rivencraft.writing.linking_book": "Craft one Link Panel Page with leather to make an unwritten Linking Book. Open it to name and bind it.",
|
||||||
"itemGroup.rivencraft": "Rivencraft",
|
"itemGroup.rivencraft": "Rivencraft",
|
||||||
"itemGroup.rivencraft.pages": "Rivencraft Pages"
|
"itemGroup.rivencraft.pages": "Rivencraft Pages"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"parent": "minecraft:block/block",
|
||||||
|
"textures": {
|
||||||
|
"particle": "rivencraft:block/ink_mixer_side",
|
||||||
|
"side": "rivencraft:block/ink_mixer_side",
|
||||||
|
"top": "rivencraft:block/ink_mixer_top",
|
||||||
|
"bottom": "rivencraft:block/ink_mixer_bottom"
|
||||||
|
},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"from": [0, 0, 0],
|
||||||
|
"to": [16, 12, 16],
|
||||||
|
"faces": {
|
||||||
|
"north": { "texture": "#side", "uv": [0, 4, 16, 16] },
|
||||||
|
"east": { "texture": "#side", "uv": [0, 4, 16, 16] },
|
||||||
|
"south": { "texture": "#side", "uv": [0, 4, 16, 16] },
|
||||||
|
"west": { "texture": "#side", "uv": [0, 4, 16, 16] },
|
||||||
|
"up": { "texture": "#top" },
|
||||||
|
"down": { "texture": "#bottom" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"parent": "rivencraft:block/ink_mixer"
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 747 B |
Binary file not shown.
|
After Width: | Height: | Size: 616 B |
Binary file not shown.
|
After Width: | Height: | Size: 715 B |
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "minecraft:block",
|
||||||
|
"pools": [{ "rolls": 1, "entries": [{ "type": "minecraft:item", "name": "rivencraft:ink_mixer" }] }]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"type": "minecraft:crafting_shaped",
|
||||||
|
"category": "misc",
|
||||||
|
"pattern": ["S S", "SVS", "PSP"],
|
||||||
|
"key": {
|
||||||
|
"S": { "item": "minecraft:stone" },
|
||||||
|
"V": { "item": "minecraft:glass_bottle" },
|
||||||
|
"P": { "tag": "minecraft:planks" }
|
||||||
|
},
|
||||||
|
"result": { "id": "rivencraft:ink_mixer", "count": 1 }
|
||||||
|
}
|
||||||
@@ -1,16 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "minecraft:crafting_shapeless",
|
"type": "rivencraft:linking_book",
|
||||||
"category": "misc",
|
"category": "misc"
|
||||||
"ingredients": [
|
|
||||||
{
|
|
||||||
"item": "minecraft:book"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"item": "minecraft:ender_pearl"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"result": {
|
|
||||||
"count": 1,
|
|
||||||
"id": "rivencraft:linking_book"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user