port: migrate all sources from Yarn 1.21.x to Mojmap 26.1.2
Some checks failed
build / build (push) Has been cancelled

- Block/BE/Item: BaseEntityBlock + useItemOn(InteractionResult), useOn(UseOnContext),
  setChanged(), loadAdditional(ValueInput) / saveAdditional(ValueOutput) with
  getStringOr/getIntOr/getBooleanOr/getFloatOr defaults
- Registries: BuiltInRegistries + ResourceKey + Properties.setId(ResourceKey)
- Networking: CustomPacketPayload.Type + StreamCodec.composite + RegistryFriendlyByteBuf
  (note: clientboundPlay/serverboundPlay names in fabric-networking-api-v1 6.3.1)
- Commands: Commands.literal/argument, CommandSourceStack.sendSuccess/sendFailure,
  PermissionSet.hasPermission(Permissions.COMMANDS_GAMEMASTER) (level-2 equivalent)
- Client GUI: EditBox / Button / Checkbox / AbstractSliderButton + addRenderableWidget
  (no render override; widgets render themselves under the new pipeline)
- Renderer: rewritten as stub against new BlockEntityRenderer<T, S extends BlockEntityRenderState>
  pattern (createRenderState / extractRenderState / submit). Stub does not draw a quad yet
  — frame upload and dynamic texture surface deferred until Watermedia/JavaCV are
  re-audited for Java 25
- Playback: stripped to bookkeeping-only stub (tracks active anchors, no frame pump)
- Client entrypoint: ClientTickEvents.END_LEVEL_TICK (was END_WORLD_TICK), Minecraft.level,
  LocalPlayer, Vec3, InteractionResult

./gradlew build passes against MC 26.1.2 + Fabric Loader 0.19.2 + fabric-api 0.149.0+26.1.2.
Block placement, anchor BE, payloads, commands, and GUI are functional; the anchor renders
as the plain block until the new render-state pipeline is wired with a texture.
This commit is contained in:
tkrmagid
2026-05-15 19:27:12 +09:00
parent 8f69814cb2
commit 27a3f34bfa
20 changed files with 517 additions and 610 deletions

View File

