From 2b50f56980fda7c09629480d5f474abcd2f99087 Mon Sep 17 00:00:00 2001 From: tkrmagid Date: Fri, 15 May 2026 20:08:33 +0900 Subject: [PATCH] render: paint video on the clicked wall face (no visible anchor block) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The anchor block becomes invisible and non-collidable; it exists only as a BlockEntity host in the air block adjacent to the clicked wall. The renderer now translates and rotates the textured quad so it sits flush against the surface of the wall the user actually clicked, on any of the six faces. Stick interaction: right-click face → place anchor at hit.relative(face), facing=face, open GUI right-click face with anchor already there → reopen the GUI sneak + left-click face with stick → delete the anchor on that face The anchor's selection outline / collision / occlusion are all empty, so the player can target the wall block behind it without interference. JavaCV / streaming polish: - Bump missing-JavaCV log to WARN so users notice when the runtime jar is not installed (previously buried at INFO). - Add HTTP resilience options: `timeout`, `reconnect`, `reconnect_streamed`, `reconnect_at_eof`, and a `user_agent` so picky servers don't 403 us. --- gradle.properties | 2 +- .../ejclaw/videoplayer/VideoPlayerClient.java | 17 +++++-- .../videoplayer/block/VideoAnchorBlock.java | 38 +++++++++++++++ .../client/playback/JavaCvBackend.java | 16 +++++-- .../client/render/VideoAnchorRenderer.java | 48 ++++++++++++------- .../videoplayer/item/VideoStickItem.java | 37 +++++++++----- .../registry/VideoPlayerBlocks.java | 7 ++- .../models/block/video_anchor.json | 18 +------ 8 files changed, 129 insertions(+), 54 deletions(-) diff --git a/gradle.properties b/gradle.properties index 15dab72..74749fc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.configuration-cache=false # Mod mod_id=video_player -mod_version=0.3.1 +mod_version=0.4.0 maven_group=com.ejclaw.videoplayer archives_base_name=video_player diff --git a/src/main/java/com/ejclaw/videoplayer/VideoPlayerClient.java b/src/main/java/com/ejclaw/videoplayer/VideoPlayerClient.java index e327031..9199fb0 100644 --- a/src/main/java/com/ejclaw/videoplayer/VideoPlayerClient.java +++ b/src/main/java/com/ejclaw/videoplayer/VideoPlayerClient.java @@ -33,9 +33,20 @@ public class VideoPlayerClient implements ClientModInitializer { ); AttackBlockCallback.EVENT.register((player, level, hand, pos, direction) -> { - if (level.isClientSide() - && player.getMainHandItem().getItem() instanceof VideoStickItem - && level.getBlockEntity(pos) instanceof VideoAnchorBlockEntity) { + if (!level.isClientSide()) return InteractionResult.PASS; + if (!(player.getMainHandItem().getItem() instanceof VideoStickItem)) return InteractionResult.PASS; + // The anchor itself is invisible / non-collidable so the player cannot left-click it + // directly. Sneak + left-click on the wall the video sits on → delete the anchor in + // the adjacent air block. + if (player.isShiftKeyDown()) { + BlockPos anchorPos = pos.relative(direction); + if (level.getBlockEntity(anchorPos) instanceof VideoAnchorBlockEntity) { + ClientPlayNetworking.send(new DeleteAnchorPayload(anchorPos)); + return InteractionResult.SUCCESS; + } + } + // Legacy / safety: if the player somehow targets the anchor block directly. + if (level.getBlockEntity(pos) instanceof VideoAnchorBlockEntity) { ClientPlayNetworking.send(new DeleteAnchorPayload(pos)); return InteractionResult.SUCCESS; } diff --git a/src/main/java/com/ejclaw/videoplayer/block/VideoAnchorBlock.java b/src/main/java/com/ejclaw/videoplayer/block/VideoAnchorBlock.java index 480e023..845c6d1 100644 --- a/src/main/java/com/ejclaw/videoplayer/block/VideoAnchorBlock.java +++ b/src/main/java/com/ejclaw/videoplayer/block/VideoAnchorBlock.java @@ -10,13 +10,26 @@ import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +/** + * Anchor block — invisible, non-collidable host for {@link VideoAnchorBlockEntity}. + * + *

