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