From 8dbde191b806c9dbb06e7c674e9ac75c25404746 Mon Sep 17 00:00:00 2001 From: Dustin Norberg Date: Mon, 21 Sep 2026 08:13:08 -0400 Subject: [PATCH] Render destination snapshots with Ponder --- README.md | 6 +- docs/PORTING.md | 6 +- gradle.properties | 2 +- .../rivencraft/client/ClientEvents.java | 1 + .../client/ClientLinkPreviewHandler.java | 19 +++++ .../preview/DestinationPreviewRenderer.java | 3 + .../PonderDestinationPreviewRenderer.java | 71 +++++++++++++++++++ .../client/screen/LinkingBookScreen.java | 13 ++++ .../network/LinkPreviewPayload.java | 47 ++++++++++++ .../rivencraft/network/ModNetworking.java | 50 ++++++++++++- .../network/RequestLinkPreviewPayload.java | 28 ++++++++ .../assets/rivencraft/lang/en_us.json | 2 +- 12 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 src/main/java/net/dnorberg/rivencraft/client/ClientLinkPreviewHandler.java create mode 100644 src/main/java/net/dnorberg/rivencraft/network/LinkPreviewPayload.java create mode 100644 src/main/java/net/dnorberg/rivencraft/network/RequestLinkPreviewPayload.java diff --git a/README.md b/README.md index 1174de6..7279fed 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,9 @@ 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. +Bound Linking Books request a small, server-validated snapshot around their +destination and render its blocks as a rotating Ponder scene on the left page. +The snapshot is visual only; the server independently validates every link. ## Requirements diff --git a/docs/PORTING.md b/docs/PORTING.md index 750407a..307c9c3 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -51,7 +51,7 @@ the symbol grammar is introduced. - [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. +- [x] Server-provided destination snapshots rendered as rotating Ponder scenes. - [x] Book Stand floating names synchronized from the stored book stack. - [x] Open the stored book screen from a Book Stand with server-validated linking. - [ ] Automated in-game tests for binding, persistence, and travel. @@ -110,6 +110,10 @@ revalidates the stand position, interaction distance, stored item, and target. Version 0.1.12 replaces the Linking Book screen's convenience text draws with explicit no-shadow rendering for clearer dark ink on the parchment texture. +Version 0.1.13 requests a bounded, server-validated 9-by-7-by-9 block snapshot +from the held book or nearby Book Stand and renders it as a clipped, rotating +Ponder world section on the Linking Book's left page. + 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 diff --git a/gradle.properties b/gradle.properties index 4a42a22..e0912c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -33,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.12 +mod_version=0.1.13 # 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/client/ClientEvents.java b/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java index 8f28f7f..cbb0638 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java +++ b/src/main/java/net/dnorberg/rivencraft/client/ClientEvents.java @@ -24,4 +24,5 @@ public final class ClientEvents { NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickItem); NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickBlock); } + } diff --git a/src/main/java/net/dnorberg/rivencraft/client/ClientLinkPreviewHandler.java b/src/main/java/net/dnorberg/rivencraft/client/ClientLinkPreviewHandler.java new file mode 100644 index 0000000..484f9d9 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/client/ClientLinkPreviewHandler.java @@ -0,0 +1,19 @@ +package net.dnorberg.rivencraft.client; + +import net.dnorberg.rivencraft.client.screen.LinkingBookScreen; +import net.dnorberg.rivencraft.network.LinkPreviewPayload; +import net.minecraft.client.Minecraft; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +public final class ClientLinkPreviewHandler { + private ClientLinkPreviewHandler() { + } + + public static void handle(LinkPreviewPayload payload, IPayloadContext context) { + context.enqueueWork(() -> { + if (Minecraft.getInstance().screen instanceof LinkingBookScreen screen) { + screen.acceptPreview(payload); + } + }); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/client/preview/DestinationPreviewRenderer.java b/src/main/java/net/dnorberg/rivencraft/client/preview/DestinationPreviewRenderer.java index 267c3c9..8f13e16 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/preview/DestinationPreviewRenderer.java +++ b/src/main/java/net/dnorberg/rivencraft/client/preview/DestinationPreviewRenderer.java @@ -1,8 +1,11 @@ package net.dnorberg.rivencraft.client.preview; import net.minecraft.client.gui.GuiGraphics; +import net.dnorberg.rivencraft.network.LinkPreviewPayload; public interface DestinationPreviewRenderer extends AutoCloseable { + void accept(LinkPreviewPayload payload); + void tick(); void render(GuiGraphics graphics, int left, int top, int width, int height, float partialTick); diff --git a/src/main/java/net/dnorberg/rivencraft/client/preview/PonderDestinationPreviewRenderer.java b/src/main/java/net/dnorberg/rivencraft/client/preview/PonderDestinationPreviewRenderer.java index 9178c1a..ad37bfa 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/preview/PonderDestinationPreviewRenderer.java +++ b/src/main/java/net/dnorberg/rivencraft/client/preview/PonderDestinationPreviewRenderer.java @@ -1,10 +1,27 @@ package net.dnorberg.rivencraft.client.preview; +import com.mojang.blaze3d.systems.RenderSystem; import net.createmod.ponder.api.level.PonderLevel; +import net.createmod.ponder.api.element.PonderSceneElement; +import net.createmod.ponder.api.scene.Selection; +import net.createmod.ponder.foundation.SelectionImpl; +import net.createmod.ponder.foundation.element.WorldSectionElementImpl; +import net.createmod.catnip.gui.UIRenderHelper; +import net.createmod.catnip.render.DefaultSuperRenderTypeBuffer; +import net.createmod.catnip.render.SuperRenderTypeBuffer; +import net.dnorberg.rivencraft.network.LinkPreviewPayload; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.RenderType; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexSorting; +import com.mojang.math.Axis; +import org.joml.Matrix4f; +import org.joml.Vector3f; /** * Initial Ponder-backed preview boundary. @@ -14,12 +31,27 @@ import net.minecraft.network.chat.Component; * state rather than reading arbitrary remote chunks from the client.

*/ public final class PonderDestinationPreviewRenderer implements DestinationPreviewRenderer { + private static final Vector3f DIFFUSE_LIGHT_0 = new Vector3f(0.4F, -1.0F, 0.7F).normalize(); + private static final Vector3f DIFFUSE_LIGHT_1 = new Vector3f(-0.4F, -0.5F, 0.7F).normalize(); private final PonderLevel previewLevel; + private final Selection selection; + private final PonderSceneElement section; private int ticks; + private boolean loaded; public PonderDestinationPreviewRenderer() { Minecraft minecraft = Minecraft.getInstance(); previewLevel = new PonderLevel(BlockPos.ZERO, minecraft.level); + selection = SelectionImpl.of(new BoundingBox(0, 0, 0, 8, 6, 8)); + section = new WorldSectionElementImpl(selection); + } + + @Override + public void accept(LinkPreviewPayload payload) { + selection.forEach(pos -> previewLevel.setBlock(pos, Blocks.AIR.defaultBlockState(), 2)); + payload.blocks().forEach(block -> previewLevel.setBlock(block.pos(), block.state(), 2)); + ((net.createmod.ponder.api.element.WorldSectionElement)section).queueRedraw(); + loaded = true; } @Override @@ -33,6 +65,11 @@ public final class PonderDestinationPreviewRenderer implements DestinationPrevie graphics.fill(left, top, left + width, top + height, 0xFF080C18); graphics.renderOutline(left, top, width, height, 0xFF7B6AA8); + if (loaded) { + renderSnapshot(graphics, left, top, width, height, partialTick); + return; + } + double angle = (ticks + partialTick) * 0.04; int centerX = left + width / 2; int centerY = top + height / 2; @@ -47,4 +84,38 @@ public final class PonderDestinationPreviewRenderer implements DestinationPrevie 0xFFB8C5E0 ); } + + private void renderSnapshot(GuiGraphics graphics, int left, int top, int width, int height, float partialTick) { + graphics.enableScissor(left + 1, top + 1, left + width - 1, top + height - 1); + RenderSystem.enableBlend(); + RenderSystem.enableDepthTest(); + RenderSystem.backupProjectionMatrix(); + RenderSystem.setupLevelDiffuseLighting(DIFFUSE_LIGHT_0, DIFFUSE_LIGHT_1); + + Matrix4f projection = new Matrix4f(RenderSystem.getProjectionMatrix()); + projection.translate(0.0F, 0.0F, 800.0F); + RenderSystem.setProjectionMatrix(projection, VertexSorting.DISTANCE_TO_ORIGIN); + + PoseStack poseStack = graphics.pose(); + poseStack.pushPose(); + poseStack.translate(0.0F, 0.0F, -800.0F); + poseStack.translate(left + width / 2.0F, top + height / 2.0F + 8.0F, 200.0F); + poseStack.mulPose(Axis.XP.rotationDegrees(-28.0F)); + poseStack.mulPose(Axis.YP.rotationDegrees((ticks + partialTick) * 0.6F)); + UIRenderHelper.flipForGuiRender(poseStack); + poseStack.scale(10.0F, 10.0F, 10.0F); + poseStack.translate(-4.5F, -1.5F, -4.5F); + + SuperRenderTypeBuffer buffer = DefaultSuperRenderTypeBuffer.getInstance(); + section.renderFirst(previewLevel, buffer, graphics, partialTick); + for (RenderType type : RenderType.chunkBufferLayers()) { + section.renderLayer(previewLevel, buffer, type, graphics, partialTick); + } + section.renderLast(previewLevel, buffer, graphics, partialTick); + buffer.draw(); + + poseStack.popPose(); + RenderSystem.restoreProjectionMatrix(); + graphics.disableScissor(); + } } diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/LinkingBookScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/LinkingBookScreen.java index fcd5f3a..34a55f5 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/screen/LinkingBookScreen.java +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/LinkingBookScreen.java @@ -7,6 +7,8 @@ import net.dnorberg.rivencraft.network.BindLinkingBookPayload; import net.dnorberg.rivencraft.network.ModNetworking; import net.dnorberg.rivencraft.network.UseLinkingBookPayload; import net.dnorberg.rivencraft.network.UseBookStandPayload; +import net.dnorberg.rivencraft.network.RequestLinkPreviewPayload; +import net.dnorberg.rivencraft.network.LinkPreviewPayload; import net.dnorberg.rivencraft.registry.ModDataComponents; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; @@ -77,6 +79,11 @@ public final class LinkingBookScreen extends Screen { .build()); } else { preview = new PonderDestinationPreviewRenderer(); + PacketDistributor.sendToServer(new RequestLinkPreviewPayload( + bookStandPos != null, + bookStandPos == null && hand == InteractionHand.OFF_HAND, + bookStandPos == null ? BlockPos.ZERO : bookStandPos + )); } } @@ -143,6 +150,12 @@ public final class LinkingBookScreen extends Screen { } } + public void acceptPreview(LinkPreviewPayload payload) { + if (preview != null) { + preview.accept(payload); + } + } + private void bind() { String name = titleInput.getValue().strip(); if (!name.isEmpty()) { diff --git a/src/main/java/net/dnorberg/rivencraft/network/LinkPreviewPayload.java b/src/main/java/net/dnorberg/rivencraft/network/LinkPreviewPayload.java new file mode 100644 index 0000000..ed8eea6 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/network/LinkPreviewPayload.java @@ -0,0 +1,47 @@ +package net.dnorberg.rivencraft.network; + +import java.util.ArrayList; +import java.util.List; +import net.dnorberg.rivencraft.Rivencraft; +import net.minecraft.core.BlockPos; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +public record LinkPreviewPayload(List blocks) implements CustomPacketPayload { + private static final int MAX_BLOCKS = 9 * 7 * 9; + public static final Type TYPE = new Type<>( + ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "link_preview") + ); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buffer, payload) -> { + buffer.writeVarInt(payload.blocks.size()); + for (SnapshotBlock block : payload.blocks) { + buffer.writeBlockPos(block.pos); + buffer.writeVarInt(Block.getId(block.state)); + } + }, + buffer -> { + int size = buffer.readVarInt(); + if (size < 0 || size > MAX_BLOCKS) { + throw new IllegalArgumentException("Invalid link preview size: " + size); + } + List blocks = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + blocks.add(new SnapshotBlock(buffer.readBlockPos(), Block.stateById(buffer.readVarInt()))); + } + return new LinkPreviewPayload(List.copyOf(blocks)); + } + ); + + @Override + public Type type() { + return TYPE; + } + + public record SnapshotBlock(BlockPos pos, BlockState state) { + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java index 7b5d829..08d8076 100644 --- a/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java +++ b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java @@ -2,12 +2,15 @@ package net.dnorberg.rivencraft.network; import net.dnorberg.rivencraft.book.LinkTarget; import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity; +import net.dnorberg.rivencraft.client.ClientLinkPreviewHandler; 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.core.BlockPos; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.world.InteractionHand; @@ -16,6 +19,9 @@ import net.minecraft.world.phys.Vec3; import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; import net.neoforged.neoforge.network.handling.IPayloadContext; import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import net.neoforged.neoforge.network.PacketDistributor; +import java.util.ArrayList; +import java.util.List; public final class ModNetworking { public static final int MAX_BOOK_TITLE_LENGTH = 32; @@ -24,10 +30,14 @@ public final class ModNetworking { } public static void register(RegisterPayloadHandlersEvent event) { - PayloadRegistrar registrar = event.registrar("1"); + PayloadRegistrar registrar = event.registrar("2"); registrar.playToServer(BindLinkingBookPayload.TYPE, BindLinkingBookPayload.STREAM_CODEC, ModNetworking::bindBook); registrar.playToServer(UseLinkingBookPayload.TYPE, UseLinkingBookPayload.STREAM_CODEC, ModNetworking::useBook); registrar.playToServer(UseBookStandPayload.TYPE, UseBookStandPayload.STREAM_CODEC, ModNetworking::useBookStand); + registrar.playToServer(RequestLinkPreviewPayload.TYPE, RequestLinkPreviewPayload.STREAM_CODEC, + ModNetworking::requestLinkPreview); + registrar.playToClient(LinkPreviewPayload.TYPE, LinkPreviewPayload.STREAM_CODEC, + ClientLinkPreviewHandler::handle); } private static void bindBook(BindLinkingBookPayload payload, IPayloadContext context) { @@ -75,6 +85,44 @@ public final class ModNetworking { } } + private static void requestLinkPreview(RequestLinkPreviewPayload payload, IPayloadContext context) { + if (!(context.player() instanceof ServerPlayer player)) { + return; + } + + ItemStack stack; + if (payload.fromStand()) { + if (player.distanceToSqr(Vec3.atCenterOf(payload.standPos())) > 64.0D + || !(player.level().getBlockEntity(payload.standPos()) instanceof BookStandBlockEntity stand)) { + return; + } + stack = stand.getBook(); + } else { + stack = player.getItemInHand(hand(payload.offhand())); + } + + LinkTarget target = stack.is(ModItems.LINKING_BOOK.get()) + ? stack.get(ModDataComponents.LINK_TARGET.get()) : null; + if (target == null) { + return; + } + + ServerLevel level = player.getServer().getLevel(target.location().dimension()); + if (level == null) { + return; + } + + BlockPos origin = target.location().pos(); + List blocks = new ArrayList<>(); + for (BlockPos pos : BlockPos.betweenClosed(origin.offset(-4, -2, -4), origin.offset(4, 4, 4))) { + var state = level.getBlockState(pos); + if (!state.isAir()) { + blocks.add(new LinkPreviewPayload.SnapshotBlock(pos.subtract(origin).offset(4, 2, 4), state)); + } + } + PacketDistributor.sendToPlayer(player, new LinkPreviewPayload(List.copyOf(blocks))); + } + private static InteractionHand hand(boolean offhand) { return offhand ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND; } diff --git a/src/main/java/net/dnorberg/rivencraft/network/RequestLinkPreviewPayload.java b/src/main/java/net/dnorberg/rivencraft/network/RequestLinkPreviewPayload.java new file mode 100644 index 0000000..9befab5 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/network/RequestLinkPreviewPayload.java @@ -0,0 +1,28 @@ +package net.dnorberg.rivencraft.network; + +import net.dnorberg.rivencraft.Rivencraft; +import net.minecraft.core.BlockPos; +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 RequestLinkPreviewPayload(boolean fromStand, boolean offhand, BlockPos standPos) + implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "request_link_preview") + ); + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.BOOL, RequestLinkPreviewPayload::fromStand, + ByteBufCodecs.BOOL, RequestLinkPreviewPayload::offhand, + BlockPos.STREAM_CODEC, RequestLinkPreviewPayload::standPos, + RequestLinkPreviewPayload::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 1c78174..a8153f3 100644 --- a/src/main/resources/assets/rivencraft/lang/en_us.json +++ b/src/main/resources/assets/rivencraft/lang/en_us.json @@ -17,7 +17,7 @@ "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", + "screen.rivencraft.book.preview_pending": "Loading destination...", "screen.rivencraft.book.click_to_link": "Click the panel to link", "itemGroup.rivencraft": "Rivencraft" }