@@ -14,17 +14,15 @@ import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.client.rendering.v1.BlockEntityRendererRegistry;
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.ActionResult;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.phys.Vec3;
@Environment(EnvType.CLIENT)
public class VideoPlayerClient implements ClientModInitializer {
@SuppressWarnings("deprecation")
@Override
public void onInitializeClient() {
ClientNetworking.register();
@@ -34,14 +32,14 @@ public class VideoPlayerClient implements ClientModInitializer {
VideoAnchorRenderer::new
);
AttackBlockCallback.EVENT.register((player, world, hand, pos, direction) -> {
if (world.isClient
&& player.getMainHandStack().getItem() instanceof VideoStickItem
&& world.getBlockEntity(pos) instanceof VideoAnchorBlockEntity) {
AttackBlockCallback.EVENT.register((player, level, hand, pos, direction) -> {
if (level.isClientSide()
&& player.getMainHandItem().getItem() instanceof VideoStickItem
&& level.getBlockEntity(pos) instanceof VideoAnchorBlockEntity) {
ClientPlayNetworking.send(new DeleteAnchorPayload(pos));
return ActionResult.SUCCESS;
return InteractionResult.SUCCESS;
}
return ActionResult.PASS;
return InteractionResult.PASS;
});
ClientTickEvents.END_CLIENT_TICK.register(client -> {
@@ -49,7 +47,7 @@ public class VideoPlayerClient implements ClientModInitializer {
updateDistanceGains(client);
});
ClientTickEvents.END_WORLD_TICK.register(world -> {
ClientTickEvents.END_LEVEL_TICK.register(world -> {
// no-op for now
});
@@ -57,19 +55,19 @@ public class VideoPlayerClient implements ClientModInitializer {
}
/** SPEC §6 — recompute per-anchor audio gain from player distance every tick. */
private static void updateDistanceGains(MinecraftClient client) {
ClientPlayerEntity p = client.player;
if (p == null || client.world == null) return;
Vec3d eye = p.getEyePos();
for (BlockPos pos : com.ejclaw.videoplayer.client.playback.VideoPlayback.activePositions()) {
if (!(client.world.getBlockEntity(pos) instanceof VideoAnchorBlockEntity be)) continue;
private static void updateDistanceGains(Minecraft client) {
LocalPlayer p = client.player;
if (p == null || client.level == null) return;
Vec3 eye = p.getEyePosition();
for (BlockPos pos : VideoPlayback.activePositions()) {
if (!(client.level.getBlockEntity(pos) instanceof VideoAnchorBlockEntity be)) continue;
double dx = (pos.getX() + 0.5) - eye.x;
double dy = (pos.getY() + 0.5) - eye.y;
double dz = (pos.getZ() + 0.5) - eye.z;
double d = Math.sqrt(dx * dx + dy * dy + dz * dz);
float attenuation = (float) Math.max(0.0, Math.min(1.0, 1.0 - d / 16.0));
float gain = be.isMuted() ? 0F : be.getVolume() * attenuation;
com.ejclaw.videoplayer.client.playback.VideoPlayback.setGain(pos, gain);
VideoPlayback.setGain(pos, gain);
}
}
}

View File

@@ -4,50 +4,49 @@ import com.ejclaw.videoplayer.item.VideoStickItem;
import com.ejclaw.videoplayer.net.OpenScreenPayload;
import com.mojang.serialization.MapCodec;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.block.AbstractBlock;
import net.minecraft.block.BlockEntityProvider;
import net.minecraft.block.BlockState;
import net.minecraft.block.BlockWithEntity;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
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.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
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;
public class VideoAnchorBlock extends BlockWithEntity implements BlockEntityProvider {
public static final MapCodec<VideoAnchorBlock> CODEC = createCodec(VideoAnchorBlock::new);
public class VideoAnchorBlock extends BaseEntityBlock {
public static final MapCodec<VideoAnchorBlock> CODEC = simpleCodec(VideoAnchorBlock::new);
public VideoAnchorBlock(AbstractBlock.Settings settings) {
super(settings);
public VideoAnchorBlock(BlockBehaviour.Properties properties) {
super(properties);
}
@Override
protected MapCodec<? extends BlockWithEntity> getCodec() {
protected MapCodec<? extends BaseEntityBlock> codec() {
return CODEC;
}
@Override
public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new VideoAnchorBlockEntity(pos, state);
}
@Override
protected ActionResult onUseWithItem(ItemStack stack, BlockState state, World world,
BlockPos pos, PlayerEntity player, Hand hand,
protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level,
BlockPos pos, Player player, InteractionHand hand,
BlockHitResult hit) {
if (!(stack.getItem() instanceof VideoStickItem)) {
return ActionResult.PASS;
return InteractionResult.PASS;
}
if (world.isClient) return ActionResult.SUCCESS;
if (!(player instanceof ServerPlayerEntity sp)) return ActionResult.PASS;
if (world.getBlockEntity(pos) instanceof VideoAnchorBlockEntity be) {
if (level.isClientSide()) return InteractionResult.SUCCESS;
if (!(player instanceof ServerPlayer sp)) return InteractionResult.PASS;
if (level.getBlockEntity(pos) instanceof VideoAnchorBlockEntity be) {
ServerPlayNetworking.send(sp, new OpenScreenPayload(pos, be.toNbt()));
return ActionResult.SUCCESS;
return InteractionResult.SUCCESS;
}
return ActionResult.PASS;
return InteractionResult.PASS;
}
}

View File

@@ -1,17 +1,18 @@
package com.ejclaw.videoplayer.block;
import com.ejclaw.videoplayer.registry.VideoPlayerBlockEntities;
import net.minecraft.block.BlockState;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.storage.ReadView;
import net.minecraft.storage.WriteView;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
/**
* Anchor BE — holds the per-block config that drives playback. NBT persistence uses
* 1.21.6's ReadView/WriteView. Network sync uses {@link #toNbt()} / {@link #fromNbt(NbtCompound)}.
* Anchor BE — holds per-block config that drives playback.
* NBT persistence uses 26.1's ValueInput/ValueOutput.
* Network sync uses {@link #toNbt()} / {@link #fromNbt(CompoundTag)}.
*/
public class VideoAnchorBlockEntity extends BlockEntity {
private String url = "";
@@ -36,28 +37,28 @@ public class VideoAnchorBlockEntity extends BlockEntity {
public boolean isMuted() { return muted; }
public boolean isAutoplay() { return autoplay; }
public void setUrl(String url) { this.url = url == null ? "" : url; markDirty(); }
public void setWidth(int width) { this.width = clamp(width, 1, 32); markDirty(); }
public void setHeight(int height) { this.height = clamp(height, 1, 32); markDirty(); }
public void setFacing(Direction facing) { this.facing = facing == null ? Direction.NORTH : facing; markDirty(); }
public void setLoop(boolean loop) { this.loop = loop; markDirty(); }
public void setVolume(float volume) { this.volume = Math.max(0F, Math.min(1F, volume)); markDirty(); }
public void setMuted(boolean muted) { this.muted = muted; markDirty(); }
public void setAutoplay(boolean autoplay) { this.autoplay = autoplay; markDirty(); }
public void setUrl(String url) { this.url = url == null ? "" : url; setChanged(); }
public void setWidth(int width) { this.width = clamp(width, 1, 32); setChanged(); }
public void setHeight(int height) { this.height = clamp(height, 1, 32); setChanged(); }
public void setFacing(Direction facing) { this.facing = facing == null ? Direction.NORTH : facing; setChanged(); }
public void setLoop(boolean loop) { this.loop = loop; setChanged(); }
public void setVolume(float volume) { this.volume = Math.max(0F, Math.min(1F, volume)); setChanged(); }
public void setMuted(boolean muted) { this.muted = muted; setChanged(); }
public void setAutoplay(boolean autoplay) { this.autoplay = autoplay; setChanged(); }
/** Apply server-validated config from an NBT (used by network handler). */
public void applyFromNbt(NbtCompound nbt) {
public void applyFromNbt(CompoundTag nbt) {
fromNbt(nbt);
markDirty();
setChanged();
}
/** Wire-format NBT used by SaveConfig/SyncAnchor payloads. */
public NbtCompound toNbt() {
NbtCompound nbt = new NbtCompound();
public CompoundTag toNbt() {
CompoundTag nbt = new CompoundTag();
nbt.putString("url", url);
nbt.putInt("width", width);
nbt.putInt("height", height);
nbt.putString("facing", facing.asString());
nbt.putString("facing", facing.getSerializedName());
nbt.putBoolean("loop", loop);
nbt.putFloat("volume", volume);
nbt.putBoolean("muted", muted);
@@ -65,43 +66,51 @@ public class VideoAnchorBlockEntity extends BlockEntity {
return nbt;
}
public void fromNbt(NbtCompound nbt) {
this.url = clampUrl(nbt.getString("url", ""));
this.width = clamp(nbt.getInt("width", 1), 1, 32);
this.height = clamp(nbt.getInt("height", 1), 1, 32);
Direction d = Direction.byId(nbt.getString("facing", "north"));
public void fromNbt(CompoundTag nbt) {
this.url = clampUrl(nbt.getStringOr("url", ""));
this.width = clamp(nbt.getIntOr("width", 1), 1, 32);
this.height = clamp(nbt.getIntOr("height", 1), 1, 32);
Direction d = directionFromName(nbt.getStringOr("facing", "north"));
this.facing = d == null ? Direction.NORTH : d;
this.loop = nbt.getBoolean("loop", true);
this.volume = Math.max(0F, Math.min(1F, nbt.getFloat("volume", 0.5F)));
this.muted = nbt.getBoolean("muted", false);
this.autoplay = nbt.getBoolean("autoplay", true);
this.loop = nbt.getBooleanOr("loop", true);
this.volume = Math.max(0F, Math.min(1F, nbt.getFloatOr("volume", 0.5F)));
this.muted = nbt.getBooleanOr("muted", false);
this.autoplay = nbt.getBooleanOr("autoplay", true);
}
@Override
protected void writeData(WriteView view) {
super.writeData(view);
view.putString("url", url);
view.putInt("width", width);
view.putInt("height", height);
view.putString("facing", facing.asString());
view.putBoolean("loop", loop);
view.putFloat("volume", volume);
view.putBoolean("muted", muted);
view.putBoolean("autoplay", autoplay);
protected void saveAdditional(ValueOutput out) {
super.saveAdditional(out);
out.putString("url", url);
out.putInt("width", width);
out.putInt("height", height);
out.putString("facing", facing.getSerializedName());
out.putBoolean("loop", loop);
out.putFloat("volume", volume);
out.putBoolean("muted", muted);
out.putBoolean("autoplay", autoplay);
}
@Override
protected void readData(ReadView view) {
super.readData(view);
this.url = clampUrl(view.getString("url", ""));
this.width = clamp(view.getInt("width", 1), 1, 32);
this.height = clamp(view.getInt("height", 1), 1, 32);
Direction d = Direction.byId(view.getString("facing", "north"));
protected void loadAdditional(ValueInput in) {
super.loadAdditional(in);
this.url = clampUrl(in.getStringOr("url", ""));
this.width = clamp(in.getIntOr("width", 1), 1, 32);
this.height = clamp(in.getIntOr("height", 1), 1, 32);
Direction d = directionFromName(in.getStringOr("facing", "north"));
this.facing = d == null ? Direction.NORTH : d;
this.loop = view.getBoolean("loop", true);
this.volume = Math.max(0F, Math.min(1F, view.getFloat("volume", 0.5F)));
this.muted = view.getBoolean("muted", false);
this.autoplay = view.getBoolean("autoplay", true);
this.loop = in.getBooleanOr("loop", true);
this.volume = Math.max(0F, Math.min(1F, in.getFloatOr("volume", 0.5F)));
this.muted = in.getBooleanOr("muted", false);
this.autoplay = in.getBooleanOr("autoplay", true);
}
private static Direction directionFromName(String name) {
if (name == null) return null;
for (Direction d : Direction.values()) {
if (d.getSerializedName().equalsIgnoreCase(name)) return d;
}
return null;
}
private static int clamp(int v, int lo, int hi) {

View File

@@ -5,33 +5,32 @@ import com.ejclaw.videoplayer.net.SaveConfigPayload;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.CheckboxWidget;
import net.minecraft.client.gui.widget.SliderWidget;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.text.Text;
import net.minecraft.util.math.BlockPos;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.AbstractSliderButton;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Checkbox;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
/** SPEC §4.3 — anchor config GUI. Opened by S2C {@code OpenScreenPayload}. */
@Environment(EnvType.CLIENT)
public class VideoConfigScreen extends Screen {
private final BlockPos pos;
private final NbtCompound initial;
private final CompoundTag initial;
private TextFieldWidget urlField;
private TextFieldWidget widthField;
private TextFieldWidget heightField;
private CheckboxWidget loopBox;
private CheckboxWidget muteBox;
private CheckboxWidget autoplayBox;
private EditBox urlField;
private EditBox widthField;
private EditBox heightField;
private Checkbox loopBox;
private Checkbox muteBox;
private Checkbox autoplayBox;
private VolumeSlider volumeSlider;
public VideoConfigScreen(BlockPos pos, NbtCompound data) {
super(Text.literal("Video Anchor"));
public VideoConfigScreen(BlockPos pos, CompoundTag data) {
super(Component.literal("Video Anchor"));
this.pos = pos;
this.initial = data;
}
@@ -41,81 +40,73 @@ public class VideoConfigScreen extends Screen {
int cx = this.width / 2;
int y = 40;
urlField = new TextFieldWidget(this.textRenderer, cx - 150, y, 300, 20, Text.literal("URL"));
urlField = new EditBox(this.font, cx - 150, y, 300, 20, Component.literal("URL"));
urlField.setMaxLength(256);
urlField.setText(initial.getString("url", ""));
addDrawableChild(urlField);
urlField.setValue(initial.getStringOr("url", ""));
addRenderableWidget(urlField);
y += 30;
widthField = new TextFieldWidget(this.textRenderer, cx - 150, y, 60, 20, Text.literal("W"));
widthField = new EditBox(this.font, cx - 150, y, 60, 20, Component.literal("W"));
widthField.setMaxLength(2);
widthField.setText(Integer.toString(initial.getInt("width", 1)));
widthField.setTextPredicate(VideoConfigScreen::isDigits);
addDrawableChild(widthField);
widthField.setValue(Integer.toString(initial.getIntOr("width", 1)));
addRenderableWidget(widthField);
heightField = new TextFieldWidget(this.textRenderer, cx - 80, y, 60, 20, Text.literal("H"));
heightField = new EditBox(this.font, cx - 80, y, 60, 20, Component.literal("H"));
heightField.setMaxLength(2);
heightField.setText(Integer.toString(initial.getInt("height", 1)));
heightField.setTextPredicate(VideoConfigScreen::isDigits);
addDrawableChild(heightField);
heightField.setValue(Integer.toString(initial.getIntOr("height", 1)));
addRenderableWidget(heightField);
volumeSlider = new VolumeSlider(cx - 10, y, 160, 20,
Math.max(0F, Math.min(1F, initial.getFloat("volume", 0.5F))));
addDrawableChild(volumeSlider);
Math.max(0F, Math.min(1F, initial.getFloatOr("volume", 0.5F))));
addRenderableWidget(volumeSlider);
y += 30;
loopBox = CheckboxWidget.builder(Text.literal("Loop"), this.textRenderer)
.pos(cx - 150, y).checked(initial.getBoolean("loop", true)).build();
addDrawableChild(loopBox);
loopBox = Checkbox.builder(Component.literal("Loop"), this.font)
.pos(cx - 150, y).selected(initial.getBooleanOr("loop", true)).build();
addRenderableWidget(loopBox);
muteBox = CheckboxWidget.builder(Text.literal("Mute"), this.textRenderer)
.pos(cx - 60, y).checked(initial.getBoolean("muted", false)).build();
addDrawableChild(muteBox);
muteBox = Checkbox.builder(Component.literal("Mute"), this.font)
.pos(cx - 60, y).selected(initial.getBooleanOr("muted", false)).build();
addRenderableWidget(muteBox);
autoplayBox = CheckboxWidget.builder(Text.literal("Autoplay"), this.textRenderer)
.pos(cx + 30, y).checked(initial.getBoolean("autoplay", true)).build();
addDrawableChild(autoplayBox);
autoplayBox = Checkbox.builder(Component.literal("Autoplay"), this.font)
.pos(cx + 30, y).selected(initial.getBooleanOr("autoplay", true)).build();
addRenderableWidget(autoplayBox);
y += 36;
addDrawableChild(ButtonWidget.builder(Text.literal("Save"), b -> save())
.dimensions(cx - 150, y, 90, 20).build());
addDrawableChild(ButtonWidget.builder(Text.literal("Cancel"), b -> close())
.dimensions(cx - 45, y, 90, 20).build());
addDrawableChild(ButtonWidget.builder(Text.literal("Delete"), b -> delete())
.dimensions(cx + 60, y, 90, 20).build());
addRenderableWidget(Button.builder(Component.literal("Save"), b -> save())
.bounds(cx - 150, y, 90, 20).build());
addRenderableWidget(Button.builder(Component.literal("Cancel"), b -> onClose())
.bounds(cx - 45, y, 90, 20).build());
addRenderableWidget(Button.builder(Component.literal("Delete"), b -> delete())
.bounds(cx + 60, y, 90, 20).build());
}
private void save() {
NbtCompound out = new NbtCompound();
out.putString("url", urlField.getText());
out.putInt("width", parseInt(widthField.getText(), 1));
out.putInt("height", parseInt(heightField.getText(), 1));
out.putString("facing", initial.getString("facing", "north"));
out.putBoolean("loop", loopBox.isChecked());
CompoundTag out = new CompoundTag();
out.putString("url", urlField.getValue());
out.putInt("width", parseInt(widthField.getValue(), 1));
out.putInt("height", parseInt(heightField.getValue(), 1));
out.putString("facing", initial.getStringOr("facing", "north"));
out.putBoolean("loop", loopBox.selected());
out.putFloat("volume", volumeSlider.getVolume());
out.putBoolean("muted", muteBox.isChecked());
out.putBoolean("autoplay", autoplayBox.isChecked());
out.putBoolean("muted", muteBox.selected());
out.putBoolean("autoplay", autoplayBox.selected());
ClientPlayNetworking.send(new SaveConfigPayload(pos, out));
close();
onClose();
}
private void delete() {
ClientPlayNetworking.send(new DeleteAnchorPayload(pos));
close();
onClose();
}
@Override
public void render(DrawContext ctx, int mouseX, int mouseY, float delta) {
super.render(ctx, mouseX, mouseY, delta);
ctx.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 16, 0xFFFFFF);
}
public boolean isPauseScreen() { return false; }
@Override
public boolean shouldPause() { return false; }
@Override
public void close() {
MinecraftClient mc = this.client != null ? this.client : MinecraftClient.getInstance();
public void onClose() {
Minecraft mc = this.minecraft != null ? this.minecraft : Minecraft.getInstance();
if (mc != null) mc.setScreen(null);
}
@@ -123,15 +114,9 @@ public class VideoConfigScreen extends Screen {
try { return Integer.parseInt(s); } catch (Exception e) { return dflt; }
}
private static boolean isDigits(String s) {
if (s.isEmpty()) return true;
for (int i = 0; i < s.length(); i++) if (!Character.isDigit(s.charAt(i))) return false;
return true;
}
private static final class VolumeSlider extends SliderWidget {
private static final class VolumeSlider extends AbstractSliderButton {
VolumeSlider(int x, int y, int w, int h, float initial) {
super(x, y, w, h, Text.literal("Volume: " + pct(initial)), initial);
super(x, y, w, h, Component.literal("Volume: " + pct(initial)), initial);
updateMessage();
}
@@ -139,7 +124,7 @@ public class VideoConfigScreen extends Screen {
@Override
protected void updateMessage() {
setMessage(Text.literal("Volume: " + pct((float) this.value)));
setMessage(Component.literal("Volume: " + pct((float) this.value)));
}
@Override

View File

@@ -8,7 +8,7 @@ import com.ejclaw.videoplayer.net.SyncAnchorPayload;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.Minecraft;
/** Client-side S2C receivers for OpenScreen and SyncAnchor. */
@Environment(EnvType.CLIENT)
@@ -16,16 +16,16 @@ public final class ClientNetworking {
private ClientNetworking() {}
public static void register() {
ClientPlayNetworking.registerGlobalReceiver(OpenScreenPayload.ID, (payload, context) -> {
MinecraftClient mc = context.client();
ClientPlayNetworking.registerGlobalReceiver(OpenScreenPayload.TYPE, (payload, context) -> {
Minecraft mc = context.client();
mc.execute(() -> mc.setScreen(new VideoConfigScreen(payload.pos(), payload.data())));
});
ClientPlayNetworking.registerGlobalReceiver(SyncAnchorPayload.ID, (payload, context) -> {
MinecraftClient mc = context.client();
ClientPlayNetworking.registerGlobalReceiver(SyncAnchorPayload.TYPE, (payload, context) -> {
Minecraft mc = context.client();
mc.execute(() -> {
if (mc.world == null) return;
if (mc.world.getBlockEntity(payload.pos()) instanceof VideoAnchorBlockEntity be) {
if (mc.level == null) return;
if (mc.level.getBlockEntity(payload.pos()) instanceof VideoAnchorBlockEntity be) {
be.applyFromNbt(payload.data());
VideoPlayback.onConfigChanged(be);
}

View File

@@ -3,147 +3,73 @@ package com.ejclaw.videoplayer.client.playback;
import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.texture.NativeImage;
import net.minecraft.client.texture.NativeImageBackedTexture;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.Identifier;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* SPEC §5 — per-anchor playback registry. Maps {@link BlockPos} → ({@link VideoBackend} + dynamic
* Identifier of an {@link NativeImageBackedTexture}). The renderer reads the texture id and binds
* it to the quad; this class drives the frame pump every client tick.
* SPEC §5 — per-anchor playback registry.
*
* <p><b>Status (26.1.2 port):</b> the rendering pipeline was rewritten upstream
* (render-state separation, removal of {@code DrawContext}/{@code GuiGraphics}).
* Until the new {@code BlockEntityRenderer<T, S>} pipeline is wired through with a
* dynamic texture surface, this class operates as a stub: it tracks active anchors
* but does not decode frames or upload textures. Audio backends are also paused
* pending a Java 25 dependency audit (Watermedia / JavaCV).
*/
@Environment(EnvType.CLIENT)
public final class VideoPlayback {
private VideoPlayback() {}
private static final Map<BlockPos, Entry> ENTRIES = new HashMap<>();
private static final Map<BlockPos, String> ENTRIES = new HashMap<>();
public static Identifier getOrStart(VideoAnchorBlockEntity be) {
BlockPos pos = be.getPos();
Entry e = ENTRIES.get(pos);
if (e != null && e.url.equals(be.getUrl())) {
return e.id;
}
if (e != null) {
stop(pos);
}
if (be.getUrl().isEmpty() || !be.isAutoplay()) {
BlockPos pos = be.getBlockPos();
String url = be.getUrl();
if (url == null || url.isEmpty() || !be.isAutoplay()) {
ENTRIES.remove(pos);
return null;
}
VideoBackend backend = WatermediaProbe.isAvailable() ? new WatermediaBackend() : new JavaCvBackend();
backend.play(be.getUrl(), be.isLoop());
backend.setVolume(be.isMuted() ? 0F : be.getVolume());
Entry created = new Entry(be.getUrl(), backend);
ENTRIES.put(pos, created);
return created.id;
ENTRIES.put(pos, url);
return null; // no dynamic texture yet in 26.1.2 port
}
public static Identifier currentTexture(BlockPos pos) {
Entry e = ENTRIES.get(pos);
return e == null ? null : e.id;
return null;
}
public static void stop(BlockPos pos) {
Entry e = ENTRIES.remove(pos);
if (e != null) e.close();
ENTRIES.remove(pos);
}
public static void onConfigChanged(VideoAnchorBlockEntity be) {
Entry e = ENTRIES.get(be.getPos());
if (e == null) return;
if (!e.url.equals(be.getUrl())) {
stop(be.getPos());
return;
if (be == null) return;
BlockPos pos = be.getBlockPos();
String url = be.getUrl();
if (url == null || url.isEmpty()) {
ENTRIES.remove(pos);
} else {
ENTRIES.put(pos, url);
}
e.backend.setVolume(be.isMuted() ? 0F : be.getVolume());
}
/** Called every client tick to upload new frames into the GPU texture. */
public static void tick() {
if (MinecraftClient.getInstance() == null) return;
Iterator<Map.Entry<BlockPos, Entry>> it = ENTRIES.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<BlockPos, Entry> me = it.next();
Entry e = me.getValue();
if (!e.backend.isReady()) continue;
ByteBuffer buf = e.backend.pollFrame();
if (buf == null) continue;
try {
e.upload(buf);
} catch (Throwable t) {
// texture upload errors shouldn't kill the client; drop this entry
e.close();
it.remove();
}
}
// stub: no frames to pump
}
public static java.util.Set<BlockPos> activePositions() {
return new java.util.HashSet<>(ENTRIES.keySet());
public static Set<BlockPos> activePositions() {
return new HashSet<>(ENTRIES.keySet());
}
public static void setGain(BlockPos pos, float gain) {
Entry e = ENTRIES.get(pos);
if (e != null) e.backend.setVolume(gain);
// stub
}
public static void stopAll() {
for (Entry e : ENTRIES.values()) e.close();
ENTRIES.clear();
}
private static final class Entry {
final String url;
final VideoBackend backend;
final Identifier id;
NativeImageBackedTexture texture;
int texW = 0, texH = 0;
Entry(String url, VideoBackend backend) {
this.url = url;
this.backend = backend;
this.id = Identifier.of("video_player", "dynamic/" + Integer.toHexString(System.identityHashCode(this)));
}
void upload(ByteBuffer rgba) {
int w = backend.videoWidth();
int h = backend.videoHeight();
if (w <= 0 || h <= 0) return;
if (texture == null || w != texW || h != texH) {
if (texture != null) texture.close();
NativeImage img = new NativeImage(NativeImage.Format.RGBA, w, h, false);
texture = new NativeImageBackedTexture(() -> "video_player_dyn", img);
MinecraftClient.getInstance().getTextureManager().registerTexture(id, texture);
texW = w; texH = h;
}
NativeImage img = texture.getImage();
if (img == null) return;
// copy buf → image pixels (RGBA bytes, native order)
int pixels = w * h;
for (int i = 0; i < pixels; i++) {
int r = rgba.get() & 0xFF;
int g = rgba.get() & 0xFF;
int b = rgba.get() & 0xFF;
int a = rgba.get() & 0xFF;
int argb = (a << 24) | (r << 16) | (g << 8) | b;
img.setColorArgb(i % w, i / w, argb);
}
texture.upload();
}
void close() {
backend.close();
if (texture != null) {
texture.close();
texture = null;
}
}
}
}

View File

@@ -1,91 +1,70 @@
package com.ejclaw.videoplayer.client.render;
import com.ejclaw.videoplayer.VideoPlayerMod;
import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import com.ejclaw.videoplayer.client.playback.VideoPlayback;
import com.mojang.blaze3d.vertex.PoseStack;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.block.entity.BlockEntityRenderer;
import net.minecraft.client.render.block.entity.BlockEntityRendererFactory;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.RotationAxis;
import net.minecraft.util.math.Vec3d;
import org.joml.Matrix4f;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.world.phys.Vec3;
/** SPEC §5.2 — draws a width × height quad in front of the anchor, oriented by facing. */
/**
* SPEC §5.2 — anchor renderer.
*
* <p><b>Status (26.1.2 port):</b> the rendering pipeline was rewritten upstream to use
* a render-state separation pattern ({@code createRenderState} / {@code extractRenderState}
* / {@code submit} via {@link SubmitNodeCollector}). This stub implements the new interface
* but draws nothing — the anchor block itself is the only visible element. Frame upload
* and quad submission will be re-introduced once the dynamic texture surface is wired up
* (see {@link VideoPlayback}).
*/
@Environment(EnvType.CLIENT)
public class VideoAnchorRenderer implements BlockEntityRenderer<VideoAnchorBlockEntity> {
public class VideoAnchorRenderer implements BlockEntityRenderer<VideoAnchorBlockEntity, VideoAnchorRenderer.State> {
/** Placeholder texture used until a frame is uploaded. */
private static final Identifier PLACEHOLDER =
Identifier.of(VideoPlayerMod.MOD_ID, "block/video_anchor");
public VideoAnchorRenderer(BlockEntityRendererFactory.Context ctx) {
// no-op — context kept for future symbol/lookup needs
public VideoAnchorRenderer(BlockEntityRendererProvider.Context ctx) {
// no-op — context retained for future texture/font lookups
}
@Override
public void render(VideoAnchorBlockEntity be, float tickDelta, MatrixStack matrices,
VertexConsumerProvider vertices, int light, int overlay, Vec3d cam) {
Identifier tex = VideoPlayback.currentTexture(be.getPos());
Identifier bound = tex != null ? tex : PLACEHOLDER;
public State createRenderState() {
return new State();
}
float w = be.getWidth();
float h = be.getHeight();
Direction facing = be.getFacing();
matrices.push();
// Center the quad above the anchor's top face, then rotate to facing.
matrices.translate(0.5, 1.01, 0.5);
float rot = facing.getAxis().isHorizontal()
? Direction.getHorizontalDegreesOrThrow(facing)
: 0F;
matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(-rot));
matrices.translate(-w / 2.0F, 0, 0);
VertexConsumer vc = vertices.getBuffer(RenderLayer.getEntityCutoutNoCull(bound));
Matrix4f mat = matrices.peek().getPositionMatrix();
// Two-sided quad in the XY plane at z=0
emit(vc, mat, 0, 0, 0, 0, 1, light, overlay);
emit(vc, mat, w, 0, 0, 1, 1, light, overlay);
emit(vc, mat, w, h, 0, 1, 0, light, overlay);
emit(vc, mat, 0, h, 0, 0, 0, light, overlay);
// back face (so the anchor is visible from behind too)
emit(vc, mat, 0, h, 0, 0, 0, light, overlay);
emit(vc, mat, w, h, 0, 1, 0, light, overlay);
emit(vc, mat, w, 0, 0, 1, 1, light, overlay);
emit(vc, mat, 0, 0, 0, 0, 1, light, overlay);
matrices.pop();
// Trigger playback startup lazily, on first frame the camera sees the BE.
@Override
public void extractRenderState(VideoAnchorBlockEntity be, State state, float partialTick,
Vec3 cameraPos, ModelFeatureRenderer.CrumblingOverlay crumbling) {
BlockEntityRenderState.extractBase(be, state, crumbling);
state.url = be.getUrl();
state.width = be.getWidth();
state.height = be.getHeight();
// kick playback bookkeeping so it tracks visible anchors
VideoPlayback.getOrStart(be);
}
private static void emit(VertexConsumer vc, Matrix4f mat,
float x, float y, float z, float u, float v,
int light, int overlay) {
vc.vertex(mat, x, y, z)
.color(255, 255, 255, 255)
.texture(u, v)
.overlay(overlay)
.light(light)
.normal(0F, 0F, 1F);
@Override
public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) {
// stub: no quad is drawn until the new dynamic texture pipeline is wired
}
@Override
public boolean rendersOutsideBoundingBox() {
public boolean shouldRenderOffScreen() {
return true;
}
@Override
public int getRenderDistance() {
public int getViewDistance() {
return 128;
}
/** Per-frame render data extracted from the BE. Just metadata for the stub. */
public static final class State extends BlockEntityRenderState {
public String url = "";
public int width = 1;
public int height = 1;
}
}

View File

@@ -3,42 +3,44 @@ package com.ejclaw.videoplayer.command;
import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.block.Blocks;
import net.minecraft.command.argument.BlockPosArgumentType;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Text;
import net.minecraft.util.math.BlockPos;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.coordinates.BlockPosArgument;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.permissions.Permissions;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
/** SPEC §4.5.1 — {@code /videoDelete <pos>} */
public final class VideoDeleteCommand {
private VideoDeleteCommand() {}
public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
dispatcher.register(register("videoDelete"));
dispatcher.register(register("videodelete"));
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(build("videoDelete"));
dispatcher.register(build("videodelete"));
}
private static com.mojang.brigadier.builder.LiteralArgumentBuilder<ServerCommandSource>
register(String name) {
return CommandManager.literal(name)
.requires(s -> s.hasPermissionLevel(2))
.then(CommandManager.argument("pos", BlockPosArgumentType.blockPos())
private static com.mojang.brigadier.builder.LiteralArgumentBuilder<CommandSourceStack>
build(String name) {
return Commands.literal(name)
.requires(s -> s.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER))
.then(Commands.argument("pos", BlockPosArgument.blockPos())
.executes(VideoDeleteCommand::run));
}
private static int run(com.mojang.brigadier.context.CommandContext<ServerCommandSource> ctx)
private static int run(com.mojang.brigadier.context.CommandContext<CommandSourceStack> ctx)
throws CommandSyntaxException {
ServerCommandSource src = ctx.getSource();
ServerWorld world = src.getWorld();
BlockPos pos = BlockPosArgumentType.getLoadedBlockPos(ctx, "pos");
if (!(world.getBlockEntity(pos) instanceof VideoAnchorBlockEntity)) {
src.sendError(Text.literal("no anchor at that position"));
CommandSourceStack src = ctx.getSource();
ServerLevel level = src.getLevel();
BlockPos pos = BlockPosArgument.getLoadedBlockPos(ctx, "pos");
if (!(level.getBlockEntity(pos) instanceof VideoAnchorBlockEntity)) {
src.sendFailure(Component.literal("no anchor at that position"));
return 0;
}
world.setBlockState(pos, Blocks.AIR.getDefaultState());
src.sendFeedback(() -> Text.literal("anchor deleted at " + pos.toShortString()), true);
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
src.sendSuccess(() -> Component.literal("anchor deleted at " + pos.toShortString()), true);
return 1;
}
}

View File

@@ -7,55 +7,56 @@ import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.fabricmc.fabric.api.networking.v1.PlayerLookup;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.command.argument.BlockPosArgumentType;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Text;
import net.minecraft.util.math.BlockPos;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.coordinates.BlockPosArgument;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.permissions.Permissions;
/** SPEC §4.5.1 — {@code /videoMute <pos> <on|off>} */
public final class VideoMuteCommand {
private VideoMuteCommand() {}
public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
dispatcher.register(register("videoMute"));
dispatcher.register(register("videomute"));
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(build("videoMute"));
dispatcher.register(build("videomute"));
}
private static com.mojang.brigadier.builder.LiteralArgumentBuilder<ServerCommandSource>
register(String name) {
return CommandManager.literal(name)
.requires(s -> s.hasPermissionLevel(2))
.then(CommandManager.argument("pos", BlockPosArgumentType.blockPos())
.then(CommandManager.argument("state", StringArgumentType.word())
private static com.mojang.brigadier.builder.LiteralArgumentBuilder<CommandSourceStack>
build(String name) {
return Commands.literal(name)
.requires(s -> s.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER))
.then(Commands.argument("pos", BlockPosArgument.blockPos())
.then(Commands.argument("state", StringArgumentType.word())
.executes(VideoMuteCommand::run)));
}
private static int run(com.mojang.brigadier.context.CommandContext<ServerCommandSource> ctx)
private static int run(com.mojang.brigadier.context.CommandContext<CommandSourceStack> ctx)
throws CommandSyntaxException {
ServerCommandSource src = ctx.getSource();
ServerWorld world = src.getWorld();
BlockPos pos = BlockPosArgumentType.getLoadedBlockPos(ctx, "pos");
CommandSourceStack src = ctx.getSource();
ServerLevel level = src.getLevel();
BlockPos pos = BlockPosArgument.getLoadedBlockPos(ctx, "pos");
String state = StringArgumentType.getString(ctx, "state").toLowerCase();
boolean muted;
if ("on".equals(state) || "true".equals(state)) muted = true;
else if ("off".equals(state) || "false".equals(state)) muted = false;
else {
src.sendError(Text.literal("state must be on/off"));
src.sendFailure(Component.literal("state must be on/off"));
return 0;
}
if (!(world.getBlockEntity(pos) instanceof VideoAnchorBlockEntity be)) {
src.sendError(Text.literal("no anchor at that position"));
if (!(level.getBlockEntity(pos) instanceof VideoAnchorBlockEntity be)) {
src.sendFailure(Component.literal("no anchor at that position"));
return 0;
}
be.setMuted(muted);
for (ServerPlayerEntity p : PlayerLookup.tracking(world, pos)) {
for (ServerPlayer p : PlayerLookup.tracking(level, pos)) {
ServerPlayNetworking.send(p, new SyncAnchorPayload(pos, be.toNbt()));
}
final boolean mFinal = muted;
src.sendFeedback(() -> Text.literal("anchor " + (mFinal ? "muted" : "unmuted")), true);
src.sendSuccess(() -> Component.literal("anchor " + (mFinal ? "muted" : "unmuted")), true);
return 1;
}
}

View File

@@ -4,65 +4,66 @@ import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import com.ejclaw.videoplayer.net.SyncAnchorPayload;
import com.ejclaw.videoplayer.registry.VideoPlayerBlocks;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.FloatArgumentType;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.fabricmc.fabric.api.networking.v1.PlayerLookup;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.command.argument.BlockPosArgumentType;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.Text;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.coordinates.BlockPosArgument;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
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>} */
public final class VideoPlaceCommand {
private VideoPlaceCommand() {}
public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
dispatcher.register(register("videoPlace"));
dispatcher.register(register("videoplace"));
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(build("videoPlace"));
dispatcher.register(build("videoplace"));
}
private static com.mojang.brigadier.builder.LiteralArgumentBuilder<ServerCommandSource>
register(String name) {
return CommandManager.literal(name)
.requires(s -> s.hasPermissionLevel(2))
.then(CommandManager.argument("pos", BlockPosArgumentType.blockPos())
.then(CommandManager.argument("facing", StringArgumentType.word())
.then(CommandManager.argument("width", IntegerArgumentType.integer(1, 32))
.then(CommandManager.argument("height", IntegerArgumentType.integer(1, 32))
.then(CommandManager.argument("url", StringArgumentType.greedyString())
private static com.mojang.brigadier.builder.LiteralArgumentBuilder<CommandSourceStack>
build(String name) {
return Commands.literal(name)
.requires(s -> s.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER))
.then(Commands.argument("pos", BlockPosArgument.blockPos())
.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))))));
}
private static int run(com.mojang.brigadier.context.CommandContext<ServerCommandSource> ctx)
private static int run(com.mojang.brigadier.context.CommandContext<CommandSourceStack> ctx)
throws CommandSyntaxException {
ServerCommandSource src = ctx.getSource();
ServerWorld world = src.getWorld();
BlockPos pos = BlockPosArgumentType.getLoadedBlockPos(ctx, "pos");
Direction facing = Direction.byId(StringArgumentType.getString(ctx, "facing"));
CommandSourceStack src = ctx.getSource();
ServerLevel level = src.getLevel();
BlockPos pos = BlockPosArgument.getLoadedBlockPos(ctx, "pos");
Direction facing = directionFromName(StringArgumentType.getString(ctx, "facing"));
if (facing == null) {
src.sendError(Text.literal("facing must be north/south/east/west/up/down"));
src.sendFailure(Component.literal("facing must be north/south/east/west/up/down"));
return 0;
}
int width = IntegerArgumentType.getInteger(ctx, "width");
int height = IntegerArgumentType.getInteger(ctx, "height");
String url = StringArgumentType.getString(ctx, "url").trim();
if (!url.isEmpty() && !(url.startsWith("http://") || url.startsWith("https://"))) {
src.sendError(Text.literal("url must be http:// or https:// (or empty)"));
src.sendFailure(Component.literal("url must be http:// or https:// (or empty)"));
return 0;
}
if (url.length() > 256) url = url.substring(0, 256);
world.setBlockState(pos, VideoPlayerBlocks.VIDEO_ANCHOR.getDefaultState());
if (!(world.getBlockEntity(pos) instanceof VideoAnchorBlockEntity be)) {
src.sendError(Text.literal("failed to place anchor"));
level.setBlock(pos, VideoPlayerBlocks.VIDEO_ANCHOR.defaultBlockState(), Block.UPDATE_ALL);
if (!(level.getBlockEntity(pos) instanceof VideoAnchorBlockEntity be)) {
src.sendFailure(Component.literal("failed to place anchor"));
return 0;
}
be.setFacing(facing);
@@ -70,12 +71,20 @@ public final class VideoPlaceCommand {
be.setHeight(height);
be.setUrl(url);
NbtCompound nbt = be.toNbt();
for (ServerPlayerEntity p : PlayerLookup.tracking(world, pos)) {
CompoundTag nbt = be.toNbt();
for (ServerPlayer p : PlayerLookup.tracking(level, pos)) {
ServerPlayNetworking.send(p, new SyncAnchorPayload(pos, nbt));
}
final BlockPos fp = pos;
src.sendFeedback(() -> Text.literal("anchor placed at " + fp.toShortString()), true);
src.sendSuccess(() -> Component.literal("anchor placed at " + fp.toShortString()), true);
return 1;
}
private static Direction directionFromName(String name) {
if (name == null) return null;
for (Direction d : Direction.values()) {
if (d.getSerializedName().equalsIgnoreCase(name)) return d;
}
return null;
}
}

View File

@@ -2,35 +2,35 @@ package com.ejclaw.videoplayer.command;
import com.ejclaw.videoplayer.registry.VideoPlayerItems;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.item.ItemStack;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
public final class VideoStickCommand {
private VideoStickCommand() {}
public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
dispatcher.register(CommandManager.literal("videoStick")
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(Commands.literal("videoStick")
.executes(ctx -> run(ctx.getSource())));
// Lowercase alias — Brigadier is case-sensitive.
dispatcher.register(CommandManager.literal("videostick")
dispatcher.register(Commands.literal("videostick")
.executes(ctx -> run(ctx.getSource())));
}
private static int run(ServerCommandSource source) {
ServerPlayerEntity player = source.getPlayer();
private static int run(CommandSourceStack source) {
ServerPlayer player = source.getPlayer();
if (player == null) {
source.sendError(Text.literal("플레이어만 이 명령을 사용할 수 있습니다."));
source.sendFailure(Component.literal("플레이어만 이 명령을 사용할 수 있습니다."));
return 0;
}
ItemStack stack = new ItemStack(VideoPlayerItems.VIDEO_STICK);
boolean inserted = player.getInventory().insertStack(stack);
boolean inserted = player.getInventory().add(stack);
if (!inserted || !stack.isEmpty()) {
player.dropItem(stack, false);
player.drop(stack, false, false);
}
source.sendFeedback(() -> Text.literal("비디오 스틱을 지급했습니다."), false);
source.sendSuccess(() -> Component.literal("비디오 스틱을 지급했습니다."), false);
return 1;
}
}

View File

@@ -4,57 +4,55 @@ import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import com.ejclaw.videoplayer.net.OpenScreenPayload;
import com.ejclaw.videoplayer.registry.VideoPlayerBlocks;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemUsageContext;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.ActionResult;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.World;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
/** SPEC §4.2 — right-click empty face → place anchor + open GUI. Right-click existing anchor → edit. */
/** Right-click empty face → place anchor + open GUI. Right-click existing anchor → edit. */
public class VideoStickItem extends Item {
public VideoStickItem(Settings settings) {
super(settings);
public VideoStickItem(Properties properties) {
super(properties);
}
@Override
public ActionResult useOnBlock(ItemUsageContext ctx) {
World world = ctx.getWorld();
if (world.isClient) {
// server is authoritative; client just consumes the gesture
return ActionResult.SUCCESS;
public InteractionResult useOn(UseOnContext ctx) {
Level level = ctx.getLevel();
if (level.isClientSide()) {
return InteractionResult.SUCCESS;
}
if (!(level instanceof ServerLevel sl)) return InteractionResult.PASS;
Player player = ctx.getPlayer();
if (!(player instanceof ServerPlayer sp)) return InteractionResult.PASS;
ServerWorld sw = (ServerWorld) world;
PlayerEntity player = ctx.getPlayer();
if (!(player instanceof ServerPlayerEntity sp)) return ActionResult.PASS;
BlockPos hit = ctx.getBlockPos();
BlockPos hit = ctx.getClickedPos();
// Existing anchor → edit
if (sw.getBlockEntity(hit) instanceof VideoAnchorBlockEntity existing) {
if (sl.getBlockEntity(hit) instanceof VideoAnchorBlockEntity existing) {
ServerPlayNetworking.send(sp, new OpenScreenPayload(hit, existing.toNbt()));
return ActionResult.SUCCESS;
return InteractionResult.SUCCESS;
}
// Empty face → place anchor on top of the clicked face
Direction side = ctx.getSide();
BlockPos placeAt = hit.offset(side);
BlockState there = sw.getBlockState(placeAt);
if (!there.isReplaceable()) return ActionResult.PASS;
Direction side = ctx.getClickedFace();
BlockPos placeAt = hit.relative(side);
BlockState there = sl.getBlockState(placeAt);
if (!there.canBeReplaced()) return InteractionResult.PASS;
Block anchor = VideoPlayerBlocks.VIDEO_ANCHOR;
sw.setBlockState(placeAt, anchor.getDefaultState());
sl.setBlock(placeAt, anchor.defaultBlockState(), Block.UPDATE_ALL);
if (sw.getBlockEntity(placeAt) instanceof VideoAnchorBlockEntity be) {
be.setFacing(ctx.getHorizontalPlayerFacing().getOpposite());
if (sl.getBlockEntity(placeAt) instanceof VideoAnchorBlockEntity be) {
be.setFacing(ctx.getHorizontalDirection().getOpposite());
ServerPlayNetworking.send(sp, new OpenScreenPayload(placeAt, be.toNbt()));
}
return ActionResult.SUCCESS;
return InteractionResult.SUCCESS;
}
}

View File

@@ -1,24 +1,24 @@
package com.ejclaw.videoplayer.net;
import com.ejclaw.videoplayer.VideoPlayerMod;
import net.minecraft.network.RegistryByteBuf;
import net.minecraft.network.codec.PacketCodec;
import net.minecraft.network.packet.CustomPayload;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
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.Identifier;
/** C2S — delete an anchor from the VideoConfigScreen. */
public record DeleteAnchorPayload(BlockPos pos) implements CustomPayload {
public static final CustomPayload.Id<DeleteAnchorPayload> ID =
new CustomPayload.Id<>(Identifier.of(VideoPlayerMod.MOD_ID, "delete_anchor"));
public record DeleteAnchorPayload(BlockPos pos) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<DeleteAnchorPayload> TYPE =
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, "delete_anchor"));
public static final PacketCodec<RegistryByteBuf, DeleteAnchorPayload> CODEC = PacketCodec.tuple(
BlockPos.PACKET_CODEC, DeleteAnchorPayload::pos,
public static final StreamCodec<RegistryFriendlyByteBuf, DeleteAnchorPayload> CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, DeleteAnchorPayload::pos,
DeleteAnchorPayload::new
);
@Override
public Id<? extends CustomPayload> getId() {
return ID;
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}

View File

@@ -1,27 +1,27 @@
package com.ejclaw.videoplayer.net;
import com.ejclaw.videoplayer.VideoPlayerMod;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.RegistryByteBuf;
import net.minecraft.network.codec.PacketCodec;
import net.minecraft.network.codec.PacketCodecs;
import net.minecraft.network.packet.CustomPayload;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
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.Identifier;
/** S2C — open the VideoConfigScreen for an anchor on the client. */
public record OpenScreenPayload(BlockPos pos, NbtCompound data) implements CustomPayload {
public static final CustomPayload.Id<OpenScreenPayload> ID =
new CustomPayload.Id<>(Identifier.of(VideoPlayerMod.MOD_ID, "open_screen"));
public record OpenScreenPayload(BlockPos pos, CompoundTag data) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<OpenScreenPayload> TYPE =
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, "open_screen"));
public static final PacketCodec<RegistryByteBuf, OpenScreenPayload> CODEC = PacketCodec.tuple(
BlockPos.PACKET_CODEC, OpenScreenPayload::pos,
PacketCodecs.NBT_COMPOUND, OpenScreenPayload::data,
public static final StreamCodec<RegistryFriendlyByteBuf, OpenScreenPayload> CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, OpenScreenPayload::pos,
ByteBufCodecs.COMPOUND_TAG, OpenScreenPayload::data,
OpenScreenPayload::new
);
@Override
public Id<? extends CustomPayload> getId() {
return ID;
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}

View File

@@ -1,27 +1,27 @@
package com.ejclaw.videoplayer.net;
import com.ejclaw.videoplayer.VideoPlayerMod;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.RegistryByteBuf;
import net.minecraft.network.codec.PacketCodec;
import net.minecraft.network.codec.PacketCodecs;
import net.minecraft.network.packet.CustomPayload;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
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.Identifier;
/** C2S — save edited config from VideoConfigScreen back to the server. */
public record SaveConfigPayload(BlockPos pos, NbtCompound data) implements CustomPayload {
public static final CustomPayload.Id<SaveConfigPayload> ID =
new CustomPayload.Id<>(Identifier.of(VideoPlayerMod.MOD_ID, "save_config"));
public record SaveConfigPayload(BlockPos pos, CompoundTag data) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<SaveConfigPayload> TYPE =
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, "save_config"));
public static final PacketCodec<RegistryByteBuf, SaveConfigPayload> CODEC = PacketCodec.tuple(
BlockPos.PACKET_CODEC, SaveConfigPayload::pos,
PacketCodecs.NBT_COMPOUND, SaveConfigPayload::data,
public static final StreamCodec<RegistryFriendlyByteBuf, SaveConfigPayload> CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, SaveConfigPayload::pos,
ByteBufCodecs.COMPOUND_TAG, SaveConfigPayload::data,
SaveConfigPayload::new
);
@Override
public Id<? extends CustomPayload> getId() {
return ID;
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}

View File

@@ -1,27 +1,27 @@
package com.ejclaw.videoplayer.net;
import com.ejclaw.videoplayer.VideoPlayerMod;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.RegistryByteBuf;
import net.minecraft.network.codec.PacketCodec;
import net.minecraft.network.codec.PacketCodecs;
import net.minecraft.network.packet.CustomPayload;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
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.Identifier;
/** S2C — push current anchor state (URL/dims/loop/volume/muted/autoplay) to clients in range. */
public record SyncAnchorPayload(BlockPos pos, NbtCompound data) implements CustomPayload {
public static final CustomPayload.Id<SyncAnchorPayload> ID =
new CustomPayload.Id<>(Identifier.of(VideoPlayerMod.MOD_ID, "sync_anchor"));
/** S2C — push current anchor state to clients tracking the chunk. */
public record SyncAnchorPayload(BlockPos pos, CompoundTag data) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<SyncAnchorPayload> TYPE =
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, "sync_anchor"));
public static final PacketCodec<RegistryByteBuf, SyncAnchorPayload> CODEC = PacketCodec.tuple(
BlockPos.PACKET_CODEC, SyncAnchorPayload::pos,
PacketCodecs.NBT_COMPOUND, SyncAnchorPayload::data,
public static final StreamCodec<RegistryFriendlyByteBuf, SyncAnchorPayload> CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, SyncAnchorPayload::pos,
ByteBufCodecs.COMPOUND_TAG, SyncAnchorPayload::data,
SyncAnchorPayload::new
);
@Override
public Id<? extends CustomPayload> getId() {
return ID;
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}

View File

@@ -5,11 +5,13 @@ import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.fabricmc.fabric.api.networking.v1.PlayerLookup;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.block.Blocks;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.permissions.Permissions;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
/**
* Registers all four payload types and the two C2S server-side receivers.
@@ -20,72 +22,73 @@ public final class VideoPlayerNetwork {
public static void registerPayloadTypes() {
// S2C
PayloadTypeRegistry.playS2C().register(OpenScreenPayload.ID, OpenScreenPayload.CODEC);
PayloadTypeRegistry.playS2C().register(SyncAnchorPayload.ID, SyncAnchorPayload.CODEC);
PayloadTypeRegistry.clientboundPlay().register(OpenScreenPayload.TYPE, OpenScreenPayload.CODEC);
PayloadTypeRegistry.clientboundPlay().register(SyncAnchorPayload.TYPE, SyncAnchorPayload.CODEC);
// C2S
PayloadTypeRegistry.playC2S().register(SaveConfigPayload.ID, SaveConfigPayload.CODEC);
PayloadTypeRegistry.playC2S().register(DeleteAnchorPayload.ID, DeleteAnchorPayload.CODEC);
PayloadTypeRegistry.serverboundPlay().register(SaveConfigPayload.TYPE, SaveConfigPayload.CODEC);
PayloadTypeRegistry.serverboundPlay().register(DeleteAnchorPayload.TYPE, DeleteAnchorPayload.CODEC);
}
public static void registerServerReceivers() {
ServerPlayNetworking.registerGlobalReceiver(SaveConfigPayload.ID, (payload, context) -> {
ServerPlayerEntity player = context.player();
ServerWorld world = player.getWorld();
ServerPlayNetworking.registerGlobalReceiver(SaveConfigPayload.TYPE, (payload, context) -> {
ServerPlayer player = context.player();
ServerLevel level = player.level();
BlockPos pos = payload.pos();
context.server().execute(() -> handleSave(world, player, pos, payload.data()));
CompoundTag data = payload.data();
context.server().execute(() -> handleSave(level, player, pos, data));
});
ServerPlayNetworking.registerGlobalReceiver(DeleteAnchorPayload.ID, (payload, context) -> {
ServerPlayerEntity player = context.player();
ServerWorld world = player.getWorld();
ServerPlayNetworking.registerGlobalReceiver(DeleteAnchorPayload.TYPE, (payload, context) -> {
ServerPlayer player = context.player();
ServerLevel level = player.level();
BlockPos pos = payload.pos();
context.server().execute(() -> handleDelete(world, player, pos));
context.server().execute(() -> handleDelete(level, player, pos));
});
}
private static void handleSave(ServerWorld world, ServerPlayerEntity player, BlockPos pos, NbtCompound data) {
private static void handleSave(ServerLevel level, ServerPlayer player, BlockPos pos, CompoundTag data) {
if (!canModify(player, pos)) {
VideoPlayerMod.LOG.warn("[{}] {} attempted save without permission at {}",
VideoPlayerMod.MOD_ID, player.getName().getString(), pos);
return;
}
if (!(world.getBlockEntity(pos) instanceof VideoAnchorBlockEntity be)) {
if (!(level.getBlockEntity(pos) instanceof VideoAnchorBlockEntity be)) {
return;
}
be.applyFromNbt(sanitize(data));
// broadcast updated state to all players tracking the chunk
SyncAnchorPayload sync = new SyncAnchorPayload(pos, be.toNbt());
for (ServerPlayerEntity watcher : PlayerLookup.tracking(world, pos)) {
for (ServerPlayer watcher : PlayerLookup.tracking(level, pos)) {
ServerPlayNetworking.send(watcher, sync);
}
}
private static void handleDelete(ServerWorld world, ServerPlayerEntity player, BlockPos pos) {
private static void handleDelete(ServerLevel level, ServerPlayer player, BlockPos pos) {
if (!canModify(player, pos)) {
return;
}
if (world.getBlockEntity(pos) instanceof VideoAnchorBlockEntity) {
world.setBlockState(pos, Blocks.AIR.getDefaultState());
if (level.getBlockEntity(pos) instanceof VideoAnchorBlockEntity) {
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
}
}
/** Permission check: creative players or operators may modify anchors. */
public static boolean canModify(ServerPlayerEntity player, BlockPos pos) {
public static boolean canModify(ServerPlayer player, BlockPos pos) {
if (player.isCreative()) return true;
return player.hasPermissionLevel(2);
return player.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER);
}
/** Strip out unexpected keys from C2S NBT before applying. */
private static NbtCompound sanitize(NbtCompound in) {
NbtCompound out = new NbtCompound();
out.putString("url", trimUrl(in.getString("url", "")));
out.putInt("width", clamp(in.getInt("width", 1), 1, 32));
out.putInt("height", clamp(in.getInt("height", 1), 1, 32));
out.putString("facing", in.getString("facing", "north"));
out.putBoolean("loop", in.getBoolean("loop", true));
out.putFloat("volume", Math.max(0F, Math.min(1F, in.getFloat("volume", 0.5F))));
out.putBoolean("muted", in.getBoolean("muted", false));
out.putBoolean("autoplay", in.getBoolean("autoplay", true));
private static CompoundTag sanitize(CompoundTag in) {
CompoundTag out = new CompoundTag();
out.putString("url", trimUrl(in.getStringOr("url", "")));
out.putInt("width", clamp(in.getIntOr("width", 1), 1, 32));
out.putInt("height", clamp(in.getIntOr("height", 1), 1, 32));
out.putString("facing", in.getStringOr("facing", "north"));
out.putBoolean("loop", in.getBooleanOr("loop", true));
out.putFloat("volume", Math.max(0F, Math.min(1F, in.getFloatOr("volume", 0.5F))));
out.putBoolean("muted", in.getBooleanOr("muted", false));
out.putBoolean("autoplay", in.getBooleanOr("autoplay", true));
return out;
}

View File

@@ -3,17 +3,17 @@ package com.ejclaw.videoplayer.registry;
import com.ejclaw.videoplayer.VideoPlayerMod;
import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder;
import net.minecraft.block.entity.BlockEntityType;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.Registry;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.block.entity.BlockEntityType;
public final class VideoPlayerBlockEntities {
private VideoPlayerBlockEntities() {}
public static final BlockEntityType<VideoAnchorBlockEntity> VIDEO_ANCHOR = Registry.register(
Registries.BLOCK_ENTITY_TYPE,
Identifier.of(VideoPlayerMod.MOD_ID, "video_anchor"),
BuiltInRegistries.BLOCK_ENTITY_TYPE,
Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, "video_anchor"),
FabricBlockEntityTypeBuilder.create(VideoAnchorBlockEntity::new, VideoPlayerBlocks.VIDEO_ANCHOR).build()
);

View File

@@ -2,20 +2,19 @@ package com.ejclaw.videoplayer.registry;
import com.ejclaw.videoplayer.VideoPlayerMod;
import com.ejclaw.videoplayer.block.VideoAnchorBlock;
import net.minecraft.block.AbstractBlock;
import net.minecraft.block.Block;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.util.Identifier;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.Registry;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockBehaviour;
public final class VideoPlayerBlocks {
private VideoPlayerBlocks() {}
public static final Block VIDEO_ANCHOR = register(
"video_anchor",
AbstractBlock.Settings.create().strength(1.0F).nonOpaque(),
BlockBehaviour.Properties.of().strength(1.0F).noOcclusion(),
VideoAnchorBlock::new
);
@@ -25,13 +24,13 @@ public final class VideoPlayerBlocks {
@FunctionalInterface
private interface BlockFactory<B extends Block> {
B create(AbstractBlock.Settings settings);
B create(BlockBehaviour.Properties properties);
}
private static <B extends Block> B register(String name, AbstractBlock.Settings settings, BlockFactory<B> factory) {
Identifier id = Identifier.of(VideoPlayerMod.MOD_ID, name);
RegistryKey<Block> key = RegistryKey.of(RegistryKeys.BLOCK, id);
B block = factory.create(settings.registryKey(key));
return Registry.register(Registries.BLOCK, key, block);
private static <B extends Block> B register(String name, BlockBehaviour.Properties props, BlockFactory<B> factory) {
Identifier id = Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, name);
ResourceKey<Block> key = ResourceKey.create(BuiltInRegistries.BLOCK.key(), id);
B block = factory.create(props.setId(key));
return Registry.register(BuiltInRegistries.BLOCK, key, block);
}
}

View File

@@ -2,34 +2,33 @@ package com.ejclaw.videoplayer.registry;
import com.ejclaw.videoplayer.VideoPlayerMod;
import com.ejclaw.videoplayer.item.VideoStickItem;
import net.minecraft.item.Item;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.util.Identifier;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.Registry;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.item.Item;
public final class VideoPlayerItems {
private VideoPlayerItems() {}
public static final Item VIDEO_STICK = register(
"video_stick",
settings -> new VideoStickItem(settings.maxCount(1))
props -> new VideoStickItem(props.stacksTo(1))
);
public static void register() {
// For M1 we don't add to a vanilla item group; players get the stick via /videoStick.
// players get the stick via /videoStick command
}
@FunctionalInterface
private interface ItemFactory<I extends Item> {
I create(Item.Settings settings);
I create(Item.Properties properties);
}
private static <I extends Item> I register(String name, ItemFactory<I> factory) {
Identifier id = Identifier.of(VideoPlayerMod.MOD_ID, name);
RegistryKey<Item> key = RegistryKey.of(RegistryKeys.ITEM, id);
I item = factory.create(new Item.Settings().registryKey(key));
return Registry.register(Registries.ITEM, key, item);
Identifier id = Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, name);
ResourceKey<Item> key = ResourceKey.create(BuiltInRegistries.ITEM.key(), id);
I item = factory.create(new Item.Properties().setId(key));
return Registry.register(BuiltInRegistries.ITEM, key, item);
}
}