Add Writing Desk paper copying and ink meter
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-09-24 05:38:51 -04:00
1 parent 385741027a
commit 1dcd3a178e
8 files changed
+309 -27

No files matched your search

+9 -3
View File
@@ -19,12 +19,18 @@ 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.
The Writing Desk can copy a selected written page from its collection tabs.
The Writing Desk is now craftable from planks, a glass bottle, and a feather.
Its four collection tabs, ink/source/page work slots, and inventory persist
Its four collection tabs, paper input, ink vial input, empty glass-bottle return,
16-unit ink meter, and inventory persist
through world saves; breaking it drops stored contents. The desk screen is a
functional foundation styled after Legacy's left tabs, central writing surface,
and right inventory. Transcription controls are not implemented yet.
and right inventory. Put vanilla paper (or a blank Rivencraft Page) 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
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
from a catalog remains future work.
Selecting a stored page on the desk surface shows its Legacy-proportioned
portrait preview and name above the player inventory.
+4 -1
View File
@@ -86,5 +86,8 @@ 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
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
desk's 16-unit ink meter refills from an Ink Vial when needed and returns a
vanilla glass bottle. 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.
mod_license=LGPL-3.0-or-later
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
mod_version=0.3.5.1
mod_version=0.3.5.2
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.
# This should match the base package used for the mod sources.
@@ -1,5 +1,8 @@
package net.dnorberg.rivencraft.block.entity;
import net.dnorberg.rivencraft.item.InkVialItem;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.writing.WritingInkService;
import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.core.BlockPos;
@@ -10,25 +13,29 @@ import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
/** The four Legacy-like collection tabs and four work slots are owned by the block, not its viewers. */
/** Four collection tabs, paper, ink vials and empty-vial return are owned by the block. */
public final class WritingDeskBlockEntity extends BlockEntity implements Container {
public static final int COLLECTIONS = 4;
public static final int INK = 4;
public static final int SOURCE = 5;
public static final int RETURN = 5;
public static final int PAPER = 6;
public static final int OUTPUT = 7;
/** Unused by new desks; retained so old saves cannot silently lose slot contents. */
public static final int LEGACY_OUTPUT = 7;
public static final int SIZE = 8;
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
private int activeCollection;
private int inkRemaining;
public WritingDeskBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.WRITING_DESK.get(), pos, state);
}
public int activeCollection() { return activeCollection; }
public int inkRemaining() { return inkRemaining; }
public void setActiveCollection(int index) {
if (index >= 0 && index < COLLECTIONS && activeCollection != index) {
activeCollection = index;
@@ -60,15 +67,49 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
if (slot < COLLECTIONS) return stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.PAGE.get());
return switch (slot) {
case INK -> stack.is(ModItems.INK_VIAL.get());
case SOURCE -> stack.is(ModItems.PAGE.get()) || stack.is(ModItems.NOTEBOOK.get());
case PAPER -> stack.is(ModItems.PAGE.get());
case OUTPUT -> false;
case RETURN -> false;
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;
default -> false;
};
}
/** The output is virtual; inputs are committed only after a result can be given to a player. */
public boolean canConsumeCopyMaterials() {
ItemStack paper = items.get(PAPER);
if (!canPlaceItem(PAPER, paper) || paper.isEmpty()) return false;
int cost = WritingInkService.symbolCopyCost();
if (inkRemaining >= cost) return true;
ItemStack vial = items.get(INK);
ItemStack returned = items.get(RETURN);
return vial.is(ModItems.INK_VIAL.get()) && inkRemaining + InkVialItem.remaining(vial) >= cost
&& (returned.isEmpty() || returned.is(Items.GLASS_BOTTLE)
&& returned.getCount() < returned.getMaxStackSize());
}
public boolean consumeCopyMaterials() {
if (!canConsumeCopyMaterials()) return false;
int cost = WritingInkService.symbolCopyCost();
if (inkRemaining < cost) {
ItemStack vial = items.get(INK);
inkRemaining += Math.clamp(InkVialItem.remaining(vial), 0, InkVialItem.CAPACITY);
vial.shrink(1);
if (vial.isEmpty()) items.set(INK, ItemStack.EMPTY);
ItemStack returned = items.get(RETURN);
if (returned.isEmpty()) items.set(RETURN, new ItemStack(Items.GLASS_BOTTLE));
else returned.grow(1);
}
inkRemaining -= cost;
removeItem(PAPER, 1);
setChanged();
return true;
}
@Override public void clearContent() {
items.clear();
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
inkRemaining = 0;
setChanged();
}
@Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
@@ -76,10 +117,12 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
ContainerHelper.loadAllItems(tag, items, registries);
activeCollection = Math.clamp(tag.getInt("ActiveCollection"), 0, COLLECTIONS - 1);
inkRemaining = Math.clamp(tag.getInt("InkRemaining"), 0, InkVialItem.CAPACITY);
}
@Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
ContainerHelper.saveAllItems(tag, items, registries);
tag.putInt("ActiveCollection", activeCollection);
tag.putInt("InkRemaining", inkRemaining);
}
}
@@ -31,6 +31,10 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
inventoryLabelY = 79;
}
@Override public void renderBackground(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
// Legacy leaves the world visible around the desk instead of dimming it.
}
@Override protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) {
int x = leftPos;
int y = topPos;
@@ -47,6 +51,11 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
slotFrame(graphics, 228, 20);
slotFrame(graphics, 228, 57);
slotFrame(graphics, 370, 57);
graphics.fill(x + 350, y + 20, x + 364, y + 75, 0xFF303035);
graphics.fill(x + 352, y + 22, x + 362, y + 73, 0xFF15151B);
int filled = 51 * menu.inkRemaining() / 16;
if (filled > 0) graphics.fill(x + 352, y + 73 - filled, x + 362, y + 73, 0xFF6666AA);
graphics.renderOutline(x + 350, y + 20, 14, 55, 0xFF484848);
for (int row = 0; row < 3; row++) {
for (int column = 0; column < 9; column++) slotFrame(graphics, 224 + column * 18, 91 + row * 18);
}
@@ -68,6 +77,11 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
super.render(graphics, mouseX, mouseY, partialTick);
renderCollection(graphics);
renderTooltip(graphics, mouseX, mouseY);
if (mouseX >= leftPos + 350 && mouseX < leftPos + 364
&& mouseY >= topPos + 20 && mouseY < topPos + 75) {
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.writing_desk.ink",
menu.inkRemaining(), 16), mouseX, mouseY);
}
}
private List<ItemStack> pages() {
@@ -133,6 +147,9 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
if (x % 31 < 30 && y % 41 < 40 && index < pages.size()
&& !pages.get(index).isEmpty()) {
selectedPage = index;
if (minecraft != null && minecraft.gameMode != null) {
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, 16 + index);
}
}
return true;
}
@@ -1,15 +1,21 @@
package net.dnorberg.rivencraft.gametest;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.registry.ModBlocks;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.writing.NotebookContents;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.inventory.ClickType;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.AABB;
import net.neoforged.neoforge.gametest.GameTestHolder;
@@ -65,21 +71,139 @@ public final class WritingDeskGameTests {
WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos);
var player = helper.makeMockServerPlayerInLevel();
player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
player.getInventory().setItem(9, new ItemStack(ModItems.PAGE.get(), 12));
player.getInventory().setItem(9, new ItemStack(Items.PAPER, 12));
WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk);
helper.assertTrue(menu.stillValid(player), "Placed desk must keep its menu valid");
helper.assertTrue(menu.clickMenuButton(player, 3), "Active collection control must accept a valid tab");
helper.assertValueEqual(desk.activeCollection(), 3, "Selected tab must be stored on the desk");
menu.quickMoveStack(player, 8);
menu.quickMoveStack(player, 9);
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 12, "Shift-click must deposit all pages");
helper.assertTrue(player.getInventory().getItem(9).isEmpty(), "Shift-click must consume player source stack");
menu.quickMoveStack(player, WritingDeskBlockEntity.PAPER);
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.PAPER).isEmpty(), "Shift-click must empty desk source slot");
int total = player.getInventory().items.stream().filter(stack -> stack.is(ModItems.PAGE.get()))
int total = player.getInventory().items.stream().filter(stack -> stack.is(Items.PAPER))
.mapToInt(ItemStack::getCount).sum();
helper.assertValueEqual(total, 12, "Round-trip must not lose or duplicate pages");
helper.getLevel().setBlock(pos, Blocks.AIR.defaultBlockState(), 3);
helper.assertFalse(menu.stillValid(player), "Removed desk must invalidate menu");
helper.succeed();
}
@GameTest(template = "empty")
public static void takingCopySpendsPaperAndOneInkThenReturnsGlassBottle(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);
ItemStack source = PageItem.createLinkPanelPage();
desk.setItem(0, source.copy());
desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER, 2));
desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
var player = helper.makeMockServerPlayerInLevel();
player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk);
menu.broadcastChanges();
helper.assertTrue(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem().is(ModItems.PAGE.get()),
"A source page with paper and ink must preview one copy");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 2,
"Previewing must not consume paper");
helper.assertValueEqual(desk.inkRemaining(), 0, "Previewing must not consume ink");
menu.clicked(WritingDeskMenu.COPY_RESULT, 0, ClickType.PICKUP, player);
helper.assertTrue(menu.getCarried().is(ModItems.PAGE.get()), "Taking result must give the copied page");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 1,
"Taking a copy must consume one paper");
helper.assertValueEqual(desk.inkRemaining(), 15, "First copy must refill to 16 and consume one ink");
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.INK).isEmpty(), "Refill must consume one vial");
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.RETURN).is(Items.GLASS_BOTTLE),
"Refill must return a vanilla glass bottle");
helper.assertTrue(desk.getItem(0).is(ModItems.PAGE.get()), "Source page must remain untouched");
helper.assertTrue(menu.quickMoveStack(player, WritingDeskMenu.COPY_RESULT).is(ModItems.PAGE.get()),
"A second copy must remain available from the same ink fill");
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.PAPER).isEmpty(),
"Two copies must consume exactly two paper");
helper.assertValueEqual(desk.inkRemaining(), 14, "Two copies must spend exactly two of sixteen ink");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.RETURN).getCount(), 1,
"A second copy must not consume another vial");
var saved = desk.saveWithoutMetadata(helper.getLevel().registryAccess());
var restored = new WritingDeskBlockEntity(pos, ModBlocks.WRITING_DESK.get().defaultBlockState());
restored.loadWithComponents(saved, helper.getLevel().registryAccess());
helper.assertValueEqual(restored.inkRemaining(), 14, "Ink meter must persist across saves");
helper.succeed();
}
@GameTest(template = "empty")
public static void shiftClickCopiesOnlyWhenResourcesAndBottleSpaceExist(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, 2));
desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
desk.setItem(WritingDeskBlockEntity.RETURN, new ItemStack(Items.GLASS_BOTTLE, 64));
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.assertTrue(menu.quickMoveStack(player, WritingDeskMenu.COPY_RESULT).isEmpty(),
"Full bottle return must block refill without consuming supplies");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 2,
"Blocked copy must leave paper untouched");
desk.removeItem(WritingDeskBlockEntity.RETURN, 1);
ItemStack copied = menu.quickMoveStack(player, WritingDeskMenu.COPY_RESULT);
helper.assertTrue(copied.is(ModItems.PAGE.get()), "Shift-click must transfer one copy");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 1,
"Shift-click must consume one paper");
helper.assertValueEqual(desk.inkRemaining(), 15, "Shift-click must consume one ink");
int copies = player.getInventory().items.stream().filter(stack -> stack.is(ModItems.PAGE.get()))
.mapToInt(ItemStack::getCount).sum();
helper.assertValueEqual(copies, 1, "Shift-click must give exactly one page");
helper.succeed();
}
@GameTest(template = "empty")
public static void selectedNotebookPageControlsCopyPreview(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);
ItemStack notebook = new ItemStack(ModItems.NOTEBOOK.get());
notebook.set(ModDataComponents.NOTEBOOK_CONTENTS.get(),
new NotebookContents(List.of(ItemStack.EMPTY, PageItem.createLinkPanelPage())));
desk.setItem(0, notebook);
desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER));
desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
var player = helper.makeMockServerPlayerInLevel();
player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk);
menu.broadcastChanges();
helper.assertTrue(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem().isEmpty(),
"An empty selected position must not create a copy");
helper.assertTrue(menu.clickMenuButton(player, 17), "Second page selection must be accepted");
menu.broadcastChanges();
helper.assertTrue(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem().is(ModItems.PAGE.get()),
"Selecting the populated page must show a copy");
helper.succeed();
}
@GameTest(template = "empty")
public static void twoViewersCannotTakeTheSameLastSheet(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 first = helper.makeMockServerPlayerInLevel();
var second = helper.makeMockServerPlayerInLevel();
first.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
second.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
WritingDeskMenu firstMenu = new WritingDeskMenu(1, first.getInventory(), desk);
WritingDeskMenu secondMenu = new WritingDeskMenu(2, second.getInventory(), desk);
firstMenu.broadcastChanges();
secondMenu.broadcastChanges();
helper.assertTrue(firstMenu.quickMoveStack(first, WritingDeskMenu.COPY_RESULT).is(ModItems.PAGE.get()),
"First viewer should take the last available copy");
helper.assertTrue(secondMenu.quickMoveStack(second, WritingDeskMenu.COPY_RESULT).isEmpty(),
"Second viewer must not copy the already-consumed paper");
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.PAPER).isEmpty(),
"Shared desk must spend paper only once");
helper.succeed();
}
}
@@ -1,8 +1,13 @@
package net.dnorberg.rivencraft.menu;
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.dnorberg.rivencraft.registry.ModMenus;
import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.writing.PageContent;
import java.util.List;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.world.Container;
import net.minecraft.world.SimpleContainer;
@@ -12,12 +17,18 @@ import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.DataSlot;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
public final class WritingDeskMenu extends AbstractContainerMenu {
private static final int PLAYER_START = WritingDeskBlockEntity.SIZE;
public static final int COPY_RESULT = WritingDeskBlockEntity.SIZE;
private static final int PLAYER_START = COPY_RESULT + 1;
private final Container desk;
private final WritingDeskBlockEntity blockEntity;
private final SimpleContainer result = new SimpleContainer(1);
private final boolean clientSide;
private int syncedActiveCollection;
private int syncedInkRemaining;
private int selectedPage;
public WritingDeskMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) {
this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos())
@@ -28,18 +39,25 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
super(ModMenus.WRITING_DESK.get(), id);
this.blockEntity = blockEntity;
this.desk = blockEntity == null ? new SimpleContainer(WritingDeskBlockEntity.SIZE) : blockEntity;
this.clientSide = inventory.player.level().isClientSide;
addDataSlot(new DataSlot() {
@Override public int get() { return WritingDeskMenu.this.blockEntity == null
? syncedActiveCollection : WritingDeskMenu.this.blockEntity.activeCollection(); }
@Override public void set(int value) { syncedActiveCollection = value; }
});
addDataSlot(new DataSlot() {
@Override public int get() { return WritingDeskMenu.this.blockEntity == null
? syncedInkRemaining : WritingDeskMenu.this.blockEntity.inkRemaining(); }
@Override public void set(int value) { syncedInkRemaining = value; }
});
for (int i = 0; i < WritingDeskBlockEntity.COLLECTIONS; i++) {
addSlot(new DeskSlot(desk, i, 12, 20 + i * 37, 1));
}
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.INK, 370, 20, 1));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.SOURCE, 228, 20, 1));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.PAPER, 228, 57, 64));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.OUTPUT, 370, 57, 64));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.INK, 370, 20, 16));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.RETURN, 370, 57, 64));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.PAPER, 228, 20, 64));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.LEGACY_OUTPUT, -1000, -1000, 64));
addSlot(new CopySlot(result, 0, 228, 57));
for (int row = 0; row < 3; row++) {
for (int column = 0; column < 9; column++) {
addSlot(new Slot(inventory, column + row * 9 + 9, 224 + column * 18, 91 + row * 18));
@@ -51,10 +69,17 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
}
public int activeCollection() { return syncedActiveCollection; }
public int inkRemaining() { return syncedInkRemaining; }
@Override public boolean clickMenuButton(Player player, int id) {
if (blockEntity == null || !stillValid(player) || id < 0 || id >= WritingDeskBlockEntity.COLLECTIONS) return false;
blockEntity.setActiveCollection(id);
if (blockEntity == null || !stillValid(player)) return false;
if (id >= 0 && id < WritingDeskBlockEntity.COLLECTIONS) {
blockEntity.setActiveCollection(id);
selectedPage = 0;
} else if (id >= 16 && id < 16 + NotebookContents.MAX_PAGES) {
selectedPage = id - 16;
} else return false;
refreshOutput();
return true;
}
@@ -64,11 +89,23 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
@Override public ItemStack quickMoveStack(Player player, int index) {
if (!stillValid(player) || index < 0 || index >= slots.size()) return ItemStack.EMPTY;
if (index == COPY_RESULT) {
refreshOutput();
if (!canCopy()) return ItemStack.EMPTY;
ItemStack copy = selectedSource().copyWithCount(1);
ItemStack movingCopy = copy.copy();
if (!moveItemStackTo(movingCopy, PLAYER_START, slots.size(), true) || !movingCopy.isEmpty()) {
return ItemStack.EMPTY;
}
blockEntity.consumeCopyMaterials();
refreshOutput();
return copy;
}
Slot slot = slots.get(index);
if (!slot.hasItem()) return ItemStack.EMPTY;
ItemStack moving = slot.getItem();
ItemStack original = moving.copy();
if (index < PLAYER_START) {
if (index < COPY_RESULT) {
if (!moveItemStackTo(moving, PLAYER_START, slots.size(), true)) return ItemStack.EMPTY;
} else if (!moveIntoDesk(moving)) {
return ItemStack.EMPTY;
@@ -81,14 +118,65 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
private boolean moveIntoDesk(ItemStack moving) {
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.PAGE.get())) {
return moveItemStackTo(moving, 6, 7, false)
|| moveItemStackTo(moving, 0, 4, false)
|| moveItemStackTo(moving, 5, 6, false);
}
if (moving.is(Items.PAPER) || moving.is(ModItems.PAGE.get())
&& PageItem.content(moving).kind() == PageContent.Kind.BLANK)
return moveItemStackTo(moving, WritingDeskBlockEntity.PAPER,
WritingDeskBlockEntity.PAPER + 1, false);
if (moving.is(ModItems.PAGE.get())) return moveItemStackTo(moving, 0, 4, false);
return false;
}
private ItemStack selectedSource() {
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.NOTEBOOK.get())) {
List<ItemStack> pages = collection.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
return selectedPage < pages.size() ? pages.get(selectedPage) : ItemStack.EMPTY;
}
return ItemStack.EMPTY;
}
private boolean canCopy() {
ItemStack source = selectedSource();
return blockEntity != null && source.is(ModItems.PAGE.get())
&& PageItem.content(source).kind() != PageContent.Kind.BLANK
&& PageItem.isUsableWriting(source) && blockEntity.canConsumeCopyMaterials();
}
private void refreshOutput() {
if (clientSide) return;
ItemStack preview = canCopy() ? selectedSource().copyWithCount(1) : ItemStack.EMPTY;
if (!ItemStack.matches(result.getItem(0), preview)) result.setItem(0, preview);
}
@Override public void broadcastChanges() {
refreshOutput();
super.broadcastChanges();
}
@Override public void clicked(int slotId, int button, net.minecraft.world.inventory.ClickType clickType,
Player player) {
if (!stillValid(player)) {
player.closeContainer();
return;
}
refreshOutput();
super.clicked(slotId, button, clickType, player);
}
private final class CopySlot extends Slot {
private CopySlot(Container container, int index, int x, int y) { super(container, index, x, y); }
@Override public boolean mayPlace(ItemStack stack) { return false; }
@Override public boolean mayPickup(Player player) {
return clientSide ? hasItem() : WritingDeskMenu.this.stillValid(player) && canCopy();
}
@Override public void onTake(Player player, ItemStack stack) {
if (!clientSide && blockEntity.consumeCopyMaterials()) refreshOutput();
super.onTake(player, stack);
}
}
private static final class DeskSlot extends Slot {
private final int limit;
private DeskSlot(Container container, int index, int x, int y, int limit) {
@@ -18,6 +18,7 @@
"item.rivencraft.ink_vial": "Ink Vial",
"block.rivencraft.writing_desk": "Writing Desk",
"container.rivencraft.writing_desk": "Writing Desk",
"screen.rivencraft.writing_desk.ink": "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.",
"screen.rivencraft.notebook.search": "Search pages",