Compare commits

...
2 Commits
Author SHA1 Message Date
dnorberg e2e348d5c5 Refine Writing Desk GUI to match Legacy layout
Build / build (push) Canceled after 0s
2026-09-24 14:54:42 -04:00
dnorberg 7f9cf2a4a5 Fix Writing Desk GUI and two-block rendering
Build / build (push) Canceled after 0s
2026-09-24 14:36:49 -04:00
19 changed files with 441 additions and 222 deletions

No files matched your search

+2
View File
@@ -14,6 +14,8 @@ The Writing Desk item texture is copied from Legacy's
assets/mystcraft/textures/items/writingdesk.png under the same license. assets/mystcraft/textures/items/writingdesk.png under the same license.
The Writing Desk block texture is copied from Legacy's The Writing Desk block texture is copied from Legacy's
assets/mystcraft/textures/entity/desk.png under the same license. assets/mystcraft/textures/entity/desk.png under the same license.
The Writing Desk GUI atlas is copied from Legacy's assets/mystcraft/gui/writingdesk.png
under the same license; desk wood and inlay textures are cropped from desk.png.
The initial build layout is derived from the official NeoForge 1.21.1 The initial build layout is derived from the official NeoForge 1.21.1
ModDevGradle MDK: https://github.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle ModDevGradle MDK: https://github.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle
+8 -3
View File
@@ -21,6 +21,10 @@ 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`. and link-panel costs in the `writing` section of `rivencraft-server.toml`.
The Writing Desk can copy a selected written page from its collection tabs. The Writing Desk can copy a selected written page from its collection tabs.
The Writing Desk is now craftable from planks, a glass bottle, and a feather. The Writing Desk is now craftable from planks, a glass bottle, and a feather.
It occupies two adjacent blocks, with one shared inventory; loaded paper and
the selected page appear on its Legacy-inspired model.
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.
Its four collection tabs, paper input, ink vial input, empty glass-bottle return, Its four collection tabs, paper input, ink vial input, empty glass-bottle return,
16-unit ink meter, and inventory persist 16-unit ink meter, and inventory persist
through world saves; breaking it drops stored contents. The desk screen is a through world saves; breaking it drops stored contents. The desk screen is a
@@ -29,9 +33,10 @@ and right inventory. Put vanilla paper in the
upper-left slot and Ink Vials in the upper-right; a copy appears in the upper-left slot and Ink Vials in the upper-right; a copy appears in the
lower-left result slot when the selected page is valid. Taking it consumes one lower-left result slot when the selected page is valid. Taking it consumes one
paper and, by default, one ink. An empty meter consumes a vial to refill and paper and, by default, one ink. An empty meter consumes a vial to refill and
returns a vanilla glass bottle to the lower-right slot. The ALL browser searches returns a vanilla glass bottle to the lower-right slot. The `AZ` control sorts
and filters symbols held in the desk's loose pages and Notebooks; selection visible pages by name; `ALL` shows pages held across every desk tab. Search and
is validated against the server's current symbol catalog. category filtering narrow the view without granting new pages, and cross-tab
copy selection is checked by the server.
Selecting a stored page on the desk surface shows its Legacy-proportioned Selecting a stored page on the desk surface shows its Legacy-proportioned
portrait preview and name above the player inventory. portrait preview and name above the player inventory.
+3 -1
View File
@@ -9,7 +9,9 @@ project's GNU LGPL v3 license. Rivencraft itself uses the same license.
| `assets/rivencraft/textures/item/page.png` | `assets/mystcraft/textures/items/page_background.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | | `assets/rivencraft/textures/item/page.png` | `assets/mystcraft/textures/items/page_background.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only |
| `assets/rivencraft/textures/gui/notebook_pages.png` | `assets/mystcraft/gui/bookui_pagel.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Notebook draws the native 30×40 page at atlas coordinates `(156, 0)` | | `assets/rivencraft/textures/gui/notebook_pages.png` | `assets/mystcraft/gui/bookui_pagel.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Notebook draws the native 30×40 page at atlas coordinates `(156, 0)` |
| `assets/rivencraft/textures/item/writing_desk.png` | `assets/mystcraft/textures/items/writingdesk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only | | `assets/rivencraft/textures/item/writing_desk.png` | `assets/mystcraft/textures/items/writingdesk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only |
| `assets/rivencraft/textures/block/writing_desk.png` | `assets/mystcraft/textures/entity/desk.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; single-block model adapts the desk geometry and UV regions from `ModelWritingDesk` | | `assets/rivencraft/textures/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/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/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.0 mod_version=0.3.6.2
immersive_portals_version=6.0.7 immersive_portals_version=6.0.7
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources. # This should match the base package used for the mod sources.
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.dnorberg.rivencraft.menu.WritingDeskMenu; import net.dnorberg.rivencraft.menu.WritingDeskMenu;
import net.dnorberg.rivencraft.writing.WritingDeskCatalog; import net.dnorberg.rivencraft.writing.WritingDeskCatalog;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Containers; import net.minecraft.world.Containers;
@@ -19,48 +20,72 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition; 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.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DirectionProperty; import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.level.block.state.properties.BooleanProperty;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.entity.LivingEntity;
import org.jetbrains.annotations.Nullable;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.BlockHitResult;
public final class WritingDeskBlock extends BaseEntityBlock { public final class WritingDeskBlock extends BaseEntityBlock {
public static final MapCodec<WritingDeskBlock> CODEC = simpleCodec(WritingDeskBlock::new); public static final MapCodec<WritingDeskBlock> CODEC = simpleCodec(WritingDeskBlock::new);
public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
public static final BooleanProperty EXTENSION = BooleanProperty.create("extension");
public WritingDeskBlock(Properties properties) { public WritingDeskBlock(Properties properties) {
super(properties); super(properties);
registerDefaultState(stateDefinition.any().setValue(FACING, net.minecraft.core.Direction.NORTH)); registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH).setValue(EXTENSION, false));
} }
@Override public MapCodec<? extends BaseEntityBlock> codec() { return CODEC; } @Override public MapCodec<? extends BaseEntityBlock> codec() { return CODEC; }
@Override protected void createBlockStateDefinition(StateDefinition.Builder<net.minecraft.world.level.block.Block, BlockState> builder) { @Override protected void createBlockStateDefinition(StateDefinition.Builder<net.minecraft.world.level.block.Block, BlockState> builder) {
builder.add(FACING); builder.add(FACING, EXTENSION);
} }
@Override public BlockState getStateForPlacement(BlockPlaceContext context) { @Override public BlockState getStateForPlacement(BlockPlaceContext context) {
return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); Direction facing = context.getHorizontalDirection().getOpposite();
BlockPos extension = context.getClickedPos().relative(facing.getClockWise());
return context.getLevel().getBlockState(extension).canBeReplaced(context)
? defaultBlockState().setValue(FACING, facing) : null;
}
@Override public void setPlacedBy(Level level, BlockPos pos, BlockState state,
@Nullable LivingEntity placer, ItemStack stack) {
super.setPlacedBy(level, pos, state, placer, stack);
level.setBlock(pos.relative(state.getValue(FACING).getClockWise()),
state.setValue(EXTENSION, true), 3);
} }
@Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; } @Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; }
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new WritingDeskBlockEntity(pos, state); return state.getValue(EXTENSION) ? null : new WritingDeskBlockEntity(pos, state);
} }
@Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, @Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
Player player, BlockHitResult hit) { Player player, BlockHitResult hit) {
BlockPos controller = state.getValue(EXTENSION)
? pos.relative(state.getValue(FACING).getCounterClockWise()) : pos;
if (!level.isClientSide && player instanceof ServerPlayer serverPlayer if (!level.isClientSide && player instanceof ServerPlayer serverPlayer
&& level.getBlockEntity(pos) instanceof WritingDeskBlockEntity desk) { && level.getBlockEntity(controller) instanceof WritingDeskBlockEntity desk) {
serverPlayer.openMenu(new SimpleMenuProvider( serverPlayer.openMenu(new SimpleMenuProvider(
(id, inventory, ignored) -> new WritingDeskMenu(id, inventory, desk), (id, inventory, ignored) -> new WritingDeskMenu(id, inventory, desk),
Component.translatable("container.rivencraft.writing_desk")), Component.translatable("container.rivencraft.writing_desk")),
buffer -> { buffer -> {
buffer.writeBlockPos(pos); buffer.writeBlockPos(controller);
WritingDeskCatalog.write(buffer, WritingDeskCatalog.available(desk)); WritingDeskCatalog.write(buffer, WritingDeskCatalog.available(desk));
}); });
} }
return InteractionResult.sidedSuccess(level.isClientSide); return InteractionResult.sidedSuccess(level.isClientSide);
} }
@Override protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState replacement, boolean moving) { @Override protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState replacement, boolean moving) {
if (!state.is(replacement.getBlock()) && !level.isClientSide if (!state.is(replacement.getBlock())) {
&& level.getBlockEntity(pos) instanceof WritingDeskBlockEntity desk) { if (!level.isClientSide && !state.getValue(EXTENSION)
Containers.dropContents(level, pos, desk); && level.getBlockEntity(pos) instanceof WritingDeskBlockEntity desk) {
level.updateNeighbourForOutputSignal(pos, this); Containers.dropContents(level, pos, desk);
level.updateNeighbourForOutputSignal(pos, this);
}
BlockPos other = pos.relative(state.getValue(FACING).getClockWise(),
state.getValue(EXTENSION) ? -1 : 1);
BlockState otherState = level.getBlockState(other);
if (otherState.is(this) && otherState.getValue(EXTENSION) != state.getValue(EXTENSION)) {
level.setBlock(other, net.minecraft.world.level.block.Blocks.AIR.defaultBlockState(), 35);
}
} }
super.onRemove(state, level, pos, replacement, moving); super.onRemove(state, level, pos, replacement, moving);
} }
@@ -8,6 +8,9 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderLookup;
import net.minecraft.core.NonNullList; import net.minecraft.core.NonNullList;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.Container; import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper; import net.minecraft.world.ContainerHelper;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
@@ -28,6 +31,7 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
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;
private ItemStack displayPage = ItemStack.EMPTY;
public WritingDeskBlockEntity(BlockPos pos, BlockState state) { public WritingDeskBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.WRITING_DESK.get(), pos, state); super(ModBlockEntities.WRITING_DESK.get(), pos, state);
@@ -35,6 +39,14 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
public int activeCollection() { return activeCollection; } public int activeCollection() { return activeCollection; }
public int inkRemaining() { return inkRemaining; } public int inkRemaining() { return inkRemaining; }
public ItemStack displayPage() { return displayPage; }
public void setDisplayPage(ItemStack page) {
ItemStack next = page.isEmpty() ? ItemStack.EMPTY : page.copyWithCount(1);
if (!ItemStack.matches(displayPage, next)) {
displayPage = next;
setChanged();
}
}
public void setActiveCollection(int index) { public void setActiveCollection(int index) {
if (index >= 0 && index < COLLECTIONS && activeCollection != index) { if (index >= 0 && index < COLLECTIONS && activeCollection != index) {
activeCollection = index; activeCollection = index;
@@ -47,13 +59,17 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
@Override public ItemStack getItem(int slot) { return items.get(slot); } @Override public ItemStack getItem(int slot) { return items.get(slot); }
@Override public ItemStack removeItem(int slot, int amount) { @Override public ItemStack removeItem(int slot, int amount) {
ItemStack removed = ContainerHelper.removeItem(items, slot, amount); ItemStack removed = ContainerHelper.removeItem(items, slot, amount);
if (!removed.isEmpty()) setChanged(); if (!removed.isEmpty()) {
if (slot < COLLECTIONS) displayPage = ItemStack.EMPTY;
setChanged();
}
return removed; return removed;
} }
@Override public ItemStack removeItemNoUpdate(int slot) { return ContainerHelper.takeItem(items, slot); } @Override public ItemStack removeItemNoUpdate(int slot) { return ContainerHelper.takeItem(items, slot); }
@Override public void setItem(int slot, ItemStack stack) { @Override public void setItem(int slot, ItemStack stack) {
items.set(slot, stack); items.set(slot, stack);
if (stack.getCount() > getMaxStackSize()) stack.setCount(getMaxStackSize()); if (stack.getCount() > getMaxStackSize()) stack.setCount(getMaxStackSize());
if (slot < COLLECTIONS) displayPage = ItemStack.EMPTY;
setChanged(); setChanged();
} }
@Override public boolean stillValid(Player player) { @Override public boolean stillValid(Player player) {
@@ -107,6 +123,7 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
items.clear(); items.clear();
items = NonNullList.withSize(SIZE, ItemStack.EMPTY); items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
inkRemaining = 0; inkRemaining = 0;
displayPage = ItemStack.EMPTY;
setChanged(); setChanged();
} }
@Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { @Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
@@ -115,11 +132,27 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
ContainerHelper.loadAllItems(tag, items, registries); ContainerHelper.loadAllItems(tag, items, registries);
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)
? ItemStack.parse(registries, tag.getCompound("DisplayPage")).orElse(ItemStack.EMPTY)
: ItemStack.EMPTY;
} }
@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("ActiveCollection", activeCollection); tag.putInt("ActiveCollection", activeCollection);
tag.putInt("InkRemaining", inkRemaining); tag.putInt("InkRemaining", inkRemaining);
if (!displayPage.isEmpty()) tag.put("DisplayPage", displayPage.save(registries));
}
@Override public Packet<ClientGamePacketListener> getUpdatePacket() {
return ClientboundBlockEntityDataPacket.create(this);
}
@Override public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
return saveWithoutMetadata(registries);
}
@Override public void setChanged() {
super.setChanged();
if (level != null && !level.isClientSide) {
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 2);
}
} }
} }
@@ -25,6 +25,7 @@ public final class ClientEvents {
@SubscribeEvent @SubscribeEvent
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) { public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerBlockEntityRenderer(ModBlockEntities.BOOK_STAND.get(), BookStandRenderer::new); event.registerBlockEntityRenderer(ModBlockEntities.BOOK_STAND.get(), BookStandRenderer::new);
event.registerBlockEntityRenderer(ModBlockEntities.WRITING_DESK.get(), WritingDeskRenderer::new);
} }
@SubscribeEvent @SubscribeEvent
@@ -0,0 +1,55 @@
package net.dnorberg.rivencraft.client;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import net.dnorberg.rivencraft.block.WritingDeskBlock;
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
/** Visible selected page and loaded paper, as on the Legacy desk. */
public final class WritingDeskRenderer implements BlockEntityRenderer<WritingDeskBlockEntity> {
public WritingDeskRenderer(BlockEntityRendererProvider.Context context) {}
@Override public void render(WritingDeskBlockEntity desk, float partialTick, PoseStack pose,
MultiBufferSource buffers, int light, int overlay) {
int paper = desk.getItem(WritingDeskBlockEntity.PAPER).getCount();
ItemStack page = desk.displayPage();
if (paper == 0 && page.isEmpty()) return;
pose.pushPose();
pose.translate(0.5, 0, 0.5);
pose.mulPose(Axis.YP.rotationDegrees(
180.0F - desk.getBlockState().getValue(WritingDeskBlock.FACING).toYRot()));
if (paper > 0) {
ItemStack sheet = new ItemStack(Items.PAPER);
int layers = Math.min(4, 1 + paper / 16);
for (int i = 0; i < layers; i++) {
drawFlat(desk, sheet, pose, buffers, light, overlay, 1.22, 0.27 + i * 0.008, -0.14, 0.55F);
}
}
if (!page.isEmpty()) {
drawFlat(desk, page, pose, buffers, light, overlay, 1.0, 0.572, 0.0, 0.8F);
}
pose.popPose();
}
private static void drawFlat(WritingDeskBlockEntity desk, ItemStack stack, PoseStack pose,
MultiBufferSource buffers, int light, int overlay,
double x, double y, double z, float scale) {
pose.pushPose();
pose.translate(x, y, z);
pose.mulPose(Axis.XP.rotationDegrees(90));
pose.scale(scale, scale, scale);
Minecraft.getInstance().getItemRenderer().renderStatic(stack, ItemDisplayContext.FIXED,
light, overlay, pose, buffers, desk.getLevel(), 0);
pose.popPose();
}
@Override public boolean shouldRenderOffScreen(WritingDeskBlockEntity blockEntity) { return true; }
@Override public int getViewDistance() { return 64; }
}
@@ -2,7 +2,8 @@ package net.dnorberg.rivencraft.client.screen;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.ArrayList;
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;
@@ -23,91 +24,134 @@ import net.minecraft.world.item.ItemStack;
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(
Rivencraft.MOD_ID, "textures/gui/writing_desk_legacy.png");
private int lastCollection = -1; private int lastCollection = -1;
private int selectedPage; private int selectedPage;
private int scrollRows; private int scrollRows;
private boolean allMode; private boolean allMode;
private ResourceLocation selectedSymbol; private boolean sortAlphabetically;
private int selectedCollection = -1;
private ResourceLocation selectedCategory; private ResourceLocation selectedCategory;
private EditBox search; private EditBox search;
private Button categoryButton; private Button categoryButton;
private Button allButton;
private Button sortButton;
private boolean draggingScrollbar;
private record DisplayPage(int collection, int index, ItemStack stack) {}
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 = 398;
imageHeight = 176; imageHeight = 176;
titleLabelX = 222; titleLabelX = 222;
inventoryLabelX = 224;
inventoryLabelY = 79;
} }
@Override protected void init() { @Override protected void init() {
super.init(); super.init();
addRenderableWidget(Button.builder(Component.literal("ALL"), button -> { sortButton = addRenderableWidget(Button.builder(Component.literal("AZ"), button -> {
allMode = true; sortAlphabetically = !sortAlphabetically;
scrollRows = 0; scrollRows = 0;
}).bounds(leftPos + 48, topPos - 18, 29, 18).build()); }).bounds(leftPos + 58, topPos - 18, 20, 18).build());
categoryButton = addRenderableWidget(Button.builder(Component.literal("All types"), button -> { allButton = addRenderableWidget(Button.builder(Component.literal("ALL"), button -> {
allMode = !allMode;
scrollRows = 0;
if (!allMode) {
selectedCollection = -1;
selectedPage = 0;
if (minecraft != null && minecraft.gameMode != null)
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, menu.activeCollection());
}
}).bounds(leftPos + 80, topPos - 18, 28, 18).build());
search = new EditBox(font, leftPos + 110, topPos - 18, 86, 18,
Component.translatable("screen.rivencraft.writing_desk.search"));
search.setMaxLength(64);
search.setHint(Component.translatable("screen.rivencraft.writing_desk.search"));
search.setResponder(value -> scrollRows = 0);
addRenderableWidget(search);
categoryButton = addRenderableWidget(Button.builder(Component.literal("C"), button -> {
List<ResourceLocation> categories = menu.catalog().stream() List<ResourceLocation> categories = menu.catalog().stream()
.map(SymbolDisplayMetadata::category).distinct().sorted().toList(); .map(SymbolDisplayMetadata::category).distinct().sorted().toList();
int next = selectedCategory == null ? 0 : categories.indexOf(selectedCategory) + 1; int next = selectedCategory == null ? 0 : categories.indexOf(selectedCategory) + 1;
selectedCategory = next >= categories.size() ? null : categories.get(next); selectedCategory = next >= categories.size() ? null : categories.get(next);
categoryButton.setMessage(Component.literal(selectedCategory == null categoryButton.setMessage(Component.literal(selectedCategory == null ? "C" : "C*"));
? "All types" : selectedCategory.getPath()));
allMode = true;
scrollRows = 0; scrollRows = 0;
}).bounds(leftPos + 79, topPos - 18, 66, 18).build()); }).bounds(leftPos + 198, topPos - 18, 18, 18).build());
search = new EditBox(font, leftPos + 147, topPos - 18, 69, 18,
Component.translatable("screen.rivencraft.writing_desk.search"));
search.setMaxLength(64);
search.setHint(Component.translatable("screen.rivencraft.writing_desk.search"));
search.setResponder(value -> {
allMode = true;
scrollRows = 0;
});
addRenderableWidget(search);
} }
private List<SymbolDisplayMetadata> filteredCatalog() { private List<DisplayPage> filteredPages() {
String query = search == null ? "" : search.getValue().trim().toLowerCase(Locale.ROOT); String query = search == null ? "" : search.getValue().trim().toLowerCase(Locale.ROOT);
return menu.catalog().stream() List<DisplayPage> entries = new ArrayList<>();
.filter(entry -> selectedCategory == null || selectedCategory.equals(entry.category())) int from = allMode ? 0 : menu.activeCollection();
.filter(entry -> query.isEmpty() int until = allMode ? 4 : from + 1;
|| entry.id().toString().toLowerCase(Locale.ROOT).contains(query) for (int collection = from; collection < until; collection++) {
|| Component.translatable(entry.nameTranslationKey()).getString() List<ItemStack> pages = pages(collection);
.toLowerCase(Locale.ROOT).contains(query)) int last = -1;
.toList(); for (int i = 0; i < pages.size(); i++) if (!pages.get(i).isEmpty()) last = i;
for (int i = 0; i <= last; i++) {
ItemStack page = pages.get(i);
if (page.isEmpty() && (allMode || sortAlphabetically || !query.isEmpty() || selectedCategory != null)) continue;
if (!page.isEmpty() && !query.isEmpty()
&& !page.getHoverName().getString().toLowerCase(Locale.ROOT).contains(query)
&& !PageItem.content(page).symbol().map(symbol -> symbol.id().toString()
.toLowerCase(Locale.ROOT).contains(query)).orElse(false)) continue;
if (!page.isEmpty() && selectedCategory != null) {
ResourceLocation id = PageItem.content(page).symbol().map(symbol -> symbol.id()).orElse(null);
if (menu.catalog().stream().noneMatch(metadata -> metadata.id().equals(id)
&& metadata.category().equals(selectedCategory))) continue;
}
entries.add(new DisplayPage(collection, i, page));
}
}
if (sortAlphabetically) entries.sort(Comparator
.comparing((DisplayPage entry) -> entry.stack().getHoverName().getString(), String.CASE_INSENSITIVE_ORDER)
.thenComparingInt(DisplayPage::collection).thenComparingInt(DisplayPage::index));
return entries;
} }
@Override public void renderBackground(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { @Override public void renderBackground(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
// Legacy leaves the world visible around the desk instead of dimming it. // Keep the world visible, but still draw our GUI backing. The vanilla
// container implementation normally calls renderBg from this method.
renderBg(graphics, partialTick, mouseX, mouseY);
} }
@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 + 4, y + 8, x + 216, y + 174, 0xFF383230); graphics.fill(x + 56, y + 14, x + 206, y + 168, 0xAA201B18);
graphics.fill(x + 44, y + 14, x + 210, y + 170, 0xFF221B1A); graphics.blit(LEGACY_DESK, x, y + 5, 56, 9, 0, 166, 56, 9, 256, 256);
graphics.fill(x + 218, y + 4, x + 396, y + 174, 0xFF555454); graphics.blit(LEGACY_DESK, x, y + 166, 56, 9, 0, 215, 56, 9, 256, 256);
graphics.fill(x + 220, y + 6, x + 394, y + 172, 0xFFB7B7B7); 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.fill(x + 6, y + 16 + i * 37, x + 40, y + 48 + i * 37, graphics.blit(LEGACY_DESK, x, y + 16 + i * 37,
i == menu.activeCollection() ? 0xFF7777B6 : 0xFF999999); 56, 36, 0, 166, 56, 54, 256, 256);
slotFrame(graphics, 12, 20 + i * 37); if (i == menu.activeCollection() && !allMode)
graphics.fill(x + 2, y + 18 + i * 37, x + 54, y + 46 + i * 37, 0x885858CC);
graphics.drawString(font, new String[]{"I", "II", "III", "IV"}[i],
x + 8, y + 25 + i * 37, 0xFF181818, false);
slotFrame(graphics, 36, 20 + i * 37);
} }
slotFrame(graphics, 370, 20); graphics.blit(LEGACY_DESK, x + 206, y + 14, 10, 130, 177, 0, 12, 166, 256, 256);
slotFrame(graphics, 228, 20); int totalRows = (filteredPages().size() + 4) / 5;
int maxScroll = Math.max(0, totalRows - 3);
scrollRows = Math.clamp(scrollRows, 0, maxScroll);
int thumbHeight = Math.max(14, 126 * 3 / Math.max(3, totalRows));
int thumbY = y + 16 + (maxScroll == 0 ? 0 : scrollRows * (126 - thumbHeight) / maxScroll);
graphics.fill(x + 207, thumbY, x + 215, thumbY + thumbHeight, 0xFFB7B7B7);
graphics.renderOutline(x + 207, thumbY, 8, thumbHeight, 0xFF424242);
slotFrame(graphics, 370, 9);
slotFrame(graphics, 228, 9);
slotFrame(graphics, 228, 57); slotFrame(graphics, 228, 57);
slotFrame(graphics, 370, 57); slotFrame(graphics, 370, 57);
graphics.fill(x + 350, y + 20, x + 364, y + 75, 0xFF303035); graphics.fill(x + 350, y + 9, x + 364, y + 64, 0xFF303035);
graphics.fill(x + 352, y + 22, x + 362, y + 73, 0xFF15151B); graphics.fill(x + 352, y + 11, x + 362, y + 62, 0xFF15151B);
int filled = 51 * menu.inkRemaining() / 16; int filled = 51 * menu.inkRemaining() / 16;
if (filled > 0) graphics.fill(x + 352, y + 73 - filled, x + 362, y + 73, 0xFF6666AA); if (filled > 0) graphics.fill(x + 352, y + 62 - filled, x + 362, y + 62, 0xFF6666AA);
graphics.renderOutline(x + 350, y + 20, 14, 55, 0xFF484848); graphics.renderOutline(x + 350, y + 9, 14, 55, 0xFF484848);
for (int row = 0; row < 3; row++) { 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, 79 + row * 18);
} }
for (int column = 0; column < 9; column++) slotFrame(graphics, 224 + column * 18, 149); for (int column = 0; column < 9; column++) slotFrame(graphics, 224 + column * 18, 137);
} }
private void slotFrame(GuiGraphics graphics, int x, int y) { private void slotFrame(GuiGraphics graphics, int x, int y) {
@@ -118,22 +162,29 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
} }
@Override protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { @Override protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {
graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0x404040, false); // Legacy leaves the inventory grid unlabeled.
} }
@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 (allMode) graphics.renderOutline(leftPos + 80, topPos - 18, 28, 18, 0xFFFFD061);
renderCollection(graphics); renderCollection(graphics);
renderTooltip(graphics, mouseX, mouseY); renderTooltip(graphics, mouseX, mouseY);
if (mouseX >= leftPos + 350 && mouseX < leftPos + 364 if (mouseX >= leftPos + 350 && mouseX < leftPos + 364
&& mouseY >= topPos + 20 && mouseY < topPos + 75) { && mouseY >= topPos + 9 && mouseY < topPos + 64) {
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
&& mouseY >= topPos - 18 && mouseY < topPos) {
graphics.renderTooltip(font, Component.literal(selectedCategory == null
? "All categories" : selectedCategory.toString()), mouseX, mouseY);
}
} }
private List<ItemStack> pages() { private List<ItemStack> pages(int collectionIndex) {
ItemStack collection = menu.getSlot(menu.activeCollection()).getItem(); ItemStack collection = menu.getSlot(collectionIndex).getItem();
if (collection.is(ModItems.PAGE.get())) return List.of(collection); if (collection.is(ModItems.PAGE.get())) return List.of(collection);
if (collection.is(ModItems.NOTEBOOK.get())) { if (collection.is(ModItems.NOTEBOOK.get())) {
return collection.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages(); return collection.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
@@ -142,73 +193,34 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
} }
private void renderCollection(GuiGraphics graphics) { private void renderCollection(GuiGraphics graphics) {
if (allMode) {
renderCatalog(graphics);
return;
}
List<ItemStack> pages = pages();
if (lastCollection != menu.activeCollection()) { if (lastCollection != menu.activeCollection()) {
lastCollection = menu.activeCollection(); lastCollection = menu.activeCollection();
scrollRows = 0; scrollRows = 0;
selectedPage = 0; selectedPage = 0;
selectedCollection = -1;
} }
int last = -1; List<DisplayPage> entries = filteredPages();
for (int i = 0; i < pages.size(); i++) if (!pages.get(i).isEmpty()) last = i; for (int i = scrollRows * 5; i < entries.size() && i < scrollRows * 5 + 15; i++) {
for (int i = scrollRows * 5; i <= last && i < scrollRows * 5 + 15; i++) {
int column = i % 5; int column = i % 5;
int row = i / 5 - scrollRows; int row = i / 5 - scrollRows;
ItemStack page = pages.get(i); DisplayPage entry = entries.get(i);
int x = leftPos + 48 + column * 31; ItemStack page = entry.stack();
int x = leftPos + 58 + column * 29;
int y = topPos + 18 + row * 41; int y = topPos + 18 + 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);
if (i == selectedPage && !page.isEmpty()) graphics.renderOutline(x, y, 30, 40, 0xFFFFD061); int active = selectedCollection >= 0 ? selectedCollection : menu.activeCollection();
} if (entry.collection() == active && entry.index() == selectedPage && !page.isEmpty())
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 renderCatalog(GuiGraphics graphics) {
List<SymbolDisplayMetadata> filtered = filteredCatalog();
if (filtered.isEmpty()) {
graphics.drawString(font, Component.translatable("screen.rivencraft.writing_desk.no_symbols"),
leftPos + 52, topPos + 24, 0xFFC4B8A3, false);
}
for (int i = scrollRows * 5; i < filtered.size() && i < scrollRows * 5 + 15; i++) {
SymbolDisplayMetadata entry = filtered.get(i);
int x = leftPos + 48 + (i % 5) * 31;
int y = topPos + 18 + (i / 5 - scrollRows) * 41;
drawSymbolPage(graphics, entry.id(), x, y);
if (Objects.equals(entry.id(), selectedSymbol)) {
graphics.renderOutline(x, y, 30, 40, 0xFFFFD061); graphics.renderOutline(x, y, 30, 40, 0xFFFFD061);
}
} }
SymbolDisplayMetadata selected = menu.catalog().stream() int active = selectedCollection >= 0 ? selectedCollection : menu.activeCollection();
.filter(entry -> Objects.equals(entry.id(), selectedSymbol)).findFirst().orElse(null); List<ItemStack> source = pages(active);
if (selected == null) return; if (selectedPage >= source.size() || source.get(selectedPage).isEmpty()) return;
graphics.pose().pushPose(); ItemStack selected = source.get(selectedPage);
graphics.pose().translate(leftPos + 294, topPos + 12, 0); drawPage(graphics, selected, leftPos + 253, topPos + 10);
graphics.pose().scale(1.35F, 1.35F, 1.0F); String title = font.plainSubstrByWidth(selected.getHoverName().getString(), 84);
drawSymbolPage(graphics, selected.id(), 0, 0); graphics.fill(leftPos + 251, topPos + 58, leftPos + 342, topPos + 73, 0xFF202020);
graphics.pose().popPose(); graphics.drawString(font, title, leftPos + 254, topPos + 61, 0xFFE0E0E0, false);
String title = font.plainSubstrByWidth(
Component.translatable(selected.nameTranslationKey()).getString(), 112);
graphics.fill(leftPos + 251, topPos + 64, leftPos + 365, topPos + 77, 0xFF202020);
graphics.drawString(font, title, leftPos + 254, topPos + 67, 0xFFE0E0E0, false);
}
private void drawSymbolPage(GuiGraphics graphics, ResourceLocation id, int x, int y) {
graphics.blit(PAGE_ATLAS, x, y, 30, 40, 156, 0, 30, 40, 256, 256);
graphics.blit(NotebookScreen.symbolTexture(id), x + 1, y + 6,
28, 28, 16, 16, 96, 96, 128, 128);
} }
private void drawPage(GuiGraphics graphics, ItemStack page, int x, int y) { private void drawPage(GuiGraphics graphics, ItemStack page, int x, int y) {
@@ -224,41 +236,40 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
@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) {
int x = (int) mouseX - leftPos - 48; if (mouseX >= leftPos + 206 && mouseX < leftPos + 217
&& mouseY >= topPos + 14 && mouseY < topPos + 144) {
draggingScrollbar = true;
setScrollFromMouse(mouseY);
return true;
}
int x = (int) mouseX - leftPos - 58;
int y = (int) mouseY - topPos - 18; int y = (int) mouseY - topPos - 18;
if (x >= 0 && x < 155 && y >= 0 && y < 123) { if (x >= 0 && x < 145 && y >= 0 && y < 123) {
int column = x / 31; int column = x / 29;
int row = y / 41; int row = y / 41;
int index = (row + scrollRows) * 5 + column; int index = (row + scrollRows) * 5 + column;
if (allMode) { List<DisplayPage> entries = filteredPages();
List<SymbolDisplayMetadata> filtered = filteredCatalog(); if (y % 41 < 40 && index < entries.size()) {
if (x % 31 < 30 && y % 41 < 40 && index < filtered.size()) { DisplayPage entry = entries.get(index);
SymbolDisplayMetadata entry = filtered.get(index); if (!entry.stack().isEmpty()) {
selectedSymbol = entry.id(); selectedCollection = entry.collection();
int serverIndex = menu.catalog().indexOf(entry); selectedPage = entry.index();
if (serverIndex >= 0 && minecraft != null && minecraft.gameMode != null) {
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, 100 + serverIndex);
}
}
} else {
List<ItemStack> pages = pages();
if (x % 31 < 30 && y % 41 < 40 && index < pages.size()
&& !pages.get(index).isEmpty()) {
selectedPage = index;
if (minecraft != null && minecraft.gameMode != null) { if (minecraft != null && minecraft.gameMode != null) {
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, 16 + index); int id = allMode ? 1000 + entry.collection() * NotebookContents.MAX_PAGES + entry.index()
: 16 + entry.index();
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, id);
} }
} }
} }
return true; return true;
} }
} }
if (button == 0 && mouseX >= leftPos + 29 && mouseX < leftPos + 40) { if (button == 0 && mouseX >= leftPos && mouseX < leftPos + 35) {
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
int top = topPos + 16 + i * 37; int top = topPos + 16 + i * 37;
if (mouseY >= top && mouseY < top + 32) { if (mouseY >= top && mouseY < top + 36) {
allMode = false; allMode = false;
selectedSymbol = null; 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, i);
} }
@@ -270,15 +281,37 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
} }
@Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { @Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
if (mouseX >= leftPos + 48 && mouseX < leftPos + 203 if (mouseX >= leftPos + 58 && mouseX < leftPos + 217
&& mouseY >= topPos + 18 && mouseY < topPos + 141) { && mouseY >= topPos + 18 && mouseY < topPos + 141) {
int rows = ((allMode ? filteredCatalog().size() : pages().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 - 3));
return true; return true;
} }
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
} }
private void setScrollFromMouse(double mouseY) {
int rows = (filteredPages().size() + 4) / 5;
int max = Math.max(0, rows - 3);
scrollRows = Math.clamp((int) Math.round((mouseY - topPos - 16) * max / 126.0), 0, max);
}
@Override public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) {
if (button == 0 && draggingScrollbar) {
setScrollFromMouse(mouseY);
return true;
}
return super.mouseDragged(mouseX, mouseY, button, dragX, dragY);
}
@Override public boolean mouseReleased(double mouseX, double mouseY, int button) {
if (button == 0 && draggingScrollbar) {
draggingScrollbar = false;
return true;
}
return super.mouseReleased(mouseX, mouseY, button);
}
@Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (search != null && search.isFocused()) { if (search != null && search.isFocused()) {
if (keyCode == 256) { if (keyCode == 256) {
@@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.gametest;
import java.util.List; import java.util.List;
import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity; import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.dnorberg.rivencraft.block.WritingDeskBlock;
import net.dnorberg.rivencraft.menu.WritingDeskMenu; import net.dnorberg.rivencraft.menu.WritingDeskMenu;
import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.registry.ModBlocks; import net.dnorberg.rivencraft.registry.ModBlocks;
@@ -12,6 +13,7 @@ import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.writing.SymbolReference; import net.dnorberg.rivencraft.writing.SymbolReference;
import net.dnorberg.rivencraft.writing.WritingDeskCatalog; import net.dnorberg.rivencraft.writing.WritingDeskCatalog;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.gametest.framework.GameTest; import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.gametest.framework.GameTestHelper;
@@ -265,4 +267,87 @@ public final class WritingDeskGameTests {
"Rejected copy must preserve ink vial"); "Rejected copy must preserve ink vial");
helper.succeed(); helper.succeed();
} }
@GameTest(template = "empty")
public static void twoBlockDeskSharesOneInventoryAndBreaksTogether(GameTestHelper helper) {
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
var block = ModBlocks.WRITING_DESK.get();
var state = block.defaultBlockState().setValue(WritingDeskBlock.FACING, Direction.NORTH);
helper.getLevel().setBlock(pos, state, 3);
block.setPlacedBy(helper.getLevel(), pos, state, null, new ItemStack(block));
BlockPos extension = pos.relative(Direction.EAST);
helper.assertTrue(helper.getLevel().getBlockState(extension).is(block),
"Placing a desk must reserve the adjacent second block");
helper.assertTrue(helper.getLevel().getBlockState(extension).getValue(WritingDeskBlock.EXTENSION),
"Adjacent half must be marked as an extension");
WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos);
helper.assertTrue(helper.getLevel().getBlockEntity(extension) == null,
"Extension must not duplicate the inventory");
desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER, 7));
helper.getLevel().destroyBlock(extension, true);
helper.assertTrue(helper.getLevel().getBlockState(pos).isAir(),
"Breaking the extension must remove the controller");
var drops = helper.getLevel().getEntitiesOfClass(ItemEntity.class, new AABB(pos).inflate(3));
int desks = drops.stream().filter(e -> e.getItem().is(block.asItem()))
.mapToInt(e -> e.getItem().getCount()).sum();
int paper = drops.stream().filter(e -> e.getItem().is(Items.PAPER))
.mapToInt(e -> e.getItem().getCount()).sum();
helper.assertValueEqual(desks, 1, "Breaking either half must drop one desk");
helper.assertValueEqual(paper, 7, "Breaking either half must drop stored paper once");
helper.succeed();
}
@GameTest(template = "empty")
public static void breakingControllerRemovesExtensionWithoutDuplicateDesk(GameTestHelper helper) {
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
var block = ModBlocks.WRITING_DESK.get();
var state = block.defaultBlockState().setValue(WritingDeskBlock.FACING, Direction.NORTH);
helper.getLevel().setBlock(pos, state, 3);
block.setPlacedBy(helper.getLevel(), pos, state, null, new ItemStack(block));
BlockPos extension = pos.relative(Direction.EAST);
helper.getLevel().destroyBlock(pos, true);
helper.assertTrue(helper.getLevel().getBlockState(extension).isAir(),
"Breaking the controller must remove the extension");
var drops = helper.getLevel().getEntitiesOfClass(ItemEntity.class, new AABB(pos).inflate(3));
int desks = drops.stream().filter(e -> e.getItem().is(block.asItem()))
.mapToInt(e -> e.getItem().getCount()).sum();
helper.assertValueEqual(desks, 1, "Controller break must drop exactly one desk");
helper.succeed();
}
@GameTest(template = "empty")
public static void openDeskDoesNotOccludeTerrain(GameTestHelper helper) {
helper.assertFalse(ModBlocks.WRITING_DESK.get().defaultBlockState().canOcclude(),
"Open shelves must not cull terrain behind the desk");
helper.succeed();
}
@GameTest(template = "empty")
public static void allPagesSelectionUsesTheRequestedPhysicalTab(GameTestHelper helper) {
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
helper.getLevel().setBlock(pos, ModBlocks.WRITING_DESK.get().defaultBlockState(), 3);
WritingDeskBlockEntity desk = (WritingDeskBlockEntity) helper.getLevel().getBlockEntity(pos);
ItemStack first = PageItem.createLinkPanelPage();
ItemStack second = PageItem.createSymbolPage(new SymbolReference(
ResourceLocation.parse("rivencraft:terrain/standard")));
desk.setItem(0, first);
ItemStack notebook = new ItemStack(ModItems.NOTEBOOK.get());
notebook.set(ModDataComponents.NOTEBOOK_CONTENTS.get(),
new NotebookContents(List.of(ItemStack.EMPTY, second)));
desk.setItem(1, notebook);
desk.setItem(WritingDeskBlockEntity.PAPER, new ItemStack(Items.PAPER));
desk.setItem(WritingDeskBlockEntity.INK, new ItemStack(ModItems.INK_VIAL.get()));
var player = helper.makeMockServerPlayerInLevel();
player.moveTo(pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5);
WritingDeskMenu menu = new WritingDeskMenu(1, player.getInventory(), desk);
helper.assertFalse(menu.clickMenuButton(player, 1000 + 2 * NotebookContents.MAX_PAGES),
"ALL view must reject empty cross-tab positions");
helper.assertTrue(menu.clickMenuButton(player, 1000 + NotebookContents.MAX_PAGES + 1),
"ALL view must accept the populated second tab page");
helper.assertTrue(ItemStack.matches(menu.getSlot(WritingDeskMenu.COPY_RESULT).getItem(), second),
"Copy preview must match the physical page selected from the other tab");
helper.assertValueEqual(desk.activeCollection(), 0,
"Selecting from ALL must not silently change the active tab");
helper.succeed();
}
} }
@@ -37,6 +37,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
private int syncedActiveCollection; private int syncedActiveCollection;
private int syncedInkRemaining; private int syncedInkRemaining;
private int selectedPage; private int selectedPage;
private int selectedCollection = -1;
private ResourceLocation selectedSymbol; private ResourceLocation selectedSymbol;
private List<SymbolDisplayMetadata> catalog = List.of(); private List<SymbolDisplayMetadata> catalog = List.of();
private final ServerPlayer serverViewer; private final ServerPlayer serverViewer;
@@ -65,20 +66,20 @@ 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, 12, 20 + i * 37, 1)); addSlot(new DeskSlot(desk, i, 36, 20 + i * 37, 1));
} }
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.INK, 370, 20, 16)); addSlot(new DeskSlot(desk, WritingDeskBlockEntity.INK, 370, 9, 16));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.RETURN, 370, 57, 64)); addSlot(new DeskSlot(desk, WritingDeskBlockEntity.RETURN, 370, 57, 64));
addSlot(new DeskSlot(desk, WritingDeskBlockEntity.PAPER, 228, 20, 64)); addSlot(new DeskSlot(desk, WritingDeskBlockEntity.PAPER, 228, 9, 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, 228, 57));
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, 91 + row * 18)); addSlot(new Slot(inventory, column + row * 9 + 9, 224 + column * 18, 79 + row * 18));
} }
} }
for (int column = 0; column < 9; column++) { for (int column = 0; column < 9; column++) {
addSlot(new Slot(inventory, column, 224 + column * 18, 149)); addSlot(new Slot(inventory, column, 224 + column * 18, 137));
} }
} }
@@ -92,9 +93,19 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
if (id >= 0 && id < WritingDeskBlockEntity.COLLECTIONS) { if (id >= 0 && id < WritingDeskBlockEntity.COLLECTIONS) {
blockEntity.setActiveCollection(id); blockEntity.setActiveCollection(id);
selectedPage = 0; selectedPage = 0;
selectedCollection = -1;
selectedSymbol = null; selectedSymbol = null;
} else if (id >= 16 && id < 16 + NotebookContents.MAX_PAGES) { } else if (id >= 16 && id < 16 + NotebookContents.MAX_PAGES) {
selectedPage = id - 16; selectedPage = id - 16;
selectedCollection = -1;
selectedSymbol = null;
} else if (id >= 1000 && id < 1000 + WritingDeskBlockEntity.COLLECTIONS * NotebookContents.MAX_PAGES) {
int offset = id - 1000;
int collection = offset / NotebookContents.MAX_PAGES;
int page = offset % NotebookContents.MAX_PAGES;
if (pageAt(collection, page).isEmpty()) return false;
selectedCollection = collection;
selectedPage = page;
selectedSymbol = null; selectedSymbol = null;
} else if (id >= 100 && id < 100 + catalog.size()) { } else if (id >= 100 && id < 100 + catalog.size()) {
if (!catalog.equals(WritingDeskCatalog.available(blockEntity))) { if (!catalog.equals(WritingDeskCatalog.available(blockEntity))) {
@@ -162,12 +173,20 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
.anyMatch(entry -> entry.id().equals(selectedSymbol)) .anyMatch(entry -> entry.id().equals(selectedSymbol))
? PageItem.createSymbolPage(reference) : ItemStack.EMPTY; ? PageItem.createSymbolPage(reference) : ItemStack.EMPTY;
} }
ItemStack collection = desk.getItem(blockEntity == null ? syncedActiveCollection : blockEntity.activeCollection()); int collectionIndex = selectedCollection >= 0 ? selectedCollection
if (collection.is(ModItems.PAGE.get())) return selectedPage == 0 ? collection : ItemStack.EMPTY; : blockEntity == null ? syncedActiveCollection : blockEntity.activeCollection();
return pageAt(collectionIndex, selectedPage);
}
private ItemStack pageAt(int collectionIndex, int pageIndex) {
if (collectionIndex < 0 || collectionIndex >= WritingDeskBlockEntity.COLLECTIONS
|| pageIndex < 0 || pageIndex >= NotebookContents.MAX_PAGES) return ItemStack.EMPTY;
ItemStack collection = desk.getItem(collectionIndex);
if (collection.is(ModItems.PAGE.get())) return pageIndex == 0 ? collection : ItemStack.EMPTY;
if (collection.is(ModItems.NOTEBOOK.get())) { if (collection.is(ModItems.NOTEBOOK.get())) {
List<ItemStack> pages = collection.getOrDefault( List<ItemStack> pages = collection.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages(); ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
return selectedPage < pages.size() ? pages.get(selectedPage) : ItemStack.EMPTY; return pageIndex < pages.size() ? pages.get(pageIndex) : ItemStack.EMPTY;
} }
return ItemStack.EMPTY; return ItemStack.EMPTY;
} }
@@ -181,6 +200,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
private void refreshOutput() { private void refreshOutput() {
if (clientSide) return; if (clientSide) return;
if (blockEntity != null) blockEntity.setDisplayPage(selectedSource());
ItemStack preview = canCopy() ? selectedSource().copyWithCount(1) : ItemStack.EMPTY; ItemStack preview = canCopy() ? selectedSource().copyWithCount(1) : ItemStack.EMPTY;
if (!ItemStack.matches(result.getItem(0), preview)) result.setItem(0, preview); if (!ItemStack.matches(result.getItem(0), preview)) result.setItem(0, preview);
} }
@@ -19,7 +19,7 @@ public final class ModBlocks {
); );
public static final DeferredBlock<WritingDeskBlock> WRITING_DESK = BLOCKS.registerBlock( public static final DeferredBlock<WritingDeskBlock> WRITING_DESK = BLOCKS.registerBlock(
"writing_desk", WritingDeskBlock::new, "writing_desk", WritingDeskBlock::new,
BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE) BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion()
); );
private ModBlocks() { private ModBlocks() {
@@ -1,8 +1,12 @@
{ {
"variants": { "variants": {
"facing=north": { "model": "rivencraft:block/writing_desk" }, "facing=north,extension=false": {"model":"rivencraft:block/writing_desk"},
"facing=east": { "model": "rivencraft:block/writing_desk", "y": 90 }, "facing=east,extension=false": {"model":"rivencraft:block/writing_desk","y":90},
"facing=south": { "model": "rivencraft:block/writing_desk", "y": 180 }, "facing=south,extension=false": {"model":"rivencraft:block/writing_desk","y":180},
"facing=west": { "model": "rivencraft:block/writing_desk", "y": 270 } "facing=west,extension=false": {"model":"rivencraft:block/writing_desk","y":270},
"facing=north,extension=true": {"model":"rivencraft:block/writing_desk_extension"},
"facing=east,extension=true": {"model":"rivencraft:block/writing_desk_extension","y":90},
"facing=south,extension=true": {"model":"rivencraft:block/writing_desk_extension","y":180},
"facing=west,extension=true": {"model":"rivencraft:block/writing_desk_extension","y":270}
} }
} }
@@ -1,69 +1,11 @@
{ {
"parent": "minecraft:block/block", "parent": "minecraft:block/block",
"ambientocclusion": false, "textures": {"wood":"rivencraft:block/writing_desk_wood","inlay":"rivencraft:block/writing_desk_inlay","particle":"rivencraft:block/writing_desk_wood"},
"textures": {
"particle": "rivencraft:block/writing_desk",
"desk": "rivencraft:block/writing_desk"
},
"elements": [ "elements": [
{ {"from":[0,0,1],"to":[16,1,15],"faces":{"up":{"texture":"#wood"},"down":{"texture":"#wood"},"north":{"texture":"#wood"},"south":{"texture":"#wood"},"east":{"texture":"#wood"},"west":{"texture":"#wood"}}},
"from": [0, 8, 0], "to": [16, 9, 16], {"from":[0,8,0],"to":[16,9,16],"faces":{"up":{"texture":"#inlay"},"down":{"texture":"#wood"},"north":{"texture":"#wood"},"south":{"texture":"#wood"},"east":{"texture":"#wood"},"west":{"texture":"#wood"}}},
"faces": { {"from":[0,0,1],"to":[1,8,15],"faces":{"up":{"texture":"#wood"},"down":{"texture":"#wood"},"north":{"texture":"#wood"},"south":{"texture":"#wood"},"east":{"texture":"#wood"},"west":{"texture":"#wood"}}},
"up": {"uv": [1, 0, 3, 2], "texture": "#desk"}, {"from":[1,3,1],"to":[16,4,15],"faces":{"up":{"texture":"#wood"},"down":{"texture":"#wood"},"north":{"texture":"#wood"},"south":{"texture":"#wood"},"east":{"texture":"#wood"},"west":{"texture":"#wood"}}},
"down": {"uv": [1, 2, 3, 4], "texture": "#desk"}, {"from":[0,9,15],"to":[16,15,16],"faces":{"up":{"texture":"#wood"},"down":{"texture":"#wood"},"north":{"texture":"#wood"},"south":{"texture":"#wood"},"east":{"texture":"#wood"},"west":{"texture":"#wood"}}}
"north": {"uv": [1, 0, 3, 0.125], "texture": "#desk"},
"south": {"uv": [1, 0, 3, 0.125], "texture": "#desk"},
"east": {"uv": [1, 0, 3, 0.125], "texture": "#desk"},
"west": {"uv": [1, 0, 3, 0.125], "texture": "#desk"}
}
},
{
"from": [0, 0, 1], "to": [2, 8, 15],
"faces": {
"north": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"},
"south": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"},
"east": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"},
"west": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"}
}
},
{
"from": [14, 0, 1], "to": [16, 8, 15],
"faces": {
"north": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"},
"south": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"},
"east": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"},
"west": {"uv": [5.625, 0.125, 5.875, 2], "texture": "#desk"}
}
},
{
"from": [1, 3, 2], "to": [15, 4, 14],
"faces": {
"up": {"uv": [0, 4.25, 3.75, 6.125], "texture": "#desk"},
"down": {"uv": [0, 4.25, 3.75, 6.125], "texture": "#desk"},
"north": {"uv": [0, 4.25, 3.75, 4.5], "texture": "#desk"},
"south": {"uv": [0, 4.25, 3.75, 4.5], "texture": "#desk"},
"east": {"uv": [0, 4.25, 3.75, 4.5], "texture": "#desk"},
"west": {"uv": [0, 4.25, 3.75, 4.5], "texture": "#desk"}
}
},
{
"from": [1, 9, 14], "to": [15, 15, 16],
"faces": {
"north": {"uv": [8, 0, 11.75, 2], "texture": "#desk"},
"south": {"uv": [8, 0, 11.75, 2], "texture": "#desk"},
"east": {"uv": [8, 0, 8.25, 2], "texture": "#desk"},
"west": {"uv": [8, 0, 8.25, 2], "texture": "#desk"},
"up": {"uv": [8, 0, 11.75, 0.25], "texture": "#desk"}
}
},
{
"from": [1, 12, 9], "to": [15, 13, 15],
"faces": {
"up": {"uv": [8, 3.625, 11.75, 4.375], "texture": "#desk"},
"down": {"uv": [8, 3.625, 11.75, 4.375], "texture": "#desk"},
"north": {"uv": [8, 3.625, 11.75, 3.75], "texture": "#desk"},
"south": {"uv": [8, 3.625, 11.75, 3.75], "texture": "#desk"}
}
}
] ]
} }
@@ -0,0 +1,11 @@
{
"parent": "minecraft:block/block",
"textures": {"wood":"rivencraft:block/writing_desk_wood","inlay":"rivencraft:block/writing_desk_inlay","particle":"rivencraft:block/writing_desk_wood"},
"elements": [
{"from":[0,0,1],"to":[16,1,15],"faces":{"up":{"texture":"#wood"},"down":{"texture":"#wood"},"north":{"texture":"#wood"},"south":{"texture":"#wood"},"east":{"texture":"#wood"},"west":{"texture":"#wood"}}},
{"from":[0,8,0],"to":[16,9,16],"faces":{"up":{"texture":"#wood"},"down":{"texture":"#wood"},"north":{"texture":"#wood"},"south":{"texture":"#wood"},"east":{"texture":"#wood"},"west":{"texture":"#wood"}}},
{"from":[15,0,1],"to":[16,8,15],"faces":{"up":{"texture":"#wood"},"down":{"texture":"#wood"},"north":{"texture":"#wood"},"south":{"texture":"#wood"},"east":{"texture":"#wood"},"west":{"texture":"#wood"}}},
{"from":[0,3,1],"to":[15,4,15],"faces":{"up":{"texture":"#wood"},"down":{"texture":"#wood"},"north":{"texture":"#wood"},"south":{"texture":"#wood"},"east":{"texture":"#wood"},"west":{"texture":"#wood"}}},
{"from":[0,9,15],"to":[16,15,16],"faces":{"up":{"texture":"#wood"},"down":{"texture":"#wood"},"north":{"texture":"#wood"},"south":{"texture":"#wood"},"east":{"texture":"#wood"},"west":{"texture":"#wood"}}}
]
}
@@ -1,3 +1,4 @@
{ {
"parent": "rivencraft:block/writing_desk" "parent": "minecraft:item/generated",
"textures": {"layer0": "rivencraft:item/writing_desk"}
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB