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] } - } - } - ] + } }