Render destination snapshots with Ponder
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-09-21 08:13:08 -04:00
1 parent 5e1a3fc150
commit 8dbde191b8
12 files changed
+241 -7

No files matched your search

+3 -3
View File
@@ -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 book back and restore the vanilla lectern. Breaking a Book Stand drops both
the stored book and a vanilla lectern. the stored book and a vanilla lectern.
Version 0.1.4 establishes a Ponder-backed destination-preview boundary and an Bound Linking Books request a small, server-validated snapshot around their
animated loading view. Transmitting a destination snapshot and rendering its destination and render its blocks as a rotating Ponder scene on the left page.
actual blocks as the rotating scene is the next preview iteration. The snapshot is visual only; the server independently validates every link.
## Requirements ## Requirements
+5 -1
View File
@@ -51,7 +51,7 @@ the symbol grammar is introduced.
- [x] Book-screen foundation with server-authoritative naming, binding, and - [x] Book-screen foundation with server-authoritative naming, binding, and
explicit link activation. explicit link activation.
- [x] Ponder dependency and isolated destination-preview level boundary. - [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] Book Stand floating names synchronized from the stored book stack.
- [x] Open the stored book screen from a Book Stand with server-validated linking. - [x] Open the stored book screen from a Book Stand with server-validated linking.
- [ ] Automated in-game tests for binding, persistence, and travel. - [ ] 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 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. 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 The preview code is separated behind a renderer interface and uses an isolated
Ponder level. The first iteration supplies the animated loading boundary; the Ponder level. The first iteration supplies the animated loading boundary; the
next iteration will add server-authoritative snapshot transport and populate next iteration will add server-authoritative snapshot transport and populate
+1 -1
View File
@@ -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. # 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. See https://semver.org/ # 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. # 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.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html # See https://maven.apache.org/guides/mini/guide-naming-conventions.html
@@ -24,4 +24,5 @@ public final class ClientEvents {
NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickItem); NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickItem);
NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickBlock); NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickBlock);
} }
} }
@@ -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);
}
});
}
}
@@ -1,8 +1,11 @@
package net.dnorberg.rivencraft.client.preview; package net.dnorberg.rivencraft.client.preview;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;
import net.dnorberg.rivencraft.network.LinkPreviewPayload;
public interface DestinationPreviewRenderer extends AutoCloseable { public interface DestinationPreviewRenderer extends AutoCloseable {
void accept(LinkPreviewPayload payload);
void tick(); void tick();
void render(GuiGraphics graphics, int left, int top, int width, int height, float partialTick); void render(GuiGraphics graphics, int left, int top, int width, int height, float partialTick);
@@ -1,10 +1,27 @@
package net.dnorberg.rivencraft.client.preview; package net.dnorberg.rivencraft.client.preview;
import com.mojang.blaze3d.systems.RenderSystem;
import net.createmod.ponder.api.level.PonderLevel; 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.Minecraft;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component; 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. * 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.</p> * state rather than reading arbitrary remote chunks from the client.</p>
*/ */
public final class PonderDestinationPreviewRenderer implements DestinationPreviewRenderer { 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 PonderLevel previewLevel;
private final Selection selection;
private final PonderSceneElement section;
private int ticks; private int ticks;
private boolean loaded;
public PonderDestinationPreviewRenderer() { public PonderDestinationPreviewRenderer() {
Minecraft minecraft = Minecraft.getInstance(); Minecraft minecraft = Minecraft.getInstance();
previewLevel = new PonderLevel(BlockPos.ZERO, minecraft.level); 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 @Override
@@ -33,6 +65,11 @@ public final class PonderDestinationPreviewRenderer implements DestinationPrevie
graphics.fill(left, top, left + width, top + height, 0xFF080C18); graphics.fill(left, top, left + width, top + height, 0xFF080C18);
graphics.renderOutline(left, top, width, height, 0xFF7B6AA8); graphics.renderOutline(left, top, width, height, 0xFF7B6AA8);
if (loaded) {
renderSnapshot(graphics, left, top, width, height, partialTick);
return;
}
double angle = (ticks + partialTick) * 0.04; double angle = (ticks + partialTick) * 0.04;
int centerX = left + width / 2; int centerX = left + width / 2;
int centerY = top + height / 2; int centerY = top + height / 2;
@@ -47,4 +84,38 @@ public final class PonderDestinationPreviewRenderer implements DestinationPrevie
0xFFB8C5E0 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();
}
} }
@@ -7,6 +7,8 @@ import net.dnorberg.rivencraft.network.BindLinkingBookPayload;
import net.dnorberg.rivencraft.network.ModNetworking; import net.dnorberg.rivencraft.network.ModNetworking;
import net.dnorberg.rivencraft.network.UseLinkingBookPayload; import net.dnorberg.rivencraft.network.UseLinkingBookPayload;
import net.dnorberg.rivencraft.network.UseBookStandPayload; 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.dnorberg.rivencraft.registry.ModDataComponents;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Button;
@@ -77,6 +79,11 @@ public final class LinkingBookScreen extends Screen {
.build()); .build());
} else { } else {
preview = new PonderDestinationPreviewRenderer(); 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() { private void bind() {
String name = titleInput.getValue().strip(); String name = titleInput.getValue().strip();
if (!name.isEmpty()) { if (!name.isEmpty()) {
@@ -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<SnapshotBlock> blocks) implements CustomPacketPayload {
private static final int MAX_BLOCKS = 9 * 7 * 9;
public static final Type<LinkPreviewPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "link_preview")
);
public static final StreamCodec<RegistryFriendlyByteBuf, LinkPreviewPayload> 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<SnapshotBlock> 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<? extends CustomPacketPayload> type() {
return TYPE;
}
public record SnapshotBlock(BlockPos pos, BlockState state) {
}
}
@@ -2,12 +2,15 @@ package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.book.LinkTarget; import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity; import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
import net.dnorberg.rivencraft.client.ClientLinkPreviewHandler;
import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.core.component.DataComponents; import net.minecraft.core.component.DataComponents;
import net.minecraft.core.BlockPos;
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.server.level.ServerLevel;
import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource; import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand; 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.event.RegisterPayloadHandlersEvent;
import net.neoforged.neoforge.network.handling.IPayloadContext; import net.neoforged.neoforge.network.handling.IPayloadContext;
import net.neoforged.neoforge.network.registration.PayloadRegistrar; 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 final class ModNetworking {
public static final int MAX_BOOK_TITLE_LENGTH = 32; public static final int MAX_BOOK_TITLE_LENGTH = 32;
@@ -24,10 +30,14 @@ public final class ModNetworking {
} }
public static void register(RegisterPayloadHandlersEvent event) { 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(BindLinkingBookPayload.TYPE, BindLinkingBookPayload.STREAM_CODEC, ModNetworking::bindBook);
registrar.playToServer(UseLinkingBookPayload.TYPE, UseLinkingBookPayload.STREAM_CODEC, ModNetworking::useBook); registrar.playToServer(UseLinkingBookPayload.TYPE, UseLinkingBookPayload.STREAM_CODEC, ModNetworking::useBook);
registrar.playToServer(UseBookStandPayload.TYPE, UseBookStandPayload.STREAM_CODEC, ModNetworking::useBookStand); 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) { 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<LinkPreviewPayload.SnapshotBlock> 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) { private static InteractionHand hand(boolean offhand) {
return offhand ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND; return offhand ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
} }
@@ -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<RequestLinkPreviewPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "request_link_preview")
);
public static final StreamCodec<RegistryFriendlyByteBuf, RequestLinkPreviewPayload> STREAM_CODEC =
StreamCodec.composite(
ByteBufCodecs.BOOL, RequestLinkPreviewPayload::fromStand,
ByteBufCodecs.BOOL, RequestLinkPreviewPayload::offhand,
BlockPos.STREAM_CODEC, RequestLinkPreviewPayload::standPos,
RequestLinkPreviewPayload::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
@@ -17,7 +17,7 @@
"screen.rivencraft.book.bind": "Bind", "screen.rivencraft.book.bind": "Bind",
"screen.rivencraft.book.link": "Link", "screen.rivencraft.book.link": "Link",
"screen.rivencraft.book.unwritten": "This Descriptive Book is unwritten", "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", "screen.rivencraft.book.click_to_link": "Click the panel to link",
"itemGroup.rivencraft": "Rivencraft" "itemGroup.rivencraft": "Rivencraft"
} }