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 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 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.