2 Commits

Author SHA1 Message Date
tkrmagid
5722c299d3 v0.4.25: add volume argument to /videoPlace, -1 mutes
Some checks failed
build / build (push) Has been cancelled
New signature:
  /videoPlace <pos> <facing> <width> <height> <volume> <url>

volume is int -1..100. 0..100 sets percent and clears mute; -1 is a CLI
shortcut that sets muted=true (underlying volume kept at 0.5 so a later
/videoMute false restores audible level without re-typing). Matches the
percent scale shown in the config GUI.
2026-05-17 03:31:13 +09:00
tkrmagid
ecd254cb78 v0.4.24: sync anchor config in vanilla BE update packet
Some checks failed
build / build (push) Has been cancelled
Bug: /videoPlace (and any path that places an anchor) reported success
on the server but the panel was invisible on the client — no quad, no
video. Walking around did not help.

Root cause: VideoAnchorBlockEntity did not override getUpdateTag() or
getUpdatePacket(). The base implementations return an empty CompoundTag
and null respectively, so url/facing/width/height were never carried in
vanilla BE sync. There is also a packet-ordering race between
level.setBlock() (queues a deferred chunk broadcast) and
ServerPlayNetworking.send(SyncAnchorPayload) (writes immediately): if
the payload arrives first, the client drops it because the BE does not
exist yet, then the chunk packet creates the BE with defaults
(url="") and the renderer silently no-ops.

Fix: override getUpdateTag(HolderLookup.Provider) → toNbt(), and
getUpdatePacket() → ClientboundBlockEntityDataPacket.create(this). NBT
key names already line up between toNbt() and loadAdditional(), so
vanilla wraps the CompoundTag in TagValueInput and existing load logic
reads it. Also fixes the 'walk far away, come back' case — that path
has no SyncAnchorPayload, just vanilla chunk re-sync.
2026-05-17 03:21:25 +09:00
3 changed files with 52 additions and 4 deletions

View File

@@ -5,7 +5,7 @@ org.gradle.configuration-cache=false
# Mod
mod_id=video_player
mod_version=0.4.23
mod_version=0.4.25
maven_group=com.ejclaw.videoplayer
archives_base_name=video_player

View File

@@ -3,7 +3,11 @@ package com.ejclaw.videoplayer.block;
import com.ejclaw.videoplayer.registry.VideoPlayerBlockEntities;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.ValueInput;
@@ -123,6 +127,35 @@ public class VideoAnchorBlockEntity extends BlockEntity {
out.putBoolean("autoplay", autoplay);
}
/**
* Vanilla chunk-load BE sync. The base implementation returns an empty tag, which means
* when a client first sees this BE (chunk loads or player walks into range) it gets default
* values — url="" in particular makes the renderer no-op and the panel appears invisible.
*
* <p>Returning {@link #toNbt()} here carries the custom fields in the vanilla packet, so
* we don't depend on the {@code SyncAnchorPayload} arriving before the chunk's block-update
* packet (there's a race: {@code level.setBlock} queues a deferred chunk broadcast while
* {@code ServerPlayNetworking.send} writes immediately; if the payload wins, the client
* drops it because the BE doesn't exist yet, then the chunk packet creates the BE with
* defaults). It also fixes "player walks far away and comes back" — that path has no
* SyncAnchorPayload at all, just vanilla chunk re-sync.
*/
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider provider) {
return toNbt();
}
/**
* Triggers a {@link ClientboundBlockEntityDataPacket} whenever the chunk tracker decides
* this BE needs to push an update. Default implementation returns {@code null} (no packet
* sent on BE change). Combined with {@link #getUpdateTag} above, every BE-state change a
* client sees carries the full config.
*/
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
return ClientboundBlockEntityDataPacket.create(this);
}
@Override
protected void loadAdditional(ValueInput in) {
super.loadAdditional(in);

View File

@@ -22,7 +22,13 @@ import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.permissions.Permissions;
import net.minecraft.world.level.block.Block;
/** SPEC §4.5.1 — {@code /videoPlace <pos> <facing> <width> <height> <url>} */
/**
* SPEC §4.5.1 — {@code /videoPlace <pos> <facing> <width> <height> <volume> <url>}.
*
* <p>{@code volume} is an integer 0..100 (percent) or {@code -1} to start muted. The percent
* form mirrors the GUI slider; {@code -1} is a CLI shortcut so admins don't need a follow-up
* {@code /videoMute} step.
*/
public final class VideoPlaceCommand {
private VideoPlaceCommand() {}
@@ -38,8 +44,9 @@ public final class VideoPlaceCommand {
.then(Commands.argument("facing", StringArgumentType.word())
.then(Commands.argument("width", IntegerArgumentType.integer(1, 32))
.then(Commands.argument("height", IntegerArgumentType.integer(1, 32))
.then(Commands.argument("url", StringArgumentType.greedyString())
.executes(VideoPlaceCommand::run))))));
.then(Commands.argument("volume", IntegerArgumentType.integer(-1, 100))
.then(Commands.argument("url", StringArgumentType.greedyString())
.executes(VideoPlaceCommand::run)))))));
}
private static int run(com.mojang.brigadier.context.CommandContext<CommandSourceStack> ctx)
@@ -54,6 +61,12 @@ public final class VideoPlaceCommand {
}
int width = IntegerArgumentType.getInteger(ctx, "width");
int height = IntegerArgumentType.getInteger(ctx, "height");
int volumeArg = IntegerArgumentType.getInteger(ctx, "volume");
// -1 is the CLI mute shortcut; the BE keeps the underlying volume so an admin can
// /videoMute false later without re-typing a level. Anything 0..100 sets %-volume and
// clears mute.
boolean placeMuted = volumeArg < 0;
float placeVolume = placeMuted ? 0.5F : (volumeArg / 100.0F);
String raw = StringArgumentType.getString(ctx, "url").trim();
// Accept either an http(s) URL or a /videoCache add <name> entry: resolveUrlOrName()
// returns the canonical URL in both cases, or null when a non-URL string didn't match
@@ -75,6 +88,8 @@ public final class VideoPlaceCommand {
be.setWidth(width);
be.setHeight(height);
be.setUrl(url);
be.setVolume(placeVolume);
be.setMuted(placeMuted);
CompoundTag nbt = be.toNbt();
for (ServerPlayer p : PlayerLookup.tracking(level, pos)) {