Refine Writing Desk collection and ink refill behavior

This commit is contained in:
dnorberg committed 2026-09-25 06:07:44 -04:00
1 parent e2e348d5c5
commit bdca7c5b5e
11 files changed
+777 -137

No files matched your search

+24 -5
View File
@@ -25,15 +25,34 @@ It occupies two adjacent blocks, with one shared inventory; loaded paper and
the selected page appear on its Legacy-inspired model. the selected page appear on its Legacy-inspired model.
Desks placed before 0.3.6.1 keep their old one-block footprint until broken Desks placed before 0.3.6.1 keep their old one-block footprint until broken
and placed again; leave an adjacent space when replacing one. and placed again; leave an adjacent space when replacing one.
Its four collection tabs, paper input, ink vial input, empty glass-bottle return, Its 88 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 using Legacy's original desk and scrollbar textures.
and right inventory. Put vanilla paper in the Four tabs are visible at once; the controls above and below them move by four
(1–4, 5–8, through 85–88). Each viewer can browse a different group. Existing
four-tab desks retain their collections and supplies when upgraded.
With a notebook selected, pick up a Page from your inventory and click an empty
position on the left surface to store one page there. Clear search/filter/sort
and turn off `ALL` to place pages at specific positions; occupied positions are
never overwritten. A notebook tab exposes all 27 notebook positions (scroll to
reach the rest); a loose-page tab exposes only its one occupied position.
Empty tabs have no page storage until a notebook or page is put in the tab slot.
Click a page on the desk surface to select it for copying. Drag a page from one
notebook position to an empty position to move it, or onto another page to swap
their positions. This also works between notebooks shown in the `ALL` view.
Drag a page out of the panel to turn it back into an item. Drop it over a player
inventory slot or desk tab to place it there; a tab with a notebook adds it to
the first open position. Dropping elsewhere leaves the page on the cursor for
normal inventory placement. The source notebook or tab loses the page only after
a valid drop.
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 available vial refills an empty ink meter
returns a vanilla glass bottle to the lower-right slot. The `AZ` control sorts when inserted or when copying uses its last ink, and returns a vanilla glass
bottle to the lower-right slot. Removing a bottle also retries a blocked refill.
Refilling waits if that slot is full or contains another item. The `AZ` control sorts
visible pages by name; `ALL` shows pages held across every desk tab. Search and visible pages by name; `ALL` shows pages held across every desk tab. Search and
category filtering narrow the view without granting new pages, and cross-tab category filtering narrow the view without granting new pages, and cross-tab
copy selection is checked by the server. copy selection is checked by the server.
+2 -1
View File
@@ -11,7 +11,8 @@ project's GNU LGPL v3 license. Rivencraft itself uses the same license.
| `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 | | `assets/rivencraft/textures/block/writing_desk.png` | `assets/mystcraft/textures/entity/desk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only |
| `assets/rivencraft/textures/block/writing_desk_wood.png` and `writing_desk_inlay.png` | `assets/mystcraft/textures/entity/desk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Cropped opaque wood and red inlay regions for the two-block model | | `assets/rivencraft/textures/block/writing_desk_wood.png` and `writing_desk_inlay.png` | `assets/mystcraft/textures/entity/desk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Cropped opaque wood and red inlay regions for the two-block model |
| `assets/rivencraft/textures/gui/writing_desk_legacy.png` | `assets/mystcraft/gui/writingdesk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; solid GUI backing sampled from Legacy atlas | | `assets/rivencraft/textures/gui/writing_desk_legacy.png` | `assets/mystcraft/gui/writingdesk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; full desk panel, tabs, and cycling controls at original atlas coordinates |
| `assets/rivencraft/textures/gui/scrollbar.png` | `assets/mystcraft/gui/scrollbar.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; tiled track and normal/pressed thumb |
| `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 |
+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.6.2 mod_version=0.3.6.8
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.
@@ -16,18 +16,19 @@ import net.minecraft.world.ContainerHelper;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items; import net.minecraft.world.item.Items;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
/** Four collection tabs, paper, ink vials and empty-vial return are owned by the block. */ /** Collection tabs, paper, ink vials and empty-vial return are owned by the block. */
public final class WritingDeskBlockEntity extends BlockEntity implements Container { public final class WritingDeskBlockEntity extends BlockEntity implements Container {
public static final int COLLECTIONS = 4; public static final int COLLECTIONS = 88;
public static final int INK = 4; public static final int INK = COLLECTIONS;
public static final int RETURN = 5; public static final int RETURN = INK + 1;
public static final int PAPER = 6; public static final int PAPER = INK + 2;
/** Unused by new desks; retained so old saves cannot silently lose slot contents. */ /** Unused by new desks; retained so old saves cannot silently lose slot contents. */
public static final int LEGACY_OUTPUT = 7; public static final int LEGACY_OUTPUT = INK + 3;
public static final int SIZE = 8; public static final int SIZE = INK + 4;
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY); private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
private int activeCollection; private int activeCollection;
private int inkRemaining; private int inkRemaining;
@@ -62,6 +63,7 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
if (!removed.isEmpty()) { if (!removed.isEmpty()) {
if (slot < COLLECTIONS) displayPage = ItemStack.EMPTY; if (slot < COLLECTIONS) displayPage = ItemStack.EMPTY;
setChanged(); setChanged();
if (slot == RETURN) refillEmptyInkBar();
} }
return removed; return removed;
} }
@@ -71,6 +73,7 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
if (stack.getCount() > getMaxStackSize()) stack.setCount(getMaxStackSize()); if (stack.getCount() > getMaxStackSize()) stack.setCount(getMaxStackSize());
if (slot < COLLECTIONS) displayPage = ItemStack.EMPTY; if (slot < COLLECTIONS) displayPage = ItemStack.EMPTY;
setChanged(); setChanged();
if (slot == INK || slot == RETURN) refillEmptyInkBar();
} }
@Override public boolean stillValid(Player player) { @Override public boolean stillValid(Player player) {
return level != null && level.isLoaded(worldPosition) return level != null && level.isLoaded(worldPosition)
@@ -89,6 +92,28 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
}; };
} }
private boolean hasBottleSpace() {
ItemStack returned = items.get(RETURN);
return returned.isEmpty() || returned.is(Items.GLASS_BOTTLE)
&& returned.getCount() < returned.getMaxStackSize();
}
/** Consume one vial only when the empty meter can return its glass bottle. */
private void refillEmptyInkBar() {
if (level == null || level.isClientSide || inkRemaining != 0 || !hasBottleSpace()) return;
ItemStack vial = items.get(INK);
if (!vial.is(ModItems.INK_VIAL.get())) return;
int ink = Math.clamp(InkVialItem.remaining(vial), 0, InkVialItem.CAPACITY);
if (ink == 0) return;
inkRemaining = ink;
vial.shrink(1);
if (vial.isEmpty()) items.set(INK, ItemStack.EMPTY);
ItemStack returned = items.get(RETURN);
if (returned.isEmpty()) items.set(RETURN, new ItemStack(Items.GLASS_BOTTLE));
else returned.grow(1);
setChanged();
}
/** The output is virtual; inputs are committed only after a result can be given to a player. */ /** The output is virtual; inputs are committed only after a result can be given to a player. */
public boolean canConsumeCopyMaterials() { public boolean canConsumeCopyMaterials() {
ItemStack paper = items.get(PAPER); ItemStack paper = items.get(PAPER);
@@ -96,10 +121,8 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
int cost = WritingInkService.symbolCopyCost(); int cost = WritingInkService.symbolCopyCost();
if (inkRemaining >= cost) return true; if (inkRemaining >= cost) return true;
ItemStack vial = items.get(INK); ItemStack vial = items.get(INK);
ItemStack returned = items.get(RETURN);
return vial.is(ModItems.INK_VIAL.get()) && inkRemaining + InkVialItem.remaining(vial) >= cost return vial.is(ModItems.INK_VIAL.get()) && inkRemaining + InkVialItem.remaining(vial) >= cost
&& (returned.isEmpty() || returned.is(Items.GLASS_BOTTLE) && hasBottleSpace();
&& returned.getCount() < returned.getMaxStackSize());
} }
public boolean consumeCopyMaterials() { public boolean consumeCopyMaterials() {
@@ -117,8 +140,13 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
inkRemaining -= cost; inkRemaining -= cost;
removeItem(PAPER, 1); removeItem(PAPER, 1);
setChanged(); setChanged();
if (inkRemaining == 0) refillEmptyInkBar();
return true; return true;
} }
@Override public void setLevel(Level level) {
super.setLevel(level);
refillEmptyInkBar();
}
@Override public void clearContent() { @Override public void clearContent() {
items.clear(); items.clear();
items = NonNullList.withSize(SIZE, ItemStack.EMPTY); items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
@@ -130,15 +158,25 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
super.loadAdditional(tag, registries); super.loadAdditional(tag, registries);
items = NonNullList.withSize(SIZE, ItemStack.EMPTY); items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
ContainerHelper.loadAllItems(tag, items, registries); ContainerHelper.loadAllItems(tag, items, registries);
// Pre-88-tab desks stored materials in slots 4..7. Move them once;
// never interpret those stacks as newly added collection tabs.
if (tag.getInt("InventoryVersion") < 1) {
for (int i = 0; i < 4; i++) {
items.set(INK + i, items.get(4 + i));
items.set(4 + i, ItemStack.EMPTY);
}
}
activeCollection = Math.clamp(tag.getInt("ActiveCollection"), 0, COLLECTIONS - 1); activeCollection = Math.clamp(tag.getInt("ActiveCollection"), 0, COLLECTIONS - 1);
inkRemaining = Math.clamp(tag.getInt("InkRemaining"), 0, InkVialItem.CAPACITY); inkRemaining = Math.clamp(tag.getInt("InkRemaining"), 0, InkVialItem.CAPACITY);
displayPage = tag.contains("DisplayPage", CompoundTag.TAG_COMPOUND) displayPage = tag.contains("DisplayPage", CompoundTag.TAG_COMPOUND)
? ItemStack.parse(registries, tag.getCompound("DisplayPage")).orElse(ItemStack.EMPTY) ? ItemStack.parse(registries, tag.getCompound("DisplayPage")).orElse(ItemStack.EMPTY)
: ItemStack.EMPTY; : ItemStack.EMPTY;
refillEmptyInkBar();
} }
@Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { @Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries); super.saveAdditional(tag, registries);
ContainerHelper.saveAllItems(tag, items, registries); ContainerHelper.saveAllItems(tag, items, registries);
tag.putInt("InventoryVersion", 1);
tag.putInt("ActiveCollection", activeCollection); tag.putInt("ActiveCollection", activeCollection);
tag.putInt("InkRemaining", inkRemaining); tag.putInt("InkRemaining", inkRemaining);
if (!displayPage.isEmpty()) tag.put("DisplayPage", displayPage.save(registries)); if (!displayPage.isEmpty()) tag.put("DisplayPage", displayPage.save(registries));
@@ -7,6 +7,7 @@ import java.util.Comparator;
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.block.entity.WritingDeskBlockEntity;
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;
@@ -18,14 +19,17 @@ import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
/** Functional storage surface; full Legacy transcription controls are a later Milestone 4 slice. */ /** Legacy desk atlas layout, with four visible tabs out of the desk's 88 collections. */
public final class WritingDeskScreen extends AbstractContainerScreen<WritingDeskMenu> { public final class WritingDeskScreen extends AbstractContainerScreen<WritingDeskMenu> {
private static final ResourceLocation PAGE_ATLAS = ResourceLocation.fromNamespaceAndPath( private static final ResourceLocation PAGE_ATLAS = ResourceLocation.fromNamespaceAndPath(
Rivencraft.MOD_ID, "textures/gui/notebook_pages.png"); Rivencraft.MOD_ID, "textures/gui/notebook_pages.png");
private static final ResourceLocation LEGACY_DESK = ResourceLocation.fromNamespaceAndPath( private static final ResourceLocation LEGACY_DESK = ResourceLocation.fromNamespaceAndPath(
Rivencraft.MOD_ID, "textures/gui/writing_desk_legacy.png"); Rivencraft.MOD_ID, "textures/gui/writing_desk_legacy.png");
private static final ResourceLocation SCROLLBAR = ResourceLocation.fromNamespaceAndPath(
Rivencraft.MOD_ID, "textures/gui/scrollbar.png");
private int lastCollection = -1; private int lastCollection = -1;
private int selectedPage; private int selectedPage;
private int scrollRows; private int scrollRows;
@@ -39,11 +43,20 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
private Button sortButton; private Button sortButton;
private boolean draggingScrollbar; private boolean draggingScrollbar;
private record DisplayPage(int collection, int index, ItemStack stack) {} private record DisplayPage(int collection, int index, ItemStack stack) {}
private DisplayPage pageDragSource;
private double pageDragStartX;
private double pageDragStartY;
private boolean draggingPage;
private boolean isPhysicalPageView() {
return !allMode && !sortAlphabetically && selectedCategory == null
&& (search == null || search.getValue().isBlank());
}
public WritingDeskScreen(WritingDeskMenu menu, Inventory inventory, Component title) { public WritingDeskScreen(WritingDeskMenu menu, Inventory inventory, Component title) {
super(menu, inventory, title); super(menu, inventory, title);
imageWidth = 398; imageWidth = 409;
imageHeight = 176; imageHeight = 186;
titleLabelX = 222; titleLabelX = 222;
} }
@@ -52,7 +65,7 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
sortButton = addRenderableWidget(Button.builder(Component.literal("AZ"), button -> { sortButton = addRenderableWidget(Button.builder(Component.literal("AZ"), button -> {
sortAlphabetically = !sortAlphabetically; sortAlphabetically = !sortAlphabetically;
scrollRows = 0; scrollRows = 0;
}).bounds(leftPos + 58, topPos - 18, 20, 18).build()); }).bounds(leftPos + 58, topPos, 20, 18).build());
allButton = addRenderableWidget(Button.builder(Component.literal("ALL"), button -> { allButton = addRenderableWidget(Button.builder(Component.literal("ALL"), button -> {
allMode = !allMode; allMode = !allMode;
scrollRows = 0; scrollRows = 0;
@@ -60,10 +73,10 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
selectedCollection = -1; selectedCollection = -1;
selectedPage = 0; selectedPage = 0;
if (minecraft != null && minecraft.gameMode != null) if (minecraft != null && minecraft.gameMode != null)
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, menu.activeCollection()); minecraft.gameMode.handleInventoryButtonClick(menu.containerId, WritingDeskMenu.PAGE_BUTTON_BASE);
} }
}).bounds(leftPos + 80, topPos - 18, 28, 18).build()); }).bounds(leftPos + 80, topPos, 28, 18).build());
search = new EditBox(font, leftPos + 110, topPos - 18, 86, 18, search = new EditBox(font, leftPos + 110, topPos, 101, 18,
Component.translatable("screen.rivencraft.writing_desk.search")); Component.translatable("screen.rivencraft.writing_desk.search"));
search.setMaxLength(64); search.setMaxLength(64);
search.setHint(Component.translatable("screen.rivencraft.writing_desk.search")); search.setHint(Component.translatable("screen.rivencraft.writing_desk.search"));
@@ -76,20 +89,22 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
selectedCategory = next >= categories.size() ? null : categories.get(next); selectedCategory = next >= categories.size() ? null : categories.get(next);
categoryButton.setMessage(Component.literal(selectedCategory == null ? "C" : "C*")); categoryButton.setMessage(Component.literal(selectedCategory == null ? "C" : "C*"));
scrollRows = 0; scrollRows = 0;
}).bounds(leftPos + 198, topPos - 18, 18, 18).build()); }).bounds(leftPos + 213, topPos, 18, 18).build());
} }
private List<DisplayPage> filteredPages() { private List<DisplayPage> filteredPages() {
String query = search == null ? "" : search.getValue().trim().toLowerCase(Locale.ROOT); String query = search == null ? "" : search.getValue().trim().toLowerCase(Locale.ROOT);
List<DisplayPage> entries = new ArrayList<>(); List<DisplayPage> entries = new ArrayList<>();
int from = allMode ? 0 : menu.activeCollection(); int from = allMode ? 0 : menu.activeCollection();
int until = allMode ? 4 : from + 1; int until = allMode ? WritingDeskBlockEntity.COLLECTIONS : from + 1;
for (int collection = from; collection < until; collection++) { for (int collection = from; collection < until; collection++) {
List<ItemStack> pages = pages(collection); List<ItemStack> pages = pages(collection);
int last = -1; int last = -1;
for (int i = 0; i < pages.size(); i++) if (!pages.get(i).isEmpty()) last = i; for (int i = 0; i < pages.size(); i++) if (!pages.get(i).isEmpty()) last = i;
// A notebook has exactly its normal GUI capacity; a loose page has one position.
if (isPhysicalPageView()) last = menu.pageCapacity(collection) - 1;
for (int i = 0; i <= last; i++) { for (int i = 0; i <= last; i++) {
ItemStack page = pages.get(i); ItemStack page = i < pages.size() ? pages.get(i) : ItemStack.EMPTY;
if (page.isEmpty() && (allMode || sortAlphabetically || !query.isEmpty() || selectedCategory != null)) continue; if (page.isEmpty() && (allMode || sortAlphabetically || !query.isEmpty() || selectedCategory != null)) continue;
if (!page.isEmpty() && !query.isEmpty() if (!page.isEmpty() && !query.isEmpty()
&& !page.getHoverName().getString().toLowerCase(Locale.ROOT).contains(query) && !page.getHoverName().getString().toLowerCase(Locale.ROOT).contains(query)
@@ -118,47 +133,48 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
@Override protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { @Override protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) {
int x = leftPos; int x = leftPos;
int y = topPos; int y = topPos;
graphics.fill(x + 56, y + 14, x + 206, y + 168, 0xAA201B18); // Legacy GuiWritingDesk draws the entire 176x166 panel, not a stretched swatch.
graphics.blit(LEGACY_DESK, x, y + 5, 56, 9, 0, 166, 56, 9, 256, 256); graphics.blit(LEGACY_DESK, x + 233, y + 20, 0, 0, 176, 166);
graphics.blit(LEGACY_DESK, x, y + 166, 56, 9, 0, 215, 56, 9, 256, 256); drawTabControl(graphics, 20, 203, menu.firstVisibleTab() > 0);
graphics.blit(LEGACY_DESK, x + 218, y + 4, 178, 170, 64, 8, 16, 16, 256, 256);
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
graphics.blit(LEGACY_DESK, x, y + 16 + i * 37, int tab = menu.firstVisibleTab() + i;
56, 36, 0, 166, 56, 54, 256, 256); int tabY = y + 29 + i * 37;
if (i == menu.activeCollection() && !allMode) if (tab == menu.activeCollection() && !allMode) graphics.setColor(0.5F, 0.5F, 1F, 1F);
graphics.fill(x + 2, y + 18 + i * 37, x + 54, y + 46 + i * 37, 0x885858CC); graphics.blit(LEGACY_DESK, x, tabY, 0, 166, 58, 37);
graphics.drawString(font, new String[]{"I", "II", "III", "IV"}[i], graphics.setColor(1F, 1F, 1F, 1F);
x + 8, y + 25 + i * 37, 0xFF181818, false); graphics.drawString(font, Integer.toString(tab + 1), x + 8, tabY + 8, 0xFF181818, false);
slotFrame(graphics, 36, 20 + i * 37); ItemStack collection = menu.getSlot(tab).getItem();
if (!collection.isEmpty()) {
String name = collection.getHoverName().getString();
float scale = Math.min(1F, 50F / Math.max(1, font.width(name)));
graphics.pose().pushPose();
graphics.pose().translate(x + 4, tabY + 25, 0);
graphics.pose().scale(scale, scale, 1);
graphics.drawString(font, name, 0, 0, 0xFF404040, false);
graphics.pose().popPose();
}
} }
graphics.blit(LEGACY_DESK, x + 206, y + 14, 10, 130, 177, 0, 12, 166, 256, 256); drawTabControl(graphics, 177, 212, menu.firstVisibleTab() < WritingDeskBlockEntity.COLLECTIONS - 4);
// GuiElementVSlider: 4-pixel caps, repeated 22-pixel track, 12x15 thumb.
graphics.blit(SCROLLBAR, x + 213, y + 20, 0, 0, 20, 4, 32, 32);
for (int offset = 4; offset < 162; offset += 22)
graphics.blit(SCROLLBAR, x + 213, y + 20 + offset, 0, 4, 20, Math.min(22, 162 - offset), 32, 32);
graphics.blit(SCROLLBAR, x + 213, y + 182, 0, 26, 20, 4, 32, 32);
int totalRows = (filteredPages().size() + 4) / 5; int totalRows = (filteredPages().size() + 4) / 5;
int maxScroll = Math.max(0, totalRows - 3); int maxScroll = Math.max(0, totalRows - 4);
scrollRows = Math.clamp(scrollRows, 0, maxScroll); scrollRows = Math.clamp(scrollRows, 0, maxScroll);
int thumbHeight = Math.max(14, 126 * 3 / Math.max(3, totalRows)); int thumbY = y + 24 + (maxScroll == 0 ? 0 : scrollRows * 143 / maxScroll);
int thumbY = y + 16 + (maxScroll == 0 ? 0 : scrollRows * (126 - thumbHeight) / maxScroll); graphics.blit(SCROLLBAR, x + 217, thumbY, 20, draggingScrollbar ? 15 : 0, 12, 15, 32, 32);
graphics.fill(x + 207, thumbY, x + 215, thumbY + thumbHeight, 0xFFB7B7B7); graphics.fill(x + 365, y + 27, x + 381, y + 97, 0xFF303035);
graphics.renderOutline(x + 207, thumbY, 8, thumbHeight, 0xFF424242); graphics.fill(x + 366, y + 28, x + 380, y + 96, 0xFF6666AA);
slotFrame(graphics, 370, 9); int filled = 68 * menu.inkRemaining() / 16;
slotFrame(graphics, 228, 9); if (filled > 0) graphics.fill(x + 366, y + 96 - filled, x + 380, y + 96, 0xFF15151B);
slotFrame(graphics, 228, 57);
slotFrame(graphics, 370, 57);
graphics.fill(x + 350, y + 9, x + 364, y + 64, 0xFF303035);
graphics.fill(x + 352, y + 11, x + 362, y + 62, 0xFF15151B);
int filled = 51 * menu.inkRemaining() / 16;
if (filled > 0) graphics.fill(x + 352, y + 62 - filled, x + 362, y + 62, 0xFF6666AA);
graphics.renderOutline(x + 350, y + 9, 14, 55, 0xFF484848);
for (int row = 0; row < 3; row++) {
for (int column = 0; column < 9; column++) slotFrame(graphics, 224 + column * 18, 79 + row * 18);
}
for (int column = 0; column < 9; column++) slotFrame(graphics, 224 + column * 18, 137);
} }
private void slotFrame(GuiGraphics graphics, int x, int y) { private void drawTabControl(GuiGraphics graphics, int y, int textureY, boolean enabled) {
graphics.fill(leftPos + x - 1, topPos + y - 1, if (!enabled) graphics.setColor(0.4F, 0.4F, 0.4F, 1F);
leftPos + x + 17, topPos + y + 17, 0xFF4D4D4D); graphics.blit(LEGACY_DESK, leftPos, topPos + y, 0, textureY, 58, 9);
graphics.fill(leftPos + x, topPos + y, graphics.setColor(1F, 1F, 1F, 1F);
leftPos + x + 16, topPos + y + 16, 0xFF858585);
} }
@Override protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { @Override protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {
@@ -167,17 +183,26 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
@Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
super.render(graphics, mouseX, mouseY, partialTick); super.render(graphics, mouseX, mouseY, partialTick);
if (sortAlphabetically) graphics.renderOutline(leftPos + 58, topPos - 18, 20, 18, 0xFFFFD061); if (sortAlphabetically) graphics.renderOutline(leftPos + 58, topPos, 20, 18, 0xFFFFD061);
if (allMode) graphics.renderOutline(leftPos + 80, topPos - 18, 28, 18, 0xFFFFD061); if (allMode) graphics.renderOutline(leftPos + 80, topPos, 28, 18, 0xFFFFD061);
renderCollection(graphics); renderCollection(graphics);
if (draggingPage && pageDragSource != null) {
DisplayPage target = pageAtMouse(mouseX, mouseY);
if (target != null && canMovePage(pageDragSource, target)) {
int x = leftPos + 58 + ((int) mouseX - leftPos - 58) / 31 * 31;
int y = topPos + 20 + ((int) mouseY - topPos - 20) / 41 * 41;
graphics.renderOutline(x, y, 30, 40, 0xFF57D478);
}
graphics.renderItem(pageDragSource.stack(), mouseX - 8, mouseY - 8);
}
renderTooltip(graphics, mouseX, mouseY); renderTooltip(graphics, mouseX, mouseY);
if (mouseX >= leftPos + 350 && mouseX < leftPos + 364 if (mouseX >= leftPos + 365 && mouseX < leftPos + 381
&& mouseY >= topPos + 9 && mouseY < topPos + 64) { && mouseY >= topPos + 27 && mouseY < topPos + 97) {
graphics.renderTooltip(font, Component.translatable("screen.rivencraft.writing_desk.ink", graphics.renderTooltip(font, Component.translatable("screen.rivencraft.writing_desk.ink",
menu.inkRemaining(), 16), mouseX, mouseY); menu.inkRemaining(), 16), mouseX, mouseY);
} }
if (mouseX >= leftPos + 198 && mouseX < leftPos + 216 if (mouseX >= leftPos + 213 && mouseX < leftPos + 231
&& mouseY >= topPos - 18 && mouseY < topPos) { && mouseY >= topPos && mouseY < topPos + 18) {
graphics.renderTooltip(font, Component.literal(selectedCategory == null graphics.renderTooltip(font, Component.literal(selectedCategory == null
? "All categories" : selectedCategory.toString()), mouseX, mouseY); ? "All categories" : selectedCategory.toString()), mouseX, mouseY);
} }
@@ -200,13 +225,13 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
selectedCollection = -1; selectedCollection = -1;
} }
List<DisplayPage> entries = filteredPages(); List<DisplayPage> entries = filteredPages();
for (int i = scrollRows * 5; i < entries.size() && i < scrollRows * 5 + 15; i++) { for (int i = scrollRows * 5; i < entries.size() && i < scrollRows * 5 + 20; i++) {
int column = i % 5; int column = i % 5;
int row = i / 5 - scrollRows; int row = i / 5 - scrollRows;
DisplayPage entry = entries.get(i); DisplayPage entry = entries.get(i);
ItemStack page = entry.stack(); ItemStack page = entry.stack();
int x = leftPos + 58 + column * 29; int x = leftPos + 58 + column * 31;
int y = topPos + 18 + row * 41; int y = topPos + 20 + row * 41;
drawPage(graphics, page, x, y); drawPage(graphics, page, x, y);
if (page.isEmpty()) graphics.fill(x, y, x + 30, y + 40, 0xB8000000); if (page.isEmpty()) graphics.fill(x, y, x + 30, y + 40, 0xB8000000);
int active = selectedCollection >= 0 ? selectedCollection : menu.activeCollection(); int active = selectedCollection >= 0 ? selectedCollection : menu.activeCollection();
@@ -217,10 +242,10 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
List<ItemStack> source = pages(active); List<ItemStack> source = pages(active);
if (selectedPage >= source.size() || source.get(selectedPage).isEmpty()) return; if (selectedPage >= source.size() || source.get(selectedPage).isEmpty()) return;
ItemStack selected = source.get(selectedPage); ItemStack selected = source.get(selectedPage);
drawPage(graphics, selected, leftPos + 253, topPos + 10); drawPage(graphics, selected, leftPos + 265, topPos + 26);
String title = font.plainSubstrByWidth(selected.getHoverName().getString(), 84); String title = font.plainSubstrByWidth(selected.getHoverName().getString(), 95);
graphics.fill(leftPos + 251, topPos + 58, leftPos + 342, topPos + 73, 0xFF202020); graphics.fill(leftPos + 261, topPos + 81, leftPos + 360, topPos + 95, 0xFF202020);
graphics.drawString(font, title, leftPos + 254, topPos + 61, 0xFFE0E0E0, false); graphics.drawString(font, title, leftPos + 263, topPos + 84, 0xFFE0E0E0, false);
} }
private void drawPage(GuiGraphics graphics, ItemStack page, int x, int y) { private void drawPage(GuiGraphics graphics, ItemStack page, int x, int y) {
@@ -234,44 +259,116 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
} }
} }
private DisplayPage pageAtMouse(double mouseX, double mouseY) {
int x = (int) Math.floor(mouseX - leftPos - 58);
int y = (int) Math.floor(mouseY - topPos - 20);
if (x < 0 || x >= 155 || y < 0 || y >= 164 || x % 31 >= 30 || y % 41 >= 40) return null;
int index = (y / 41 + scrollRows) * 5 + x / 31;
List<DisplayPage> entries = filteredPages();
return index < entries.size() ? entries.get(index) : null;
}
private boolean canMovePage(DisplayPage source, DisplayPage target) {
return !source.stack().isEmpty()
&& (menu.getSlot(source.collection()).getItem().is(ModItems.NOTEBOOK.get())
|| menu.getSlot(source.collection()).getItem().is(ModItems.PAGE.get()))
&& (menu.getSlot(target.collection()).getItem().is(ModItems.NOTEBOOK.get())
|| menu.getSlot(target.collection()).getItem().is(ModItems.PAGE.get()))
&& (source.collection() != target.collection() || source.index() != target.index());
}
private int extractTargetSlot(double mouseX, double mouseY) {
for (Slot slot : menu.slots) {
int index = slot.index;
if (index >= WritingDeskBlockEntity.COLLECTIONS && index <= WritingDeskMenu.COPY_RESULT) continue;
if (!slot.isActive() || !isHovering(slot.x, slot.y, 16, 16, mouseX, mouseY)) continue;
ItemStack held = pageDragSource.stack();
if (!slot.mayPlace(held)) return -1;
ItemStack current = slot.getItem();
if (current.isEmpty()) return index;
if (index < WritingDeskBlockEntity.COLLECTIONS && current.is(ModItems.NOTEBOOK.get())) {
List<ItemStack> pages = current.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
if (pages.size() < NotebookContents.MAX_PAGES || pages.stream().anyMatch(ItemStack::isEmpty))
return index;
} else if (index > WritingDeskMenu.COPY_RESULT && ItemStack.isSameItemSameComponents(current, held)
&& current.getCount() < slot.getMaxStackSize(held)) return index;
return -1;
}
return -1;
}
@Override public boolean mouseClicked(double mouseX, double mouseY, int button) { @Override public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (button == 0) { if (button == 0) {
if (mouseX >= leftPos + 206 && mouseX < leftPos + 217 pageDragSource = null;
&& mouseY >= topPos + 14 && mouseY < topPos + 144) { draggingPage = false;
}
int surfaceX = (int) Math.floor(mouseX - leftPos - 58);
int surfaceY = (int) Math.floor(mouseY - topPos - 20);
if ((button == 0 || button == 1) && !menu.getCarried().isEmpty()
&& surfaceX >= 0 && surfaceX < 155 && surfaceY >= 0 && surfaceY < 164) {
if (menu.getCarried().is(ModItems.PAGE.get()) && surfaceX % 31 < 30 && surfaceY % 41 < 40) {
int index = (surfaceY / 41 + scrollRows) * 5 + surfaceX / 31;
List<DisplayPage> entries = filteredPages();
// Only displayed positions are addressable. Filtering never turns
// empty screen space into additional storage for a loose-page tab.
int collection = index < entries.size() ? entries.get(index).collection() : menu.activeCollection();
int page = index < entries.size() ? entries.get(index).index() : index;
if (index < entries.size() && page < menu.pageCapacity(collection)
&& minecraft != null && minecraft.gameMode != null) {
minecraft.gameMode.handleInventoryButtonClick(menu.containerId,
WritingDeskMenu.PLACE_PAGE_BUTTON_BASE + collection * NotebookContents.MAX_PAGES + page);
}
}
return true;
}
if (button == 0) {
if (mouseX >= leftPos && mouseX < leftPos + 58) {
int y = (int) mouseY - topPos;
if (y >= 20 && y < 29 || y >= 177 && y < 186) {
if (minecraft != null && minecraft.gameMode != null)
minecraft.gameMode.handleInventoryButtonClick(menu.containerId,
y < 29 ? WritingDeskMenu.PREVIOUS_TABS : WritingDeskMenu.NEXT_TABS);
return true;
}
}
if (mouseX >= leftPos + 213 && mouseX < leftPos + 233
&& mouseY >= topPos + 20 && mouseY < topPos + 186) {
draggingScrollbar = true; draggingScrollbar = true;
setScrollFromMouse(mouseY); setScrollFromMouse(mouseY);
return true; return true;
} }
int x = (int) mouseX - leftPos - 58; int x = (int) Math.floor(mouseX - leftPos - 58);
int y = (int) mouseY - topPos - 18; int y = (int) Math.floor(mouseY - topPos - 20);
if (x >= 0 && x < 145 && y >= 0 && y < 123) { if (x >= 0 && x < 155 && y >= 0 && y < 164) {
int column = x / 29; DisplayPage entry = pageAtMouse(mouseX, mouseY);
int row = y / 41; if (entry != null && !entry.stack().isEmpty()) {
int index = (row + scrollRows) * 5 + column; selectedCollection = entry.collection();
List<DisplayPage> entries = filteredPages(); selectedPage = entry.index();
if (y % 41 < 40 && index < entries.size()) { if (menu.getCarried().isEmpty()) {
DisplayPage entry = entries.get(index); pageDragSource = entry;
if (!entry.stack().isEmpty()) { pageDragStartX = mouseX;
selectedCollection = entry.collection(); pageDragStartY = mouseY;
selectedPage = entry.index(); }
if (minecraft != null && minecraft.gameMode != null) { if (minecraft != null && minecraft.gameMode != null) {
int id = allMode ? 1000 + entry.collection() * NotebookContents.MAX_PAGES + entry.index() int id = WritingDeskMenu.ALL_PAGE_BUTTON_BASE
: 16 + entry.index(); + entry.collection() * NotebookContents.MAX_PAGES + entry.index();
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, id); minecraft.gameMode.handleInventoryButtonClick(menu.containerId, id);
}
} }
} }
return true; return true;
} }
} }
if (button == 0 && mouseX >= leftPos && mouseX < leftPos + 35) { if (button == 0 && mouseX >= leftPos && mouseX < leftPos + 58) {
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
int top = topPos + 16 + i * 37; int top = topPos + 29 + i * 37;
if (mouseY >= top && mouseY < top + 36) { boolean overSlot = mouseX >= leftPos + 36 && mouseX < leftPos + 54
&& mouseY >= top + 4 && mouseY < top + 22;
if (mouseY >= top && mouseY < top + 37 && !overSlot) {
allMode = false; allMode = false;
selectedCollection = -1; selectedCollection = -1;
if (minecraft != null && minecraft.gameMode != null) { if (minecraft != null && minecraft.gameMode != null) {
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, i); minecraft.gameMode.handleInventoryButtonClick(menu.containerId, menu.firstVisibleTab() + i);
} }
return true; return true;
} }
@@ -281,10 +378,10 @@ 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 + 58 && mouseX < leftPos + 217 if (mouseX >= leftPos + 58 && mouseX < leftPos + 233
&& mouseY >= topPos + 18 && mouseY < topPos + 141) { && mouseY >= topPos + 20 && mouseY < topPos + 186) {
int rows = (filteredPages().size() + 4) / 5; int rows = (filteredPages().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 - 4));
return true; return true;
} }
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
@@ -292,11 +389,16 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
private void setScrollFromMouse(double mouseY) { private void setScrollFromMouse(double mouseY) {
int rows = (filteredPages().size() + 4) / 5; int rows = (filteredPages().size() + 4) / 5;
int max = Math.max(0, rows - 3); int max = Math.max(0, rows - 4);
scrollRows = Math.clamp((int) Math.round((mouseY - topPos - 16) * max / 126.0), 0, max); scrollRows = Math.clamp((int) Math.round((mouseY - topPos - 31.5) * max / 143.0), 0, max);
} }
@Override public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { @Override public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) {
if (button == 0 && pageDragSource != null) {
if (Math.abs(mouseX - pageDragStartX) >= 4 || Math.abs(mouseY - pageDragStartY) >= 4)
draggingPage = true;
return true;
}
if (button == 0 && draggingScrollbar) { if (button == 0 && draggingScrollbar) {
setScrollFromMouse(mouseY); setScrollFromMouse(mouseY);
return true; return true;
@@ -305,6 +407,27 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
} }
@Override public boolean mouseReleased(double mouseX, double mouseY, int button) { @Override public boolean mouseReleased(double mouseX, double mouseY, int button) {
if (button == 0 && pageDragSource != null) {
if (draggingPage) {
DisplayPage target = pageAtMouse(mouseX, mouseY);
if (target != null && canMovePage(pageDragSource, target)
&& minecraft != null && minecraft.gameMode != null) {
selectedCollection = target.collection();
selectedPage = target.index();
minecraft.gameMode.handleInventoryButtonClick(menu.containerId,
WritingDeskMenu.movePageButtonId(pageDragSource.collection(), pageDragSource.index(),
target.collection(), target.index()));
} else if (target == null && minecraft != null && minecraft.gameMode != null) {
int targetSlot = extractTargetSlot(mouseX, mouseY);
minecraft.gameMode.handleInventoryButtonClick(menu.containerId,
WritingDeskMenu.extractPageButtonId(pageDragSource.collection(), pageDragSource.index(),
targetSlot));
}
}
pageDragSource = null;
draggingPage = false;
return true;
}
if (button == 0 && draggingScrollbar) { if (button == 0 && draggingScrollbar) {
draggingScrollbar = false; draggingScrollbar = false;
return true; return true;
@@ -61,10 +61,10 @@ public final class WritingDeskGameTests {
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 bottles = drops.stream().filter(e -> e.getItem().is(Items.GLASS_BOTTLE)).mapToInt(e -> e.getItem().getCount()).sum();
int pages = drops.stream().filter(e -> e.getItem().is(Items.PAPER)).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(bottles, 1, "Consumed vial must return one glass bottle on break");
helper.assertValueEqual(pages, 12, "Pages must drop without loss"); helper.assertValueEqual(pages, 12, "Pages must drop without loss");
helper.succeed(); helper.succeed();
} }
@@ -81,7 +81,7 @@ public final class WritingDeskGameTests {
helper.assertTrue(menu.stillValid(player), "Placed desk must keep its menu valid"); 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.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"); helper.assertValueEqual(desk.activeCollection(), 3, "Selected tab must be stored on the desk");
menu.quickMoveStack(player, 9); menu.quickMoveStack(player, WritingDeskMenu.COPY_RESULT + 1);
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 12, "Shift-click must deposit all pages"); 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"); helper.assertTrue(player.getInventory().getItem(9).isEmpty(), "Shift-click must consume player source stack");
menu.quickMoveStack(player, WritingDeskBlockEntity.PAPER); menu.quickMoveStack(player, WritingDeskBlockEntity.PAPER);
@@ -111,12 +111,12 @@ public final class WritingDeskGameTests {
"A source page with paper and ink must preview one copy"); "A source page with paper and ink must preview one copy");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 2, helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 2,
"Previewing must not consume paper"); "Previewing must not consume paper");
helper.assertValueEqual(desk.inkRemaining(), 0, "Previewing must not consume ink"); helper.assertValueEqual(desk.inkRemaining(), 16, "Inserting a vial must fill the empty bar before copying");
menu.clicked(WritingDeskMenu.COPY_RESULT, 0, ClickType.PICKUP, player); 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.assertTrue(menu.getCarried().is(ModItems.PAGE.get()), "Taking result must give the copied page");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 1, helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 1,
"Taking a copy must consume one paper"); "Taking a copy must consume one paper");
helper.assertValueEqual(desk.inkRemaining(), 15, "First copy must refill to 16 and consume one ink"); helper.assertValueEqual(desk.inkRemaining(), 15, "First copy must consume one ink");
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.INK).isEmpty(), "Refill must consume one vial"); helper.assertTrue(desk.getItem(WritingDeskBlockEntity.INK).isEmpty(), "Refill must consume one vial");
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.RETURN).is(Items.GLASS_BOTTLE), helper.assertTrue(desk.getItem(WritingDeskBlockEntity.RETURN).is(Items.GLASS_BOTTLE),
"Refill must return a vanilla glass bottle"); "Refill must return a vanilla glass bottle");
@@ -135,6 +135,48 @@ public final class WritingDeskGameTests {
helper.succeed(); helper.succeed();
} }
@GameTest(template = "empty")
public static void emptyDeskInkBarRefillsWithoutCopying(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(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get(), 2));
helper.assertValueEqual(desk.inkRemaining(), 16, "Inserting a vial must refill immediately");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.INK).getCount(), 1,
"Refill must consume exactly one vial");
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.RETURN).is(Items.GLASS_BOTTLE),
"Refill must put a vanilla glass bottle in the return slot");
helper.runAfterDelay(2, () -> {
helper.assertValueEqual(desk.inkRemaining(), 16, "Full bar must not refill again");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.INK).getCount(), 1,
"Full bar must leave the second vial untouched");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.RETURN).getCount(), 1,
"Full bar must not create another bottle");
helper.succeed();
});
}
@GameTest(template = "empty")
public static void emptyDeskInkBarWaitsForBottleSpace(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(WritingDeskBlockEntity.RETURN, new ItemStack(Items.GLASS_BOTTLE, 64));
desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
helper.assertValueEqual(desk.inkRemaining(), 0, "Full bottle slot must block automatic refill");
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.INK).is(ModItems.INK_VIAL.get()),
"Blocked refill must preserve the vial");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.RETURN).getCount(), 64,
"Blocked refill must preserve the bottle stack");
desk.removeItem(WritingDeskBlockEntity.RETURN, 1);
helper.assertValueEqual(desk.inkRemaining(), 16, "Removing a bottle must immediately permit refill");
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.INK).isEmpty(),
"Refill must consume the waiting vial");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.RETURN).getCount(), 64,
"Refill must return one glass bottle to the available space");
helper.succeed();
}
@GameTest(template = "empty") @GameTest(template = "empty")
public static void shiftClickCopiesOnlyWhenResourcesAndBottleSpaceExist(GameTestHelper helper) { public static void shiftClickCopiesOnlyWhenResourcesAndBottleSpaceExist(GameTestHelper helper) {
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1)); BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
@@ -142,8 +184,8 @@ public final class WritingDeskGameTests {
WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos); WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos);
desk.setItem(0, PageItem.createLinkPanelPage()); desk.setItem(0, PageItem.createLinkPanelPage());
desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER, 2)); 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)); desk.setItem(WritingDeskBlockEntity.RETURN, new ItemStack(Items.GLASS_BOTTLE, 64));
desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
var player = helper.makeMockServerPlayerInLevel(); var player = helper.makeMockServerPlayerInLevel();
player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk); WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk);
@@ -180,7 +222,7 @@ public final class WritingDeskGameTests {
menu.broadcastChanges(); menu.broadcastChanges();
helper.assertTrue(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem().isEmpty(), helper.assertTrue(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem().isEmpty(),
"An empty selected position must not create a copy"); "An empty selected position must not create a copy");
helper.assertTrue(menu.clickMenuButton(player, 17), "Second page selection must be accepted"); helper.assertTrue(menu.clickMenuButton(player, WritingDeskMenu.PAGE_BUTTON_BASE + 1), "Second page selection must be accepted");
menu.broadcastChanges(); menu.broadcastChanges();
helper.assertTrue(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem().is(ModItems.PAGE.get()), helper.assertTrue(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem().is(ModItems.PAGE.get()),
"Selecting the populated page must show a copy"); "Selecting the populated page must show a copy");
@@ -230,8 +272,8 @@ public final class WritingDeskGameTests {
var player = helper.makeMockServerPlayerInLevel(); var player = helper.makeMockServerPlayerInLevel();
player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk); WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk);
helper.assertFalse(menu.clickMenuButton(player, 101), "Out-of-range catalog index must be rejected"); helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.CATALOG_BUTTON_BASE + 1), "Out-of-range catalog index must be rejected");
helper.assertTrue(menu.clickMenuButton(player, 100), "Known catalog symbol must be selectable"); helper.assertTrue(menu.clickMenuButton(player, WritingDeskMenu.CATALOG_BUTTON_BASE), "Known catalog symbol must be selectable");
desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER)); desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER));
desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get())); desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
menu.broadcastChanges(); menu.broadcastChanges();
@@ -262,9 +304,9 @@ public final class WritingDeskGameTests {
"Full inventory must reject shift-click copy"); "Full inventory must reject shift-click copy");
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 1, helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 1,
"Rejected copy must preserve paper"); "Rejected copy must preserve paper");
helper.assertValueEqual(desk.inkRemaining(), 0, "Rejected copy must preserve ink meter"); helper.assertValueEqual(desk.inkRemaining(), 16, "Rejected copy must preserve the filled ink meter");
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.INK).is(ModItems.INK_VIAL.get()), helper.assertTrue(desk.getItem(WritingDeskBlockEntity.INK).isEmpty(),
"Rejected copy must preserve ink vial"); "The vial should already have refilled the empty meter");
helper.succeed(); helper.succeed();
} }
@@ -340,9 +382,9 @@ public final class WritingDeskGameTests {
var player = helper.makeMockServerPlayerInLevel(); var player = helper.makeMockServerPlayerInLevel();
player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5); player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk); WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk);
helper.assertFalse(menu.clickMenuButton(player, 1000 + 2 * NotebookContents.MAX_PAGES), helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.ALL_PAGE_BUTTON_BASE + 2 * NotebookContents.MAX_PAGES),
"ALL view must reject empty cross-tab positions"); "ALL view must reject empty cross-tab positions");
helper.assertTrue(menu.clickMenuButton(player, 1000 + NotebookContents.MAX_PAGES + 1), helper.assertTrue(menu.clickMenuButton(player, WritingDeskMenu.ALL_PAGE_BUTTON_BASE + NotebookContents.MAX_PAGES + 1),
"ALL view must accept the populated second tab page"); "ALL view must accept the populated second tab page");
helper.assertTrue(ItemStack.matches(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem(), second), helper.assertTrue(ItemStack.matches(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem(), second),
"Copy preview must match the physical page selected from the other tab"); "Copy preview must match the physical page selected from the other tab");
@@ -350,4 +392,231 @@ public final class WritingDeskGameTests {
"Selecting from ALL must not silently change the active tab"); "Selecting from ALL must not silently change the active tab");
helper.succeed(); helper.succeed();
} }
@GameTest(template = "empty")
public static void tabGroupsCycleByFourAndProtectHiddenSlots(GameTestHelper helper) {
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3);
var desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos);
var player = helper.makeMockServerPlayerInLevel();
player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
var menu = new WritingDeskMenu(1, player.getInventory(), desk);
var otherViewer = new WritingDeskMenu(2, player.getInventory(), desk);
menu.clickMenuButton(player, WritingDeskMenu.PREVIOUS_TABS);
helper.assertValueEqual(menu.firstVisibleTab(), 0, "Previous must stop at tabs 1-4");
menu.clickMenuButton(player, WritingDeskMenu.NEXT_TABS);
helper.assertValueEqual(menu.firstVisibleTab(), 4, "Next must show tabs 5-8");
helper.assertValueEqual(otherViewer.firstVisibleTab(), 0, "Cycling must not move another viewer's slots");
helper.assertFalse(menu.getSlot(0).isActive(), "Earlier tab slots must be hidden");
helper.assertTrue(menu.getSlot(4).isActive(), "New group's slots must be interactive");
desk.setItem(0, new ItemStack(ModItems.NOTEBOOK.get()));
menu.clicked(0, 0, ClickType.PICKUP, player);
helper.assertTrue(menu.getCarried().isEmpty(), "Hidden slot clicks must not take items");
helper.assertTrue(menu.quickMoveStack(player, 0).isEmpty(), "Hidden shift-click must be rejected");
player.getInventory().setItem(9, new ItemStack(ModItems.NOTEBOOK.get()));
menu.quickMoveStack(player, WritingDeskMenu.COPY_RESULT + 1);
helper.assertTrue(desk.getItem(4).is(ModItems.NOTEBOOK.get()), "Shift-click must use the visible group");
for (int i = 0; i < 30; i++) menu.clickMenuButton(player, WritingDeskMenu.NEXT_TABS);
helper.assertValueEqual(menu.firstVisibleTab(), 84, "Last group must be tabs 85-88");
helper.assertTrue(menu.clickMenuButton(player, 87), "Tab 88 must be selectable without page-ID collisions");
helper.assertValueEqual(desk.activeCollection(), 87, "Tab 88 must become active");
helper.assertFalse(menu.clickMenuButton(player, 88), "Tab 89 must not exist");
desk.setItem(87, PageItem.createLinkPanelPage());
desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER));
desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
helper.assertTrue(menu.clickMenuButton(player, WritingDeskMenu.ALL_PAGE_BUTTON_BASE + 87 * NotebookContents.MAX_PAGES),
"ALL must select a physical page from tab 88");
helper.assertTrue(menu.quickMoveStack(player, WritingDeskMenu.COPY_RESULT).is(ModItems.PAGE.get()),
"Tab 88's page must be copyable");
menu.clickMenuButton(player, WritingDeskMenu.PREVIOUS_TABS);
helper.assertValueEqual(menu.firstVisibleTab(), 80, "Previous must move back four tabs");
helper.succeed();
}
@GameTest(template = "empty")
public static void oldDeskInventoryMigratesAndAll88TabsPersist(GameTestHelper helper) {
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
var registries = helper.getLevel().registryAccess();
var oldItems = net.minecraft.core.NonNullList.withSize(8, ItemStack.EMPTY);
oldItems.set(0, new ItemStack(ModItems.NOTEBOOK.get()));
oldItems.set(3, PageItem.createLinkPanelPage());
oldItems.set(4, new ItemStack(ModItems.INK_VIAL.get(), 2));
oldItems.set(5, new ItemStack(Items.GLASS_BOTTLE, 3));
oldItems.set(6, new ItemStack(Items.PAPER, 12));
oldItems.set(7, PageItem.createLinkPanelPage());
var oldTag = new net.minecraft.nbt.CompoundTag();
net.minecraft.world.ContainerHelper.saveAllItems(oldTag, oldItems, registries);
oldTag.putInt("InkRemaining", 7);
oldTag.putInt("ActiveCollection", 3);
var desk = new WritingDeskBlockEntity(pos, ModBlocks.WRITING_DESK.get().defaultBlockState());
desk.loadWithComponents(oldTag, registries);
for (int i = 0; i < 4; i++) {
helper.assertTrue(ItemStack.matches(desk.getItem(i), oldItems.get(i)), "Original tab must survive migration");
helper.assertTrue(ItemStack.matches(desk.getItem(WritingDeskBlockEntity.INK + i), oldItems.get(4 + i)),
"Original supplies and legacy output must survive migration");
helper.assertTrue(desk.getItem(4 + i).isEmpty(), "Supplies must not be duplicated into new tabs");
}
helper.assertValueEqual(desk.inkRemaining(), 7, "Migration must preserve ink meter");
helper.assertValueEqual(desk.activeCollection(), 3, "Migration must preserve active tab");
for (int i = 0; i < WritingDeskBlockEntity.COLLECTIONS; i++) desk.setItem(i, PageItem.createLinkPanelPage());
desk.setActiveCollection(87);
var restored = new WritingDeskBlockEntity(pos, ModBlocks.WRITING_DESK.get().defaultBlockState());
restored.loadWithComponents(desk.saveWithoutMetadata(registries), registries);
for (int i = 0; i < WritingDeskBlockEntity.COLLECTIONS; i++)
helper.assertTrue(restored.getItem(i).is(ModItems.PAGE.get()), "All 88 tab contents must survive save/load");
helper.assertValueEqual(restored.activeCollection(), 87, "Tab 88 selection must survive save/load");
helper.assertValueEqual(restored.getItem(WritingDeskBlockEntity.PAPER).getCount(), 12, "Reload must not repeat migration");
helper.succeed();
}
@GameTest(template = "empty")
public static void surfacePlacesCarriedPagesWithoutLossOrOverwrite(GameTestHelper helper) {
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3);
var desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos);
desk.setItem(87, new ItemStack(ModItems.NOTEBOOK.get()));
var player = helper.makeMockServerPlayerInLevel();
player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
var menu = new WritingDeskMenu(1, player.getInventory(), desk);
ItemStack held = PageItem.createLinkPanelPage().copyWithCount(3);
ItemStack expectedPage = held.copyWithCount(1);
player.getInventory().setItem(9, held);
menu.clicked(WritingDeskMenu.COPY_RESULT + 1, 0, ClickType.PICKUP, player);
int lastPosition = WritingDeskMenu.PLACE_PAGE_BUTTON_BASE + 87 * NotebookContents.MAX_PAGES + 26;
helper.assertTrue(menu.clickMenuButton(player, lastPosition), "Surface must place into final notebook position");
helper.assertValueEqual(menu.getCarried().getCount(), 2, "Placing must consume exactly one held page");
var pages = desk.getItem(87).get(ModDataComponents.NOTEBOOK_CONTENTS.get()).pages();
helper.assertValueEqual(pages.size(), 27, "Placement must preserve addressed position");
helper.assertTrue(pages.get(0).isEmpty() && pages.get(25).isEmpty(), "Placement must preserve preceding gaps");
helper.assertTrue(ItemStack.matches(pages.get(26), expectedPage), "Stored page must preserve components");
helper.assertFalse(menu.clickMenuButton(player, lastPosition), "Occupied position must not be overwritten");
helper.assertValueEqual(menu.getCarried().getCount(), 2, "Rejected placement must preserve cursor");
helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.PLACE_PAGE_BUTTON_BASE + 88 * NotebookContents.MAX_PAGES),
"Out-of-range placement must be rejected");
helper.assertValueEqual(menu.pageCapacity(87), NotebookContents.MAX_PAGES, "Notebook tab must expose normal notebook capacity");
helper.assertValueEqual(menu.pageCapacity(0), 0, "Empty tab has no page storage");
helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.PLACE_PAGE_BUTTON_BASE), "Empty tab must require a collection first");
desk.setItem(0, PageItem.createLinkPanelPage());
helper.assertValueEqual(menu.pageCapacity(0), 1, "Loose page tab must expose exactly one position");
helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.PLACE_PAGE_BUTTON_BASE), "Loose page must not be replaced");
helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.PLACE_PAGE_BUTTON_BASE + 1), "Loose page tab must not grow extra positions");
helper.assertValueEqual(menu.getCarried().getCount(), 2, "Rejected placements must preserve cursor");
helper.assertTrue(menu.clickMenuButton(player, lastPosition - 1), "A second empty notebook position must accept a page");
helper.assertTrue(menu.clickMenuButton(player, lastPosition - 2), "A third empty notebook position must accept the last page");
helper.assertTrue(menu.getCarried().isEmpty(), "Final placement must clear cursor");
menu.setCarried(new ItemStack(Items.DIAMOND));
helper.assertFalse(menu.clickMenuButton(player, lastPosition - 3), "Non-page items must be rejected");
helper.assertTrue(menu.getCarried().is(Items.DIAMOND), "Rejected item must remain held");
var restored = new WritingDeskBlockEntity(pos, ModBlocks.WRITING_DESK.get().defaultBlockState());
restored.loadWithComponents(desk.saveWithoutMetadata(helper.getLevel().registryAccess()), helper.getLevel().registryAccess());
helper.assertValueEqual(restored.getItem(87).get(ModDataComponents.NOTEBOOK_CONTENTS.get()).pageCount(), 3,
"Placed pages must persist in the notebook");
helper.succeed();
}
@GameTest(template = "empty")
public static void deskSurfaceDragMovesAndSwapsNotebookPages(GameTestHelper helper) {
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3);
var desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos);
ItemStack linkPanel = PageItem.createLinkPanelPage();
ItemStack terrain = PageItem.createSymbolPage(new SymbolReference(
ResourceLocation.parse("rivencraft:terrain/standard")));
ItemStack firstBook = new ItemStack(ModItems.NOTEBOOK.get());
firstBook.set(ModDataComponents.NOTEBOOK_CONTENTS.get(), new NotebookContents(List.of(linkPanel, terrain)));
desk.setItem(0, firstBook);
desk.setItem(1, new ItemStack(ModItems.NOTEBOOK.get()));
desk.setItem(2, PageItem.createLinkPanelPage());
var player = helper.makeMockServerPlayerInLevel();
player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
var menu = new WritingDeskMenu(1, player.getInventory(), desk);
int moveToEmpty = WritingDeskMenu.movePageButtonId(0, 0, 0, 26);
helper.assertTrue(menu.clickMenuButton(player, moveToEmpty), "Drag must move to a distant empty notebook position");
var pages = desk.getItem(0).get(ModDataComponents.NOTEBOOK_CONTENTS.get()).pages();
helper.assertTrue(pages.get(0).isEmpty(), "Moving must clear the source position");
helper.assertTrue(ItemStack.matches(pages.get(26), linkPanel), "Moving must preserve page components");
helper.assertValueEqual(desk.getItem(0).get(ModDataComponents.NOTEBOOK_CONTENTS.get()).pageCount(), 2,
"Moving must preserve total page count");
helper.assertTrue(menu.clickMenuButton(player, WritingDeskMenu.movePageButtonId(0, 26, 0, 1)),
"Drag onto an occupied position must swap pages");
pages = desk.getItem(0).get(ModDataComponents.NOTEBOOK_CONTENTS.get()).pages();
helper.assertTrue(ItemStack.matches(pages.get(1), linkPanel) && ItemStack.matches(pages.get(26), terrain),
"Swap must preserve both pages");
helper.assertTrue(menu.clickMenuButton(player, WritingDeskMenu.movePageButtonId(0, 1, 1, 5)),
"Drag into another notebook must work");
var otherPages = desk.getItem(1).get(ModDataComponents.NOTEBOOK_CONTENTS.get()).pages();
helper.assertTrue(ItemStack.matches(otherPages.get(5), linkPanel)
&& desk.getItem(0).get(ModDataComponents.NOTEBOOK_CONTENTS.get()).pages().get(1).isEmpty(),
"Cross notebook move must have one destination and no old source");
helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.movePageButtonId(0, 1, 1, 6)),
"Dragging an empty source must be rejected");
helper.assertTrue(menu.clickMenuButton(player, WritingDeskMenu.movePageButtonId(2, 0, 1, 6)),
"A loose page should move into a notebook position");
helper.assertTrue(desk.getItem(2).isEmpty()
&& desk.getItem(1).get(ModDataComponents.NOTEBOOK_CONTENTS.get()).pages().get(6).is(ModItems.PAGE.get()),
"Moving a loose page must empty its tab without losing the item");
helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.movePageButtonId(1, 5, 1, 5)),
"A no-op drag must be rejected");
menu.setCarried(new ItemStack(Items.DIAMOND));
helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.movePageButtonId(1, 5, 1, 6)),
"Held inventory items must not be moved by a panel drag");
helper.assertTrue(ItemStack.matches(desk.getItem(1).get(ModDataComponents.NOTEBOOK_CONTENTS.get()).pages().get(5), linkPanel),
"Rejected drags must leave notebook data intact");
helper.succeed();
}
@GameTest(template = "empty")
public static void deskSurfaceDragExtractsPagesToInventoryAndTabs(GameTestHelper helper) {
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3);
var desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos);
ItemStack page = PageItem.createLinkPanelPage();
ItemStack source = new ItemStack(ModItems.NOTEBOOK.get());
source.set(ModDataComponents.NOTEBOOK_CONTENTS.get(),
new NotebookContents(List.of(page, page, page, page, page, page)));
desk.setItem(0, source);
ItemStack otherBook = new ItemStack(ModItems.NOTEBOOK.get());
ItemStack terrain = PageItem.createSymbolPage(new SymbolReference(
ResourceLocation.parse("rivencraft:terrain/standard")));
otherBook.set(ModDataComponents.NOTEBOOK_CONTENTS.get(), new NotebookContents(List.of(terrain)));
desk.setItem(2, otherBook);
var player = helper.makeMockServerPlayerInLevel();
player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
player.getInventory().setItem(10, page.copyWithCount(2));
var menu = new WritingDeskMenu(1, player.getInventory(), desk);
int inventoryStart = WritingDeskMenu.COPY_RESULT + 1;
helper.assertTrue(menu.clickMenuButton(player, WritingDeskMenu.extractPageButtonId(0, 0, -1)),
"Dropping away from a slot must return the page to the cursor");
helper.assertTrue(ItemStack.matches(menu.getCarried(), page), "Cursor must hold the original page item");
helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.extractPageButtonId(0, 1, inventoryStart)),
"Extraction must not replace an occupied cursor");
menu.setCarried(ItemStack.EMPTY);
helper.assertTrue(menu.clickMenuButton(player, WritingDeskMenu.extractPageButtonId(0, 1, inventoryStart)),
"Dropping on player inventory must transfer a page");
helper.assertTrue(ItemStack.matches(player.getInventory().getItem(9), page),
"Transferred page must retain its components");
helper.assertTrue(menu.clickMenuButton(player, WritingDeskMenu.extractPageButtonId(0, 2, 1)),
"Dropping on an empty tab must store a loose page");
helper.assertTrue(ItemStack.matches(desk.getItem(1), page), "Tab must hold the extracted page");
helper.assertTrue(menu.clickMenuButton(player, WritingDeskMenu.extractPageButtonId(0, 3, 2)),
"Dropping on a notebook tab must insert the page");
var otherPages = desk.getItem(2).get(ModDataComponents.NOTEBOOK_CONTENTS.get()).pages();
helper.assertTrue(ItemStack.matches(otherPages.get(0), terrain)
&& ItemStack.matches(otherPages.get(1), page), "Notebook drop must preserve existing pages");
helper.assertTrue(menu.clickMenuButton(player, WritingDeskMenu.extractPageButtonId(0, 4, inventoryStart + 1)),
"Dropping on a matching inventory stack must merge one page");
helper.assertValueEqual(player.getInventory().getItem(10).getCount(), 3, "Merge must add exactly one page");
helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.extractPageButtonId(0, 5, 1)),
"Occupied loose-page tabs must reject a drop");
helper.assertFalse(menu.clickMenuButton(player, WritingDeskMenu.extractPageButtonId(0, 5, WritingDeskBlockEntity.INK)),
"Ink slot must not accept an extracted page");
helper.assertValueEqual(desk.getItem(0).get(ModDataComponents.NOTEBOOK_CONTENTS.get()).pageCount(), 1,
"Rejected drops must leave the remaining source page in the notebook");
helper.assertTrue(menu.clickMenuButton(player, WritingDeskMenu.extractPageButtonId(1, 0, -1)),
"A loose page shown on the panel must be draggable back to the cursor");
helper.assertTrue(desk.getItem(1).isEmpty() && ItemStack.matches(menu.getCarried(), page),
"Extracting a loose page must empty its tab and return the same item");
helper.succeed();
}
} }
@@ -29,13 +29,25 @@ import net.minecraft.world.item.Items;
public final class WritingDeskMenu extends AbstractContainerMenu { public final class WritingDeskMenu extends AbstractContainerMenu {
public static final int COPY_RESULT = WritingDeskBlockEntity.SIZE; public static final int COPY_RESULT = WritingDeskBlockEntity.SIZE;
public static final int VISIBLE_TABS = 4;
public static final int PREVIOUS_TABS = 90;
public static final int NEXT_TABS = 91;
public static final int PAGE_BUTTON_BASE = 128;
public static final int CATALOG_BUTTON_BASE = 512;
public static final int ALL_PAGE_BUTTON_BASE = 32768;
public static final int PLACE_PAGE_BUTTON_BASE = 65536;
private static final int PAGE_ADDRESS_COUNT = WritingDeskBlockEntity.COLLECTIONS * NotebookContents.MAX_PAGES;
public static final int MOVE_PAGE_BUTTON_BASE = PLACE_PAGE_BUTTON_BASE + PAGE_ADDRESS_COUNT;
private static final int PLAYER_START = COPY_RESULT + 1; private static final int PLAYER_START = COPY_RESULT + 1;
private static final int EXTRACT_TARGET_COUNT = PLAYER_START + 36 + 1;
public static final int EXTRACT_PAGE_BUTTON_BASE = MOVE_PAGE_BUTTON_BASE + PAGE_ADDRESS_COUNT * PAGE_ADDRESS_COUNT;
private final Container desk; private final Container desk;
private final WritingDeskBlockEntity blockEntity; private final WritingDeskBlockEntity blockEntity;
private final SimpleContainer result = new SimpleContainer(1); private final SimpleContainer result = new SimpleContainer(1);
private final boolean clientSide; private final boolean clientSide;
private int syncedActiveCollection; private int syncedActiveCollection;
private int syncedInkRemaining; private int syncedInkRemaining;
private int firstVisibleTab;
private int selectedPage; private int selectedPage;
private int selectedCollection = -1; private int selectedCollection = -1;
private ResourceLocation selectedSymbol; private ResourceLocation selectedSymbol;
@@ -55,6 +67,11 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
this.clientSide = inventory.player.level().isClientSide; this.clientSide = inventory.player.level().isClientSide;
this.serverViewer = inventory.player instanceof ServerPlayer serverPlayer ? serverPlayer : null; this.serverViewer = inventory.player instanceof ServerPlayer serverPlayer ? serverPlayer : null;
if (blockEntity != null && !clientSide) catalog = WritingDeskCatalog.available(blockEntity); if (blockEntity != null && !clientSide) catalog = WritingDeskCatalog.available(blockEntity);
firstVisibleTab = blockEntity == null ? 0 : blockEntity.activeCollection() / VISIBLE_TABS * VISIBLE_TABS;
addDataSlot(new DataSlot() {
@Override public int get() { return firstVisibleTab; }
@Override public void set(int value) { firstVisibleTab = value; }
});
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(); }
@@ -66,53 +83,94 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
@Override public void set(int value) { syncedInkRemaining = value; } @Override public void set(int value) { syncedInkRemaining = value; }
}); });
for (int i = 0; i < WritingDeskBlockEntity.COLLECTIONS; i++) { for (int i = 0; i < WritingDeskBlockEntity.COLLECTIONS; i++) {
addSlot(new DeskSlot(desk, i, 36, 20 + i * 37, 1)); addSlot(new DeskSlot(desk, i, 37, 34 + (i % VISIBLE_TABS) * 37, 1));
} }
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.INK, 370, 9, 16)); addSlot(new DeskSlot(desk, WritingDeskBlockEntity.INK, 385, 28, 16));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.RETURN, 370, 57, 64)); addSlot(new DeskSlot(desk, WritingDeskBlockEntity.RETURN, 385, 80, 64));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.PAPER, 228, 9, 64)); addSlot(new DeskSlot(desk, WritingDeskBlockEntity.PAPER, 241, 28, 64));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.LEGACY_OUTPUT, -1000, -1000, 64)); addSlot(new DeskSlot(desk, WritingDeskBlockEntity.LEGACY_OUTPUT, -1000, -1000, 64));
addSlot(new CopySlot(result, 0, 228, 57)); addSlot(new CopySlot(result, 0, 241, 80));
for (int row = 0; row < 3; row++) { for (int row = 0; row < 3; row++) {
for (int column = 0; column < 9; column++) { for (int column = 0; column < 9; column++) {
addSlot(new Slot(inventory, column + row * 9 + 9, 224 + column * 18, 79 + row * 18)); addSlot(new Slot(inventory, column + row * 9 + 9, 241 + column * 18, 104 + row * 18));
} }
} }
for (int column = 0; column < 9; column++) { for (int column = 0; column < 9; column++) {
addSlot(new Slot(inventory, column, 224 + column * 18, 137)); addSlot(new Slot(inventory, column, 241 + column * 18, 162));
} }
} }
public int activeCollection() { return syncedActiveCollection; } public int activeCollection() { return syncedActiveCollection; }
public int firstVisibleTab() { return firstVisibleTab; }
public int inkRemaining() { return syncedInkRemaining; } public int inkRemaining() { return syncedInkRemaining; }
public int pageCapacity(int collectionIndex) {
if (collectionIndex < 0 || collectionIndex >= WritingDeskBlockEntity.COLLECTIONS) return 0;
ItemStack collection = desk.getItem(collectionIndex);
return collection.is(ModItems.NOTEBOOK.get()) ? NotebookContents.MAX_PAGES
: collection.is(ModItems.PAGE.get()) ? 1 : 0;
}
public List<SymbolDisplayMetadata> catalog() { return catalog; } public List<SymbolDisplayMetadata> catalog() { return catalog; }
public void setCatalog(List<SymbolDisplayMetadata> symbols) { catalog = List.copyOf(symbols); } public void setCatalog(List<SymbolDisplayMetadata> symbols) { catalog = List.copyOf(symbols); }
public static int movePageButtonId(int sourceCollection, int sourcePage, int targetCollection, int targetPage) {
int source = sourceCollection * NotebookContents.MAX_PAGES + sourcePage;
int target = targetCollection * NotebookContents.MAX_PAGES + targetPage;
return MOVE_PAGE_BUTTON_BASE + source * PAGE_ADDRESS_COUNT + target;
}
/** targetSlot is -1 for the cursor, otherwise a visible player or desk-tab slot. */
public static int extractPageButtonId(int sourceCollection, int sourcePage, int targetSlot) {
int source = sourceCollection * NotebookContents.MAX_PAGES + sourcePage;
return EXTRACT_PAGE_BUTTON_BASE + source * EXTRACT_TARGET_COUNT + targetSlot + 1;
}
@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 == PREVIOUS_TABS || id == NEXT_TABS) {
firstVisibleTab = Math.clamp(firstVisibleTab + (id == NEXT_TABS ? VISIBLE_TABS : -VISIBLE_TABS),
0, WritingDeskBlockEntity.COLLECTIONS - VISIBLE_TABS);
return true;
} else if (id >= 0 && id < WritingDeskBlockEntity.COLLECTIONS) {
if (id < firstVisibleTab || id >= firstVisibleTab + VISIBLE_TABS) return false;
blockEntity.setActiveCollection(id); blockEntity.setActiveCollection(id);
selectedPage = 0; selectedPage = 0;
selectedCollection = -1; selectedCollection = -1;
selectedSymbol = null; selectedSymbol = null;
} else if (id >= 16 && id < 16 + NotebookContents.MAX_PAGES) { } else if (id >= EXTRACT_PAGE_BUTTON_BASE
selectedPage = id - 16; && id < EXTRACT_PAGE_BUTTON_BASE + PAGE_ADDRESS_COUNT * EXTRACT_TARGET_COUNT) {
int offset = id - EXTRACT_PAGE_BUTTON_BASE;
int source = offset / EXTRACT_TARGET_COUNT;
int targetSlot = offset % EXTRACT_TARGET_COUNT - 1;
if (!extractPage(source / NotebookContents.MAX_PAGES, source % NotebookContents.MAX_PAGES,
targetSlot)) return false;
} else if (id >= MOVE_PAGE_BUTTON_BASE && id < MOVE_PAGE_BUTTON_BASE + PAGE_ADDRESS_COUNT * PAGE_ADDRESS_COUNT) {
int offset = id - MOVE_PAGE_BUTTON_BASE;
int source = offset / PAGE_ADDRESS_COUNT;
int target = offset % PAGE_ADDRESS_COUNT;
if (!movePage(source / NotebookContents.MAX_PAGES, source % NotebookContents.MAX_PAGES,
target / NotebookContents.MAX_PAGES, target % NotebookContents.MAX_PAGES)) return false;
} else if (id >= PLACE_PAGE_BUTTON_BASE
&& id < PLACE_PAGE_BUTTON_BASE + WritingDeskBlockEntity.COLLECTIONS * NotebookContents.MAX_PAGES) {
int offset = id - PLACE_PAGE_BUTTON_BASE;
if (!placeCarriedPage(offset / NotebookContents.MAX_PAGES, offset % NotebookContents.MAX_PAGES)) return false;
} else if (id >= PAGE_BUTTON_BASE && id < PAGE_BUTTON_BASE + NotebookContents.MAX_PAGES) {
selectedPage = id - PAGE_BUTTON_BASE;
selectedCollection = -1; selectedCollection = -1;
selectedSymbol = null; selectedSymbol = null;
} else if (id >= 1000 && id < 1000 + WritingDeskBlockEntity.COLLECTIONS * NotebookContents.MAX_PAGES) { } else if (id >= ALL_PAGE_BUTTON_BASE && id < ALL_PAGE_BUTTON_BASE + WritingDeskBlockEntity.COLLECTIONS * NotebookContents.MAX_PAGES) {
int offset = id - 1000; int offset = id - ALL_PAGE_BUTTON_BASE;
int collection = offset / NotebookContents.MAX_PAGES; int collection = offset / NotebookContents.MAX_PAGES;
int page = offset % NotebookContents.MAX_PAGES; int page = offset % NotebookContents.MAX_PAGES;
if (pageAt(collection, page).isEmpty()) return false; if (pageAt(collection, page).isEmpty()) return false;
selectedCollection = collection; selectedCollection = collection;
selectedPage = page; selectedPage = page;
selectedSymbol = null; selectedSymbol = null;
} else if (id >= 100 && id < 100 + catalog.size()) { } else if (id >= CATALOG_BUTTON_BASE && id < CATALOG_BUTTON_BASE + catalog.size()) {
if (!catalog.equals(WritingDeskCatalog.available(blockEntity))) { if (!catalog.equals(WritingDeskCatalog.available(blockEntity))) {
refreshCatalog(); refreshCatalog();
return false; return false;
} }
ResourceLocation requested = catalog.get(id - 100).id(); ResourceLocation requested = catalog.get(id - CATALOG_BUTTON_BASE).id();
if (!WritingDeskCatalog.available(blockEntity).stream().anyMatch(entry -> entry.id().equals(requested))) { if (!WritingDeskCatalog.available(blockEntity).stream().anyMatch(entry -> entry.id().equals(requested))) {
return false; return false;
} }
@@ -122,6 +180,129 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
return true; return true;
} }
private boolean placeCarriedPage(int collectionIndex, int pageIndex) {
// Resolve the actual storage position on the server, never a filtered list index.
ItemStack carried = getCarried();
if (clientSide || !carried.is(ModItems.PAGE.get())) return false;
ItemStack collection = desk.getItem(collectionIndex);
if (collection.is(ModItems.NOTEBOOK.get())) {
var pages = new java.util.ArrayList<>(collection.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages());
while (pages.size() <= pageIndex) pages.add(ItemStack.EMPTY);
if (!pages.get(pageIndex).isEmpty()) return false;
pages.set(pageIndex, carried.copyWithCount(1));
ItemStack updated = collection.copy();
updated.set(ModDataComponents.NOTEBOOK_CONTENTS.get(), new NotebookContents(pages));
desk.setItem(collectionIndex, updated);
} else return false;
carried.shrink(1);
setCarried(carried.isEmpty() ? ItemStack.EMPTY : carried);
selectedCollection = collectionIndex;
selectedPage = pageIndex;
selectedSymbol = null;
refreshCatalog();
broadcastChanges();
return true;
}
private boolean movePage(int sourceCollection, int sourcePage, int targetCollection, int targetPage) {
if (!getCarried().isEmpty() || sourceCollection == targetCollection && sourcePage == targetPage) return false;
ItemStack sourceBook = desk.getItem(sourceCollection);
ItemStack targetBook = desk.getItem(targetCollection);
boolean sourceNotebook = sourceBook.is(ModItems.NOTEBOOK.get());
boolean targetNotebook = targetBook.is(ModItems.NOTEBOOK.get());
if ((!sourceNotebook && (!sourceBook.is(ModItems.PAGE.get()) || sourcePage != 0))
|| (!targetNotebook && (!targetBook.is(ModItems.PAGE.get()) || targetPage != 0))) return false;
var sourcePages = sourceNotebook ? new java.util.ArrayList<>(sourceBook.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()) : null;
if (sourceNotebook && (sourcePage >= sourcePages.size() || sourcePages.get(sourcePage).isEmpty())) return false;
ItemStack moving = sourceNotebook ? sourcePages.get(sourcePage) : sourceBook;
if (moving.getCount() != 1) return false;
if (!targetNotebook && targetBook.getCount() != 1) return false;
var targetPages = targetNotebook ? sourceCollection == targetCollection ? sourcePages
: new java.util.ArrayList<>(targetBook.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()) : null;
if (targetNotebook) while (targetPages.size() <= targetPage) targetPages.add(ItemStack.EMPTY);
ItemStack displaced = targetNotebook ? targetPages.get(targetPage) : targetBook;
if (targetNotebook) targetPages.set(targetPage, moving);
if (sourceNotebook) {
sourcePages.set(sourcePage, displaced);
ItemStack updatedSource = sourceBook.copy();
updatedSource.set(ModDataComponents.NOTEBOOK_CONTENTS.get(), new NotebookContents(sourcePages));
desk.setItem(sourceCollection, updatedSource);
} else desk.setItem(sourceCollection, displaced.isEmpty() ? ItemStack.EMPTY : displaced.copyWithCount(1));
if (targetNotebook && sourceCollection != targetCollection) {
ItemStack updatedTarget = targetBook.copy();
updatedTarget.set(ModDataComponents.NOTEBOOK_CONTENTS.get(), new NotebookContents(targetPages));
desk.setItem(targetCollection, updatedTarget);
} else if (!targetNotebook) desk.setItem(targetCollection, moving.copyWithCount(1));
selectedCollection = targetCollection;
selectedPage = targetPage;
selectedSymbol = null;
refreshCatalog();
broadcastChanges();
return true;
}
private boolean extractPage(int sourceCollection, int sourcePage, int targetSlot) {
if (!getCarried().isEmpty() || targetSlot < -1 || targetSlot >= slots.size()) return false;
ItemStack sourceBook = desk.getItem(sourceCollection);
boolean sourceNotebook = sourceBook.is(ModItems.NOTEBOOK.get());
if (!sourceNotebook && (!sourceBook.is(ModItems.PAGE.get()) || sourcePage != 0)) return false;
var sourcePages = sourceNotebook ? new java.util.ArrayList<>(sourceBook.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()) : null;
if (sourceNotebook && (sourcePage >= sourcePages.size() || sourcePages.get(sourcePage).isEmpty())) return false;
ItemStack moving = sourceNotebook ? sourcePages.get(sourcePage).copyWithCount(1) : sourceBook.copyWithCount(1);
if (!sourceNotebook && sourceBook.getCount() != 1) return false;
if (targetSlot == -1) {
setCarried(moving);
} else {
if (targetSlot >= WritingDeskBlockEntity.COLLECTIONS && targetSlot < PLAYER_START) return false;
Slot target = slots.get(targetSlot);
if (!target.isActive() || !target.mayPlace(moving)) return false;
ItemStack current = target.getItem();
if (targetSlot < WritingDeskBlockEntity.COLLECTIONS && current.is(ModItems.NOTEBOOK.get())) {
var targetPages = targetSlot == sourceCollection ? sourcePages
: new java.util.ArrayList<>(current.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages());
int position = -1;
for (int i = 0; i < NotebookContents.MAX_PAGES; i++) {
if (i >= targetPages.size() || targetPages.get(i).isEmpty()) {
position = i;
break;
}
}
if (position < 0) return false;
while (targetPages.size() <= position) targetPages.add(ItemStack.EMPTY);
targetPages.set(position, moving);
if (targetSlot != sourceCollection) {
ItemStack updatedTarget = current.copy();
updatedTarget.set(ModDataComponents.NOTEBOOK_CONTENTS.get(), new NotebookContents(targetPages));
target.setByPlayer(updatedTarget);
}
} else if (current.isEmpty()) {
target.setByPlayer(moving);
} else if (targetSlot >= PLAYER_START && ItemStack.isSameItemSameComponents(current, moving)
&& current.getCount() < target.getMaxStackSize(moving)) {
target.setByPlayer(current.copyWithCount(current.getCount() + 1));
} else return false;
}
if (sourceNotebook) {
sourcePages.set(sourcePage, ItemStack.EMPTY);
ItemStack updatedSource = sourceBook.copy();
updatedSource.set(ModDataComponents.NOTEBOOK_CONTENTS.get(), new NotebookContents(sourcePages));
desk.setItem(sourceCollection, updatedSource);
} else desk.setItem(sourceCollection, ItemStack.EMPTY);
selectedCollection = sourceCollection;
selectedPage = sourcePage;
selectedSymbol = null;
refreshCatalog();
broadcastChanges();
return true;
}
@Override public boolean stillValid(Player player) { @Override public boolean stillValid(Player player) {
return blockEntity != null && blockEntity.stillValid(player); return blockEntity != null && blockEntity.stillValid(player);
} }
@@ -141,6 +322,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
return copy; return copy;
} }
Slot slot = slots.get(index); Slot slot = slots.get(index);
if (!slot.isActive()) return ItemStack.EMPTY;
if (!slot.hasItem()) return ItemStack.EMPTY; if (!slot.hasItem()) return ItemStack.EMPTY;
ItemStack moving = slot.getItem(); ItemStack moving = slot.getItem();
ItemStack original = moving.copy(); ItemStack original = moving.copy();
@@ -155,12 +337,13 @@ 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,
if (moving.is(ModItems.NOTEBOOK.get())) return moveItemStackTo(moving, 0, 4, false); WritingDeskBlockEntity.INK, WritingDeskBlockEntity.INK + 1, false);
if (moving.is(ModItems.NOTEBOOK.get())) return moveItemStackTo(moving, firstVisibleTab, firstVisibleTab + VISIBLE_TABS, false);
if (moving.is(Items.PAPER)) if (moving.is(Items.PAPER))
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, firstVisibleTab, firstVisibleTab + VISIBLE_TABS, false);
return false; return false;
} }
@@ -230,6 +413,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
} }
refreshCatalog(); refreshCatalog();
refreshOutput(); refreshOutput();
if (slotId >= 0 && slotId < slots.size() && !slots.get(slotId).isActive()) return;
super.clicked(slotId, button, clickType, player); super.clicked(slotId, button, clickType, player);
} }
@@ -245,13 +429,19 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
} }
} }
private static final class DeskSlot extends Slot { private final class DeskSlot extends Slot {
private final int limit; private final int limit;
private DeskSlot(Container container, int index, int x, int y, int limit) { private DeskSlot(Container container, int index, int x, int y, int limit) {
super(container, index, x, y); super(container, index, x, y);
this.limit = limit; this.limit = limit;
} }
@Override public boolean mayPlace(ItemStack stack) { return container.canPlaceItem(getContainerSlot(), stack); } @Override public boolean isActive() {
int slot = getContainerSlot();
return slot >= WritingDeskBlockEntity.COLLECTIONS
|| slot >= firstVisibleTab && slot < firstVisibleTab + VISIBLE_TABS;
}
@Override public boolean mayPickup(Player player) { return isActive(); }
@Override public boolean mayPlace(ItemStack stack) { return isActive() && container.canPlaceItem(getContainerSlot(), stack); }
@Override public int getMaxStackSize() { return limit; } @Override public int getMaxStackSize() { return limit; }
} }
} }
@@ -49,7 +49,7 @@ 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("11");
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,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 B

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 B

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B