Compare commits

..
4 Commits
Author SHA1 Message Date
dnorberg 5066e71897 Match Legacy notebook sparse portrait pages
Build / build (push) Canceled after 0s
2026-09-24 05:02:58 -04:00
dnorberg 02901c9cbc Remove notebook rename flow and add Pages creative tab
Build / build (push) Canceled after 0s
2026-09-24 04:46:51 -04:00
dnorberg d6fd32994f Add writing ink and baseline link panel preparation
Build / build (push) Canceled after 0s
2026-09-24 04:38:05 -04:00
dnorberg 143da0a940 Keep notebook rename keys out of inventory shortcuts
Build / build (push) Canceled after 0s
2026-09-24 04:27:51 -04:00
22 changed files with 434 additions and 156 deletions

No files matched your search

+4
View File
@@ -6,6 +6,10 @@ It is not an official release of Mystcraft.
Mystcraft Legacy source: https://github.com/Mystcraft/Mystcraft-Legacy Mystcraft Legacy source: https://github.com/Mystcraft/Mystcraft-Legacy
Mystcraft Legacy copyright: XCompWiz and contributors Mystcraft Legacy copyright: XCompWiz and contributors
Mystcraft Legacy license: GNU Lesser General Public License version 3 Mystcraft Legacy license: GNU Lesser General Public License version 3
The animated ink-vial item texture in this project is copied from Mystcraft
Legacy's assets/mystcraft/textures/items/ink_vial.png under that license.
The Notebook page atlas is copied from Legacy's assets/mystcraft/gui/bookui_pagel.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
+7 -2
View File
@@ -13,8 +13,13 @@ 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; written-page creation will become available legacy runtime-composition rules. Craft an Ink Vial from two black dye, a glass
through the Writing Desk work that follows. bottle, and a water bucket; the bucket is returned. Hold a blank Page in one
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.
The spent vial returns a glass bottle. Server owners can adjust the symbol-copy
and link-panel costs in the `writing` section of `rivencraft-server.toml`.
Symbol-page transcription will become available through the Writing Desk.
## Linking Book ## Linking Book
+1
View File
@@ -7,6 +7,7 @@ project's GNU LGPL v3 license. Rivencraft itself uses the same license.
| Rivencraft asset | Upstream asset | Upstream revision | Changes | | Rivencraft asset | Upstream asset | Upstream revision | Changes |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `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/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 |
+24
View File
@@ -64,3 +64,27 @@ identifier. Built-in pages reproduce Mystcraft Legacy's four-word poem
composition from its page background and symbol-component sheet. Data-pack composition from its page background and symbol-component sheet. Data-pack
symbols without a bundled resource-pack model use the legacy question-mark symbols without a bundled resource-pack model use the legacy question-mark
fallback while keeping their real identifier and display metadata. fallback while keeping their real identifier and display metadata.
The Notebook surface displays pages by their persistent slot index instead of
packing occupied pages together. Empty positions through the last placed page
appear darkened. While carrying a Page, all available positions are shown for
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
square item models remain unchanged in inventories.
## Ink and baseline link panels
An Ink Vial is crafted from two black dye, a glass bottle, and a water bucket.
The bucket remains after crafting. Each vial holds 16 ink units, stored in a
persistent, network-synchronized component; only vials with identical remaining
ink stack together. When depleted, a vial returns a glass bottle that can be
used to craft another. The animated item texture comes from Mystcraft Legacy.
The server config sets ink costs independently for symbol copying (default one
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
server checks the held items, blank state, and sufficient ink before consuming
anything. A stack of blank Pages yields exactly one Link Panel Page per use;
if the inventory is full, the result is dropped instead of deleted. Symbol
copying will be implemented by the Writing Desk. Link-property modifiers are
deferred beyond this baseline flow.
+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.3.3 mod_version=0.3.4.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.
@@ -1,21 +1,19 @@
package net.dnorberg.rivencraft.client.screen; package net.dnorberg.rivencraft.client.screen;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.menu.NotebookMenu; import net.dnorberg.rivencraft.menu.NotebookMenu;
import net.dnorberg.rivencraft.network.RenameNotebookPagePayload;
import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Inventory;
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.neoforged.neoforge.network.PacketDistributor;
/** A page surface modelled on Legacy's folder GUI, backed by the Notebook menu. */ /** A page surface modelled on Legacy's folder GUI, backed by the Notebook menu. */
public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu> { public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu> {
@@ -27,8 +25,16 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
private static final int PAGE_STEP_X = 31; private static final int PAGE_STEP_X = 31;
private static final int PAGE_STEP_Y = 41; private static final int PAGE_STEP_Y = 41;
private static final int PAGE_COLUMNS = 5; private static final int PAGE_COLUMNS = 5;
private static final ResourceLocation PAGE_ATLAS = texture("gui/notebook_pages");
private static final ResourceLocation GENERIC_SYMBOL = texture("item/page_symbol");
private static final ResourceLocation UNRESOLVED_SYMBOL = texture("item/page_unresolved");
private static final ResourceLocation STANDARD_TERRAIN = texture("item/page_symbol_terrain_standard");
private static final ResourceLocation NATIVE_BIOMES = texture("item/page_symbol_biome_native");
private static final ResourceLocation NORMAL_LIGHTING = texture("item/page_symbol_lighting_normal");
private static final ResourceLocation NORMAL_WEATHER = texture("item/page_symbol_weather_normal");
private static final ResourceLocation NORMAL_TIME = texture("item/page_symbol_celestial_normal_time");
private static final ResourceLocation CAVES = texture("item/page_symbol_feature_caves");
private EditBox textInput; private EditBox textInput;
private boolean renaming;
private boolean draggingScrollbar; private boolean draggingScrollbar;
private boolean surfaceClickPendingRelease; private boolean surfaceClickPendingRelease;
private int selectedSlot = -1; private int selectedSlot = -1;
@@ -52,42 +58,36 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
allButton.active = false; allButton.active = false;
addRenderableWidget(sortButton); addRenderableWidget(sortButton);
addRenderableWidget(allButton); addRenderableWidget(allButton);
textInput = new EditBox(font, leftPos + 48, topPos, 86, 18, textInput = new EditBox(font, leftPos + 48, topPos, 128, 18,
Component.translatable("screen.rivencraft.notebook.search")); Component.translatable("screen.rivencraft.notebook.search"));
textInput.setMaxLength(32); textInput.setMaxLength(32);
textInput.setHint(Component.translatable("screen.rivencraft.notebook.search")); textInput.setHint(Component.translatable("screen.rivencraft.notebook.search"));
textInput.setResponder(value -> { textInput.setResponder(value -> scroll = 0);
if (!renaming) scroll = 0;
});
addRenderableWidget(textInput); addRenderableWidget(textInput);
addRenderableWidget(Button.builder(Component.literal("R"), button -> toggleRename())
.bounds(leftPos + 136, topPos, 18, 18)
.tooltip(Tooltip.create(Component.translatable("screen.rivencraft.notebook.rename_toggle")))
.build());
} }
@Override @Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
super.render(graphics, mouseX, mouseY, partialTick); super.render(graphics, mouseX, mouseY, partialTick);
List<Integer> pages = visiblePages(); int pageSlots = displayedPageSlots();
scroll = Math.min(scroll, maxScroll(pages.size())); scroll = Math.min(scroll, maxScroll(pageSlots));
graphics.enableScissor(leftPos, topPos + SURFACE_TOP, leftPos + SURFACE_RIGHT, graphics.enableScissor(leftPos, topPos + SURFACE_TOP, leftPos + SURFACE_RIGHT,
topPos + SURFACE_BOTTOM); topPos + SURFACE_BOTTOM);
for (int position = 0; position < pages.size(); position++) { for (int index = 0; index < pageSlots; index++) {
int x = leftPos + (position % PAGE_COLUMNS) * PAGE_STEP_X; int x = leftPos + (index % PAGE_COLUMNS) * PAGE_STEP_X;
int y = topPos + SURFACE_TOP + (position / PAGE_COLUMNS) * PAGE_STEP_Y - scroll; int y = topPos + SURFACE_TOP + (index / PAGE_COLUMNS) * PAGE_STEP_Y - scroll;
if (y + PAGE_HEIGHT > topPos + SURFACE_TOP && y < topPos + SURFACE_BOTTOM) { if (y + PAGE_HEIGHT > topPos + SURFACE_TOP && y < topPos + SURFACE_BOTTOM) {
drawPage(graphics, pages.get(position), x, y); drawPage(graphics, index, x, y);
} }
} }
graphics.disableScissor(); graphics.disableScissor();
drawScrollbar(graphics, pages.size()); drawScrollbar(graphics, pageSlots);
int hovered = pageAt(mouseX, mouseY, pages); int hovered = pageAt(mouseX, mouseY);
if (!menu.getCarried().isEmpty() && inSurface(mouseX, mouseY)) { if (!menu.getCarried().isEmpty() && inSurface(mouseX, mouseY)) {
graphics.renderItem(menu.getCarried(), mouseX - 8, mouseY - 8); graphics.renderItem(menu.getCarried(), mouseX - 8, mouseY - 8);
} else if (hovered >= 0) { } else if (hovered >= 0 && !menu.getSlot(hovered).getItem().isEmpty()) {
ItemStack page = menu.getSlot(hovered).getItem(); ItemStack page = menu.getSlot(hovered).getItem();
if (!page.isEmpty() && matchesSearch(page)) graphics.renderTooltip(font, page, mouseX, mouseY); if (matchesSearch(page)) graphics.renderTooltip(font, page, mouseX, mouseY);
} else { } else {
renderTooltip(graphics, mouseX, mouseY); renderTooltip(graphics, mouseX, mouseY);
} }
@@ -113,14 +113,16 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
public boolean mouseClicked(double mouseX, double mouseY, int button) { public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (inScrollbar(mouseX, mouseY) && button == 0) { if (inScrollbar(mouseX, mouseY) && button == 0) {
draggingScrollbar = true; draggingScrollbar = true;
setScrollFromMouse(mouseY, visiblePages().size()); setScrollFromMouse(mouseY, displayedPageSlots());
return true; return true;
} }
int index = pageAt(mouseX, mouseY, visiblePages()); int index = pageAt(mouseX, mouseY);
if (index >= 0 && (button == 0 || button == 1)) { if (index >= 0 && (button == 0 || button == 1)) {
ItemStack page = menu.getSlot(index).getItem(); ItemStack page = menu.getSlot(index).getItem();
if (!page.isEmpty() && !matchesSearch(page)) return true; if ((!page.isEmpty() && !matchesSearch(page))
if (renaming) finishRename(); || (page.isEmpty() && !menu.getCarried().is(ModItems.PAGE.get()))) {
return true;
}
setFocused(null); setFocused(null);
textInput.setFocused(false); textInput.setFocused(false);
selectedSlot = index; selectedSlot = index;
@@ -134,7 +136,7 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
@Override @Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) {
if (draggingScrollbar && button == 0) { if (draggingScrollbar && button == 0) {
setScrollFromMouse(mouseY, visiblePages().size()); setScrollFromMouse(mouseY, displayedPageSlots());
return true; return true;
} }
return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); return super.mouseDragged(mouseX, mouseY, button, dragX, dragY);
@@ -155,7 +157,7 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
if (inSurface(mouseX, mouseY) || inScrollbar(mouseX, mouseY)) { if (inSurface(mouseX, mouseY) || inScrollbar(mouseX, mouseY)) {
scroll = Math.clamp(scroll - (int)Math.signum(scrollY) * 20, scroll = Math.clamp(scroll - (int)Math.signum(scrollY) * 20,
0, maxScroll(visiblePages().size())); 0, maxScroll(displayedPageSlots()));
return true; return true;
} }
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
@@ -163,12 +165,15 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
@Override @Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) { public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (renaming && textInput.isFocused() && (keyCode == 257 || keyCode == 335)) { if (textInput.isFocused()) {
finishRename(); if (keyCode == 256) {
return true; setFocused(null);
} textInput.setFocused(false);
if (!textInput.isFocused() && keyCode == 82 && selectedSlot >= 0) { } else {
toggleRename(); textInput.keyPressed(keyCode, scanCode, modifiers);
}
// EditBox does not consume ordinary letter key presses; the matching charTyped
// event handles text input. Do not pass them to the container's inventory keys.
return true; return true;
} }
if (!textInput.isFocused() && selectedSlot >= 0 && (keyCode == 263 || keyCode == 262)) { if (!textInput.isFocused() && selectedSlot >= 0 && (keyCode == 263 || keyCode == 262)) {
@@ -178,44 +183,33 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
return super.keyPressed(keyCode, scanCode, modifiers); return super.keyPressed(keyCode, scanCode, modifiers);
} }
@Override private int displayedPageSlots() {
public void onClose() { if (menu.getCarried().is(ModItems.PAGE.get())) {
commitRename(); return NotebookMenu.NOTEBOOK_SLOTS;
super.onClose(); }
} for (int index = NotebookMenu.NOTEBOOK_SLOTS - 1; index >= 0; index--) {
if (!menu.getSlot(index).getItem().isEmpty()) {
private List<Integer> visiblePages() { return index + 1;
List<Integer> pages = new ArrayList<>();
int firstEmpty = -1;
for (int index = 0; index < NotebookMenu.NOTEBOOK_SLOTS; index++) {
ItemStack stack = menu.getSlot(index).getItem();
if (stack.isEmpty()) {
if (firstEmpty < 0) firstEmpty = index;
} else {
pages.add(index);
} }
} }
if (firstEmpty >= 0 && menu.getCarried().is(ModItems.PAGE.get())) { return 0;
pages.add(firstEmpty);
}
return pages;
} }
private boolean matchesSearch(ItemStack page) { private boolean matchesSearch(ItemStack page) {
return renaming || textInput.getValue().isBlank() return textInput.getValue().isBlank()
|| page.getHoverName().getString().toLowerCase(Locale.ROOT) || page.getHoverName().getString().toLowerCase(Locale.ROOT)
.contains(textInput.getValue().toLowerCase(Locale.ROOT)); .contains(textInput.getValue().toLowerCase(Locale.ROOT));
} }
private int pageAt(double mouseX, double mouseY, List<Integer> pages) { private int pageAt(double mouseX, double mouseY) {
if (!inSurface(mouseX, mouseY)) return -1; if (!inSurface(mouseX, mouseY)) return -1;
int localX = (int)mouseX - leftPos; int localX = (int)mouseX - leftPos;
int localY = (int)mouseY - topPos - SURFACE_TOP + scroll; int localY = (int)mouseY - topPos - SURFACE_TOP + scroll;
int column = localX / PAGE_STEP_X; int column = localX / PAGE_STEP_X;
int position = (localY / PAGE_STEP_Y) * PAGE_COLUMNS + column; int index = (localY / PAGE_STEP_Y) * PAGE_COLUMNS + column;
return column < PAGE_COLUMNS && localX % PAGE_STEP_X < PAGE_WIDTH return column < PAGE_COLUMNS && localX % PAGE_STEP_X < PAGE_WIDTH
&& localY % PAGE_STEP_Y < PAGE_HEIGHT && position < pages.size() && localY % PAGE_STEP_Y < PAGE_HEIGHT && index < displayedPageSlots()
? pages.get(position) : -1; ? index : -1;
} }
private boolean inSurface(double mouseX, double mouseY) { private boolean inSurface(double mouseX, double mouseY) {
@@ -257,46 +251,44 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
private void drawPage(GuiGraphics graphics, int index, int x, int y) { private void drawPage(GuiGraphics graphics, int index, int x, int y) {
ItemStack page = menu.getSlot(index).getItem(); ItemStack page = menu.getSlot(index).getItem();
boolean visible = !page.isEmpty() && matchesSearch(page); boolean dimmed = page.isEmpty() || !matchesSearch(page);
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, // Legacy's Notebook draws the atlas' native 30x40 page, not a stretched item icon.
visible ? 0xFFE5D4A7 : 0x704B4945); graphics.blit(PAGE_ATLAS, x, y, PAGE_WIDTH, PAGE_HEIGHT,
graphics.fill(x + 2, y + 2, x + PAGE_WIDTH - 2, y + PAGE_HEIGHT - 2, 156, 0, PAGE_WIDTH, PAGE_HEIGHT, 256, 256);
visible ? 0xFFF4E5C3 : 0x603B3934); if (!page.isEmpty()) {
graphics.renderOutline(x, y, PAGE_WIDTH, PAGE_HEIGHT, switch (PageItem.content(page).kind()) {
index == selectedSlot ? 0xFFFFD061 : 0xFF564A34); case BLANK -> { }
if (visible) { case LINK_PANEL -> graphics.fill(x + 5, y + 6, x + 25, y + 20, 0xFF000000);
graphics.pose().pushPose(); case SYMBOL, UNRESOLVED_SYMBOL -> graphics.blit(symbolTexture(page), x + 1, y + 6,
graphics.pose().translate(x + 3, y + 8, 0); 28, 28, 16, 16, 96, 96, 128, 128);
graphics.pose().scale(1.5F, 1.5F, 1.0F); }
graphics.renderItem(page, 0, 0); }
graphics.pose().popPose(); if (dimmed) {
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0xB8000000);
}
if (index == selectedSlot && !page.isEmpty()) {
graphics.renderOutline(x, y, PAGE_WIDTH, PAGE_HEIGHT, 0xFFFFD061);
} }
} }
private void toggleRename() { private static ResourceLocation symbolTexture(ItemStack page) {
if (renaming) { var content = PageItem.content(page);
finishRename(); if (content.kind() == net.dnorberg.rivencraft.writing.PageContent.Kind.UNRESOLVED_SYMBOL) {
} else if (selectedSlot >= 0 && !menu.getSlot(selectedSlot).getItem().isEmpty()) { return UNRESOLVED_SYMBOL;
renaming = true;
textInput.setValue(menu.getSlot(selectedSlot).getItem().getHoverName().getString());
textInput.setHint(Component.translatable("screen.rivencraft.notebook.rename"));
setFocused(textInput);
textInput.setFocused(true);
} }
return switch (content.symbol().orElseThrow().id().toString()) {
case "rivencraft:terrain/standard" -> STANDARD_TERRAIN;
case "rivencraft:biome/native" -> NATIVE_BIOMES;
case "rivencraft:lighting/normal" -> NORMAL_LIGHTING;
case "rivencraft:weather/normal" -> NORMAL_WEATHER;
case "rivencraft:celestial/normal_time" -> NORMAL_TIME;
case "rivencraft:feature/caves" -> CAVES;
default -> GENERIC_SYMBOL;
};
} }
private void finishRename() { private static ResourceLocation texture(String path) {
commitRename(); return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/" + path + ".png");
renaming = false;
textInput.setValue("");
textInput.setHint(Component.translatable("screen.rivencraft.notebook.search"));
}
private void commitRename() {
if (renaming && selectedSlot >= 0 && !menu.getSlot(selectedSlot).getItem().isEmpty()) {
PacketDistributor.sendToServer(new RenameNotebookPagePayload(
menu.containerId, selectedSlot, textInput.getValue()));
}
} }
private void moveSelected(boolean next) { private void moveSelected(boolean next) {
@@ -15,6 +15,8 @@ public final class RivencraftConfig {
public static final ModConfigSpec SPEC; public static final ModConfigSpec SPEC;
private static final ModConfigSpec.EnumValue<ReturnLinkMode> RETURN_LINK_MODE; private static final ModConfigSpec.EnumValue<ReturnLinkMode> RETURN_LINK_MODE;
private static final ModConfigSpec.IntValue SYMBOL_INK_COST;
private static final ModConfigSpec.IntValue LINK_PANEL_INK_COST;
static { static {
ModConfigSpec.Builder builder = new ModConfigSpec.Builder(); ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
@@ -25,6 +27,12 @@ public final class RivencraftConfig {
) )
.defineEnum("return_link_mode", ReturnLinkMode.ON_CREATION); .defineEnum("return_link_mode", ReturnLinkMode.ON_CREATION);
builder.pop(); builder.pop();
builder.push("writing");
SYMBOL_INK_COST = builder.comment("Ink units consumed when the Writing Desk copies one symbol page.")
.defineInRange("symbol_ink_cost", 1, 1, 16);
LINK_PANEL_INK_COST = builder.comment("Ink units consumed when preparing one baseline link-panel page.")
.defineInRange("link_panel_ink_cost", 2, 1, 16);
builder.pop();
SPEC = builder.build(); SPEC = builder.build();
} }
@@ -34,4 +42,12 @@ public final class RivencraftConfig {
public static ReturnLinkMode returnLinkMode() { public static ReturnLinkMode returnLinkMode() {
return RETURN_LINK_MODE.get(); return RETURN_LINK_MODE.get();
} }
public static int symbolInkCost() {
return SYMBOL_INK_COST.get();
}
public static int linkPanelInkCost() {
return LINK_PANEL_INK_COST.get();
}
} }
@@ -11,6 +11,8 @@ import net.dnorberg.rivencraft.writing.SymbolReference;
import net.dnorberg.rivencraft.writing.WritingEntry; import net.dnorberg.rivencraft.writing.WritingEntry;
import net.dnorberg.rivencraft.writing.NotebookContents; import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.item.InkVialItem;
import net.dnorberg.rivencraft.writing.WritingInkService;
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.menu.NotebookMenu; import net.dnorberg.rivencraft.menu.NotebookMenu;
@@ -151,26 +153,27 @@ public final class WritingDataGameTests {
} }
@GameTest(template = "empty") @GameTest(template = "empty")
public static void notebookMenuValidatesInsertionAndPersistsReorderAndRename(GameTestHelper helper) { public static void notebookMenuValidatesInsertionAndPersistsReorder(GameTestHelper helper) {
var player = helper.makeMockServerPlayerInLevel(); var player = helper.makeMockServerPlayerInLevel();
ItemStack notebook = new ItemStack(ModItems.NOTEBOOK.get()); ItemStack notebook = new ItemStack(ModItems.NOTEBOOK.get());
player.setItemInHand(InteractionHand.MAIN_HAND, notebook); player.setItemInHand(InteractionHand.MAIN_HAND, notebook);
NotebookMenu menu = new NotebookMenu(7, player.getInventory(), InteractionHand.MAIN_HAND); NotebookMenu menu = new NotebookMenu(7, player.getInventory(), InteractionHand.MAIN_HAND);
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,
net.minecraft.network.chat.Component.literal("Terrain Base"));
ItemStack second = PageItem.createRecoveredPage(reference("example:removed_symbol")); ItemStack second = PageItem.createRecoveredPage(reference("example:removed_symbol"));
helper.assertFalse(menu.getSlot(0).mayPlace(new ItemStack(Items.DIRT)), helper.assertFalse(menu.getSlot(0).mayPlace(new ItemStack(Items.DIRT)),
"Notebook slots must reject non-Page items"); "Notebook slots must reject non-Page items");
menu.getSlot(0).set(first); menu.getSlot(0).set(first);
menu.getSlot(1).set(second); menu.getSlot(1).set(second);
menu.renamePage(0, "Terrain Base");
helper.assertTrue(menu.clickMenuButton(player, NotebookMenu.MOVE_NEXT_BASE), helper.assertTrue(menu.clickMenuButton(player, NotebookMenu.MOVE_NEXT_BASE),
"The server menu must accept a valid adjacent reorder"); "The server menu must accept a valid adjacent reorder");
NotebookContents saved = notebook.get(ModDataComponents.NOTEBOOK_CONTENTS.get()); NotebookContents saved = notebook.get(ModDataComponents.NOTEBOOK_CONTENTS.get());
helper.assertTrue(saved != null, "Menu changes must persist immediately to the Notebook item"); helper.assertTrue(saved != null, "Menu changes must persist immediately to the Notebook item");
helper.assertValueEqual(saved.pages().get(1).getHoverName().getString(), "Terrain Base", helper.assertValueEqual(saved.pages().get(1).getHoverName().getString(), "Terrain Base",
"Reordering must preserve the renamed Page stack"); "Reordering must preserve existing Page item data");
helper.assertValueEqual(PageItem.content(saved.pages().get(0)).kind(), helper.assertValueEqual(PageItem.content(saved.pages().get(0)).kind(),
PageContent.Kind.UNRESOLVED_SYMBOL, PageContent.Kind.UNRESOLVED_SYMBOL,
"Reordering must preserve unresolved pages"); "Reordering must preserve unresolved pages");
@@ -200,6 +203,118 @@ public final class WritingDataGameTests {
helper.succeed(); helper.succeed();
} }
@GameTest(template = "empty")
public static void notebookMenuPreservesSparsePagePositions(GameTestHelper helper) {
var player = helper.makeMockServerPlayerInLevel();
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(ModItems.NOTEBOOK.get()));
NotebookMenu menu = new NotebookMenu(9, player.getInventory(), InteractionHand.MAIN_HAND);
ItemStack page = PageItem.createSymbolPage(reference("rivencraft:terrain/standard"));
menu.setCarried(page);
menu.clicked(9, 0, ClickType.PICKUP, player);
helper.assertTrue(menu.getCarried().isEmpty(),
"Clicking a distant empty Notebook slot must place the carried Page");
NotebookContents saved = player.getMainHandItem().get(ModDataComponents.NOTEBOOK_CONTENTS.get());
helper.assertTrue(saved != null && saved.pages().size() > 9,
"Notebook persistence must extend through the highest occupied position");
for (int index = 0; index < 9; index++) {
helper.assertTrue(saved.pages().get(index).isEmpty(),
"Earlier empty page positions must remain empty rather than being packed together");
}
helper.assertValueEqual(PageItem.content(saved.pages().get(9)).kind(), PageContent.Kind.SYMBOL,
"The Page must remain in the selected distant position");
helper.succeed();
}
@GameTest(template = "empty")
public static void inkVialsPersistAndOnlyStackWithMatchingContents(GameTestHelper helper) {
ItemStack vial = new ItemStack(ModItems.INK_VIAL.get());
vial.set(ModDataComponents.INK_REMAINING.get(), 7);
CompoundTag serialized = (CompoundTag) vial.saveOptional(helper.getLevel().registryAccess());
ItemStack restored = ItemStack.parseOptional(helper.getLevel().registryAccess(), serialized);
helper.assertValueEqual(InkVialItem.remaining(restored), 7,
"Remaining ink must survive ItemStack persistence");
helper.assertTrue(ItemStack.isSameItemSameComponents(vial, restored),
"Vials with equal ink contents must be eligible to stack");
helper.assertFalse(ItemStack.isSameItemSameComponents(vial, new ItemStack(ModItems.INK_VIAL.get())),
"Vials with different ink contents must not stack");
helper.succeed();
}
@GameTest(template = "empty")
public static void linkPanelPreparationConsumesOnePageAndConfiguredInkOnlyOnSuccess(GameTestHelper helper) {
var player = helper.makeMockServerPlayerInLevel();
ItemStack pages = new ItemStack(ModItems.PAGE.get(), 2);
ItemStack vial = new ItemStack(ModItems.INK_VIAL.get());
player.setItemInHand(InteractionHand.MAIN_HAND, vial);
player.setItemInHand(InteractionHand.OFF_HAND, pages);
helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
"A blank Page and sufficient ink must prepare one link panel");
helper.assertValueEqual(pages.getCount(), 1, "Preparation must consume exactly one blank Page");
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()),
InkVialItem.CAPACITY - WritingInkService.linkPanelCost(),
"Preparation must consume exactly the server-configured ink cost");
long panels = player.getInventory().items.stream()
.filter(stack -> stack.is(ModItems.PAGE.get())
&& PageItem.content(stack).kind() == PageContent.Kind.LINK_PANEL)
.mapToInt(ItemStack::getCount).sum();
helper.assertValueEqual(panels, 1L, "Preparation must output exactly one link panel");
ItemStack written = PageItem.createSymbolPage(reference("rivencraft:terrain/standard"));
player.setItemInHand(InteractionHand.OFF_HAND, written);
int inkBefore = InkVialItem.remaining(player.getMainHandItem());
helper.assertFalse(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
"A written Page must not be overwritten as a link panel");
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()), inkBefore,
"Rejected writing must not consume ink");
helper.assertValueEqual(PageItem.content(written).kind(), PageContent.Kind.SYMBOL,
"Rejected writing must not modify the input Page");
ItemStack lowInk = new ItemStack(ModItems.INK_VIAL.get());
lowInk.set(ModDataComponents.INK_REMAINING.get(), WritingInkService.linkPanelCost() - 1);
player.setItemInHand(InteractionHand.MAIN_HAND, lowInk);
player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(ModItems.PAGE.get()));
helper.assertFalse(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
"Insufficient ink must reject link-panel preparation");
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()),
WritingInkService.linkPanelCost() - 1,
"Insufficient ink must remain untouched");
helper.assertValueEqual(PageItem.content(player.getOffhandItem()).kind(), PageContent.Kind.BLANK,
"Insufficient ink must leave the Page blank");
lowInk.set(ModDataComponents.INK_REMAINING.get(), WritingInkService.linkPanelCost());
helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
"Exact remaining ink must be usable");
helper.assertTrue(player.getMainHandItem().is(Items.GLASS_BOTTLE),
"A depleted Ink Vial must return a reusable glass bottle");
helper.assertValueEqual(PageItem.content(player.getOffhandItem()).kind(),
PageContent.Kind.LINK_PANEL,
"A single blank Page must be replaced by one Link Panel Page");
helper.succeed();
}
@GameTest(template = "empty")
public static void stackedInkVialsSplitWithoutChangingTheOtherVials(GameTestHelper helper) {
var player = helper.makeMockServerPlayerInLevel();
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(ModItems.INK_VIAL.get(), 2));
player.setItemInHand(InteractionHand.OFF_HAND, new ItemStack(ModItems.PAGE.get()));
helper.assertTrue(WritingInkService.prepareLinkPanel(player, InteractionHand.MAIN_HAND),
"A stack of full vials must support preparing one panel");
helper.assertValueEqual(player.getMainHandItem().getCount(), 1,
"Using one vial must leave the other full vial in hand");
helper.assertValueEqual(InkVialItem.remaining(player.getMainHandItem()), InkVialItem.CAPACITY,
"The unconsumed vial must remain full");
long partialVials = player.getInventory().items.stream()
.filter(stack -> stack.is(ModItems.INK_VIAL.get())
&& InkVialItem.remaining(stack) == InkVialItem.CAPACITY - WritingInkService.linkPanelCost())
.mapToInt(ItemStack::getCount).sum();
helper.assertValueEqual(partialVials, 1L,
"The used vial must reappear with only its own ink reduced");
helper.succeed();
}
private static SymbolReference reference(String id) { private static SymbolReference reference(String id) {
return new SymbolReference(ResourceLocation.parse(id)); return new SymbolReference(ResourceLocation.parse(id));
} }
@@ -0,0 +1,47 @@
package net.dnorberg.rivencraft.item;
import java.util.List;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.writing.WritingInkService;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
/** A stackable vial; the ink component is per vial, not shared across a stack. */
public final class InkVialItem extends Item {
public static final int CAPACITY = 16;
public InkVialItem(Properties properties) {
super(properties);
}
public static int remaining(ItemStack vial) {
return vial.getOrDefault(ModDataComponents.INK_REMAINING.get(), CAPACITY);
}
@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
if (!WritingInkService.canPrepareLinkPanel(player, hand)) {
return InteractionResultHolder.pass(player.getItemInHand(hand));
}
if (!level.isClientSide) {
WritingInkService.prepareLinkPanel(player, hand);
}
return InteractionResultHolder.sidedSuccess(player.getItemInHand(hand), level.isClientSide);
}
@Override
public void appendHoverText(ItemStack stack, TooltipContext context,
List<Component> tooltip, TooltipFlag flag) {
tooltip.add(Component.translatable("item.rivencraft.ink_vial.remaining",
remaining(stack), CAPACITY).withStyle(ChatFormatting.GRAY));
tooltip.add(Component.translatable("item.rivencraft.ink_vial.usage")
.withStyle(ChatFormatting.GRAY));
}
}
@@ -6,9 +6,7 @@ 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.minecraft.core.NonNullList; import net.minecraft.core.NonNullList;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.world.Container; import net.minecraft.world.Container;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
import net.minecraft.world.SimpleContainer; import net.minecraft.world.SimpleContainer;
@@ -118,23 +116,6 @@ public final class NotebookMenu extends AbstractContainerMenu {
return true; return true;
} }
public void renamePage(int slot, String name) {
if (slot < 0 || slot >= NOTEBOOK_SLOTS) {
return;
}
ItemStack page = contents.getItem(slot);
if (page.isEmpty() || !page.is(ModItems.PAGE.get())) {
return;
}
if (name.isBlank()) {
page.remove(DataComponents.CUSTOM_NAME);
} else {
page.set(DataComponents.CUSTOM_NAME, Component.literal(name));
}
contents.setChanged();
broadcastChanges();
}
@Override @Override
public void clicked(int slotId, int button, ClickType clickType, Player player) { public void clicked(int slotId, int button, ClickType clickType, Player player) {
if (!stillValid(player)) { if (!stillValid(player)) {
@@ -12,7 +12,6 @@ import net.dnorberg.rivencraft.client.ClientAgeCelestialAngles;
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;
import net.dnorberg.rivencraft.menu.NotebookMenu;
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.minecraft.core.component.DataComponents; import net.minecraft.core.component.DataComponents;
@@ -50,8 +49,6 @@ public final class ModNetworking {
public static void register(RegisterPayloadHandlersEvent event) { public static void register(RegisterPayloadHandlersEvent event) {
PayloadRegistrar registrar = event.registrar("10"); PayloadRegistrar registrar = event.registrar("10");
registrar.playToServer(RenameNotebookPagePayload.TYPE, RenameNotebookPagePayload.STREAM_CODEC,
ModNetworking::renameNotebookPage);
registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC, registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC,
ModNetworking::requestDescriptiveBookScreen); ModNetworking::requestDescriptiveBookScreen);
registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC, registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC,
@@ -73,16 +70,6 @@ public final class ModNetworking {
ClientAgeCelestialAngles::handle); ClientAgeCelestialAngles::handle);
} }
private static void renameNotebookPage(RenameNotebookPagePayload payload, IPayloadContext context) {
if (!(context.player() instanceof ServerPlayer player)
|| !(player.containerMenu instanceof NotebookMenu menu)
|| menu.containerId != payload.containerId()
|| payload.name().length() > MAX_BOOK_TITLE_LENGTH) {
return;
}
menu.renamePage(payload.slot(), normalizeTitle(payload.name()));
}
private static void requestDescriptiveBookScreen(RequestDescriptiveBookScreenPayload payload, IPayloadContext context) { private static void requestDescriptiveBookScreen(RequestDescriptiveBookScreenPayload payload, IPayloadContext context) {
if (!(context.player() instanceof ServerPlayer player)) { if (!(context.player() instanceof ServerPlayer player)) {
return; return;
@@ -1,20 +0,0 @@
package net.dnorberg.rivencraft.network;
import io.netty.buffer.ByteBuf;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record RenameNotebookPagePayload(int containerId, int slot, String name) implements CustomPacketPayload {
public static final Type<RenameNotebookPagePayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "rename_notebook_page"));
public static final StreamCodec<ByteBuf, RenameNotebookPagePayload> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.VAR_INT, RenameNotebookPagePayload::containerId,
ByteBufCodecs.VAR_INT, RenameNotebookPagePayload::slot,
ByteBufCodecs.STRING_UTF8, RenameNotebookPagePayload::name,
RenameNotebookPagePayload::new);
@Override public Type<? extends CustomPacketPayload> type() { return TYPE; }
}
@@ -1,6 +1,7 @@
package net.dnorberg.rivencraft.registry; package net.dnorberg.rivencraft.registry;
import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.CreativeModeTab;
@@ -19,8 +20,18 @@ public final class ModCreativeTabs {
.displayItems((parameters, output) -> { .displayItems((parameters, output) -> {
output.accept(ModItems.DESCRIPTIVE_BOOK.get()); output.accept(ModItems.DESCRIPTIVE_BOOK.get());
output.accept(ModItems.LINKING_BOOK.get()); output.accept(ModItems.LINKING_BOOK.get());
output.accept(ModItems.PAGE.get());
output.accept(ModItems.NOTEBOOK.get()); output.accept(ModItems.NOTEBOOK.get());
output.accept(ModItems.INK_VIAL.get());
})
.build());
public static final DeferredHolder<CreativeModeTab, CreativeModeTab> PAGES =
CREATIVE_TABS.register("rivencraft_pages", () -> CreativeModeTab.builder()
.title(Component.translatable("itemGroup.rivencraft.pages"))
.icon(PageItem::createLinkPanelPage)
.displayItems((parameters, output) -> {
output.accept(ModItems.PAGE.get());
output.accept(PageItem.createLinkPanelPage());
}) })
.build()); .build());
@@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.book.AgeReference;
import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.NotebookContents; import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata; import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
import com.mojang.serialization.Codec;
import net.minecraft.core.component.DataComponentType; import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@@ -60,6 +61,12 @@ public final class ModDataComponents {
.networkSynchronized(NotebookContents.STREAM_CODEC) .networkSynchronized(NotebookContents.STREAM_CODEC)
.cacheEncoding() .cacheEncoding()
.build()); .build());
public static final DeferredHolder<DataComponentType<?>, DataComponentType<Integer>> INK_REMAINING =
DATA_COMPONENTS.register("ink_remaining", () -> DataComponentType.<Integer>builder()
.persistent(Codec.intRange(0, 16))
.networkSynchronized(ByteBufCodecs.VAR_INT)
.cacheEncoding()
.build());
private ModDataComponents() { private ModDataComponents() {
} }
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.item.NotebookItem; import net.dnorberg.rivencraft.item.NotebookItem;
import net.dnorberg.rivencraft.item.InkVialItem;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.neoforged.bus.api.IEventBus; import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredItem; import net.neoforged.neoforge.registries.DeferredItem;
@@ -33,6 +34,11 @@ public final class ModItems {
NotebookItem::new, NotebookItem::new,
new Item.Properties().stacksTo(1) new Item.Properties().stacksTo(1)
); );
public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem(
"ink_vial",
InkVialItem::new,
new Item.Properties().stacksTo(16)
);
private ModItems() { private ModItems() {
} }
@@ -0,0 +1,77 @@
package net.dnorberg.rivencraft.writing;
import net.dnorberg.rivencraft.config.RivencraftConfig;
import net.dnorberg.rivencraft.item.InkVialItem;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
/** Server-owned costs and the baseline blank-page-to-link-panel transaction. */
public final class WritingInkService {
private WritingInkService() {
}
public static int symbolCopyCost() {
return RivencraftConfig.symbolInkCost();
}
public static int linkPanelCost() {
return RivencraftConfig.linkPanelInkCost();
}
public static boolean canPrepareLinkPanel(Player player, InteractionHand vialHand) {
ItemStack vial = player.getItemInHand(vialHand);
ItemStack page = player.getItemInHand(other(vialHand));
int remaining = InkVialItem.remaining(vial);
return vial.is(ModItems.INK_VIAL.get()) && remaining >= linkPanelCost()
&& remaining <= InkVialItem.CAPACITY
&& page.is(ModItems.PAGE.get()) && PageItem.content(page).kind() == PageContent.Kind.BLANK;
}
public static boolean prepareLinkPanel(Player player, InteractionHand vialHand) {
if (player.level().isClientSide || !canPrepareLinkPanel(player, vialHand)) {
return false;
}
InteractionHand pageHand = other(vialHand);
ItemStack page = player.getItemInHand(pageHand);
ItemStack vial = player.getItemInHand(vialHand);
int newInk = InkVialItem.remaining(vial) - linkPanelCost();
ItemStack panel = PageItem.createLinkPanelPage();
ItemStack spentVial = newInk == 0
? new ItemStack(Items.GLASS_BOTTLE)
: new ItemStack(ModItems.INK_VIAL.get());
if (newInk > 0) {
spentVial.set(ModDataComponents.INK_REMAINING.get(), newInk);
}
// All validation and output construction is complete before changing either input.
if (page.getCount() == 1) {
player.setItemInHand(pageHand, panel);
} else {
page.shrink(1);
giveOrDrop(player, panel);
}
if (vial.getCount() == 1) {
player.setItemInHand(vialHand, spentVial);
} else {
vial.shrink(1);
giveOrDrop(player, spentVial);
}
return true;
}
private static InteractionHand other(InteractionHand hand) {
return hand == InteractionHand.MAIN_HAND ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
}
private static void giveOrDrop(Player player, ItemStack stack) {
if (!player.getInventory().add(stack)) {
player.drop(stack, false);
}
}
}
@@ -15,9 +15,10 @@
"item.rivencraft.notebook": "Notebook", "item.rivencraft.notebook": "Notebook",
"item.rivencraft.notebook.pages": "Pages: %s/%s", "item.rivencraft.notebook.pages": "Pages: %s/%s",
"item.rivencraft.notebook.unresolved": "Unresolved pages: %s", "item.rivencraft.notebook.unresolved": "Unresolved pages: %s",
"screen.rivencraft.notebook.rename": "Rename selected page", "item.rivencraft.ink_vial": "Ink Vial",
"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.",
"screen.rivencraft.notebook.search": "Search pages", "screen.rivencraft.notebook.search": "Search pages",
"screen.rivencraft.notebook.rename_toggle": "Rename selected page (R); use left/right arrows to reorder",
"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",
"message.rivencraft.linking_book.bind_hint": "Sneak-use to bind this book to your current location", "message.rivencraft.linking_book.bind_hint": "Sneak-use to bind this book to your current location",
@@ -97,5 +98,6 @@
"screen.rivencraft.book.explore": "Explore", "screen.rivencraft.book.explore": "Explore",
"screen.rivencraft.book.exploring": "Exploring", "screen.rivencraft.book.exploring": "Exploring",
"screen.rivencraft.book.return_from_explore": "Right-click to return", "screen.rivencraft.book.return_from_explore": "Right-click to return",
"itemGroup.rivencraft": "Rivencraft" "itemGroup.rivencraft": "Rivencraft",
"itemGroup.rivencraft.pages": "Rivencraft Pages"
} }
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "rivencraft:item/ink_vial"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,3 @@
{
"animation": {}
}
@@ -0,0 +1,14 @@
{
"type": "minecraft:crafting_shapeless",
"category": "misc",
"ingredients": [
{ "item": "minecraft:black_dye" },
{ "item": "minecraft:black_dye" },
{ "item": "minecraft:glass_bottle" },
{ "item": "minecraft:water_bucket" }
],
"result": {
"count": 1,
"id": "rivencraft:ink_vial"
}
}