Compare commits

..
3 Commits
Author SHA1 Message Date
dnorberg 385741027a Show Writing Desk page previews like Legacy
Build / build (push) Canceled after 0s
2026-09-24 05:17:31 -04:00
dnorberg 972aeaaed6 Add Writing Desk storage and menu foundation
Build / build (push) Canceled after 0s
2026-09-24 05:13:35 -04:00
dnorberg 5066e71897 Match Legacy notebook sparse portrait pages
Build / build (push) Canceled after 0s
2026-09-24 05:02:58 -04:00
26 changed files with 674 additions and 48 deletions

No files matched your search

+4
View File
@@ -8,6 +8,10 @@ Mystcraft Legacy copyright: XCompWiz and contributors
Mystcraft Legacy license: GNU Lesser General Public License version 3
The animated ink-vial item texture in this project is copied from Mystcraft
Legacy's assets/mystcraft/textures/items/ink_vial.png under that license.
The Notebook page atlas is copied from Legacy's assets/mystcraft/gui/bookui_pagel.png
under the same license.
The Writing Desk item texture is copied from Legacy's
assets/mystcraft/textures/items/writingdesk.png under the same license.
The initial build layout is derived from the official NeoForge 1.21.1
ModDevGradle MDK: https://github.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle
+7
View File
@@ -20,6 +20,13 @@ Each vial holds 16 ink units, and the default link-panel cost is two units.
The spent vial returns a glass bottle. Server owners can adjust the symbol-copy
and link-panel costs in the `writing` section of `rivencraft-server.toml`.
Symbol-page transcription will become available through the Writing Desk.
The Writing Desk is now craftable from planks, a glass bottle, and a feather.
Its four collection tabs, ink/source/page work slots, and inventory persist
through world saves; breaking it drops stored contents. The desk screen is a
functional foundation styled after Legacy's left tabs, central writing surface,
and right inventory. Transcription controls are not implemented yet.
Selecting a stored page on the desk surface shows its Legacy-proportioned
portrait preview and name above the player inventory.
## Linking Book
+2
View File
@@ -7,6 +7,8 @@ project's GNU LGPL v3 license. Rivencraft itself uses the same license.
| Rivencraft asset | Upstream asset | Upstream revision | Changes |
| --- | --- | --- | --- |
| `assets/rivencraft/textures/item/page.png` | `assets/mystcraft/textures/items/page_background.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only |
| `assets/rivencraft/textures/gui/notebook_pages.png` | `assets/mystcraft/gui/bookui_pagel.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Notebook draws the native 30×40 page at atlas coordinates `(156, 0)` |
| `assets/rivencraft/textures/item/writing_desk.png` | `assets/mystcraft/textures/items/writingdesk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only |
| `assets/rivencraft/textures/item/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_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 |
+7
View File
@@ -65,6 +65,13 @@ composition from its page background and symbol-component sheet. Data-pack
symbols without a bundled resource-pack model use the legacy question-mark
fallback while keeping their real identifier and display metadata.
The Notebook surface displays pages by their persistent slot index instead of
packing occupied pages together. Empty positions through the last placed page
appear darkened. While carrying a Page, all available positions are shown for
placement. The surface draws the native 30×40 portrait page region from
Legacy's book UI atlas; symbols are layered at a uniform scale, while the
square item models remain unchanged in inventories.
## Ink and baseline link panels
An Ink Vial is crafted from two black dye, a glass bottle, and a water bucket.
+1 -1
View File
@@ -35,7 +35,7 @@ mod_name=Rivencraft
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=LGPL-3.0-or-later
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
mod_version=0.3.4.1
mod_version=0.3.5.1
immersive_portals_version=6.0.7
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
@@ -0,0 +1,63 @@
package net.dnorberg.rivencraft.block;
import com.mojang.serialization.MapCodec;
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Containers;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.phys.BlockHitResult;
public final class WritingDeskBlock extends BaseEntityBlock {
public static final MapCodec<WritingDeskBlock> CODEC = simpleCodec(WritingDeskBlock::new);
public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
public WritingDeskBlock(Properties properties) {
super(properties);
registerDefaultState(stateDefinition.any().setValue(FACING, net.minecraft.core.Direction.NORTH));
}
@Override public MapCodec<? extends BaseEntityBlock> codec() { return CODEC; }
@Override protected void createBlockStateDefinition(StateDefinition.Builder<net.minecraft.world.level.block.Block, BlockState> builder) {
builder.add(FACING);
}
@Override public BlockState getStateForPlacement(BlockPlaceContext context) {
return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
}
@Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; }
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new WritingDeskBlockEntity(pos, state);
}
@Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
Player player, BlockHitResult hit) {
if (!level.isClientSide && player instanceof ServerPlayer serverPlayer
&& level.getBlockEntity(pos) instanceof WritingDeskBlockEntity desk) {
serverPlayer.openMenu(new SimpleMenuProvider(
(id, inventory, ignored) -> new WritingDeskMenu(id, inventory, desk),
Component.translatable("container.rivencraft.writing_desk")),
buffer -> buffer.writeBlockPos(pos));
}
return InteractionResult.sidedSuccess(level.isClientSide);
}
@Override protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState replacement, boolean moving) {
if (!state.is(replacement.getBlock()) && !level.isClientSide
&& level.getBlockEntity(pos) instanceof WritingDeskBlockEntity desk) {
Containers.dropContents(level, pos, desk);
level.updateNeighbourForOutputSignal(pos, this);
}
super.onRemove(state, level, pos, replacement, moving);
}
}
@@ -0,0 +1,85 @@
package net.dnorberg.rivencraft.block.entity;
import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.NonNullList;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
/** The four Legacy-like collection tabs and four work slots are owned by the block, not its viewers. */
public final class WritingDeskBlockEntity extends BlockEntity implements Container {
public static final int COLLECTIONS = 4;
public static final int INK = 4;
public static final int SOURCE = 5;
public static final int PAPER = 6;
public static final int OUTPUT = 7;
public static final int SIZE = 8;
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
private int activeCollection;
public WritingDeskBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.WRITING_DESK.get(), pos, state);
}
public int activeCollection() { return activeCollection; }
public void setActiveCollection(int index) {
if (index >= 0 && index < COLLECTIONS && activeCollection != index) {
activeCollection = index;
setChanged();
}
}
@Override public int getContainerSize() { return SIZE; }
@Override public boolean isEmpty() { return items.stream().allMatch(ItemStack::isEmpty); }
@Override public ItemStack getItem(int slot) { return items.get(slot); }
@Override public ItemStack removeItem(int slot, int amount) {
ItemStack removed = ContainerHelper.removeItem(items, slot, amount);
if (!removed.isEmpty()) setChanged();
return removed;
}
@Override public ItemStack removeItemNoUpdate(int slot) { return ContainerHelper.takeItem(items, slot); }
@Override public void setItem(int slot, ItemStack stack) {
items.set(slot, stack);
if (stack.getCount() > getMaxStackSize()) stack.setCount(getMaxStackSize());
setChanged();
}
@Override public boolean stillValid(Player player) {
return level != null && level.isLoaded(worldPosition)
&& level.getBlockEntity(worldPosition) == this
&& player.distanceToSqr(worldPosition.getX() + 0.5, worldPosition.getY() + 0.5,
worldPosition.getZ() + 0.5) <= 64.0;
}
@Override public boolean canPlaceItem(int slot, ItemStack stack) {
if (slot < COLLECTIONS) return stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.PAGE.get());
return switch (slot) {
case INK -> stack.is(ModItems.INK_VIAL.get());
case SOURCE -> stack.is(ModItems.PAGE.get()) || stack.is(ModItems.NOTEBOOK.get());
case PAPER -> stack.is(ModItems.PAGE.get());
case OUTPUT -> false;
default -> false;
};
}
@Override public void clearContent() {
items.clear();
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
setChanged();
}
@Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
ContainerHelper.loadAllItems(tag, items, registries);
activeCollection = Math.clamp(tag.getInt("ActiveCollection"), 0, COLLECTIONS - 1);
}
@Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
ContainerHelper.saveAllItems(tag, items, registries);
tag.putInt("ActiveCollection", activeCollection);
}
}
@@ -14,6 +14,7 @@ import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.client.event.EntityRenderersEvent;
import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
import net.dnorberg.rivencraft.client.screen.NotebookScreen;
import net.dnorberg.rivencraft.client.screen.WritingDeskScreen;
import net.dnorberg.rivencraft.registry.ModMenus;
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
@@ -29,6 +30,7 @@ public final class ClientEvents {
@SubscribeEvent
public static void registerScreens(RegisterMenuScreensEvent event) {
event.register(ModMenus.NOTEBOOK.get(), NotebookScreen::new);
event.register(ModMenus.WRITING_DESK.get(), WritingDeskScreen::new);
}
@SubscribeEvent
@@ -1,8 +1,8 @@
package net.dnorberg.rivencraft.client.screen;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.menu.NotebookMenu;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.client.gui.GuiGraphics;
@@ -10,6 +10,7 @@ import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.ClickType;
import net.minecraft.world.item.ItemStack;
@@ -24,6 +25,15 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
private static final int PAGE_STEP_X = 31;
private static final int PAGE_STEP_Y = 41;
private static final int PAGE_COLUMNS = 5;
private static final ResourceLocation PAGE_ATLAS = texture("gui/notebook_pages");
private static final ResourceLocation GENERIC_SYMBOL = texture("item/page_symbol");
private static final ResourceLocation UNRESOLVED_SYMBOL = texture("item/page_unresolved");
private static final ResourceLocation STANDARD_TERRAIN = texture("item/page_symbol_terrain_standard");
private static final ResourceLocation NATIVE_BIOMES = texture("item/page_symbol_biome_native");
private static final ResourceLocation NORMAL_LIGHTING = texture("item/page_symbol_lighting_normal");
private static final ResourceLocation NORMAL_WEATHER = texture("item/page_symbol_weather_normal");
private static final ResourceLocation NORMAL_TIME = texture("item/page_symbol_celestial_normal_time");
private static final ResourceLocation CAVES = texture("item/page_symbol_feature_caves");
private EditBox textInput;
private boolean draggingScrollbar;
private boolean surfaceClickPendingRelease;
@@ -59,25 +69,25 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
super.render(graphics, mouseX, mouseY, partialTick);
List<Integer> pages = visiblePages();
scroll = Math.min(scroll, maxScroll(pages.size()));
int pageSlots = displayedPageSlots();
scroll = Math.min(scroll, maxScroll(pageSlots));
graphics.enableScissor(leftPos, topPos + SURFACE_TOP, leftPos + SURFACE_RIGHT,
topPos + SURFACE_BOTTOM);
for (int position = 0; position < pages.size(); position++) {
int x = leftPos + (position % PAGE_COLUMNS) * PAGE_STEP_X;
int y = topPos + SURFACE_TOP + (position / PAGE_COLUMNS) * PAGE_STEP_Y - scroll;
for (int index = 0; index < pageSlots; index++) {
int x = leftPos + (index % PAGE_COLUMNS) * PAGE_STEP_X;
int y = topPos + SURFACE_TOP + (index / PAGE_COLUMNS) * PAGE_STEP_Y - scroll;
if (y + PAGE_HEIGHT > topPos + SURFACE_TOP && y < topPos + SURFACE_BOTTOM) {
drawPage(graphics, pages.get(position), x, y);
drawPage(graphics, index, x, y);
}
}
graphics.disableScissor();
drawScrollbar(graphics, pages.size());
int hovered = pageAt(mouseX, mouseY, pages);
drawScrollbar(graphics, pageSlots);
int hovered = pageAt(mouseX, mouseY);
if (!menu.getCarried().isEmpty() && inSurface(mouseX, mouseY)) {
graphics.renderItem(menu.getCarried(), mouseX - 8, mouseY - 8);
} else if (hovered >= 0) {
} else if (hovered >= 0 && !menu.getSlot(hovered).getItem().isEmpty()) {
ItemStack page = menu.getSlot(hovered).getItem();
if (!page.isEmpty() && matchesSearch(page)) graphics.renderTooltip(font, page, mouseX, mouseY);
if (matchesSearch(page)) graphics.renderTooltip(font, page, mouseX, mouseY);
} else {
renderTooltip(graphics, mouseX, mouseY);
}
@@ -103,13 +113,16 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (inScrollbar(mouseX, mouseY) && button == 0) {
draggingScrollbar = true;
setScrollFromMouse(mouseY, visiblePages().size());
setScrollFromMouse(mouseY, displayedPageSlots());
return true;
}
int index = pageAt(mouseX, mouseY, visiblePages());
int index = pageAt(mouseX, mouseY);
if (index >= 0 && (button == 0 || button == 1)) {
ItemStack page = menu.getSlot(index).getItem();
if (!page.isEmpty() && !matchesSearch(page)) return true;
if ((!page.isEmpty() && !matchesSearch(page))
|| (page.isEmpty() && !menu.getCarried().is(ModItems.PAGE.get()))) {
return true;
}
setFocused(null);
textInput.setFocused(false);
selectedSlot = index;
@@ -123,7 +136,7 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
@Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) {
if (draggingScrollbar && button == 0) {
setScrollFromMouse(mouseY, visiblePages().size());
setScrollFromMouse(mouseY, displayedPageSlots());
return true;
}
return super.mouseDragged(mouseX, mouseY, button, dragX, dragY);
@@ -144,7 +157,7 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
if (inSurface(mouseX, mouseY) || inScrollbar(mouseX, mouseY)) {
scroll = Math.clamp(scroll - (int)Math.signum(scrollY) * 20,
0, maxScroll(visiblePages().size()));
0, maxScroll(displayedPageSlots()));
return true;
}
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
@@ -170,21 +183,16 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
return super.keyPressed(keyCode, scanCode, modifiers);
}
private List<Integer> visiblePages() {
List<Integer> pages = new ArrayList<>();
int firstEmpty = -1;
for (int index = 0; index < NotebookMenu.NOTEBOOK_SLOTS; index++) {
ItemStack stack = menu.getSlot(index).getItem();
if (stack.isEmpty()) {
if (firstEmpty < 0) firstEmpty = index;
} else {
pages.add(index);
private int displayedPageSlots() {
if (menu.getCarried().is(ModItems.PAGE.get())) {
return NotebookMenu.NOTEBOOK_SLOTS;
}
for (int index = NotebookMenu.NOTEBOOK_SLOTS - 1; index >= 0; index--) {
if (!menu.getSlot(index).getItem().isEmpty()) {
return index + 1;
}
}
if (firstEmpty >= 0 && menu.getCarried().is(ModItems.PAGE.get())) {
pages.add(firstEmpty);
}
return pages;
return 0;
}
private boolean matchesSearch(ItemStack page) {
@@ -193,15 +201,15 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
.contains(textInput.getValue().toLowerCase(Locale.ROOT));
}
private int pageAt(double mouseX, double mouseY, List<Integer> pages) {
private int pageAt(double mouseX, double mouseY) {
if (!inSurface(mouseX, mouseY)) return -1;
int localX = (int)mouseX - leftPos;
int localY = (int)mouseY - topPos - SURFACE_TOP + scroll;
int column = localX / PAGE_STEP_X;
int position = (localY / PAGE_STEP_Y) * PAGE_COLUMNS + column;
int index = (localY / PAGE_STEP_Y) * PAGE_COLUMNS + column;
return column < PAGE_COLUMNS && localX % PAGE_STEP_X < PAGE_WIDTH
&& localY % PAGE_STEP_Y < PAGE_HEIGHT && position < pages.size()
? pages.get(position) : -1;
&& localY % PAGE_STEP_Y < PAGE_HEIGHT && index < displayedPageSlots()
? index : -1;
}
private boolean inSurface(double mouseX, double mouseY) {
@@ -243,20 +251,44 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
private void drawPage(GuiGraphics graphics, int index, int x, int y) {
ItemStack page = menu.getSlot(index).getItem();
boolean visible = !page.isEmpty() && matchesSearch(page);
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT,
visible ? 0xFFE5D4A7 : 0x704B4945);
graphics.fill(x + 2, y + 2, x + PAGE_WIDTH - 2, y + PAGE_HEIGHT - 2,
visible ? 0xFFF4E5C3 : 0x603B3934);
graphics.renderOutline(x, y, PAGE_WIDTH, PAGE_HEIGHT,
index == selectedSlot ? 0xFFFFD061 : 0xFF564A34);
if (visible) {
graphics.pose().pushPose();
graphics.pose().translate(x + 3, y + 8, 0);
graphics.pose().scale(1.5F, 1.5F, 1.0F);
graphics.renderItem(page, 0, 0);
graphics.pose().popPose();
boolean dimmed = page.isEmpty() || !matchesSearch(page);
// Legacy's Notebook draws the atlas' native 30x40 page, not a stretched item icon.
graphics.blit(PAGE_ATLAS, x, y, PAGE_WIDTH, PAGE_HEIGHT,
156, 0, PAGE_WIDTH, PAGE_HEIGHT, 256, 256);
if (!page.isEmpty()) {
switch (PageItem.content(page).kind()) {
case BLANK -> { }
case LINK_PANEL -> graphics.fill(x + 5, y + 6, x + 25, y + 20, 0xFF000000);
case SYMBOL, UNRESOLVED_SYMBOL -> graphics.blit(symbolTexture(page), x + 1, y + 6,
28, 28, 16, 16, 96, 96, 128, 128);
}
}
if (dimmed) {
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0xB8000000);
}
if (index == selectedSlot && !page.isEmpty()) {
graphics.renderOutline(x, y, PAGE_WIDTH, PAGE_HEIGHT, 0xFFFFD061);
}
}
static ResourceLocation symbolTexture(ItemStack page) {
var content = PageItem.content(page);
if (content.kind() == net.dnorberg.rivencraft.writing.PageContent.Kind.UNRESOLVED_SYMBOL) {
return UNRESOLVED_SYMBOL;
}
return switch (content.symbol().orElseThrow().id().toString()) {
case "rivencraft:terrain/standard" -> STANDARD_TERRAIN;
case "rivencraft:biome/native" -> NATIVE_BIOMES;
case "rivencraft:lighting/normal" -> NORMAL_LIGHTING;
case "rivencraft:weather/normal" -> NORMAL_WEATHER;
case "rivencraft:celestial/normal_time" -> NORMAL_TIME;
case "rivencraft:feature/caves" -> CAVES;
default -> GENERIC_SYMBOL;
};
}
private static ResourceLocation texture(String path) {
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/" + path + ".png");
}
private void moveSelected(boolean next) {
@@ -0,0 +1,163 @@
package net.dnorberg.rivencraft.client.screen;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.writing.NotebookContents;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
/** Functional storage surface; full Legacy transcription controls are a later Milestone 4 slice. */
public final class WritingDeskScreen extends AbstractContainerScreen<WritingDeskMenu> {
private static final ResourceLocation PAGE_ATLAS = ResourceLocation.fromNamespaceAndPath(
Rivencraft.MOD_ID, "textures/gui/notebook_pages.png");
private int lastCollection = -1;
private int selectedPage;
private int scrollRows;
public WritingDeskScreen(WritingDeskMenu menu, Inventory inventory, Component title) {
super(menu, inventory, title);
imageWidth = 398;
imageHeight = 176;
titleLabelX = 222;
inventoryLabelX = 224;
inventoryLabelY = 79;
}
@Override protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) {
int x = leftPos;
int y = topPos;
graphics.fill(x + 4, y + 8, x + 216, y + 174, 0xFF383230);
graphics.fill(x + 44, y + 14, x + 210, y + 170, 0xFF221B1A);
graphics.fill(x + 218, y + 4, x + 396, y + 174, 0xFF555454);
graphics.fill(x + 220, y + 6, x + 394, y + 172, 0xFFB7B7B7);
for (int i = 0; i < 4; i++) {
graphics.fill(x + 6, y + 16 + i * 37, x + 40, y + 48 + i * 37,
i == menu.activeCollection() ? 0xFF7777B6 : 0xFF999999);
slotFrame(graphics, 12, 20 + i * 37);
}
slotFrame(graphics, 370, 20);
slotFrame(graphics, 228, 20);
slotFrame(graphics, 228, 57);
slotFrame(graphics, 370, 57);
for (int row = 0; row < 3; row++) {
for (int column = 0; column < 9; column++) slotFrame(graphics, 224 + column * 18, 91 + row * 18);
}
for (int column = 0; column < 9; column++) slotFrame(graphics, 224 + column * 18, 149);
}
private void slotFrame(GuiGraphics graphics, int x, int y) {
graphics.fill(leftPos + x - 1, topPos + y - 1,
leftPos + x + 17, topPos + y + 17, 0xFF4D4D4D);
graphics.fill(leftPos + x, topPos + y,
leftPos + x + 16, topPos + y + 16, 0xFF858585);
}
@Override protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {
graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0x404040, false);
}
@Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
super.render(graphics, mouseX, mouseY, partialTick);
renderCollection(graphics);
renderTooltip(graphics, mouseX, mouseY);
}
private List<ItemStack> pages() {
ItemStack collection = menu.getSlot(menu.activeCollection()).getItem();
if (collection.is(ModItems.PAGE.get())) return List.of(collection);
if (collection.is(ModItems.NOTEBOOK.get())) {
return collection.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
}
return List.of();
}
private void renderCollection(GuiGraphics graphics) {
List<ItemStack> pages = pages();
if (lastCollection != menu.activeCollection()) {
lastCollection = menu.activeCollection();
scrollRows = 0;
selectedPage = 0;
}
int last = -1;
for (int i = 0; i < pages.size(); i++) if (!pages.get(i).isEmpty()) last = i;
for (int i = scrollRows * 5; i <= last && i < scrollRows * 5 + 15; i++) {
int column = i % 5;
int row = i / 5 - scrollRows;
ItemStack page = pages.get(i);
int x = leftPos + 48 + column * 31;
int y = topPos + 18 + row * 41;
drawPage(graphics, page, x, y);
if (page.isEmpty()) graphics.fill(x, y, x + 30, y + 40, 0xB8000000);
if (i == selectedPage && !page.isEmpty()) graphics.renderOutline(x, y, 30, 40, 0xFFFFD061);
}
if (selectedPage >= pages.size() || pages.get(selectedPage).isEmpty()) return;
ItemStack selected = pages.get(selectedPage);
graphics.pose().pushPose();
graphics.pose().translate(leftPos + 294, topPos + 12, 0);
graphics.pose().scale(1.35F, 1.35F, 1.0F);
drawPage(graphics, selected, 0, 0);
graphics.pose().popPose();
String title = font.plainSubstrByWidth(selected.getHoverName().getString(), 112);
graphics.fill(leftPos + 251, topPos + 64, leftPos + 365, topPos + 77, 0xFF202020);
graphics.drawString(font, title, leftPos + 254, topPos + 67, 0xFFE0E0E0, false);
}
private void drawPage(GuiGraphics graphics, ItemStack page, int x, int y) {
graphics.blit(PAGE_ATLAS, x, y, 30, 40, 156, 0, 30, 40, 256, 256);
if (page.isEmpty()) return;
switch (PageItem.content(page).kind()) {
case BLANK -> { }
case LINK_PANEL -> graphics.fill(x + 5, y + 6, x + 25, y + 20, 0xFF000000);
case SYMBOL, UNRESOLVED_SYMBOL -> graphics.blit(NotebookScreen.symbolTexture(page), x + 1, y + 6,
28, 28, 16, 16, 96, 96, 128, 128);
}
}
@Override public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (button == 0) {
int x = (int) mouseX - leftPos - 48;
int y = (int) mouseY - topPos - 18;
if (x >= 0 && x < 155 && y >= 0 && y < 123) {
int column = x / 31;
int row = y / 41;
int index = (row + scrollRows) * 5 + column;
List<ItemStack> pages = pages();
if (x % 31 < 30 && y % 41 < 40 && index < pages.size()
&& !pages.get(index).isEmpty()) {
selectedPage = index;
}
return true;
}
}
if (button == 0 && mouseX >= leftPos + 29 && mouseX < leftPos + 40) {
for (int i = 0; i < 4; i++) {
int top = topPos + 16 + i * 37;
if (mouseY >= top && mouseY < top + 32) {
if (minecraft != null && minecraft.gameMode != null) {
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, i);
}
return true;
}
}
}
return super.mouseClicked(mouseX, mouseY, button);
}
@Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
if (mouseX >= leftPos + 48 && mouseX < leftPos + 203
&& mouseY >= topPos + 18 && mouseY < topPos + 141) {
int rows = (pages().size() + 4) / 5;
scrollRows = Math.clamp(scrollRows - (int) Math.signum(scrollY), 0, Math.max(0, rows - 3));
return true;
}
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
}
}
@@ -203,6 +203,29 @@ public final class WritingDataGameTests {
helper.succeed();
}
@GameTest(template = "empty")
public static void notebookMenuPreservesSparsePagePositions(GameTestHelper helper) {
var player = helper.makeMockServerPlayerInLevel();
player.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(ModItems.NOTEBOOK.get()));
NotebookMenu menu = new NotebookMenu(9, player.getInventory(), InteractionHand.MAIN_HAND);
ItemStack page = PageItem.createSymbolPage(reference("rivencraft:terrain/standard"));
menu.setCarried(page);
menu.clicked(9, 0, ClickType.PICKUP, player);
helper.assertTrue(menu.getCarried().isEmpty(),
"Clicking a distant empty Notebook slot must place the carried Page");
NotebookContents saved = player.getMainHandItem().get(ModDataComponents.NOTEBOOK_CONTENTS.get());
helper.assertTrue(saved != null && saved.pages().size() > 9,
"Notebook persistence must extend through the highest occupied position");
for (int index = 0; index < 9; index++) {
helper.assertTrue(saved.pages().get(index).isEmpty(),
"Earlier empty page positions must remain empty rather than being packed together");
}
helper.assertValueEqual(PageItem.content(saved.pages().get(9)).kind(), PageContent.Kind.SYMBOL,
"The Page must remain in the selected distant position");
helper.succeed();
}
@GameTest(template = "empty")
public static void inkVialsPersistAndOnlyStackWithMatchingContents(GameTestHelper helper) {
ItemStack vial = new ItemStack(ModItems.INK_VIAL.get());
@@ -0,0 +1,85 @@
package net.dnorberg.rivencraft.gametest;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
import net.dnorberg.rivencraft.registry.ModBlocks;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.AABB;
import net.neoforged.neoforge.gametest.GameTestHolder;
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
@GameTestHolder(Rivencraft.MOD_ID)
@PrefixGameTestTemplate(false)
public final class WritingDeskGameTests {
private WritingDeskGameTests() {}
@GameTest(template = "empty")
public static void inventoryAndActiveCollectionPersist(GameTestHelper helper) {
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
var original = new WritingDeskBlockEntity(pos, ModBlocks.WRITING_DESK.get().defaultBlockState());
original.setItem(0, new ItemStack(ModItems.NOTEBOOK.get()));
original.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
original.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(ModItems.PAGE.get(), 12));
original.setActiveCollection(2);
var saved = original.saveWithoutMetadata(helper.getLevel().registryAccess());
var restored = new WritingDeskBlockEntity(pos, ModBlocks.WRITING_DESK.get().defaultBlockState());
restored.loadWithComponents(saved, helper.getLevel().registryAccess());
helper.assertTrue(restored.getItem(0).is(ModItems.NOTEBOOK.get()), "Notebook lost on save/load");
helper.assertTrue(restored.getItem(WritingDeskBlockEntity.INK).is(ModItems.INK_VIAL.get()), "Ink lost on save/load");
helper.assertValueEqual(restored.getItem(WritingDeskBlockEntity.PAPER).getCount(), 12, "Pages lost on save/load");
helper.assertValueEqual(restored.activeCollection(), 2, "Active collection lost on save/load");
helper.succeed();
}
@GameTest(template = "empty")
public static void breakingDeskDropsItsStoredItemsOnce(GameTestHelper helper) {
BlockPos relative = new BlockPos(1, 1, 1);
BlockPos pos = helper.absolutePos(relative);
helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3);
WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos);
desk.setItem(0, new ItemStack(ModItems.NOTEBOOK.get()));
desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(ModItems.PAGE.get(), 12));
helper.getLevel().setBlock(pos, Blocks.AIR.defaultBlockState(), 3);
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 ink = drops.stream().filter(e -> e.getItem().is(ModItems.INK_VIAL.get())).mapToInt(e -> e.getItem().getCount()).sum();
int pages = drops.stream().filter(e -> e.getItem().is(ModItems.PAGE.get())).mapToInt(e -> e.getItem().getCount()).sum();
helper.assertValueEqual(notebooks, 1, "Notebook must drop exactly once");
helper.assertValueEqual(ink, 1, "Ink must drop exactly once");
helper.assertValueEqual(pages, 12, "Pages must drop without loss");
helper.succeed();
}
@GameTest(template = "empty")
public static void menuTransfersWithoutDuplicationAndClosesWhenReplaced(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);
var player = helper.makeMockServerPlayerInLevel();
player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
player.getInventory().setItem(9, new ItemStack(ModItems.PAGE.get(), 12));
WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk);
helper.assertTrue(menu.stillValid(player), "Placed desk must keep its menu valid");
helper.assertTrue(menu.clickMenuButton(player, 3), "Active collection control must accept a valid tab");
helper.assertValueEqual(desk.activeCollection(), 3, "Selected tab must be stored on the desk");
menu.quickMoveStack(player, 8);
helper.assertValueEqual(desk.getItem(WritingDeskBlockEntity.PAPER).getCount(), 12, "Shift-click must deposit all pages");
helper.assertTrue(player.getInventory().getItem(9).isEmpty(), "Shift-click must consume player source stack");
menu.quickMoveStack(player, WritingDeskBlockEntity.PAPER);
helper.assertTrue(desk.getItem(WritingDeskBlockEntity.PAPER).isEmpty(), "Shift-click must empty desk source slot");
int total = player.getInventory().items.stream().filter(stack -> stack.is(ModItems.PAGE.get()))
.mapToInt(ItemStack::getCount).sum();
helper.assertValueEqual(total, 12, "Round-trip must not lose or duplicate pages");
helper.getLevel().setBlock(pos, Blocks.AIR.defaultBlockState(), 3);
helper.assertFalse(menu.stillValid(player), "Removed desk must invalidate menu");
helper.succeed();
}
}
@@ -0,0 +1,101 @@
package net.dnorberg.rivencraft.menu;
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.registry.ModMenus;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.world.Container;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.DataSlot;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
public final class WritingDeskMenu extends AbstractContainerMenu {
private static final int PLAYER_START = WritingDeskBlockEntity.SIZE;
private final Container desk;
private final WritingDeskBlockEntity blockEntity;
private int syncedActiveCollection;
public WritingDeskMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) {
this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos())
instanceof WritingDeskBlockEntity found ? found : null);
}
public WritingDeskMenu(int id, Inventory inventory, WritingDeskBlockEntity blockEntity) {
super(ModMenus.WRITING_DESK.get(), id);
this.blockEntity = blockEntity;
this.desk = blockEntity == null ? new SimpleContainer(WritingDeskBlockEntity.SIZE) : blockEntity;
addDataSlot(new DataSlot() {
@Override public int get() { return WritingDeskMenu.this.blockEntity == null
? syncedActiveCollection : WritingDeskMenu.this.blockEntity.activeCollection(); }
@Override public void set(int value) { syncedActiveCollection = value; }
});
for (int i = 0; i < WritingDeskBlockEntity.COLLECTIONS; i++) {
addSlot(new DeskSlot(desk, i, 12, 20 + i * 37, 1));
}
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.INK, 370, 20, 1));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.SOURCE, 228, 20, 1));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.PAPER, 228, 57, 64));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.OUTPUT, 370, 57, 64));
for (int row = 0; row < 3; row++) {
for (int column = 0; column < 9; column++) {
addSlot(new Slot(inventory, column + row * 9 + 9, 224 + column * 18, 91 + row * 18));
}
}
for (int column = 0; column < 9; column++) {
addSlot(new Slot(inventory, column, 224 + column * 18, 149));
}
}
public int activeCollection() { return syncedActiveCollection; }
@Override public boolean clickMenuButton(Player player, int id) {
if (blockEntity == null || !stillValid(player) || id < 0 || id >= WritingDeskBlockEntity.COLLECTIONS) return false;
blockEntity.setActiveCollection(id);
return true;
}
@Override public boolean stillValid(Player player) {
return blockEntity != null && blockEntity.stillValid(player);
}
@Override public ItemStack quickMoveStack(Player player, int index) {
if (!stillValid(player) || index < 0 || index >= slots.size()) return ItemStack.EMPTY;
Slot slot = slots.get(index);
if (!slot.hasItem()) return ItemStack.EMPTY;
ItemStack moving = slot.getItem();
ItemStack original = moving.copy();
if (index < PLAYER_START) {
if (!moveItemStackTo(moving, PLAYER_START, slots.size(), true)) return ItemStack.EMPTY;
} else if (!moveIntoDesk(moving)) {
return ItemStack.EMPTY;
}
if (moving.isEmpty()) slot.setByPlayer(ItemStack.EMPTY);
else slot.setChanged();
return original;
}
private boolean moveIntoDesk(ItemStack moving) {
if (moving.is(ModItems.INK_VIAL.get())) return moveItemStackTo(moving, 4, 5, false);
if (moving.is(ModItems.NOTEBOOK.get())) return moveItemStackTo(moving, 0, 4, false);
if (moving.is(ModItems.PAGE.get())) {
return moveItemStackTo(moving, 6, 7, false)
|| moveItemStackTo(moving, 0, 4, false)
|| moveItemStackTo(moving, 5, 6, false);
}
return false;
}
private static final class DeskSlot extends Slot {
private final int limit;
private DeskSlot(Container container, int index, int x, int y, int limit) {
super(container, index, x, y);
this.limit = limit;
}
@Override public boolean mayPlace(ItemStack stack) { return container.canPlaceItem(getContainerSlot(), stack); }
@Override public int getMaxStackSize() { return limit; }
}
}
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.registry;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.neoforged.bus.api.IEventBus;
@@ -16,6 +17,9 @@ public final class ModBlockEntities {
BLOCK_ENTITIES.register("book_stand", () -> BlockEntityType.Builder
.of(BookStandBlockEntity::new, ModBlocks.BOOK_STAND.get())
.build(null));
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<WritingDeskBlockEntity>> WRITING_DESK =
BLOCK_ENTITIES.register("writing_desk", () -> BlockEntityType.Builder
.of(WritingDeskBlockEntity::new, ModBlocks.WRITING_DESK.get()).build(null));
private ModBlockEntities() {
}
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.registry;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.block.BookStandBlock;
import net.dnorberg.rivencraft.block.WritingDeskBlock;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.neoforged.bus.api.IEventBus;
@@ -16,6 +17,10 @@ public final class ModBlocks {
BookStandBlock::new,
BlockBehaviour.Properties.ofFullCopy(Blocks.LECTERN)
);
public static final DeferredBlock<WritingDeskBlock> WRITING_DESK = BLOCKS.registerBlock(
"writing_desk", WritingDeskBlock::new,
BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE)
);
private ModBlocks() {
}
@@ -22,6 +22,7 @@ public final class ModCreativeTabs {
output.accept(ModItems.LINKING_BOOK.get());
output.accept(ModItems.NOTEBOOK.get());
output.accept(ModItems.INK_VIAL.get());
output.accept(ModItems.WRITING_DESK.get());
})
.build());
@@ -7,6 +7,7 @@ import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.item.NotebookItem;
import net.dnorberg.rivencraft.item.InkVialItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.BlockItem;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredItem;
import net.neoforged.neoforge.registries.DeferredRegister;
@@ -39,6 +40,7 @@ public final class ModItems {
InkVialItem::new,
new Item.Properties().stacksTo(16)
);
public static final DeferredItem<BlockItem> WRITING_DESK = ITEMS.registerSimpleBlockItem(ModBlocks.WRITING_DESK);
private ModItems() {
}
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.registry;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.menu.NotebookMenu;
import net.dnorberg.rivencraft.menu.WritingDeskMenu;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.inventory.MenuType;
import net.neoforged.bus.api.IEventBus;
@@ -15,6 +16,8 @@ public final class ModMenus {
public static final DeferredHolder<MenuType<?>, MenuType<NotebookMenu>> NOTEBOOK =
MENUS.register("notebook", () -> IMenuTypeExtension.create(NotebookMenu::new));
public static final DeferredHolder<MenuType<?>, MenuType<WritingDeskMenu>> WRITING_DESK =
MENUS.register("writing_desk", () -> IMenuTypeExtension.create(WritingDeskMenu::new));
private ModMenus() {
}
@@ -0,0 +1,8 @@
{
"variants": {
"facing=north": { "model": "rivencraft:block/writing_desk" },
"facing=east": { "model": "rivencraft:block/writing_desk", "y": 90 },
"facing=south": { "model": "rivencraft:block/writing_desk", "y": 180 },
"facing=west": { "model": "rivencraft:block/writing_desk", "y": 270 }
}
}
@@ -16,6 +16,8 @@
"item.rivencraft.notebook.pages": "Pages: %s/%s",
"item.rivencraft.notebook.unresolved": "Unresolved pages: %s",
"item.rivencraft.ink_vial": "Ink Vial",
"block.rivencraft.writing_desk": "Writing Desk",
"container.rivencraft.writing_desk": "Writing Desk",
"item.rivencraft.ink_vial.remaining": "Ink: %s/%s",
"item.rivencraft.ink_vial.usage": "Use with a blank Page in the other hand to prepare a Link Panel.",
"screen.rivencraft.notebook.search": "Search pages",
@@ -0,0 +1,8 @@
{
"parent": "minecraft:block/cube_bottom_top",
"textures": {
"top": "rivencraft:item/writing_desk",
"side": "minecraft:block/oak_planks",
"bottom": "minecraft:block/oak_planks"
}
}
@@ -0,0 +1,4 @@
{
"parent": "minecraft:item/generated",
"textures": { "layer0": "rivencraft:item/writing_desk" }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

@@ -0,0 +1,4 @@
{
"type": "minecraft:block",
"pools": [{ "rolls": 1, "entries": [{ "type": "minecraft:item", "name": "rivencraft:writing_desk" }] }]
}
@@ -0,0 +1,11 @@
{
"type": "minecraft:crafting_shaped",
"category": "misc",
"pattern": ["V F", "PPP", "P P"],
"key": {
"V": { "item": "minecraft:glass_bottle" },
"F": { "item": "minecraft:feather" },
"P": { "tag": "minecraft:planks" }
},
"result": { "id": "rivencraft:writing_desk", "count": 1 }
}