Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
114a6ee21e | ||
|
|
8246640f49 | ||
|
|
ba2faace15 |
No files matched your search
@@ -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
|
||||||
@@ -76,7 +76,7 @@ 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.
|
larger ordered collection of Pages between desks and the binder.
|
||||||
|
|
||||||
With JEI installed, Rivencraft displays the Ink Mixer operation in its own
|
With JEI installed, Rivencraft displays the Ink Mixer operation in its own
|
||||||
category and the hand-use, copying, and binding flows in a Writing category.
|
category and the hand-use, copying, binding, and Linking Book crafting flows in a Writing category.
|
||||||
JEI is optional; vanilla crafting
|
JEI is optional; vanilla crafting
|
||||||
recipes and all writing interactions work without it. For a development client
|
recipes and all writing interactions work without it. For a development client
|
||||||
with JEI, run `./gradlew runClient -PwithJei=true`.
|
with JEI, run `./gradlew runClient -PwithJei=true`.
|
||||||
@@ -104,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
|
||||||
@@ -113,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.
|
||||||
@@ -188,6 +189,10 @@ known upstream limits are documented in
|
|||||||
```
|
```
|
||||||
|
|
||||||
The built mod is written to `build/libs/`.
|
The built mod is written to `build/libs/`.
|
||||||
|
The default dedicated-server GameTest run is deliberately small. Use
|
||||||
|
`./gradlew runGameTestServer -PwithArchivedGameTests=true` for the complete
|
||||||
|
regression suite before a release or when investigating a related issue. See
|
||||||
|
[docs/GAMETESTS.md](docs/GAMETESTS.md).
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
|
|||||||
+22
-1
@@ -17,6 +17,18 @@ tasks.named('wrapper', Wrapper).configure {
|
|||||||
version = mod_version
|
version = mod_version
|
||||||
group = mod_group_id
|
group = mod_group_id
|
||||||
|
|
||||||
|
List<String> archivedGameTestSuites = ['linking', 'writing', 'desk', 'binder', 'mixer', 'recipe']
|
||||||
|
List<String> selectedArchivedGameTestSuites = providers.gradleProperty('withArchivedGameTests')
|
||||||
|
.map { it.toBoolean() }.orElse(false).get() ? archivedGameTestSuites
|
||||||
|
: providers.gradleProperty('archivedGameTestSuites').orElse('').get()
|
||||||
|
.split(',').collect { it.trim() }.findAll { !it.isEmpty() }
|
||||||
|
if (!archivedGameTestSuites.containsAll(selectedArchivedGameTestSuites)) {
|
||||||
|
throw new GradleException("Unknown archived GameTest suite. Choose from: ${archivedGameTestSuites.join(', ')}")
|
||||||
|
}
|
||||||
|
String enabledGameTestNamespaces = ([mod_id] + selectedArchivedGameTestSuites.collect {
|
||||||
|
"rivencraft_archive_${it}"
|
||||||
|
}).join(',')
|
||||||
|
|
||||||
sourceSets.main.resources {
|
sourceSets.main.resources {
|
||||||
// Include resources generated by data generators.
|
// Include resources generated by data generators.
|
||||||
srcDir('src/generated/resources')
|
srcDir('src/generated/resources')
|
||||||
@@ -26,6 +38,15 @@ sourceSets.main.resources {
|
|||||||
exclude("src/generated/**/.cache") // datagen cache files
|
exclude("src/generated/**/.cache") // datagen cache files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Archived suites use separate namespaces but share the same empty structure.
|
||||||
|
tasks.named('processResources', ProcessResources).configure {
|
||||||
|
archivedGameTestSuites.each { suite ->
|
||||||
|
from('src/main/resources/data/rivencraft/structure/empty.nbt') {
|
||||||
|
into "data/rivencraft_archive_${suite}/structure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
name = 'JaredsMaven'
|
name = 'JaredsMaven'
|
||||||
@@ -89,7 +110,7 @@ neoForge {
|
|||||||
// Keep generated test Ages separate from worlds used by the dev client/server.
|
// Keep generated test Ages separate from worlds used by the dev client/server.
|
||||||
gameDirectory = project.file(project.findProperty('gameTestRunDir')
|
gameDirectory = project.file(project.findProperty('gameTestRunDir')
|
||||||
?: layout.buildDirectory.dir('gametest-run').get().asFile)
|
?: layout.buildDirectory.dir('gametest-run').get().asFile)
|
||||||
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
|
systemProperty 'neoforge.enabledGameTestNamespaces', enabledGameTestNamespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
data {
|
data {
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# GameTest suites
|
||||||
|
|
||||||
|
The default dedicated-server run is a small smoke suite: 16 Rivencraft tests
|
||||||
|
cover core linking, deterministic Age definition, writing persistence, legacy
|
||||||
|
data migration, workstation transactions, and forged/replayed menu actions.
|
||||||
|
NeoForge currently adds two of its own tests, so the runner reports 18.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./gradlew runGameTestServer
|
||||||
|
```
|
||||||
|
|
||||||
|
The remaining 46 Rivencraft tests are archived, not deleted. They cover
|
||||||
|
completed milestone details such as every Age terrain profile, Notebook and
|
||||||
|
Desk surface interactions, refill edge cases, book assembly controls, and
|
||||||
|
workstation break/drop behavior. Run only the relevant archive alongside the
|
||||||
|
core tests when investigating an issue:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./gradlew runGameTestServer -ParchivedGameTestSuites=desk
|
||||||
|
./gradlew runGameTestServer -ParchivedGameTestSuites=linking,binder
|
||||||
|
```
|
||||||
|
|
||||||
|
Available suite names are `linking`, `writing`, `desk`, `binder`, `mixer`, and
|
||||||
|
`recipe`. Run all 62 Rivencraft tests (64 including NeoForge's two) before a
|
||||||
|
release or after broad cross-system changes:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./gradlew runGameTestServer -PwithArchivedGameTests=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Each archive is selected by its `rivencraft_archive_<suite>` GameTest namespace.
|
||||||
|
The suites share the normal empty test structure and do not need Create, Ponder, or
|
||||||
|
Immersive Portals. The core suite delegates to the original regression methods
|
||||||
|
for its selected cases, so the two suites do not maintain duplicate test logic.
|
||||||
|
|
||||||
|
GameTest Ages are created in `build/gametest-run/world`, not in a development
|
||||||
|
client save. The Gradle task removes that **dedicated** world automatically if
|
||||||
|
it exceeds 512 MiB; override the threshold with `-PgameTestMaxWorldMiB=128`,
|
||||||
|
for example. Cleanup never targets a custom `-PgameTestRunDir` or a player
|
||||||
|
world. A fresh test world can also help diagnose a slow or stalled worldgen
|
||||||
|
run. The full suite should still be run before shipping changes in the
|
||||||
|
archived systems.
|
||||||
+4
-3
@@ -35,7 +35,8 @@ acceptance issue and all required implementation issues are complete.
|
|||||||
[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, notebooks, desks, book assembly, and the baseline
|
4. **Writing system** — pages, notebooks, desks, book assembly, and the baseline
|
||||||
Ink Mixer for Link Panels. Symbol effects are deferred until Milestone 5.
|
Ink Mixer for Link Panels. Symbol effects are deferred until Milestone 5.
|
||||||
**In progress.**
|
**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.
|
||||||
@@ -63,11 +64,11 @@ 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; its first release
|
The Ink Mixer is included in Milestone 4 after book assembly; it
|
||||||
uses paper and Ink Vials to make baseline Link Panels. Adding modifier
|
uses paper and Ink Vials to make baseline Link Panels. Adding modifier
|
||||||
ingredients and symbol-driven link properties belongs to Milestone 5.
|
ingredients and symbol-driven link properties belongs to Milestone 5.
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ unstable in a particular graphics or mod configuration.
|
|||||||
|
|
||||||
For preview-related releases:
|
For preview-related releases:
|
||||||
|
|
||||||
1. Run `./gradlew runGameTestServer` to verify the complete selection matrix,
|
1. Run `./gradlew runGameTestServer -PwithArchivedGameTests=true` to verify the complete selection matrix,
|
||||||
core Age creation, persistence, and travel without optional renderers.
|
core Age creation, persistence, and travel without optional renderers.
|
||||||
2. Open held Linking and Descriptive Books once with each available backend.
|
2. Open held Linking and Descriptive Books once with each available backend.
|
||||||
3. Open both book types from a Book Stand and confirm Explore is disabled.
|
3. Open both book types from a Book Stand and confirm Explore is disabled.
|
||||||
@@ -75,4 +75,3 @@ For preview-related releases:
|
|||||||
preview loader remains active.
|
preview loader remains active.
|
||||||
5. With Ponder, reopen different destinations and confirm the previous
|
5. With Ponder, reopen different destinations and confirm the previous
|
||||||
snapshot does not bleed into the next one.
|
snapshot does not bleed into the next one.
|
||||||
|
|
||||||
+57
-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,32 @@ 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.
|
||||||
|
|
||||||
|
The core and archived dedicated-server [GameTests](GAMETESTS.md) together
|
||||||
|
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.
|
||||||
+1
-1
@@ -36,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.9.1
|
mod_version=0.4.0.2
|
||||||
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);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -195,26 +195,19 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe
|
|||||||
if (pages.isEmpty()) return ItemStack.EMPTY;
|
if (pages.isEmpty()) return ItemStack.EMPTY;
|
||||||
int end = nextPageIndex();
|
int end = nextPageIndex();
|
||||||
if (index < 0 || index > end) return ItemStack.EMPTY;
|
if (index < 0 || index > end) return ItemStack.EMPTY;
|
||||||
boolean link = false;
|
ItemStack linkPanel = ItemStack.EMPTY;
|
||||||
List<ItemStack> writing = new ArrayList<>();
|
List<ItemStack> writing = new ArrayList<>();
|
||||||
for (ItemStack page : pages) {
|
for (ItemStack page : pages) {
|
||||||
if (page.isEmpty()) continue;
|
if (page.isEmpty()) continue;
|
||||||
if (!page.is(ModItems.PAGE.get()) || page.getCount() != 1) return ItemStack.EMPTY;
|
if (!page.is(ModItems.PAGE.get()) || page.getCount() != 1) return ItemStack.EMPTY;
|
||||||
if (PageItem.content(page).kind() == PageContent.Kind.LINK_PANEL) {
|
if (PageItem.content(page).kind() == PageContent.Kind.LINK_PANEL) {
|
||||||
if (link || !items.get(LINK_PANEL).isEmpty()) return ItemStack.EMPTY;
|
if (!linkPanel.isEmpty() || !items.get(LINK_PANEL).isEmpty()) return ItemStack.EMPTY;
|
||||||
link = true;
|
linkPanel = page;
|
||||||
} else if (validPage(page)) writing.add(page);
|
} else if (validPage(page)) writing.add(page);
|
||||||
else return ItemStack.EMPTY;
|
else return ItemStack.EMPTY;
|
||||||
}
|
}
|
||||||
if (end + writing.size() > PAGE_COUNT || writing.isEmpty() && !link) return ItemStack.EMPTY;
|
if (end + writing.size() > PAGE_COUNT || writing.isEmpty() && linkPanel.isEmpty()) return ItemStack.EMPTY;
|
||||||
if (link) {
|
if (!linkPanel.isEmpty()) items.set(LINK_PANEL, linkPanel.copy());
|
||||||
for (ItemStack page : pages) {
|
|
||||||
if (!page.isEmpty() && PageItem.content(page).kind() == PageContent.Kind.LINK_PANEL) {
|
|
||||||
items.set(LINK_PANEL, page.copy());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int current = end - 1; current >= index; current--)
|
for (int current = end - 1; current >= index; current--)
|
||||||
items.set(FIRST_PAGE + current + writing.size(), items.get(FIRST_PAGE + current));
|
items.set(FIRST_PAGE + current + writing.size(), items.get(FIRST_PAGE + current));
|
||||||
for (ItemStack page : writing) items.set(FIRST_PAGE + index++, page.copy());
|
for (ItemStack page : writing) items.set(FIRST_PAGE + index++, page.copy());
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.dnorberg.rivencraft.block.entity;
|
package net.dnorberg.rivencraft.block.entity;
|
||||||
|
|
||||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||||
|
import net.dnorberg.rivencraft.writing.InkVialExchange;
|
||||||
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||||
import net.dnorberg.rivencraft.registry.ModItems;
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
import net.dnorberg.rivencraft.writing.WritingInkService;
|
import net.dnorberg.rivencraft.writing.WritingInkService;
|
||||||
@@ -64,35 +65,21 @@ public final class InkMixerBlockEntity extends BlockEntity implements Container
|
|||||||
worldPosition.getZ() + 0.5) <= 64.0;
|
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. */
|
/** No server tick: refill only when ink, bottle space, or consumption changes. */
|
||||||
private void refillEmptyBasin() {
|
private void refillEmptyBasin() {
|
||||||
if (level == null || level.isClientSide || inkRemaining != 0 || !hasBottleSpace()) return;
|
if (level != null && !level.isClientSide && inkRemaining == 0) {
|
||||||
ItemStack vial = items.get(INK);
|
int addedInk = InkVialExchange.exchange(items, INK, BOTTLES);
|
||||||
if (!vial.is(ModItems.INK_VIAL.get())) return;
|
if (addedInk > 0) {
|
||||||
int amount = Math.clamp(InkVialItem.remaining(vial), 0, InkVialItem.CAPACITY);
|
inkRemaining = addedInk;
|
||||||
if (amount == 0) return;
|
setChanged();
|
||||||
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() {
|
public boolean canCreateLinkPanel() {
|
||||||
if (!items.get(PAPER).is(Items.PAPER)) return false;
|
if (!items.get(PAPER).is(Items.PAPER)) return false;
|
||||||
int cost = WritingInkService.linkPanelCost();
|
int cost = WritingInkService.linkPanelCost();
|
||||||
if (inkRemaining >= cost) return true;
|
return InkVialExchange.canPay(items, INK, BOTTLES, inkRemaining, cost);
|
||||||
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. */
|
/** Commit inputs only when a virtual output is actually taken. */
|
||||||
@@ -100,13 +87,7 @@ public final class InkMixerBlockEntity extends BlockEntity implements Container
|
|||||||
if (!canCreateLinkPanel()) return false;
|
if (!canCreateLinkPanel()) return false;
|
||||||
int cost = WritingInkService.linkPanelCost();
|
int cost = WritingInkService.linkPanelCost();
|
||||||
if (inkRemaining < cost) {
|
if (inkRemaining < cost) {
|
||||||
ItemStack vial = items.get(INK);
|
inkRemaining += InkVialExchange.exchange(items, INK, BOTTLES);
|
||||||
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;
|
inkRemaining -= cost;
|
||||||
items.get(PAPER).shrink(1);
|
items.get(PAPER).shrink(1);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.dnorberg.rivencraft.block.entity;
|
package net.dnorberg.rivencraft.block.entity;
|
||||||
|
|
||||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||||
|
import net.dnorberg.rivencraft.writing.InkVialExchange;
|
||||||
import net.dnorberg.rivencraft.writing.WritingInkService;
|
import net.dnorberg.rivencraft.writing.WritingInkService;
|
||||||
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
import net.dnorberg.rivencraft.registry.ModBlockEntities;
|
||||||
import net.dnorberg.rivencraft.registry.ModItems;
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
@@ -92,26 +93,15 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasBottleSpace() {
|
|
||||||
ItemStack returned = items.get(RETURN);
|
|
||||||
return returned.isEmpty() || returned.is(Items.GLASS_BOTTLE)
|
|
||||||
&& returned.getCount() < returned.getMaxStackSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Consume one vial only when the empty meter can return its glass bottle. */
|
/** Consume one vial only when the empty meter can return its glass bottle. */
|
||||||
private void refillEmptyInkBar() {
|
private void refillEmptyInkBar() {
|
||||||
if (level == null || level.isClientSide || inkRemaining != 0 || !hasBottleSpace()) return;
|
if (level != null && !level.isClientSide && inkRemaining == 0) {
|
||||||
ItemStack vial = items.get(INK);
|
int addedInk = InkVialExchange.exchange(items, INK, RETURN);
|
||||||
if (!vial.is(ModItems.INK_VIAL.get())) return;
|
if (addedInk > 0) {
|
||||||
int ink = Math.clamp(InkVialItem.remaining(vial), 0, InkVialItem.CAPACITY);
|
inkRemaining = addedInk;
|
||||||
if (ink == 0) return;
|
setChanged();
|
||||||
inkRemaining = ink;
|
}
|
||||||
vial.shrink(1);
|
}
|
||||||
if (vial.isEmpty()) items.set(INK, ItemStack.EMPTY);
|
|
||||||
ItemStack returned = items.get(RETURN);
|
|
||||||
if (returned.isEmpty()) items.set(RETURN, new ItemStack(Items.GLASS_BOTTLE));
|
|
||||||
else returned.grow(1);
|
|
||||||
setChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The output is virtual; inputs are committed only after a result can be given to a player. */
|
/** The output is virtual; inputs are committed only after a result can be given to a player. */
|
||||||
@@ -119,23 +109,14 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
|
|||||||
ItemStack paper = items.get(PAPER);
|
ItemStack paper = items.get(PAPER);
|
||||||
if (!canPlaceItem(PAPER, paper) || paper.isEmpty()) return false;
|
if (!canPlaceItem(PAPER, paper) || paper.isEmpty()) return false;
|
||||||
int cost = WritingInkService.symbolCopyCost();
|
int cost = WritingInkService.symbolCopyCost();
|
||||||
if (inkRemaining >= cost) return true;
|
return InkVialExchange.canPay(items, INK, RETURN, inkRemaining, cost);
|
||||||
ItemStack vial = items.get(INK);
|
|
||||||
return vial.is(ModItems.INK_VIAL.get()) && inkRemaining + InkVialItem.remaining(vial) >= cost
|
|
||||||
&& hasBottleSpace();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean consumeCopyMaterials() {
|
public boolean consumeCopyMaterials() {
|
||||||
if (!canConsumeCopyMaterials()) return false;
|
if (!canConsumeCopyMaterials()) return false;
|
||||||
int cost = WritingInkService.symbolCopyCost();
|
int cost = WritingInkService.symbolCopyCost();
|
||||||
if (inkRemaining < cost) {
|
if (inkRemaining < cost) {
|
||||||
ItemStack vial = items.get(INK);
|
inkRemaining += InkVialExchange.exchange(items, INK, RETURN);
|
||||||
inkRemaining += Math.clamp(InkVialItem.remaining(vial), 0, InkVialItem.CAPACITY);
|
|
||||||
vial.shrink(1);
|
|
||||||
if (vial.isEmpty()) items.set(INK, ItemStack.EMPTY);
|
|
||||||
ItemStack returned = items.get(RETURN);
|
|
||||||
if (returned.isEmpty()) items.set(RETURN, new ItemStack(Items.GLASS_BOTTLE));
|
|
||||||
else returned.grow(1);
|
|
||||||
}
|
}
|
||||||
inkRemaining -= cost;
|
inkRemaining -= cost;
|
||||||
removeItem(PAPER, 1);
|
removeItem(PAPER, 1);
|
||||||
@@ -148,7 +129,6 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
|
|||||||
refillEmptyInkBar();
|
refillEmptyInkBar();
|
||||||
}
|
}
|
||||||
@Override public void clearContent() {
|
@Override public void clearContent() {
|
||||||
items.clear();
|
|
||||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||||
inkRemaining = 0;
|
inkRemaining = 0;
|
||||||
displayPage = ItemStack.EMPTY;
|
displayPage = ItemStack.EMPTY;
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ 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.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;
|
||||||
@@ -250,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;
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ 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.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;
|
||||||
@@ -215,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;
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ public final class RivencraftJeiPlugin implements IModPlugin {
|
|||||||
new WritingGuide(List.of(panel, new ItemStack(Items.PAPER), new ItemStack(ModItems.INK_VIAL.get())),
|
new WritingGuide(List.of(panel, new ItemStack(Items.PAPER), new ItemStack(ModItems.INK_VIAL.get())),
|
||||||
panel, "jei.rivencraft.writing.copy"),
|
panel, "jei.rivencraft.writing.copy"),
|
||||||
new WritingGuide(List.of(new ItemStack(Items.LEATHER), panel),
|
new WritingGuide(List.of(new ItemStack(Items.LEATHER), panel),
|
||||||
new ItemStack(ModItems.DESCRIPTIVE_BOOK.get()), "jei.rivencraft.writing.bind")
|
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)));
|
registration.addRecipes(INK_MIXER, List.of(new InkMixerGuide(panel)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import net.minecraft.world.phys.AABB;
|
|||||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
@GameTestHolder(Rivencraft.MOD_ID)
|
@GameTestHolder("rivencraft_archive_binder")
|
||||||
@PrefixGameTestTemplate(false)
|
@PrefixGameTestTemplate(false)
|
||||||
public final class BookBinderGameTests {
|
public final class BookBinderGameTests {
|
||||||
private BookBinderGameTests() {}
|
private BookBinderGameTests() {}
|
||||||
@@ -74,7 +74,24 @@ 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();
|
||||||
|
}
|
||||||
|
|
||||||
public static void assemblyIsAtomicAndRetainsPageOrder(GameTestHelper helper) {
|
public static void assemblyIsAtomicAndRetainsPageOrder(GameTestHelper helper) {
|
||||||
BookBinderBlockEntity binder = place(helper);
|
BookBinderBlockEntity binder = place(helper);
|
||||||
binder.setTitle("A New Age");
|
binder.setTitle("A New Age");
|
||||||
@@ -237,6 +254,38 @@ public final class BookBinderGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,94 @@
|
|||||||
|
package net.dnorberg.rivencraft.gametest;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.gametest.framework.GameTest;
|
||||||
|
import net.minecraft.gametest.framework.GameTestHelper;
|
||||||
|
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||||
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
|
/** Small default smoke suite; the detailed milestone regressions are opt-in. */
|
||||||
|
@GameTestHolder(Rivencraft.MOD_ID)
|
||||||
|
@PrefixGameTestTemplate(false)
|
||||||
|
public final class CoreGameTests {
|
||||||
|
private CoreGameTests() {}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void previewBackendSelection(GameTestHelper helper) {
|
||||||
|
LinkingGameTests.previewBackendSelectionCoversOptionalDependencyMatrix(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void linkTargetPersistence(GameTestHelper helper) {
|
||||||
|
LinkingGameTests.linkTargetSurvivesItemSerialization(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void deterministicAgeDefinition(GameTestHelper helper) {
|
||||||
|
LinkingGameTests.ageDefinitionIsCreatedOnceWithDeterministicData(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void blockedLinkDestination(GameTestHelper helper) {
|
||||||
|
LinkingGameTests.travelRejectsBlockedDestination(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void legacyWritingAndPageFreeBooks(GameTestHelper helper) {
|
||||||
|
WritingDataGameTests.oldBareListWritingAndPageFreeBooksRemainReadable(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void pagePersistenceAndUnknownSymbols(GameTestHelper helper) {
|
||||||
|
WritingDataGameTests.pageItemPreservesContentAndRejectsForgedUnknownSymbols(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void notebookPersistence(GameTestHelper helper) {
|
||||||
|
WritingDataGameTests.notebookContentsPreserveOrderGapsNamesAndUnknownPages(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void deskCopyTransaction(GameTestHelper helper) {
|
||||||
|
WritingDeskGameTests.takingCopySpendsPaperAndOneInkThenReturnsGlassBottle(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void deskRejectsForgedAndStaleActions(GameTestHelper helper) {
|
||||||
|
WritingDeskGameTests.forgedDeskSelectionsAndStaleMenuCannotCopy(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void twoBlockDeskDropsOnce(GameTestHelper helper) {
|
||||||
|
WritingDeskGameTests.twoBlockDeskSharesOneInventoryAndBreaksTogether(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void oldDeskMigration(GameTestHelper helper) {
|
||||||
|
WritingDeskGameTests.oldDeskInventoryMigratesAndAll88TabsPersist(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void binderKeepsMissingSymbolsRecoverable(GameTestHelper helper) {
|
||||||
|
BookBinderGameTests.missingSymbolPageSurvivesReloadButCannotBeAssembled(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void binderAssemblyTransaction(GameTestHelper helper) {
|
||||||
|
BookBinderGameTests.assemblyIsAtomicAndRetainsPageOrder(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void binderRejectsForgedAndReplayedActions(GameTestHelper helper) {
|
||||||
|
BookBinderGameTests.forgedBinderActionsAndReplayedOutputCannotDuplicateBook(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void mixerOutputTransaction(GameTestHelper helper) {
|
||||||
|
InkMixerGameTests.linkPanelRequiresTakingOutputAndPersistsInk(helper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void linkingBookRecipe(GameTestHelper helper) {
|
||||||
|
LinkingBookRecipeGameTests.legacyRecipeRequiresExactlyLeatherAndLinkPanel(helper);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package net.dnorberg.rivencraft.gametest;
|
package net.dnorberg.rivencraft.gametest;
|
||||||
|
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
|
||||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||||
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||||
@@ -12,11 +11,13 @@ import net.minecraft.gametest.framework.GameTestHelper;
|
|||||||
import net.minecraft.world.inventory.ClickType;
|
import net.minecraft.world.inventory.ClickType;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.item.Items;
|
import net.minecraft.world.item.Items;
|
||||||
|
import net.minecraft.world.entity.item.ItemEntity;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.phys.AABB;
|
||||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
@GameTestHolder(Rivencraft.MOD_ID)
|
@GameTestHolder("rivencraft_archive_mixer")
|
||||||
@PrefixGameTestTemplate(false)
|
@PrefixGameTestTemplate(false)
|
||||||
public final class InkMixerGameTests {
|
public final class InkMixerGameTests {
|
||||||
private InkMixerGameTests() {}
|
private InkMixerGameTests() {}
|
||||||
@@ -27,7 +28,6 @@ public final class InkMixerGameTests {
|
|||||||
return (InkMixerBlockEntity) helper.getLevel().getBlockEntity(pos);
|
return (InkMixerBlockEntity) helper.getLevel().getBlockEntity(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
|
||||||
public static void linkPanelRequiresTakingOutputAndPersistsInk(GameTestHelper helper) {
|
public static void linkPanelRequiresTakingOutputAndPersistsInk(GameTestHelper helper) {
|
||||||
InkMixerBlockEntity mixer = place(helper);
|
InkMixerBlockEntity mixer = place(helper);
|
||||||
mixer.setItem(InkMixerBlockEntity.PAPER, new ItemStack(Items.PAPER, 2));
|
mixer.setItem(InkMixerBlockEntity.PAPER, new ItemStack(Items.PAPER, 2));
|
||||||
@@ -98,6 +98,14 @@ public final class InkMixerGameTests {
|
|||||||
"Shift-click must consume only one paper");
|
"Shift-click must consume only one paper");
|
||||||
helper.getLevel().setBlock(mixer.getBlockPos(), Blocks.AIR.defaultBlockState(), 3);
|
helper.getLevel().setBlock(mixer.getBlockPos(), Blocks.AIR.defaultBlockState(), 3);
|
||||||
helper.assertFalse(menu.stillValid(player), "Breaking the mixer must invalidate its menu");
|
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();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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_archive_recipe")
|
||||||
|
@PrefixGameTestTemplate(false)
|
||||||
|
public final class LinkingBookRecipeGameTests {
|
||||||
|
private LinkingBookRecipeGameTests() {}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ import net.neoforged.neoforge.gametest.GameTestHolder;
|
|||||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
/** Server-side regression coverage for persistent linking-book data and travel safety. */
|
/** Server-side regression coverage for persistent linking-book data and travel safety. */
|
||||||
@GameTestHolder(Rivencraft.MOD_ID)
|
@GameTestHolder("rivencraft_archive_linking")
|
||||||
@PrefixGameTestTemplate(false)
|
@PrefixGameTestTemplate(false)
|
||||||
public final class LinkingGameTests {
|
public final class LinkingGameTests {
|
||||||
private static final BlockPos ORIGIN = new BlockPos(0, 1, 0);
|
private static final BlockPos ORIGIN = new BlockPos(0, 1, 0);
|
||||||
@@ -42,7 +42,6 @@ public final class LinkingGameTests {
|
|||||||
private LinkingGameTests() {
|
private LinkingGameTests() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
|
||||||
public static void previewBackendSelectionCoversOptionalDependencyMatrix(GameTestHelper helper) {
|
public static void previewBackendSelectionCoversOptionalDependencyMatrix(GameTestHelper helper) {
|
||||||
assertPreviewBackend(helper, RivencraftClientConfig.PreviewMode.AUTO, true, true,
|
assertPreviewBackend(helper, RivencraftClientConfig.PreviewMode.AUTO, true, true,
|
||||||
PreviewBackendSelector.Backend.IMMERSIVE_PORTALS);
|
PreviewBackendSelector.Backend.IMMERSIVE_PORTALS);
|
||||||
@@ -127,7 +126,6 @@ public final class LinkingGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
|
||||||
public static void linkTargetSurvivesItemSerialization(GameTestHelper helper) {
|
public static void linkTargetSurvivesItemSerialization(GameTestHelper helper) {
|
||||||
LinkTarget expected = new LinkTarget(
|
LinkTarget expected = new LinkTarget(
|
||||||
GlobalPos.of(helper.getLevel().dimension(), helper.absolutePos(ORIGIN)),
|
GlobalPos.of(helper.getLevel().dimension(), helper.absolutePos(ORIGIN)),
|
||||||
@@ -181,7 +179,6 @@ public final class LinkingGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
|
||||||
public static void ageDefinitionIsCreatedOnceWithDeterministicData(GameTestHelper helper) {
|
public static void ageDefinitionIsCreatedOnceWithDeterministicData(GameTestHelper helper) {
|
||||||
UUID ageId = UUID.randomUUID();
|
UUID ageId = UUID.randomUUID();
|
||||||
String requestedName = "Test Age " + ageId;
|
String requestedName = "Test Age " + ageId;
|
||||||
@@ -666,7 +663,6 @@ public final class LinkingGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
|
||||||
public static void travelRejectsBlockedDestination(GameTestHelper helper) {
|
public static void travelRejectsBlockedDestination(GameTestHelper helper) {
|
||||||
BlockPos absoluteOrigin = helper.absolutePos(ORIGIN);
|
BlockPos absoluteOrigin = helper.absolutePos(ORIGIN);
|
||||||
for (BlockPos position : BlockPos.betweenClosed(
|
for (BlockPos position : BlockPos.betweenClosed(
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ 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 net.dnorberg.rivencraft.Rivencraft;
|
import java.util.UUID;
|
||||||
|
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;
|
||||||
@@ -29,7 +30,7 @@ import net.neoforged.neoforge.gametest.GameTestHolder;
|
|||||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
/** Acceptance coverage for Milestone 4's persistent writing contracts. */
|
/** Acceptance coverage for Milestone 4's persistent writing contracts. */
|
||||||
@GameTestHolder(Rivencraft.MOD_ID)
|
@GameTestHolder("rivencraft_archive_writing")
|
||||||
@PrefixGameTestTemplate(false)
|
@PrefixGameTestTemplate(false)
|
||||||
public final class WritingDataGameTests {
|
public final class WritingDataGameTests {
|
||||||
private WritingDataGameTests() {
|
private WritingDataGameTests() {
|
||||||
@@ -60,7 +61,6 @@ public final class WritingDataGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
|
||||||
public static void oldBareListWritingAndPageFreeBooksRemainReadable(GameTestHelper helper) {
|
public static void oldBareListWritingAndPageFreeBooksRemainReadable(GameTestHelper helper) {
|
||||||
OrderedWriting writing = new OrderedWriting(List.of(
|
OrderedWriting writing = new OrderedWriting(List.of(
|
||||||
WritingEntry.linkPanel(), WritingEntry.symbol(reference("example:removed_symbol"))));
|
WritingEntry.linkPanel(), WritingEntry.symbol(reference("example:removed_symbol"))));
|
||||||
@@ -71,8 +71,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();
|
||||||
@@ -120,7 +124,6 @@ public final class WritingDataGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
|
||||||
public static void pageItemPreservesContentAndRejectsForgedUnknownSymbols(GameTestHelper helper) {
|
public static void pageItemPreservesContentAndRejectsForgedUnknownSymbols(GameTestHelper helper) {
|
||||||
SymbolReference known = reference("rivencraft:terrain/standard");
|
SymbolReference known = reference("rivencraft:terrain/standard");
|
||||||
SymbolReference unknown = reference("example:forged_symbol");
|
SymbolReference unknown = reference("example:forged_symbol");
|
||||||
@@ -136,6 +139,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 {
|
||||||
@@ -146,7 +153,6 @@ public final class WritingDataGameTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
|
||||||
public static void notebookContentsPreserveOrderGapsNamesAndUnknownPages(GameTestHelper helper) {
|
public static void notebookContentsPreserveOrderGapsNamesAndUnknownPages(GameTestHelper helper) {
|
||||||
ItemStack first = PageItem.createSymbolPage(reference("rivencraft:terrain/standard"));
|
ItemStack first = PageItem.createSymbolPage(reference("rivencraft:terrain/standard"));
|
||||||
first.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME,
|
first.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package net.dnorberg.rivencraft.gametest;
|
package net.dnorberg.rivencraft.gametest;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
|
||||||
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||||
import net.dnorberg.rivencraft.block.WritingDeskBlock;
|
import net.dnorberg.rivencraft.block.WritingDeskBlock;
|
||||||
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
|
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
|
||||||
@@ -26,7 +25,7 @@ import net.minecraft.world.phys.AABB;
|
|||||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||||
|
|
||||||
@GameTestHolder(Rivencraft.MOD_ID)
|
@GameTestHolder("rivencraft_archive_desk")
|
||||||
@PrefixGameTestTemplate(false)
|
@PrefixGameTestTemplate(false)
|
||||||
public final class WritingDeskGameTests {
|
public final class WritingDeskGameTests {
|
||||||
private WritingDeskGameTests() {}
|
private WritingDeskGameTests() {}
|
||||||
@@ -94,7 +93,6 @@ public final class WritingDeskGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
|
||||||
public static void takingCopySpendsPaperAndOneInkThenReturnsGlassBottle(GameTestHelper helper) {
|
public static void takingCopySpendsPaperAndOneInkThenReturnsGlassBottle(GameTestHelper helper) {
|
||||||
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
|
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||||
helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3);
|
helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3);
|
||||||
@@ -249,11 +247,39 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
@@ -310,7 +336,6 @@ public final class WritingDeskGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
|
||||||
public static void twoBlockDeskSharesOneInventoryAndBreaksTogether(GameTestHelper helper) {
|
public static void twoBlockDeskSharesOneInventoryAndBreaksTogether(GameTestHelper helper) {
|
||||||
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
|
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||||
var block = ModBlocks.WRITING_DESK.get();
|
var block = ModBlocks.WRITING_DESK.get();
|
||||||
@@ -433,7 +458,6 @@ public final class WritingDeskGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
|
||||||
public static void oldDeskInventoryMigratesAndAll88TabsPersist(GameTestHelper helper) {
|
public static void oldDeskInventoryMigratesAndAll88TabsPersist(GameTestHelper helper) {
|
||||||
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
|
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||||
var registries = helper.getLevel().registryAccess();
|
var registries = helper.getLevel().registryAccess();
|
||||||
|
|||||||
@@ -166,15 +166,12 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
|||||||
selectedPage = page;
|
selectedPage = page;
|
||||||
selectedSymbol = null;
|
selectedSymbol = null;
|
||||||
} else if (id >= CATALOG_BUTTON_BASE && id < CATALOG_BUTTON_BASE + catalog.size()) {
|
} else if (id >= CATALOG_BUTTON_BASE && id < CATALOG_BUTTON_BASE + catalog.size()) {
|
||||||
if (!catalog.equals(WritingDeskCatalog.available(blockEntity))) {
|
List<SymbolDisplayMetadata> available = WritingDeskCatalog.available(blockEntity);
|
||||||
refreshCatalog();
|
if (!catalog.equals(available)) {
|
||||||
|
refreshCatalog(available);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ResourceLocation requested = catalog.get(id - CATALOG_BUTTON_BASE).id();
|
selectedSymbol = available.get(id - CATALOG_BUTTON_BASE).id();
|
||||||
if (!WritingDeskCatalog.available(blockEntity).stream().anyMatch(entry -> entry.id().equals(requested))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
selectedSymbol = requested;
|
|
||||||
} else return false;
|
} else return false;
|
||||||
refreshOutput();
|
refreshOutput();
|
||||||
return true;
|
return true;
|
||||||
@@ -390,7 +387,11 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|
|||||||
|
|
||||||
private void refreshCatalog() {
|
private void refreshCatalog() {
|
||||||
if (clientSide || blockEntity == null) return;
|
if (clientSide || blockEntity == null) return;
|
||||||
List<SymbolDisplayMetadata> current = WritingDeskCatalog.available(blockEntity);
|
refreshCatalog(WritingDeskCatalog.available(blockEntity));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshCatalog(List<SymbolDisplayMetadata> current) {
|
||||||
|
if (clientSide || blockEntity == null) return;
|
||||||
if (!current.equals(catalog)) {
|
if (!current.equals(catalog)) {
|
||||||
catalog = current;
|
catalog = current;
|
||||||
if (serverViewer != null && serverViewer.containerMenu == this) {
|
if (serverViewer != null && serverViewer.containerMenu == this) {
|
||||||
|
|||||||
@@ -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,43 @@
|
|||||||
|
package net.dnorberg.rivencraft.writing;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||||
|
import net.dnorberg.rivencraft.registry.ModItems;
|
||||||
|
import net.minecraft.core.NonNullList;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.Items;
|
||||||
|
|
||||||
|
/** Shared, server-side vial-to-meter exchange for the Desk and Ink Mixer. */
|
||||||
|
public final class InkVialExchange {
|
||||||
|
private InkVialExchange() {}
|
||||||
|
|
||||||
|
public static boolean canPay(NonNullList<ItemStack> items, int inkSlot, int bottleSlot,
|
||||||
|
int storedInk, int cost) {
|
||||||
|
return storedInk >= cost || canExchange(items, inkSlot, bottleSlot)
|
||||||
|
&& storedInk + vialInk(items.get(inkSlot)) >= cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns added ink; zero means no inputs were changed. */
|
||||||
|
public static int exchange(NonNullList<ItemStack> items, int inkSlot, int bottleSlot) {
|
||||||
|
if (!canExchange(items, inkSlot, bottleSlot)) return 0;
|
||||||
|
ItemStack vial = items.get(inkSlot);
|
||||||
|
int addedInk = vialInk(vial);
|
||||||
|
vial.shrink(1);
|
||||||
|
if (vial.isEmpty()) items.set(inkSlot, ItemStack.EMPTY);
|
||||||
|
ItemStack bottles = items.get(bottleSlot);
|
||||||
|
if (bottles.isEmpty()) items.set(bottleSlot, new ItemStack(Items.GLASS_BOTTLE));
|
||||||
|
else bottles.grow(1);
|
||||||
|
return addedInk;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean canExchange(NonNullList<ItemStack> items, int inkSlot, int bottleSlot) {
|
||||||
|
ItemStack vial = items.get(inkSlot);
|
||||||
|
ItemStack bottles = items.get(bottleSlot);
|
||||||
|
return vial.is(ModItems.INK_VIAL.get()) && vialInk(vial) > 0
|
||||||
|
&& (bottles.isEmpty() || bottles.is(Items.GLASS_BOTTLE)
|
||||||
|
&& bottles.getCount() < bottles.getMaxStackSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int vialInk(ItemStack vial) {
|
||||||
|
return Math.clamp(InkVialItem.remaining(vial), 0, InkVialItem.CAPACITY);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@ package net.dnorberg.rivencraft.writing;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
|
||||||
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
|
||||||
import net.dnorberg.rivencraft.item.PageItem;
|
import net.dnorberg.rivencraft.item.PageItem;
|
||||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||||
@@ -20,7 +20,7 @@ public final class WritingDeskCatalog {
|
|||||||
private WritingDeskCatalog() {}
|
private WritingDeskCatalog() {}
|
||||||
|
|
||||||
public static List<SymbolDisplayMetadata> available(WritingDeskBlockEntity desk) {
|
public static List<SymbolDisplayMetadata> available(WritingDeskBlockEntity desk) {
|
||||||
Map<ResourceLocation, SymbolDisplayMetadata> unique = new TreeMap<>();
|
Map<ResourceLocation, SymbolDisplayMetadata> unique = new HashMap<>();
|
||||||
for (int slot = 0; slot < WritingDeskBlockEntity.COLLECTIONS; slot++) {
|
for (int slot = 0; slot < WritingDeskBlockEntity.COLLECTIONS; slot++) {
|
||||||
ItemStack collection = desk.getItem(slot);
|
ItemStack collection = desk.getItem(slot);
|
||||||
if (collection.is(ModItems.PAGE.get())) add(unique, collection);
|
if (collection.is(ModItems.PAGE.get())) add(unique, collection);
|
||||||
|
|||||||
@@ -136,6 +136,7 @@
|
|||||||
"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.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.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.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"
|
||||||
}
|
}
|
||||||
@@ -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