Add Writing Desk symbol catalog and paper transcription
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-09-24 14:08:12 -04:00
1 parent 05a06a4dc8
commit e511e7c10b
23 files changed
+442 -52

No files matched your search

+2
View File
@@ -12,6 +12,8 @@ The Notebook page atlas is copied from Legacy's assets/mystcraft/gui/bookui_page
under the same license. under the same license.
The Writing Desk item texture is copied from Legacy's The Writing Desk item texture is copied from Legacy's
assets/mystcraft/textures/items/writingdesk.png under the same license. assets/mystcraft/textures/items/writingdesk.png under the same license.
The Writing Desk block texture is copied from Legacy's
assets/mystcraft/textures/entity/desk.png under the same license.
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
+5 -4
View File
@@ -14,7 +14,7 @@ have not been ported yet.
Milestone 4 now includes stackable blank Pages plus server-created symbol, Milestone 4 now includes stackable blank Pages plus server-created symbol,
unresolved-symbol, and baseline link-panel states. Page visuals follow the unresolved-symbol, and baseline link-panel states. Page visuals follow the
legacy runtime-composition rules. Craft an Ink Vial from two black dye, a glass legacy runtime-composition rules. Craft an Ink Vial from two black dye, a glass
bottle, and a water bucket; the bucket is returned. Hold a blank Page in one bottle, and a water bucket; the bucket is returned. Hold vanilla paper in one
hand and use the Ink Vial in the other to prepare a baseline Link Panel Page. 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
@@ -25,12 +25,13 @@ Its four collection tabs, paper input, ink vial input, empty glass-bottle return
16-unit ink meter, and inventory persist 16-unit ink meter, and inventory persist
through world saves; breaking it drops stored contents. The desk screen is a through world saves; breaking it drops stored contents. The desk screen is a
functional foundation styled after Legacy's left tabs, central writing surface, functional foundation styled after Legacy's left tabs, central writing surface,
and right inventory. Put vanilla paper (or a blank Rivencraft Page) in the and right inventory. Put vanilla paper in the
upper-left slot and Ink Vials in the upper-right; a copy appears in the upper-left slot and Ink Vials in the upper-right; a copy appears in the
lower-left result slot when the selected page is valid. Taking it consumes one lower-left result slot when the selected page is valid. Taking it consumes one
paper and, by default, one ink. An empty meter consumes a vial to refill and paper and, by default, one ink. An empty meter consumes a vial to refill and
returns a vanilla glass bottle to the lower-right slot. Writing new symbols returns a vanilla glass bottle to the lower-right slot. The ALL browser searches
from a catalog remains future work. and filters symbols held in the desk's loose pages and Notebooks; selection
is validated against the server's current symbol catalog.
Selecting a stored page on the desk surface shows its Legacy-proportioned Selecting a stored page on the desk surface shows its Legacy-proportioned
portrait preview and name above the player inventory. portrait preview and name above the player inventory.
+1
View File
@@ -9,6 +9,7 @@ project's GNU LGPL v3 license. Rivencraft itself uses the same license.
| `assets/rivencraft/textures/item/page.png` | `assets/mystcraft/textures/items/page_background.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | | `assets/rivencraft/textures/item/page.png` | `assets/mystcraft/textures/items/page_background.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only |
| `assets/rivencraft/textures/gui/notebook_pages.png` | `assets/mystcraft/gui/bookui_pagel.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Notebook draws the native 30×40 page at atlas coordinates `(156, 0)` | | `assets/rivencraft/textures/gui/notebook_pages.png` | `assets/mystcraft/gui/bookui_pagel.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Notebook draws the native 30×40 page at atlas coordinates `(156, 0)` |
| `assets/rivencraft/textures/item/writing_desk.png` | `assets/mystcraft/textures/items/writingdesk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | | `assets/rivencraft/textures/item/writing_desk.png` | `assets/mystcraft/textures/items/writingdesk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only |
| `assets/rivencraft/textures/block/writing_desk.png` | `assets/mystcraft/textures/entity/desk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; single-block model adapts the desk geometry and UV regions from `ModelWritingDesk` |
| `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 |
+5 -5
View File
@@ -53,8 +53,8 @@ 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 turn a blank page into a known symbol Only server-side writing operations may create a known symbol
page or link panel. The shared `PageItem` creation and validation gates reject page or link panel from vanilla paper. The shared `PageItem` creation and validation gates reject
new pages for identifiers absent from the server catalog. Later Writing Desk new pages for identifiers absent from the server catalog. Later Writing Desk
and Book Binder packets must call these gates rather than accepting page and Book Binder packets must call these gates rather than accepting page
contents supplied by a client. contents supplied by a client.
@@ -82,9 +82,9 @@ used to craft another. The animated item texture comes from Mystcraft Legacy.
The server config sets ink costs independently for symbol copying (default one The server config sets ink costs independently for symbol copying (default one
unit) and baseline link-panel preparation (default two units). To prepare a unit) and baseline link-panel preparation (default two units). To prepare a
panel, hold a blank Page in one hand and use an Ink Vial in the other. The panel, hold vanilla paper in one hand and use an Ink Vial in the other. The
server checks the held items, blank state, and sufficient ink before consuming server checks the held items and sufficient ink before consuming
anything. A stack of blank Pages yields exactly one Link Panel Page per use; anything. A stack of paper yields exactly one Link Panel Page per use;
if the inventory is full, the result is dropped instead of deleted. Symbol if the inventory is full, the result is dropped instead of deleted. Symbol
copying is implemented by the Writing Desk: it previews a copy of the selected copying is implemented by the Writing Desk: it previews a copy of the selected
resolved page, then spends paper and ink only when that copy is taken. The resolved page, then spends paper and ink only when that copy is taken. The
+1 -1
View File
@@ -35,7 +35,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.5.2 mod_version=0.3.6.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.
@@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.block;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity; import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.dnorberg.rivencraft.menu.WritingDeskMenu; import net.dnorberg.rivencraft.menu.WritingDeskMenu;
import net.dnorberg.rivencraft.writing.WritingDeskCatalog;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
@@ -48,7 +49,10 @@ public final class WritingDeskBlock extends BaseEntityBlock {
serverPlayer.openMenu(new SimpleMenuProvider( serverPlayer.openMenu(new SimpleMenuProvider(
(id, inventory, ignored) -> new WritingDeskMenu(id, inventory, desk), (id, inventory, ignored) -> new WritingDeskMenu(id, inventory, desk),
Component.translatable("container.rivencraft.writing_desk")), Component.translatable("container.rivencraft.writing_desk")),
buffer -> buffer.writeBlockPos(pos)); buffer -> {
buffer.writeBlockPos(pos);
WritingDeskCatalog.write(buffer, WritingDeskCatalog.available(desk));
});
} }
return InteractionResult.sidedSuccess(level.isClientSide); return InteractionResult.sidedSuccess(level.isClientSide);
} }
@@ -1,7 +1,6 @@
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.item.PageItem;
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;
@@ -68,9 +67,7 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
return switch (slot) { return switch (slot) {
case INK -> stack.is(ModItems.INK_VIAL.get()); case INK -> stack.is(ModItems.INK_VIAL.get());
case RETURN -> false; case RETURN -> false;
case PAPER -> stack.is(Items.PAPER) case PAPER -> stack.is(Items.PAPER);
|| stack.is(ModItems.PAGE.get()) && PageItem.content(stack).kind()
== net.dnorberg.rivencraft.writing.PageContent.Kind.BLANK;
case LEGACY_OUTPUT -> false; case LEGACY_OUTPUT -> false;
default -> false; default -> false;
}; };
@@ -0,0 +1,20 @@
package net.dnorberg.rivencraft.client;
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
import net.dnorberg.rivencraft.network.WritingDeskCatalogPayload;
import net.minecraft.client.Minecraft;
import net.neoforged.neoforge.network.handling.IPayloadContext;
public final class ClientWritingDeskCatalog {
private ClientWritingDeskCatalog() {}
public static void handle(WritingDeskCatalogPayload payload, IPayloadContext context) {
context.enqueueWork(() -> {
var player = Minecraft.getInstance().player;
if (player != null && player.containerMenu instanceof WritingDeskMenu menu
&& menu.containerId == payload.containerId()) {
menu.setCatalog(payload.symbols());
}
});
}
}
@@ -276,7 +276,11 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
if (content.kind() == net.dnorberg.rivencraft.writing.PageContent.Kind.UNRESOLVED_SYMBOL) { if (content.kind() == net.dnorberg.rivencraft.writing.PageContent.Kind.UNRESOLVED_SYMBOL) {
return UNRESOLVED_SYMBOL; return UNRESOLVED_SYMBOL;
} }
return switch (content.symbol().orElseThrow().id().toString()) { return symbolTexture(content.symbol().orElseThrow().id());
}
static ResourceLocation symbolTexture(ResourceLocation id) {
return switch (id.toString()) {
case "rivencraft:terrain/standard" -> STANDARD_TERRAIN; case "rivencraft:terrain/standard" -> STANDARD_TERRAIN;
case "rivencraft:biome/native" -> NATIVE_BIOMES; case "rivencraft:biome/native" -> NATIVE_BIOMES;
case "rivencraft:lighting/normal" -> NORMAL_LIGHTING; case "rivencraft:lighting/normal" -> NORMAL_LIGHTING;
@@ -1,13 +1,18 @@
package net.dnorberg.rivencraft.client.screen; package net.dnorberg.rivencraft.client.screen;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Objects;
import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.menu.WritingDeskMenu; import net.dnorberg.rivencraft.menu.WritingDeskMenu;
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.NotebookContents; import net.dnorberg.rivencraft.writing.NotebookContents;
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.EditBox;
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;
@@ -21,6 +26,11 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
private int lastCollection = -1; private int lastCollection = -1;
private int selectedPage; private int selectedPage;
private int scrollRows; private int scrollRows;
private boolean allMode;
private ResourceLocation selectedSymbol;
private ResourceLocation selectedCategory;
private EditBox search;
private Button categoryButton;
public WritingDeskScreen(WritingDeskMenu menu, Inventory inventory, Component title) { public WritingDeskScreen(WritingDeskMenu menu, Inventory inventory, Component title) {
super(menu, inventory, title); super(menu, inventory, title);
@@ -31,6 +41,44 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
inventoryLabelY = 79; inventoryLabelY = 79;
} }
@Override protected void init() {
super.init();
addRenderableWidget(Button.builder(Component.literal("ALL"), button -> {
allMode = true;
scrollRows = 0;
}).bounds(leftPos + 48, topPos - 18, 29, 18).build());
categoryButton = addRenderableWidget(Button.builder(Component.literal("All types"), button -> {
List<ResourceLocation> categories = menu.catalog().stream()
.map(SymbolDisplayMetadata::category).distinct().sorted().toList();
int next = selectedCategory == null ? 0 : categories.indexOf(selectedCategory) + 1;
selectedCategory = next >= categories.size() ? null : categories.get(next);
categoryButton.setMessage(Component.literal(selectedCategory == null
? "All types" : selectedCategory.getPath()));
allMode = true;
scrollRows = 0;
}).bounds(leftPos + 79, topPos - 18, 66, 18).build());
search = new EditBox(font, leftPos + 147, topPos - 18, 69, 18,
Component.translatable("screen.rivencraft.writing_desk.search"));
search.setMaxLength(64);
search.setHint(Component.translatable("screen.rivencraft.writing_desk.search"));
search.setResponder(value -> {
allMode = true;
scrollRows = 0;
});
addRenderableWidget(search);
}
private List<SymbolDisplayMetadata> filteredCatalog() {
String query = search == null ? "" : search.getValue().trim().toLowerCase(Locale.ROOT);
return menu.catalog().stream()
.filter(entry -> selectedCategory == null || selectedCategory.equals(entry.category()))
.filter(entry -> query.isEmpty()
|| entry.id().toString().toLowerCase(Locale.ROOT).contains(query)
|| Component.translatable(entry.nameTranslationKey()).getString()
.toLowerCase(Locale.ROOT).contains(query))
.toList();
}
@Override public void renderBackground(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { @Override public void renderBackground(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
// Legacy leaves the world visible around the desk instead of dimming it. // Legacy leaves the world visible around the desk instead of dimming it.
} }
@@ -94,6 +142,10 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
} }
private void renderCollection(GuiGraphics graphics) { private void renderCollection(GuiGraphics graphics) {
if (allMode) {
renderCatalog(graphics);
return;
}
List<ItemStack> pages = pages(); List<ItemStack> pages = pages();
if (lastCollection != menu.activeCollection()) { if (lastCollection != menu.activeCollection()) {
lastCollection = menu.activeCollection(); lastCollection = menu.activeCollection();
@@ -124,6 +176,41 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
graphics.drawString(font, title, leftPos + 254, topPos + 67, 0xFFE0E0E0, false); graphics.drawString(font, title, leftPos + 254, topPos + 67, 0xFFE0E0E0, false);
} }
private void renderCatalog(GuiGraphics graphics) {
List<SymbolDisplayMetadata> filtered = filteredCatalog();
if (filtered.isEmpty()) {
graphics.drawString(font, Component.translatable("screen.rivencraft.writing_desk.no_symbols"),
leftPos + 52, topPos + 24, 0xFFC4B8A3, false);
}
for (int i = scrollRows * 5; i < filtered.size() && i < scrollRows * 5 + 15; i++) {
SymbolDisplayMetadata entry = filtered.get(i);
int x = leftPos + 48 + (i % 5) * 31;
int y = topPos + 18 + (i / 5 - scrollRows) * 41;
drawSymbolPage(graphics, entry.id(), x, y);
if (Objects.equals(entry.id(), selectedSymbol)) {
graphics.renderOutline(x, y, 30, 40, 0xFFFFD061);
}
}
SymbolDisplayMetadata selected = menu.catalog().stream()
.filter(entry -> Objects.equals(entry.id(), selectedSymbol)).findFirst().orElse(null);
if (selected == null) return;
graphics.pose().pushPose();
graphics.pose().translate(leftPos + 294, topPos + 12, 0);
graphics.pose().scale(1.35F, 1.35F, 1.0F);
drawSymbolPage(graphics, selected.id(), 0, 0);
graphics.pose().popPose();
String title = font.plainSubstrByWidth(
Component.translatable(selected.nameTranslationKey()).getString(), 112);
graphics.fill(leftPos + 251, topPos + 64, leftPos + 365, topPos + 77, 0xFF202020);
graphics.drawString(font, title, leftPos + 254, topPos + 67, 0xFFE0E0E0, false);
}
private void drawSymbolPage(GuiGraphics graphics, ResourceLocation id, int x, int y) {
graphics.blit(PAGE_ATLAS, x, y, 30, 40, 156, 0, 30, 40, 256, 256);
graphics.blit(NotebookScreen.symbolTexture(id), x + 1, y + 6,
28, 28, 16, 16, 96, 96, 128, 128);
}
private void drawPage(GuiGraphics graphics, ItemStack page, int x, int y) { private void drawPage(GuiGraphics graphics, ItemStack page, int x, int y) {
graphics.blit(PAGE_ATLAS, x, y, 30, 40, 156, 0, 30, 40, 256, 256); graphics.blit(PAGE_ATLAS, x, y, 30, 40, 156, 0, 30, 40, 256, 256);
if (page.isEmpty()) return; if (page.isEmpty()) return;
@@ -143,6 +230,17 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
int column = x / 31; int column = x / 31;
int row = y / 41; int row = y / 41;
int index = (row + scrollRows) * 5 + column; int index = (row + scrollRows) * 5 + column;
if (allMode) {
List<SymbolDisplayMetadata> filtered = filteredCatalog();
if (x % 31 < 30 && y % 41 < 40 && index < filtered.size()) {
SymbolDisplayMetadata entry = filtered.get(index);
selectedSymbol = entry.id();
int serverIndex = menu.catalog().indexOf(entry);
if (serverIndex >= 0 && minecraft != null && minecraft.gameMode != null) {
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, 100 + serverIndex);
}
}
} else {
List<ItemStack> pages = pages(); List<ItemStack> pages = pages();
if (x % 31 < 30 && y % 41 < 40 && index < pages.size() if (x % 31 < 30 && y % 41 < 40 && index < pages.size()
&& !pages.get(index).isEmpty()) { && !pages.get(index).isEmpty()) {
@@ -151,6 +249,7 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, 16 + index); minecraft.gameMode.handleInventoryButtonClick(menu.containerId, 16 + index);
} }
} }
}
return true; return true;
} }
} }
@@ -158,6 +257,8 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
int top = topPos + 16 + i * 37; int top = topPos + 16 + i * 37;
if (mouseY >= top && mouseY < top + 32) { if (mouseY >= top && mouseY < top + 32) {
allMode = false;
selectedSymbol = null;
if (minecraft != null && minecraft.gameMode != null) { if (minecraft != null && minecraft.gameMode != null) {
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, i); minecraft.gameMode.handleInventoryButtonClick(menu.containerId, i);
} }
@@ -171,10 +272,23 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
@Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { @Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
if (mouseX >= leftPos + 48 && mouseX < leftPos + 203 if (mouseX >= leftPos + 48 && mouseX < leftPos + 203
&& mouseY >= topPos + 18 && mouseY < topPos + 141) { && mouseY >= topPos + 18 && mouseY < topPos + 141) {
int rows = (pages().size() + 4) / 5; int rows = ((allMode ? filteredCatalog().size() : pages().size()) + 4) / 5;
scrollRows = Math.clamp(scrollRows - (int) Math.signum(scrollY), 0, Math.max(0, rows - 3)); scrollRows = Math.clamp(scrollRows - (int) Math.signum(scrollY), 0, Math.max(0, rows - 3));
return true; return true;
} }
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
} }
@Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (search != null && search.isFocused()) {
if (keyCode == 256) {
setFocused(null);
search.setFocused(false);
} else {
search.keyPressed(keyCode, scanCode, modifiers);
}
return true;
}
return super.keyPressed(keyCode, scanCode, modifiers);
}
} }
@@ -244,14 +244,14 @@ public final class WritingDataGameTests {
@GameTest(template = "empty") @GameTest(template = "empty")
public static void linkPanelPreparationConsumesOnePageAndConfiguredInkOnlyOnSuccess(GameTestHelper helper) { public static void linkPanelPreparationConsumesOnePageAndConfiguredInkOnlyOnSuccess(GameTestHelper helper) {
var player = helper.makeMockServerPlayerInLevel(); var player = helper.makeMockServerPlayerInLevel();
ItemStack pages = new ItemStack(ModItems.PAGE.get(), 2); ItemStack pages = new ItemStack(Items.PAPER, 2);
ItemStack vial = new ItemStack(ModItems.INK_VIAL.get()); ItemStack vial = new ItemStack(ModItems.INK_VIAL.get());
player.setItemInHand(InteractionHand.MAIN_HAND, vial); player.setItemInHand(InteractionHand.MAIN_HAND, vial);
player.setItemInHand(InteractionHand.OFF_HAND, pages); player.setItemInHand(InteractionHand.OFF_HAND, pages);
helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND), helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
"A blank Page and sufficient ink must prepare one link panel"); "Paper and sufficient ink must prepare one link panel");
helper.assertValueEqual(pages.getCount(), 1, "Preparation must consume exactly one blank Page"); helper.assertValueEqual(pages.getCount(), 1, "Preparation must consume exactly one paper");
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()), helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()),
InkVialItem.CAPACITY - WritingInkService.linkPanelCost(), InkVialItem.CAPACITY - WritingInkService.linkPanelCost(),
"Preparation must consume exactly the server-configured ink cost"); "Preparation must consume exactly the server-configured ink cost");
@@ -274,14 +274,14 @@ public final class WritingDataGameTests {
ItemStack lowInk = new ItemStack(ModItems.INK_VIAL.get()); ItemStack lowInk = new ItemStack(ModItems.INK_VIAL.get());
lowInk.set(ModDataComponents.INK_REMAINING.get(), WritingInkService.linkPanelCost() - 1); lowInk.set(ModDataComponents.INK_REMAINING.get(), WritingInkService.linkPanelCost() - 1);
player.setItemInHand(InteractionHand.MAIN_HAND, lowInk); player.setItemInHand(InteractionHand.MAIN_HAND, lowInk);
player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(ModItems.PAGE.get())); player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(Items.PAPER));
helper.assertFalse(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND), helper.assertFalse(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
"Insufficient ink must reject link-panel preparation"); "Insufficient ink must reject link-panel preparation");
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()), helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()),
WritingInkService.linkPanelCost() - 1, WritingInkService.linkPanelCost() - 1,
"Insufficient ink must remain untouched"); "Insufficient ink must remain untouched");
helper.assertValueEqual(PageItem.content(player.getOffhandItem()).kind(), PageContent.Kind.BLANK, helper.assertTrue(player.getOffhandItem().is(Items.PAPER),
"Insufficient ink must leave the Page blank"); "Insufficient ink must leave paper untouched");
lowInk.set(ModDataComponents.INK_REMAINING.get(), WritingInkService.linkPanelCost()); lowInk.set(ModDataComponents.INK_REMAINING.get(), WritingInkService.linkPanelCost());
helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND), helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
@@ -290,7 +290,7 @@ public final class WritingDataGameTests {
"A depleted Ink Vial must return a reusable glass bottle"); "A depleted Ink Vial must return a reusable glass bottle");
helper.assertValueEqual(PageItem.content(player.getOffhandItem()).kind(), helper.assertValueEqual(PageItem.content(player.getOffhandItem()).kind(),
PageContent.Kind.LINK_PANEL, PageContent.Kind.LINK_PANEL,
"A single blank Page must be replaced by one Link Panel Page"); "A single paper must be replaced by one Link Panel Page");
helper.succeed(); helper.succeed();
} }
@@ -298,7 +298,7 @@ public final class WritingDataGameTests {
public static void stackedInkVialsSplitWithoutChangingTheOtherVials(GameTestHelper helper) { public static void stackedInkVialsSplitWithoutChangingTheOtherVials(GameTestHelper helper) {
var player = helper.makeMockServerPlayerInLevel(); var player = helper.makeMockServerPlayerInLevel();
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(ModItems.INK_VIAL.get(), 2)); player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(ModItems.INK_VIAL.get(), 2));
player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(ModItems.PAGE.get())); player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(Items.PAPER));
helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND), helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
"A stack of full vials must support preparing one panel"); "A stack of full vials must support preparing one panel");
@@ -9,7 +9,10 @@ import net.dnorberg.rivencraft.registry.ModBlocks;
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.NotebookContents; import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.writing.SymbolReference;
import net.dnorberg.rivencraft.writing.WritingDeskCatalog;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.gametest.framework.GameTest; import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.item.ItemEntity;
@@ -32,7 +35,7 @@ public final class WritingDeskGameTests {
var original = new WritingDeskBlockEntity(pos, ModBlocks.WRITING_DESK.get().defaultBlockState()); var original = new WritingDeskBlockEntity(pos, ModBlocks.WRITING_DESK.get().defaultBlockState());
original.setItem(0, new ItemStack(ModItems.NOTEBOOK.get())); original.setItem(0, new ItemStack(ModItems.NOTEBOOK.get()));
original.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get())); original.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
original.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(ModItems.PAGE.get(), 12)); original.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER, 12));
original.setActiveCollection(2); original.setActiveCollection(2);
var saved = original.saveWithoutMetadata(helper.getLevel().registryAccess()); var saved = original.saveWithoutMetadata(helper.getLevel().registryAccess());
var restored = new WritingDeskBlockEntity(pos, ModBlocks.WRITING_DESK.get().defaultBlockState()); var restored = new WritingDeskBlockEntity(pos, ModBlocks.WRITING_DESK.get().defaultBlockState());
@@ -52,12 +55,12 @@ public final class WritingDeskGameTests {
WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos); WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos);
desk.setItem(0, new ItemStack(ModItems.NOTEBOOK.get())); desk.setItem(0, new ItemStack(ModItems.NOTEBOOK.get()));
desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get())); desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(ModItems.PAGE.get(), 12)); desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER, 12));
helper.getLevel().setBlock(pos, Blocks.AIR.defaultBlockState(), 3); helper.getLevel().setBlock(pos, Blocks.AIR.defaultBlockState(), 3);
var drops = helper.getLevel().getEntitiesOfClass(ItemEntity.class, new AABB(pos).inflate(2)); var drops = helper.getLevel().getEntitiesOfClass(ItemEntity.class, new AABB(pos).inflate(2));
int notebooks = drops.stream().filter(e -> e.getItem().is(ModItems.NOTEBOOK.get())).mapToInt(e -> e.getItem().getCount()).sum(); int notebooks = drops.stream().filter(e -> e.getItem().is(ModItems.NOTEBOOK.get())).mapToInt(e -> e.getItem().getCount()).sum();
int ink = drops.stream().filter(e -> e.getItem().is(ModItems.INK_VIAL.get())).mapToInt(e -> e.getItem().getCount()).sum(); int ink = drops.stream().filter(e -> e.getItem().is(ModItems.INK_VIAL.get())).mapToInt(e -> e.getItem().getCount()).sum();
int pages = drops.stream().filter(e -> e.getItem().is(ModItems.PAGE.get())).mapToInt(e -> e.getItem().getCount()).sum(); int pages = drops.stream().filter(e -> e.getItem().is(Items.PAPER)).mapToInt(e -> e.getItem().getCount()).sum();
helper.assertValueEqual(notebooks, 1, "Notebook must drop exactly once"); helper.assertValueEqual(notebooks, 1, "Notebook must drop exactly once");
helper.assertValueEqual(ink, 1, "Ink must drop exactly once"); helper.assertValueEqual(ink, 1, "Ink must drop exactly once");
helper.assertValueEqual(pages, 12, "Pages must drop without loss"); helper.assertValueEqual(pages, 12, "Pages must drop without loss");
@@ -206,4 +209,60 @@ public final class WritingDeskGameTests {
"Shared desk must spend paper only once"); "Shared desk must spend paper only once");
helper.succeed(); helper.succeed();
} }
@GameTest(template = "empty")
public static void catalogAggregatesKnownSymbolsAcrossCollections(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);
ResourceLocation known = ResourceLocation.parse("rivencraft:terrain/standard");
ItemStack page = PageItem.createSymbolPage(new SymbolReference(known));
ItemStack notebook = new ItemStack(ModItems.NOTEBOOK.get());
notebook.set(ModDataComponents.NOTEBOOK_CONTENTS.get(), new NotebookContents(List.of(page.copy())));
desk.setItem(0, page);
desk.setItem(1, notebook);
helper.assertValueEqual(WritingDeskCatalog.available(desk).size(), 1,
"Duplicate symbols across loose pages and notebooks must appear once");
helper.assertValueEqual(WritingDeskCatalog.available(desk).getFirst().id(), known,
"Catalog must retain the authoritative symbol ID");
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, 101), "Out-of-range catalog index must be rejected");
helper.assertTrue(menu.clickMenuButton(player, 100), "Known catalog symbol must be selectable");
desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER));
desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
menu.broadcastChanges();
helper.assertTrue(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem().is(ModItems.PAGE.get()),
"Catalog selection must preview a copy of the known symbol");
desk.setItem(0, ItemStack.EMPTY);
desk.setItem(1, ItemStack.EMPTY);
menu.broadcastChanges();
helper.assertTrue(menu.catalog().isEmpty(), "Removed sources must invalidate catalog for the viewer");
helper.assertTrue(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem().isEmpty(),
"Removed sources must invalidate the copy output");
helper.succeed();
}
@GameTest(template = "empty")
public static void fullInventoryCannotConsumeCopyMaterials(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);
for (int i = 0; i < 36; i++) player.getInventory().setItem(i, new ItemStack(Items.COBBLESTONE, 64));
WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk);
helper.assertTrue(menu.quickMoveStack(player, WritingDeskMenu.COPY_RESULT).isEmpty(),
"Full inventory must reject shift-click copy");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 1,
"Rejected copy must preserve paper");
helper.assertValueEqual(desk.inkRemaining(), 0, "Rejected copy must preserve ink meter");
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.INK).is(ModItems.INK_VIAL.get()),
"Rejected copy must preserve ink vial");
helper.succeed();
}
} }
@@ -7,6 +7,14 @@ import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.registry.ModMenus; import net.dnorberg.rivencraft.registry.ModMenus;
import net.dnorberg.rivencraft.writing.NotebookContents; import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.SymbolCatalog;
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
import net.dnorberg.rivencraft.writing.SymbolReference;
import net.dnorberg.rivencraft.writing.WritingDeskCatalog;
import net.dnorberg.rivencraft.network.WritingDeskCatalogPayload;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.neoforged.neoforge.network.PacketDistributor;
import java.util.List; import java.util.List;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.world.Container; import net.minecraft.world.Container;
@@ -29,10 +37,14 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
private int syncedActiveCollection; private int syncedActiveCollection;
private int syncedInkRemaining; private int syncedInkRemaining;
private int selectedPage; private int selectedPage;
private ResourceLocation selectedSymbol;
private List<SymbolDisplayMetadata> catalog = List.of();
private final ServerPlayer serverViewer;
public WritingDeskMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) { public WritingDeskMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) {
this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()) this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos())
instanceof WritingDeskBlockEntity found ? found : null); instanceof WritingDeskBlockEntity found ? found : null);
catalog = WritingDeskCatalog.read(data);
} }
public WritingDeskMenu(int id, Inventory inventory, WritingDeskBlockEntity blockEntity) { public WritingDeskMenu(int id, Inventory inventory, WritingDeskBlockEntity blockEntity) {
@@ -40,6 +52,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
this.blockEntity = blockEntity; this.blockEntity = blockEntity;
this.desk = blockEntity == null ? new SimpleContainer(WritingDeskBlockEntity.SIZE) : blockEntity; this.desk = blockEntity == null ? new SimpleContainer(WritingDeskBlockEntity.SIZE) : blockEntity;
this.clientSide = inventory.player.level().isClientSide; this.clientSide = inventory.player.level().isClientSide;
this.serverViewer = inventory.player instanceof ServerPlayer serverPlayer ? serverPlayer : null;
if (blockEntity != null && !clientSide) catalog = WritingDeskCatalog.available(blockEntity);
addDataSlot(new DataSlot() { addDataSlot(new DataSlot() {
@Override public int get() { return WritingDeskMenu.this.blockEntity == null @Override public int get() { return WritingDeskMenu.this.blockEntity == null
? syncedActiveCollection : WritingDeskMenu.this.blockEntity.activeCollection(); } ? syncedActiveCollection : WritingDeskMenu.this.blockEntity.activeCollection(); }
@@ -70,14 +84,28 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
public int activeCollection() { return syncedActiveCollection; } public int activeCollection() { return syncedActiveCollection; }
public int inkRemaining() { return syncedInkRemaining; } public int inkRemaining() { return syncedInkRemaining; }
public List<SymbolDisplayMetadata> catalog() { return catalog; }
public void setCatalog(List<SymbolDisplayMetadata> symbols) { catalog = List.copyOf(symbols); }
@Override public boolean clickMenuButton(Player player, int id) { @Override public boolean clickMenuButton(Player player, int id) {
if (blockEntity == null || !stillValid(player)) return false; if (blockEntity == null || !stillValid(player)) return false;
if (id >= 0 && id < WritingDeskBlockEntity.COLLECTIONS) { if (id >= 0 && id < WritingDeskBlockEntity.COLLECTIONS) {
blockEntity.setActiveCollection(id); blockEntity.setActiveCollection(id);
selectedPage = 0; selectedPage = 0;
selectedSymbol = null;
} else if (id >= 16 && id < 16 + NotebookContents.MAX_PAGES) { } else if (id >= 16 && id < 16 + NotebookContents.MAX_PAGES) {
selectedPage = id - 16; selectedPage = id - 16;
selectedSymbol = null;
} else if (id >= 100 && id < 100 + catalog.size()) {
if (!catalog.equals(WritingDeskCatalog.available(blockEntity))) {
refreshCatalog();
return false;
}
ResourceLocation requested = catalog.get(id - 100).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;
@@ -118,8 +146,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
private boolean moveIntoDesk(ItemStack moving) { private boolean moveIntoDesk(ItemStack moving) {
if (moving.is(ModItems.INK_VIAL.get())) return moveItemStackTo(moving, 4, 5, false); if (moving.is(ModItems.INK_VIAL.get())) return moveItemStackTo(moving, 4, 5, false);
if (moving.is(ModItems.NOTEBOOK.get())) return moveItemStackTo(moving, 0, 4, false); if (moving.is(ModItems.NOTEBOOK.get())) return moveItemStackTo(moving, 0, 4, false);
if (moving.is(Items.PAPER) || moving.is(ModItems.PAGE.get()) if (moving.is(Items.PAPER))
&& PageItem.content(moving).kind() == PageContent.Kind.BLANK)
return moveItemStackTo(moving, WritingDeskBlockEntity.PAPER, return moveItemStackTo(moving, WritingDeskBlockEntity.PAPER,
WritingDeskBlockEntity.PAPER + 1, false); WritingDeskBlockEntity.PAPER + 1, false);
if (moving.is(ModItems.PAGE.get())) return moveItemStackTo(moving, 0, 4, false); if (moving.is(ModItems.PAGE.get())) return moveItemStackTo(moving, 0, 4, false);
@@ -127,6 +154,14 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
} }
private ItemStack selectedSource() { private ItemStack selectedSource() {
if (selectedSymbol != null) {
SymbolReference reference = new SymbolReference(selectedSymbol);
return SymbolCatalog.isKnown(reference)
&& blockEntity != null
&& WritingDeskCatalog.available(blockEntity).stream()
.anyMatch(entry -> entry.id().equals(selectedSymbol))
? PageItem.createSymbolPage(reference) : ItemStack.EMPTY;
}
ItemStack collection = desk.getItem(blockEntity == null ? syncedActiveCollection : blockEntity.activeCollection()); ItemStack collection = desk.getItem(blockEntity == null ? syncedActiveCollection : blockEntity.activeCollection());
if (collection.is(ModItems.PAGE.get())) return selectedPage == 0 ? collection : ItemStack.EMPTY; if (collection.is(ModItems.PAGE.get())) return selectedPage == 0 ? collection : ItemStack.EMPTY;
if (collection.is(ModItems.NOTEBOOK.get())) { if (collection.is(ModItems.NOTEBOOK.get())) {
@@ -150,7 +185,19 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
if (!ItemStack.matches(result.getItem(0), preview)) result.setItem(0, preview); if (!ItemStack.matches(result.getItem(0), preview)) result.setItem(0, preview);
} }
private void refreshCatalog() {
if (clientSide || blockEntity == null) return;
List<SymbolDisplayMetadata> current = WritingDeskCatalog.available(blockEntity);
if (!current.equals(catalog)) {
catalog = current;
if (serverViewer != null && serverViewer.containerMenu == this) {
PacketDistributor.sendToPlayer(serverViewer, new WritingDeskCatalogPayload(containerId, catalog));
}
}
}
@Override public void broadcastChanges() { @Override public void broadcastChanges() {
refreshCatalog();
refreshOutput(); refreshOutput();
super.broadcastChanges(); super.broadcastChanges();
} }
@@ -161,6 +208,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
player.closeContainer(); player.closeContainer();
return; return;
} }
refreshCatalog();
refreshOutput(); refreshOutput();
super.clicked(slotId, button, clickType, player); super.clicked(slotId, button, clickType, player);
} }
@@ -9,6 +9,7 @@ import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
import net.dnorberg.rivencraft.client.ClientLinkPreviewHandler; import net.dnorberg.rivencraft.client.ClientLinkPreviewHandler;
import net.dnorberg.rivencraft.client.ClientBookInteractionHandler; import net.dnorberg.rivencraft.client.ClientBookInteractionHandler;
import net.dnorberg.rivencraft.client.ClientAgeCelestialAngles; import net.dnorberg.rivencraft.client.ClientAgeCelestialAngles;
import net.dnorberg.rivencraft.client.ClientWritingDeskCatalog;
import net.dnorberg.rivencraft.event.AgeCelestialAngleSync; import net.dnorberg.rivencraft.event.AgeCelestialAngleSync;
import net.dnorberg.rivencraft.item.DescriptiveBookItem; import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.item.LinkingBookItem;
@@ -68,6 +69,8 @@ public final class ModNetworking {
ClientLinkPreviewHandler::handle); ClientLinkPreviewHandler::handle);
registrar.playToClient(AgeCelestialAnglePayload.TYPE, AgeCelestialAnglePayload.STREAM_CODEC, registrar.playToClient(AgeCelestialAnglePayload.TYPE, AgeCelestialAnglePayload.STREAM_CODEC,
ClientAgeCelestialAngles::handle); ClientAgeCelestialAngles::handle);
registrar.playToClient(WritingDeskCatalogPayload.TYPE, WritingDeskCatalogPayload.STREAM_CODEC,
ClientWritingDeskCatalog::handle);
} }
private static void requestDescriptiveBookScreen(RequestDescriptiveBookScreenPayload payload, IPayloadContext context) { private static void requestDescriptiveBookScreen(RequestDescriptiveBookScreenPayload payload, IPayloadContext context) {
@@ -0,0 +1,26 @@
package net.dnorberg.rivencraft.network;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
import net.dnorberg.rivencraft.writing.WritingDeskCatalog;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
/** Current server-approved symbols from this open Writing Desk menu. */
public record WritingDeskCatalogPayload(int containerId, List<SymbolDisplayMetadata> symbols)
implements CustomPacketPayload {
public static final Type<WritingDeskCatalogPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "writing_desk_catalog"));
public static final StreamCodec<RegistryFriendlyByteBuf, WritingDeskCatalogPayload> STREAM_CODEC =
StreamCodec.of(
(buffer, payload) -> {
buffer.writeVarInt(payload.containerId());
WritingDeskCatalog.write(buffer, payload.symbols());
},
buffer -> new WritingDeskCatalogPayload(buffer.readVarInt(), WritingDeskCatalog.read(buffer)));
@Override public Type<? extends CustomPacketPayload> type() { return TYPE; }
}
@@ -31,7 +31,6 @@ 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());
}) })
.build()); .build());
@@ -0,0 +1,63 @@
package net.dnorberg.rivencraft.writing;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
/** A bounded, server-derived view of symbols physically present in the desk. */
public final class WritingDeskCatalog {
private static final int MAX_ENTRIES = WritingDeskBlockEntity.COLLECTIONS * NotebookContents.MAX_PAGES;
private WritingDeskCatalog() {}
public static List<SymbolDisplayMetadata> available(WritingDeskBlockEntity desk) {
Map<ResourceLocation, SymbolDisplayMetadata> unique = new TreeMap<>();
for (int slot = 0; slot < WritingDeskBlockEntity.COLLECTIONS; slot++) {
ItemStack collection = desk.getItem(slot);
if (collection.is(ModItems.PAGE.get())) add(unique, collection);
if (collection.is(ModItems.NOTEBOOK.get())) {
List<ItemStack> pages = collection.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
pages.forEach(page -> add(unique, page));
}
}
return unique.values().stream()
.sorted(Comparator.comparing((SymbolDisplayMetadata symbol) -> symbol.category().toString())
.thenComparing(symbol -> symbol.id().toString()))
.toList();
}
private static void add(Map<ResourceLocation, SymbolDisplayMetadata> unique, ItemStack page) {
if (!page.is(ModItems.PAGE.get())) return;
PageContent content = PageItem.content(page).resolve();
if (content.kind() != PageContent.Kind.SYMBOL) return;
SymbolReference reference = content.symbol().orElseThrow();
SymbolCatalog.find(reference).ifPresent(definition ->
unique.put(reference.id(), definition.display(reference.id())));
}
public static void write(RegistryFriendlyByteBuf buffer, List<SymbolDisplayMetadata> entries) {
if (entries.size() > MAX_ENTRIES) throw new IllegalArgumentException("Desk symbol catalog exceeds limit");
buffer.writeVarInt(entries.size());
entries.forEach(entry -> SymbolDisplayMetadata.STREAM_CODEC.encode(buffer, entry));
}
public static List<SymbolDisplayMetadata> read(RegistryFriendlyByteBuf buffer) {
int count = buffer.readVarInt();
if (count < 0 || count > MAX_ENTRIES) throw new IllegalArgumentException("Invalid desk symbol count " + count);
List<SymbolDisplayMetadata> entries = new ArrayList<>(count);
for (int index = 0; index < count; index++) {
entries.add(SymbolDisplayMetadata.STREAM_CODEC.decode(buffer));
}
return List.copyOf(entries);
}
}
@@ -29,7 +29,7 @@ public final class WritingInkService {
int remaining = InkVialItem.remaining(vial); int remaining = InkVialItem.remaining(vial);
return vial.is(ModItems.INK_VIAL.get()) && remaining >= linkPanelCost() return vial.is(ModItems.INK_VIAL.get()) && remaining >= linkPanelCost()
&& remaining <= InkVialItem.CAPACITY && remaining <= InkVialItem.CAPACITY
&& page.is(ModItems.PAGE.get()) && PageItem.content(page).kind() == PageContent.Kind.BLANK; && page.is(Items.PAPER);
} }
public static boolean prepareLinkPanel(Player player, InteractionHand vialHand) { public static boolean prepareLinkPanel(Player player, InteractionHand vialHand) {
@@ -19,8 +19,10 @@
"block.rivencraft.writing_desk": "Writing Desk", "block.rivencraft.writing_desk": "Writing Desk",
"container.rivencraft.writing_desk": "Writing Desk", "container.rivencraft.writing_desk": "Writing Desk",
"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.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 a blank Page 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.",
"screen.rivencraft.notebook.search": "Search pages", "screen.rivencraft.notebook.search": "Search pages",
"screen.rivencraft.notebook.move_previous": "Move selected page left", "screen.rivencraft.notebook.move_previous": "Move selected page left",
"screen.rivencraft.notebook.move_next": "Move selected page right", "screen.rivencraft.notebook.move_next": "Move selected page right",
@@ -1,8 +1,69 @@
{ {
"parent": "minecraft:block/cube_bottom_top", "parent": "minecraft:block/block",
"ambientocclusion": false,
"textures": { "textures": {
"top": "rivencraft:item/writing_desk", "particle": "rivencraft:block/writing_desk",
"side": "minecraft:block/oak_planks", "desk": "rivencraft:block/writing_desk"
"bottom": "minecraft:block/oak_planks" },
"elements": [
{
"from": [0, 8, 0], "to": [16, 9, 16],
"faces": {
"up": {"uv": [1, 0, 3, 2], "texture": "#desk"},
"down": {"uv": [1, 2, 3, 4], "texture": "#desk"},
"north": {"uv": [1, 0, 3, 0.125], "texture": "#desk"},
"south": {"uv": [1, 0, 3, 0.125], "texture": "#desk"},
"east": {"uv": [1, 0, 3, 0.125], "texture": "#desk"},
"west": {"uv": [1, 0, 3, 0.125], "texture": "#desk"}
} }
},
{
"from": [0, 0, 1], "to": [2, 8, 15],
"faces": {
"north": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"},
"south": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"},
"east": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"},
"west": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"}
}
},
{
"from": [14, 0, 1], "to": [16, 8, 15],
"faces": {
"north": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"},
"south": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"},
"east": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"},
"west": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"}
}
},
{
"from": [1, 3, 2], "to": [15, 4, 14],
"faces": {
"up": {"uv": [0, 4.25, 3.75, 6.125], "texture": "#desk"},
"down": {"uv": [0, 4.25, 3.75, 6.125], "texture": "#desk"},
"north": {"uv": [0, 4.25, 3.75, 4.5], "texture": "#desk"},
"south": {"uv": [0, 4.25, 3.75, 4.5], "texture": "#desk"},
"east": {"uv": [0, 4.25, 3.75, 4.5], "texture": "#desk"},
"west": {"uv": [0, 4.25, 3.75, 4.5], "texture": "#desk"}
}
},
{
"from": [1, 9, 14], "to": [15, 15, 16],
"faces": {
"north": {"uv": [8, 0, 11.75, 2], "texture": "#desk"},
"south": {"uv": [8, 0, 11.75, 2], "texture": "#desk"},
"east": {"uv": [8, 0, 8.25, 2], "texture": "#desk"},
"west": {"uv": [8, 0, 8.25, 2], "texture": "#desk"},
"up": {"uv": [8, 0, 11.75, 0.25], "texture": "#desk"}
}
},
{
"from": [1, 12, 9], "to": [15, 13, 15],
"faces": {
"up": {"uv": [8, 3.625, 11.75, 4.375], "texture": "#desk"},
"down": {"uv": [8, 3.625, 11.75, 4.375], "texture": "#desk"},
"north": {"uv": [8, 3.625, 11.75, 3.75], "texture": "#desk"},
"south": {"uv": [8, 3.625, 11.75, 3.75], "texture": "#desk"}
}
}
]
} }
@@ -1,4 +1,3 @@
{ {
"parent": "minecraft:item/generated", "parent": "rivencraft:block/writing_desk"
"textures": { "layer0": "rivencraft:item/writing_desk" }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

@@ -1,13 +0,0 @@
{
"type": "minecraft:crafting_shapeless",
"category": "misc",
"ingredients": [
{
"item": "minecraft:paper"
}
],
"result": {
"count": 1,
"id": "rivencraft:page"
}
}