The block exists only so a {@link BlockEntity} can be attached to a position; visually it is + * completely empty (no model, no selection outline, no collision). The video itself is drawn by + * {@link com.ejclaw.videoplayer.client.render.VideoAnchorRenderer} flush against the wall the + * player clicked, not as a textured surface on this block. + */ public class VideoAnchorBlock extends BaseEntityBlock { public static final MapCodec CODEC = simpleCodec(VideoAnchorBlock::new); @@ -34,6 +47,31 @@ public class VideoAnchorBlock extends BaseEntityBlock { return new VideoAnchorBlockEntity(pos, state); } + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } + + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext ctx) { + return Shapes.empty(); + } + + @Override + protected VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext ctx) { + return Shapes.empty(); + } + + @Override + protected VoxelShape getOcclusionShape(BlockState state) { + return Shapes.empty(); + } + + @Override + protected boolean propagatesSkylightDown(BlockState state) { + return true; + } + @Override protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, diff --git a/src/main/java/com/ejclaw/videoplayer/client/playback/JavaCvBackend.java b/src/main/java/com/ejclaw/videoplayer/client/playback/JavaCvBackend.java index e1ddee7..c10c518 100644 --- a/src/main/java/com/ejclaw/videoplayer/client/playback/JavaCvBackend.java +++ b/src/main/java/com/ejclaw/videoplayer/client/playback/JavaCvBackend.java @@ -108,9 +108,14 @@ public class JavaCvBackend implements VideoBackend { Method setOpt = grabberCls.getMethod("setOption", String.class, String.class); Method setSampleFormat = grabberCls.getMethod("setSampleFormat", int.class); - // mp4/http(s) network tuning - try { setOpt.invoke(grabber, "rw_timeout", "5000000"); } catch (Throwable ignored) {} - try { setOpt.invoke(grabber, "stimeout", "5000000"); } catch (Throwable ignored) {} + // HTTP(S) tuning for streaming URLs (e.g. webm via Range / chunked transfer). + try { setOpt.invoke(grabber, "rw_timeout", "10000000"); } catch (Throwable ignored) {} + try { setOpt.invoke(grabber, "timeout", "10000000"); } catch (Throwable ignored) {} + try { setOpt.invoke(grabber, "reconnect", "1"); } catch (Throwable ignored) {} + try { setOpt.invoke(grabber, "reconnect_streamed", "1"); } catch (Throwable ignored) {} + try { setOpt.invoke(grabber, "reconnect_at_eof", "1"); } catch (Throwable ignored) {} + try { setOpt.invoke(grabber, "user_agent", + "video_player/" + com.ejclaw.videoplayer.VideoPlayerMod.MOD_ID); } catch (Throwable ignored) {} // Force interleaved signed 16-bit PCM so the audio sink path is single-shape. try { setSampleFormat.invoke(grabber, AV_SAMPLE_FMT_S16); } catch (Throwable ignored) {} @@ -162,7 +167,10 @@ public class JavaCvBackend implements VideoBackend { if (audioLine == null) Thread.sleep(15); } } catch (ClassNotFoundException cnf) { - VideoPlayerMod.LOG.info("[{}] JavaCV not on classpath; backend inactive", VideoPlayerMod.MOD_ID); + VideoPlayerMod.LOG.warn( + "[{}] JavaCV not on classpath — install org.bytedeco:javacv-platform (or javacv + ffmpeg natives)" + + " to enable video/audio playback. Anchor placeholder will remain visible.", + VideoPlayerMod.MOD_ID); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } catch (Throwable t) { diff --git a/src/main/java/com/ejclaw/videoplayer/client/render/VideoAnchorRenderer.java b/src/main/java/com/ejclaw/videoplayer/client/render/VideoAnchorRenderer.java index 49fedb1..ab075ea 100644 --- a/src/main/java/com/ejclaw/videoplayer/client/render/VideoAnchorRenderer.java +++ b/src/main/java/com/ejclaw/videoplayer/client/render/VideoAnchorRenderer.java @@ -20,15 +20,19 @@ import net.minecraft.world.phys.Vec3; import org.joml.Matrix4f; /** - * SPEC §5.2 — submits a width×height textured quad in front of the anchor, oriented by facing. + * Draws the video as a textured quad on the surface of the block the user clicked. * - *

Ported to 26.1.2's render-state pipeline: per-frame BE state is captured in - * {@link State} via {@link #extractRenderState}, then drawn via - * {@link SubmitNodeCollector#submitCustomGeometry} during {@link #submit}. + *

The anchor BE lives in the air block adjacent to the clicked wall. Its {@code facing} + * field is the surface normal of the wall (= the {@link Direction} the player clicked). The + * quad is rotated so its normal aligns with that direction and shifted so it sits flush against + * the wall surface, with a tiny outward offset to avoid z-fighting. */ @Environment(EnvType.CLIENT) public class VideoAnchorRenderer implements BlockEntityRenderer { + /** Tiny outward offset so the quad doesn't z-fight with the wall. */ + private static final float SURFACE_EPSILON = 0.001F; + public VideoAnchorRenderer(BlockEntityRendererProvider.Context ctx) { // no-op } @@ -44,8 +48,7 @@ public class VideoAnchorRenderer implements BlockEntityRenderer { - // Front face (visible from the direction the anchor faces) + // Front face (visible from outside, looking back at the wall) emit(vc, mat, 0F, 0F, 0F, 0F, 1F, light); emit(vc, mat, w, 0F, 0F, 1F, 1F, light); emit(vc, mat, w, h, 0F, 1F, 0F, light); emit(vc, mat, 0F, h, 0F, 0F, 0F, light); - // Back face (visible from behind) + // Back face (in case the player ends up on the other side, e.g. clipping into the wall) emit(vc, mat, 0F, h, 0F, 0F, 0F, light); emit(vc, mat, w, h, 0F, 1F, 0F, light); emit(vc, mat, w, 0F, 0F, 1F, 1F, light); @@ -85,6 +89,18 @@ public class VideoAnchorRenderer implements BlockEntityRenderer { /* identity: local +Z already faces world +Z (south) */ } + case NORTH -> pose.mulPose(Axis.YP.rotationDegrees(180F)); + case EAST -> pose.mulPose(Axis.YP.rotationDegrees(-90F)); + case WEST -> pose.mulPose(Axis.YP.rotationDegrees(90F)); + case UP -> pose.mulPose(Axis.XP.rotationDegrees(-90F)); + case DOWN -> pose.mulPose(Axis.XP.rotationDegrees(90F)); + } + } + private static void emit(com.mojang.blaze3d.vertex.VertexConsumer vc, Matrix4f mat, float x, float y, float z, float u, float v, int light) { vc.addVertex(mat, x, y, z) @@ -110,6 +126,6 @@ public class VideoAnchorRenderer implements BlockEntityRenderer + *

  • If a video anchor already exists in the adjacent air (= an anchor already drawn on this + * face), reopen its config GUI.
  • + *
  • Otherwise place an invisible anchor in the adjacent air block, set its facing to the + * clicked face direction (so the renderer draws the quad flush against this face), and + * open the config GUI.
  • + * + * The anchor block itself is invisible / non-collidable, so visually no new block appears — + * the video just shows up on the face the user clicked. + */ public class VideoStickItem extends Item { public VideoStickItem(Properties properties) { super(properties); @@ -33,25 +44,27 @@ public class VideoStickItem extends Item { if (!(player instanceof ServerPlayer sp)) return InteractionResult.PASS; BlockPos hit = ctx.getClickedPos(); + Direction face = ctx.getClickedFace(); + BlockPos anchorPos = hit.relative(face); - // Existing anchor → edit - if (sl.getBlockEntity(hit) instanceof VideoAnchorBlockEntity existing) { - ServerPlayNetworking.send(sp, new OpenScreenPayload(hit, existing.toNbt())); + // Existing anchor on this face → reopen edit GUI. + if (sl.getBlockEntity(anchorPos) instanceof VideoAnchorBlockEntity existing) { + ServerPlayNetworking.send(sp, new OpenScreenPayload(anchorPos, existing.toNbt())); return InteractionResult.SUCCESS; } - // Empty face → place anchor on top of the clicked face - Direction side = ctx.getClickedFace(); - BlockPos placeAt = hit.relative(side); - BlockState there = sl.getBlockState(placeAt); + // Need an empty / replaceable space in front of the clicked face. + BlockState there = sl.getBlockState(anchorPos); if (!there.canBeReplaced()) return InteractionResult.PASS; Block anchor = VideoPlayerBlocks.VIDEO_ANCHOR; - sl.setBlock(placeAt, anchor.defaultBlockState(), Block.UPDATE_ALL); + sl.setBlock(anchorPos, anchor.defaultBlockState(), Block.UPDATE_ALL); - if (sl.getBlockEntity(placeAt) instanceof VideoAnchorBlockEntity be) { - be.setFacing(ctx.getHorizontalDirection().getOpposite()); - ServerPlayNetworking.send(sp, new OpenScreenPayload(placeAt, be.toNbt())); + if (sl.getBlockEntity(anchorPos) instanceof VideoAnchorBlockEntity be) { + // Surface normal of the wall we're painting on points outward in the same direction + // as the face the player clicked. + be.setFacing(face); + ServerPlayNetworking.send(sp, new OpenScreenPayload(anchorPos, be.toNbt())); } return InteractionResult.SUCCESS; } diff --git a/src/main/java/com/ejclaw/videoplayer/registry/VideoPlayerBlocks.java b/src/main/java/com/ejclaw/videoplayer/registry/VideoPlayerBlocks.java index e0f11ed..f1bae24 100644 --- a/src/main/java/com/ejclaw/videoplayer/registry/VideoPlayerBlocks.java +++ b/src/main/java/com/ejclaw/videoplayer/registry/VideoPlayerBlocks.java @@ -14,7 +14,12 @@ public final class VideoPlayerBlocks { public static final Block VIDEO_ANCHOR = register( "video_anchor", - BlockBehaviour.Properties.of().strength(1.0F).noOcclusion(), + BlockBehaviour.Properties.of() + .noCollision() + .noOcclusion() + .instabreak() + .replaceable() + .strength(0F), VideoAnchorBlock::new ); diff --git a/src/main/resources/assets/video_player/models/block/video_anchor.json b/src/main/resources/assets/video_player/models/block/video_anchor.json index 5295e9b..654f948 100644 --- a/src/main/resources/assets/video_player/models/block/video_anchor.json +++ b/src/main/resources/assets/video_player/models/block/video_anchor.json @@ -1,21 +1,5 @@ { - "parent": "block/block", "textures": { - "all": "video_player:block/video_anchor", "particle": "video_player:block/video_anchor" - }, - "elements": [ - { - "from": [0, 0, 0], - "to": [16, 2, 16], - "faces": { - "down": { "texture": "#all", "uv": [0, 0, 16, 16] }, - "up": { "texture": "#all", "uv": [0, 0, 16, 16] }, - "north": { "texture": "#all", "uv": [0, 0, 16, 2] }, - "south": { "texture": "#all", "uv": [0, 0, 16, 2] }, - "east": { "texture": "#all", "uv": [0, 0, 16, 2] }, - "west": { "texture": "#all", "uv": [0, 0, 16, 2] } - } - } - ] + } }