From 2b916ccd0b442c94d3ebeafd7d3847014b129545 Mon Sep 17 00:00:00 2001
From: Dustin Norberg
Date: Mon, 21 Sep 2026 07:24:12 -0400
Subject: [PATCH] Add book GUI and Ponder preview foundation
---
README.md | 17 ++-
build.gradle | 3 +-
docs/PORTING.md | 16 +++
gradle.properties | 3 +-
.../net/dnorberg/rivencraft/Rivencraft.java | 2 +
.../rivencraft/book/AgeReference.java | 15 ++-
.../client/ClientBookInteractionHandler.java | 29 +++++
.../rivencraft/client/ClientEvents.java | 7 ++
.../preview/DestinationPreviewRenderer.java | 13 ++
.../PonderDestinationPreviewRenderer.java | 50 ++++++++
.../client/screen/DescriptiveBookScreen.java | 39 ++++++
.../client/screen/LinkingBookScreen.java | 111 ++++++++++++++++++
.../rivencraft/item/DescriptiveBookItem.java | 2 +-
.../rivencraft/item/LinkingBookItem.java | 14 +--
.../network/BindLinkingBookPayload.java | 26 ++++
.../rivencraft/network/ModNetworking.java | 72 ++++++++++++
.../network/UseLinkingBookPayload.java | 24 ++++
.../assets/rivencraft/lang/en_us.json | 11 +-
.../templates/META-INF/neoforge.mods.toml | 7 ++
19 files changed, 437 insertions(+), 24 deletions(-)
create mode 100644 src/main/java/net/dnorberg/rivencraft/client/ClientBookInteractionHandler.java
create mode 100644 src/main/java/net/dnorberg/rivencraft/client/preview/DestinationPreviewRenderer.java
create mode 100644 src/main/java/net/dnorberg/rivencraft/client/preview/PonderDestinationPreviewRenderer.java
create mode 100644 src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java
create mode 100644 src/main/java/net/dnorberg/rivencraft/client/screen/LinkingBookScreen.java
create mode 100644 src/main/java/net/dnorberg/rivencraft/network/BindLinkingBookPayload.java
create mode 100644 src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java
create mode 100644 src/main/java/net/dnorberg/rivencraft/network/UseLinkingBookPayload.java
diff --git a/README.md b/README.md
index 9475624..be8f641 100644
--- a/README.md
+++ b/README.md
@@ -12,11 +12,13 @@ been ported yet.
## Linking Book
-Craft a Linking Book from a vanilla book and an ender pearl. Sneak-right-click
-the unbound book to record the current location, then right-click it to return
-there. The destination is stored on the item and survives inventory and world
-saves. Travel only succeeds if the target dimension is loaded and a solid,
-collision-free landing position can be found nearby.
+Craft a Linking Book from a vanilla book and an ender pearl, then use it in the
+air. An unbound book opens a screen where it can be named and bound to the
+current location. A bound book opens its destination screen and travels only
+when the Link button is pressed. The destination is stored on the item and
+survives inventory and world saves. Travel only succeeds if the target
+dimension is loaded and a solid, collision-free landing position can be found
+nearby.
Right-click an empty vanilla lectern with a Rivencraft book to convert it into
a Book Stand and place the book on it. Use a Linking Book on the stand by
@@ -24,11 +26,16 @@ right-clicking with an empty hand. Sneak-right-click with an empty hand to take
the book back and restore the vanilla lectern. Breaking a Book Stand drops both
the stored book and a vanilla lectern.
+Version 0.1.4 establishes a Ponder-backed destination-preview boundary and an
+animated loading view. Transmitting a destination snapshot and rendering its
+actual blocks as the rotating scene is the next preview iteration.
+
## Requirements
- A Java 21 JDK
- Minecraft 1.21.1
- NeoForge 21.1.251 or newer in the 21.1 line
+- Ponder 1.0.82 for Minecraft 1.21.1 (client)
## Build and run
diff --git a/build.gradle b/build.gradle
index 63c4c8f..b38c381 100644
--- a/build.gradle
+++ b/build.gradle
@@ -27,7 +27,7 @@ sourceSets.main.resources {
}
repositories {
- // Add here additional repositories if required by some of the dependencies below.
+ maven { url = 'https://maven.createmod.net' }
}
base {
@@ -116,6 +116,7 @@ configurations {
}
dependencies {
+ implementation "net.createmod.ponder:ponder-neoforge:${ponder_version}+mc${minecraft_version}"
// Example optional mod dependency with JEI
// The JEI API is declared for compile time use, while the full JEI artifact is used at runtime
// compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}"
diff --git a/docs/PORTING.md b/docs/PORTING.md
index 6755a2f..1e9db9a 100644
--- a/docs/PORTING.md
+++ b/docs/PORTING.md
@@ -48,6 +48,11 @@ the symbol grammar is introduced.
- [ ] Return-link creation as part of Descriptive Book travel.
- [x] Lectern-to-Book-Stand conversion, displayed book, activation, removal,
persistence, and drops.
+- [x] Book-screen foundation with server-authoritative naming, binding, and
+ explicit link activation.
+- [x] Ponder dependency and isolated destination-preview level boundary.
+- [ ] Server-provided destination snapshots rendered as rotating Ponder scenes.
+- [ ] Book Stand floating names and opening the stored book screen.
- [ ] Automated in-game tests for binding, persistence, and travel.
The temporary shapeless Linking Book recipe makes the linking subsystem easy
@@ -69,6 +74,17 @@ initial cooldown used Minecraft's item-type cooldown system, which delayed all
Linking Book stacks at once. Version 0.1.3 removes that cooldown to match the
legacy behavior.
+Version 0.1.4 moves Linking Book binding and travel behind a book screen. The
+client requests an action, while the server revalidates the held book and owns
+all changes to its destination and custom name. Descriptive Book Age references
+now carry a display name alongside their stable UUID, with backward-compatible
+decoding for existing UUID-only data.
+
+The preview code is separated behind a renderer interface and uses an isolated
+Ponder level. The first iteration supplies the animated loading boundary; the
+next iteration will add server-authoritative snapshot transport and populate
+that level with the destination blocks.
+
## Version policy
Versions follow `x.y.z`:
diff --git a/gradle.properties b/gradle.properties
index a635e38..78a7958 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -19,6 +19,7 @@ minecraft_version=1.21.1
minecraft_version_range=[1.21.1]
# The Neo version must agree with the Minecraft version to get a valid artifact
neo_version=21.1.251
+ponder_version=1.0.82
# The loader version range can only use the major version of FML as bounds
loader_version_range=[1,)
@@ -32,7 +33,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. See https://semver.org/
-mod_version=0.1.3
+mod_version=0.1.4
# 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.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
diff --git a/src/main/java/net/dnorberg/rivencraft/Rivencraft.java b/src/main/java/net/dnorberg/rivencraft/Rivencraft.java
index 8820ff7..ee3f4ec 100644
--- a/src/main/java/net/dnorberg/rivencraft/Rivencraft.java
+++ b/src/main/java/net/dnorberg/rivencraft/Rivencraft.java
@@ -7,6 +7,7 @@ import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.registry.ModBlocks;
import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.dnorberg.rivencraft.event.BookStandConversionHandler;
+import net.dnorberg.rivencraft.network.ModNetworking;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.common.NeoForge;
@@ -18,6 +19,7 @@ public final class Rivencraft {
public static final Logger LOGGER = LogUtils.getLogger();
public Rivencraft(IEventBus modEventBus) {
+ modEventBus.addListener(ModNetworking::register);
ModDataComponents.register(modEventBus);
ModBlocks.register(modEventBus);
ModBlockEntities.register(modEventBus);
diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeReference.java b/src/main/java/net/dnorberg/rivencraft/book/AgeReference.java
index e6a2c37..486fbfb 100644
--- a/src/main/java/net/dnorberg/rivencraft/book/AgeReference.java
+++ b/src/main/java/net/dnorberg/rivencraft/book/AgeReference.java
@@ -1,6 +1,7 @@
package net.dnorberg.rivencraft.book;
import com.mojang.serialization.Codec;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
import io.netty.buffer.ByteBuf;
import java.util.UUID;
import net.minecraft.core.UUIDUtil;
@@ -13,11 +14,21 @@ import net.minecraft.network.codec.StreamCodec;
* Books only retain the identifier so dimension regeneration or migration does
* not invalidate every existing book.
*/
-public record AgeReference(UUID id) {
- public static final Codec CODEC = UUIDUtil.STRING_CODEC.xmap(AgeReference::new, AgeReference::id);
+public record AgeReference(UUID id, String name) {
+ private static final String UNNAMED_AGE = "Unnamed Age";
+ private static final Codec RECORD_CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ UUIDUtil.STRING_CODEC.fieldOf("id").forGetter(AgeReference::id),
+ Codec.STRING.optionalFieldOf("name", UNNAMED_AGE).forGetter(AgeReference::name)
+ ).apply(instance, AgeReference::new));
+ public static final Codec CODEC = Codec.withAlternative(
+ RECORD_CODEC,
+ UUIDUtil.STRING_CODEC.xmap(id -> new AgeReference(id, UNNAMED_AGE), AgeReference::id)
+ );
public static final StreamCodec STREAM_CODEC = StreamCodec.composite(
UUIDUtil.STREAM_CODEC,
AgeReference::id,
+ net.minecraft.network.codec.ByteBufCodecs.STRING_UTF8,
+ AgeReference::name,
AgeReference::new
);
diff --git a/src/main/java/net/dnorberg/rivencraft/client/ClientBookInteractionHandler.java b/src/main/java/net/dnorberg/rivencraft/client/ClientBookInteractionHandler.java
new file mode 100644
index 0000000..a1cb738
--- /dev/null
+++ b/src/main/java/net/dnorberg/rivencraft/client/ClientBookInteractionHandler.java
@@ -0,0 +1,29 @@
+package net.dnorberg.rivencraft.client;
+
+import net.dnorberg.rivencraft.client.screen.LinkingBookScreen;
+import net.dnorberg.rivencraft.client.screen.DescriptiveBookScreen;
+import net.dnorberg.rivencraft.registry.ModItems;
+import net.minecraft.client.Minecraft;
+import net.minecraft.world.InteractionResult;
+import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
+
+public final class ClientBookInteractionHandler {
+ private ClientBookInteractionHandler() {
+ }
+
+ public static void onRightClickItem(PlayerInteractEvent.RightClickItem event) {
+ boolean linkingBook = event.getItemStack().is(ModItems.LINKING_BOOK.get());
+ boolean descriptiveBook = event.getItemStack().is(ModItems.DESCRIPTIVE_BOOK.get());
+ if (!linkingBook && !descriptiveBook) {
+ return;
+ }
+
+ event.setCanceled(true);
+ event.setCancellationResult(InteractionResult.SUCCESS);
+ if (linkingBook) {
+ Minecraft.getInstance().setScreen(new LinkingBookScreen(event.getHand(), event.getItemStack().copy()));
+ } else {
+ Minecraft.getInstance().setScreen(new DescriptiveBookScreen(event.getItemStack().copy()));
+ }
+ }
+}
diff --git a/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java b/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java
index 4dec8ba..8be65b9 100644
--- a/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java
+++ b/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java
@@ -5,6 +5,8 @@ import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
+import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
+import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.client.event.EntityRenderersEvent;
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
@@ -16,4 +18,9 @@ public final class ClientEvents {
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerBlockEntityRenderer(ModBlockEntities.BOOK_STAND.get(), BookStandRenderer::new);
}
+
+ @SubscribeEvent
+ public static void clientSetup(FMLClientSetupEvent event) {
+ NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickItem);
+ }
}
diff --git a/src/main/java/net/dnorberg/rivencraft/client/preview/DestinationPreviewRenderer.java b/src/main/java/net/dnorberg/rivencraft/client/preview/DestinationPreviewRenderer.java
new file mode 100644
index 0000000..267c3c9
--- /dev/null
+++ b/src/main/java/net/dnorberg/rivencraft/client/preview/DestinationPreviewRenderer.java
@@ -0,0 +1,13 @@
+package net.dnorberg.rivencraft.client.preview;
+
+import net.minecraft.client.gui.GuiGraphics;
+
+public interface DestinationPreviewRenderer extends AutoCloseable {
+ void tick();
+
+ void render(GuiGraphics graphics, int left, int top, int width, int height, float partialTick);
+
+ @Override
+ default void close() {
+ }
+}
diff --git a/src/main/java/net/dnorberg/rivencraft/client/preview/PonderDestinationPreviewRenderer.java b/src/main/java/net/dnorberg/rivencraft/client/preview/PonderDestinationPreviewRenderer.java
new file mode 100644
index 0000000..9178c1a
--- /dev/null
+++ b/src/main/java/net/dnorberg/rivencraft/client/preview/PonderDestinationPreviewRenderer.java
@@ -0,0 +1,50 @@
+package net.dnorberg.rivencraft.client.preview;
+
+import net.createmod.ponder.api.level.PonderLevel;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.core.BlockPos;
+import net.minecraft.network.chat.Component;
+
+/**
+ * Initial Ponder-backed preview boundary.
+ *
+ * The isolated level is ready to receive server-provided snapshot blocks.
+ * Until snapshot networking lands, the panel renders an animated loading
+ * state rather than reading arbitrary remote chunks from the client.
+ */
+public final class PonderDestinationPreviewRenderer implements DestinationPreviewRenderer {
+ private final PonderLevel previewLevel;
+ private int ticks;
+
+ public PonderDestinationPreviewRenderer() {
+ Minecraft minecraft = Minecraft.getInstance();
+ previewLevel = new PonderLevel(BlockPos.ZERO, minecraft.level);
+ }
+
+ @Override
+ public void tick() {
+ ticks++;
+ previewLevel.tick();
+ }
+
+ @Override
+ public void render(GuiGraphics graphics, int left, int top, int width, int height, float partialTick) {
+ graphics.fill(left, top, left + width, top + height, 0xFF080C18);
+ graphics.renderOutline(left, top, width, height, 0xFF7B6AA8);
+
+ double angle = (ticks + partialTick) * 0.04;
+ int centerX = left + width / 2;
+ int centerY = top + height / 2;
+ int orbitX = centerX + (int)(Math.cos(angle) * Math.max(8, width / 4));
+ int orbitY = centerY + (int)(Math.sin(angle) * Math.max(4, height / 8));
+ graphics.fill(orbitX - 2, orbitY - 2, orbitX + 3, orbitY + 3, 0xFF8ED6FF);
+ graphics.drawCenteredString(
+ Minecraft.getInstance().font,
+ Component.translatable("screen.rivencraft.book.preview_pending"),
+ centerX,
+ centerY - 4,
+ 0xFFB8C5E0
+ );
+ }
+}
diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java
new file mode 100644
index 0000000..517e0cd
--- /dev/null
+++ b/src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java
@@ -0,0 +1,39 @@
+package net.dnorberg.rivencraft.client.screen;
+
+import net.dnorberg.rivencraft.book.AgeReference;
+import net.dnorberg.rivencraft.registry.ModDataComponents;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.gui.components.Button;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.Component;
+import net.minecraft.world.item.ItemStack;
+
+public final class DescriptiveBookScreen extends Screen {
+ private final AgeReference age;
+
+ public DescriptiveBookScreen(ItemStack book) {
+ super(book.getHoverName());
+ age = book.get(ModDataComponents.AGE_REFERENCE.get());
+ }
+
+ @Override
+ protected void init() {
+ addRenderableWidget(Button.builder(Component.translatable("gui.done"), button -> onClose())
+ .bounds(width / 2 - 75, height / 2 + 55, 150, 20)
+ .build());
+ }
+
+ @Override
+ public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
+ super.render(graphics, mouseX, mouseY, partialTick);
+ int centerX = width / 2;
+ graphics.drawCenteredString(font, title, centerX, height / 2 - 70, 0xFFFFFFFF);
+ if (age == null) {
+ graphics.drawCenteredString(font, Component.translatable("screen.rivencraft.book.unwritten"),
+ centerX, height / 2 - 4, 0xFFAAAAAA);
+ } else {
+ graphics.drawCenteredString(font, Component.literal(age.name()), centerX, height / 2 - 16, 0xFFE7D7A9);
+ graphics.drawCenteredString(font, Component.literal(age.id().toString()), centerX, height / 2 + 4, 0xFF777777);
+ }
+ }
+}
diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/LinkingBookScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/LinkingBookScreen.java
new file mode 100644
index 0000000..c1b89f7
--- /dev/null
+++ b/src/main/java/net/dnorberg/rivencraft/client/screen/LinkingBookScreen.java
@@ -0,0 +1,111 @@
+package net.dnorberg.rivencraft.client.screen;
+
+import net.dnorberg.rivencraft.book.LinkTarget;
+import net.dnorberg.rivencraft.client.preview.DestinationPreviewRenderer;
+import net.dnorberg.rivencraft.client.preview.PonderDestinationPreviewRenderer;
+import net.dnorberg.rivencraft.network.BindLinkingBookPayload;
+import net.dnorberg.rivencraft.network.ModNetworking;
+import net.dnorberg.rivencraft.network.UseLinkingBookPayload;
+import net.dnorberg.rivencraft.registry.ModDataComponents;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.gui.components.Button;
+import net.minecraft.client.gui.components.EditBox;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.core.BlockPos;
+import net.minecraft.network.chat.Component;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.item.ItemStack;
+import net.neoforged.neoforge.network.PacketDistributor;
+
+public final class LinkingBookScreen extends Screen {
+ private static final int PANEL_WIDTH = 180;
+ private static final int PREVIEW_HEIGHT = 100;
+
+ private final InteractionHand hand;
+ private final ItemStack book;
+ private final LinkTarget target;
+ private DestinationPreviewRenderer preview;
+ private EditBox titleInput;
+
+ public LinkingBookScreen(InteractionHand hand, ItemStack book) {
+ super(book.getHoverName());
+ this.hand = hand;
+ this.book = book;
+ target = book.get(ModDataComponents.LINK_TARGET.get());
+ }
+
+ @Override
+ protected void init() {
+ int left = (width - PANEL_WIDTH) / 2;
+ int buttonY = height / 2 + 72;
+ if (target == null) {
+ titleInput = new EditBox(font, left, height / 2 - 10, PANEL_WIDTH, 20,
+ Component.translatable("screen.rivencraft.book.title"));
+ titleInput.setMaxLength(ModNetworking.MAX_BOOK_TITLE_LENGTH);
+ titleInput.setHint(Component.translatable("screen.rivencraft.book.title_hint"));
+ addRenderableWidget(titleInput);
+ setInitialFocus(titleInput);
+ addRenderableWidget(Button.builder(Component.translatable("screen.rivencraft.book.bind"), button -> bind())
+ .bounds(left, height / 2 + 20, 86, 20)
+ .build());
+ addRenderableWidget(Button.builder(Component.translatable("gui.cancel"), button -> onClose())
+ .bounds(left + 94, height / 2 + 20, 86, 20)
+ .build());
+ } else {
+ preview = new PonderDestinationPreviewRenderer();
+ addRenderableWidget(Button.builder(Component.translatable("screen.rivencraft.book.link"), button -> link())
+ .bounds(left, buttonY, 86, 20)
+ .build());
+ addRenderableWidget(Button.builder(Component.translatable("gui.done"), button -> onClose())
+ .bounds(left + 94, buttonY, 86, 20)
+ .build());
+ }
+ }
+
+ @Override
+ public void tick() {
+ if (preview != null) {
+ preview.tick();
+ }
+ }
+
+ @Override
+ public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
+ super.render(graphics, mouseX, mouseY, partialTick);
+ int centerX = width / 2;
+ graphics.drawCenteredString(font, title, centerX, height / 2 - 82, 0xFFFFFFFF);
+ if (target == null) {
+ graphics.drawCenteredString(font, Component.translatable("screen.rivencraft.book.name_prompt"),
+ centerX, height / 2 - 34, 0xFFB8B8B8);
+ return;
+ }
+
+ int left = (width - PANEL_WIDTH) / 2;
+ int previewTop = height / 2 - 60;
+ preview.render(graphics, left, previewTop, PANEL_WIDTH, PREVIEW_HEIGHT, partialTick);
+ BlockPos pos = target.location().pos();
+ graphics.drawCenteredString(font, Component.literal(
+ target.location().dimension().location() + " " + pos.getX() + ", " + pos.getY() + ", " + pos.getZ()),
+ centerX, previewTop + PREVIEW_HEIGHT + 8, 0xFFB8B8B8);
+ }
+
+ @Override
+ public void removed() {
+ if (preview != null) {
+ preview.close();
+ }
+ }
+
+ private void bind() {
+ String name = titleInput.getValue().strip();
+ if (!name.isEmpty()) {
+ PacketDistributor.sendToServer(new BindLinkingBookPayload(hand == InteractionHand.OFF_HAND, name));
+ onClose();
+ }
+ }
+
+ private void link() {
+ PacketDistributor.sendToServer(new UseLinkingBookPayload(hand == InteractionHand.OFF_HAND));
+ onClose();
+ }
+}
diff --git a/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java b/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java
index 42f4469..38bc059 100644
--- a/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java
+++ b/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java
@@ -37,7 +37,7 @@ public final class DescriptiveBookItem extends Item {
AgeReference age = stack.get(ModDataComponents.AGE_REFERENCE.get());
MutableComponent status = age == null
? Component.translatable("item.rivencraft.descriptive_book.unwritten")
- : Component.translatable("item.rivencraft.descriptive_book.age", age.shortId());
+ : Component.translatable("item.rivencraft.descriptive_book.age", age.name());
tooltipComponents.add(status.withStyle(ChatFormatting.GRAY));
}
}
diff --git a/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java b/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java
index 68cb06e..cf601d7 100644
--- a/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java
+++ b/src/main/java/net/dnorberg/rivencraft/item/LinkingBookItem.java
@@ -33,19 +33,9 @@ public final class LinkingBookItem extends Item {
LinkTarget target = stack.get(ModDataComponents.LINK_TARGET.get());
if (target == null) {
- if (!player.isShiftKeyDown()) {
- player.displayClientMessage(Component.translatable("message.rivencraft.linking_book.bind_hint"), true);
- return InteractionResultHolder.fail(stack);
- }
-
- target = LinkTarget.from(serverPlayer);
- stack.set(ModDataComponents.LINK_TARGET.get(), target);
- level.playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN, SoundSource.PLAYERS, 1.0F, 1.0F);
- player.displayClientMessage(Component.translatable("message.rivencraft.linking_book.bound"), true);
- return InteractionResultHolder.success(stack);
+ return InteractionResultHolder.consume(stack);
}
-
- return useBoundBook(serverPlayer, stack);
+ return InteractionResultHolder.consume(stack);
}
public static InteractionResultHolder useBoundBook(ServerPlayer player, ItemStack stack) {
diff --git a/src/main/java/net/dnorberg/rivencraft/network/BindLinkingBookPayload.java b/src/main/java/net/dnorberg/rivencraft/network/BindLinkingBookPayload.java
new file mode 100644
index 0000000..b09b2db
--- /dev/null
+++ b/src/main/java/net/dnorberg/rivencraft/network/BindLinkingBookPayload.java
@@ -0,0 +1,26 @@
+package net.dnorberg.rivencraft.network;
+
+import net.dnorberg.rivencraft.Rivencraft;
+import net.minecraft.network.RegistryFriendlyByteBuf;
+import net.minecraft.network.codec.ByteBufCodecs;
+import net.minecraft.network.codec.StreamCodec;
+import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
+import net.minecraft.resources.ResourceLocation;
+
+public record BindLinkingBookPayload(boolean offhand, String title) implements CustomPacketPayload {
+ public static final Type TYPE = new Type<>(
+ ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "bind_linking_book")
+ );
+ public static final StreamCodec STREAM_CODEC = StreamCodec.composite(
+ ByteBufCodecs.BOOL,
+ BindLinkingBookPayload::offhand,
+ ByteBufCodecs.STRING_UTF8,
+ BindLinkingBookPayload::title,
+ BindLinkingBookPayload::new
+ );
+
+ @Override
+ public Type extends CustomPacketPayload> type() {
+ return TYPE;
+ }
+}
diff --git a/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java
new file mode 100644
index 0000000..cb1c8f8
--- /dev/null
+++ b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java
@@ -0,0 +1,72 @@
+package net.dnorberg.rivencraft.network;
+
+import net.dnorberg.rivencraft.book.LinkTarget;
+import net.dnorberg.rivencraft.item.LinkingBookItem;
+import net.dnorberg.rivencraft.registry.ModDataComponents;
+import net.dnorberg.rivencraft.registry.ModItems;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.network.chat.Component;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.sounds.SoundEvents;
+import net.minecraft.sounds.SoundSource;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.item.ItemStack;
+import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
+import net.neoforged.neoforge.network.handling.IPayloadContext;
+import net.neoforged.neoforge.network.registration.PayloadRegistrar;
+
+public final class ModNetworking {
+ public static final int MAX_BOOK_TITLE_LENGTH = 32;
+
+ private ModNetworking() {
+ }
+
+ public static void register(RegisterPayloadHandlersEvent event) {
+ PayloadRegistrar registrar = event.registrar("1");
+ registrar.playToServer(BindLinkingBookPayload.TYPE, BindLinkingBookPayload.STREAM_CODEC, ModNetworking::bindBook);
+ registrar.playToServer(UseLinkingBookPayload.TYPE, UseLinkingBookPayload.STREAM_CODEC, ModNetworking::useBook);
+ }
+
+ private static void bindBook(BindLinkingBookPayload payload, IPayloadContext context) {
+ if (!(context.player() instanceof ServerPlayer player)) {
+ return;
+ }
+
+ ItemStack stack = player.getItemInHand(hand(payload.offhand()));
+ if (!stack.is(ModItems.LINKING_BOOK.get()) || stack.has(ModDataComponents.LINK_TARGET.get())) {
+ return;
+ }
+
+ String title = normalizeTitle(payload.title());
+ if (title.isEmpty()) {
+ return;
+ }
+
+ stack.set(ModDataComponents.LINK_TARGET.get(), LinkTarget.from(player));
+ stack.set(DataComponents.CUSTOM_NAME, Component.literal(title));
+ player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN, SoundSource.PLAYERS, 1.0F, 1.0F);
+ player.displayClientMessage(Component.translatable("message.rivencraft.linking_book.bound", title), true);
+ }
+
+ private static void useBook(UseLinkingBookPayload payload, IPayloadContext context) {
+ if (!(context.player() instanceof ServerPlayer player)) {
+ return;
+ }
+
+ ItemStack stack = player.getItemInHand(hand(payload.offhand()));
+ if (stack.is(ModItems.LINKING_BOOK.get()) && stack.has(ModDataComponents.LINK_TARGET.get())) {
+ LinkingBookItem.useBoundBook(player, stack);
+ }
+ }
+
+ private static InteractionHand hand(boolean offhand) {
+ return offhand ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
+ }
+
+ private static String normalizeTitle(String title) {
+ String normalized = title.strip();
+ return normalized.length() <= MAX_BOOK_TITLE_LENGTH
+ ? normalized
+ : normalized.substring(0, MAX_BOOK_TITLE_LENGTH).stripTrailing();
+ }
+}
diff --git a/src/main/java/net/dnorberg/rivencraft/network/UseLinkingBookPayload.java b/src/main/java/net/dnorberg/rivencraft/network/UseLinkingBookPayload.java
new file mode 100644
index 0000000..6d5130f
--- /dev/null
+++ b/src/main/java/net/dnorberg/rivencraft/network/UseLinkingBookPayload.java
@@ -0,0 +1,24 @@
+package net.dnorberg.rivencraft.network;
+
+import net.dnorberg.rivencraft.Rivencraft;
+import net.minecraft.network.RegistryFriendlyByteBuf;
+import net.minecraft.network.codec.ByteBufCodecs;
+import net.minecraft.network.codec.StreamCodec;
+import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
+import net.minecraft.resources.ResourceLocation;
+
+public record UseLinkingBookPayload(boolean offhand) implements CustomPacketPayload {
+ public static final Type TYPE = new Type<>(
+ ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "use_linking_book")
+ );
+ public static final StreamCodec STREAM_CODEC = StreamCodec.composite(
+ ByteBufCodecs.BOOL,
+ UseLinkingBookPayload::offhand,
+ UseLinkingBookPayload::new
+ );
+
+ @Override
+ public Type extends CustomPacketPayload> type() {
+ return TYPE;
+ }
+}
diff --git a/src/main/resources/assets/rivencraft/lang/en_us.json b/src/main/resources/assets/rivencraft/lang/en_us.json
index b6c0490..c36035e 100644
--- a/src/main/resources/assets/rivencraft/lang/en_us.json
+++ b/src/main/resources/assets/rivencraft/lang/en_us.json
@@ -1,15 +1,22 @@
{
"item.rivencraft.descriptive_book": "Descriptive Book",
"item.rivencraft.descriptive_book.unwritten": "Unwritten",
- "item.rivencraft.descriptive_book.age": "Age %s",
+ "item.rivencraft.descriptive_book.age": "%s",
"item.rivencraft.linking_book": "Linking Book",
"item.rivencraft.linking_book.unbound": "Unbound — sneak-use to bind",
"item.rivencraft.linking_book.destination": "%s (%s, %s, %s)",
"message.rivencraft.linking_book.bind_hint": "Sneak-use to bind this book to your current location",
"message.rivencraft.linking_book.unbound": "This Linking Book is unbound",
- "message.rivencraft.linking_book.bound": "Linking Book bound",
+ "message.rivencraft.linking_book.bound": "Linking Book bound as %s",
"message.rivencraft.linking_book.linked": "Linked",
"message.rivencraft.linking_book.missing_dimension": "That dimension is unavailable",
"message.rivencraft.linking_book.unsafe": "No safe destination was found near the link",
+ "screen.rivencraft.book.title": "Book title",
+ "screen.rivencraft.book.title_hint": "Name this Linking Book",
+ "screen.rivencraft.book.name_prompt": "Name the book before binding it to this location",
+ "screen.rivencraft.book.bind": "Bind",
+ "screen.rivencraft.book.link": "Link",
+ "screen.rivencraft.book.unwritten": "This Descriptive Book is unwritten",
+ "screen.rivencraft.book.preview_pending": "Ponder preview loading",
"itemGroup.rivencraft": "Rivencraft"
}
diff --git a/src/main/templates/META-INF/neoforge.mods.toml b/src/main/templates/META-INF/neoforge.mods.toml
index 2872977..a5c4d97 100644
--- a/src/main/templates/META-INF/neoforge.mods.toml
+++ b/src/main/templates/META-INF/neoforge.mods.toml
@@ -86,6 +86,13 @@ A spiritual successor and modern port of Mystcraft for Minecraft 1.21.1 on NeoFo
ordering="NONE"
side="BOTH"
+[[dependencies.${mod_id}]]
+ modId="ponder"
+ type="required"
+ versionRange="[1.0.82,2.0.0)"
+ ordering="AFTER"
+ side="CLIENT"
+
# Features are specific properties of the game environment, that you may want to declare you require. This example declares
# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't
# stop your mod loading on the server for example.