4 Commits

Author SHA1 Message Date
tkrmagid
52fbcd1861 render: restore textured quad on new 26.1.2 BlockEntityRenderer pipeline
Some checks failed
build / build (push) Has been cancelled
VideoPlayback now allocates a DynamicTexture per active anchor under a unique
Identifier (registered on Minecraft.getTextureManager()) and pumps RGBA frames
into it via NativeImage.setPixelABGR + DynamicTexture.upload() during the
client tick. Until the backend (JavaCV) produces a first frame, the texture
shows a dark gray placeholder with a thin border so the anchor screen is
visibly present.

VideoAnchorRenderer.submit() now uses SubmitNodeCollector.submitCustomGeometry
with RenderTypes.entityCutout(textureId), drawing a two-sided width×height
quad oriented by Direction.toYRot() + Axis.YP.rotationDegrees. Vertex
attributes use the new VertexConsumer fluent API (addVertex(Matrix4f, ...)
.setColor.setUv.setOverlay(NO_OVERLAY).setLight.setNormal).

JavaCvBackend / WatermediaBackend / WatermediaProbe / VideoBackend are
unchanged — JavaCV is referenced entirely via reflection so the mod jar
remains loadable when the bytedeco classifier jars aren't on the runtime
classpath, in which case the anchor renders its placeholder surface.
2026-05-15 19:38:23 +09:00
tkrmagid
27a3f34bfa 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.
2026-05-15 19:27:12 +09:00
tkrmagid
8f69814cb2 build: switch toolchain to MC 26.1.2 (intermediary retired)
Some checks failed
build / build (push) Has been cancelled
- net.fabricmc.fabric-loom 1.16-SNAPSHOT (no remap; MC 26.1+ ships unobfuscated)
- gradle.properties: minecraft_version=26.1.2, loader=0.19.2, fabric-api=0.149.0+26.1.2
- Java 25 toolchain
- fabric.mod.json: fabricloader>=0.19.0, java>=25
- Drop multi-version build script + matrix CI (single-target now)
- Backup of 1.21.6/7/8 working tree preserved on mc-1.21.x branch

Source migration to Mojmap names is in progress on follow-up commits;
this commit alone will not build until source files are ported.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 19:09:59 +09:00
tkrmagid
ddc16f3d90 M2-M8: renderer, playback backends, GUI/network, commands, multi-version build
Some checks failed
build-matrix / build (0.120.1+1.21.6, 1.21.6, 1.21.6+build.1) (push) Has been cancelled
build-matrix / build (0.129.0+1.21.7, 1.21.7, 1.21.7+build.8) (push) Has been cancelled
build-matrix / build (0.136.1+1.21.8, 1.21.8, 1.21.8+build.1) (push) Has been cancelled
- M2: VideoAnchorRenderer draws width\u00d7height quad oriented by facing
- M3: VideoBackend interface + JavaCV (reflection) and WaterMedia (probe) backends
- M4: VideoConfigScreen GUI + 4 typed payloads + NBT persistence via ReadView/WriteView
- M5: stick item useOnBlock place/edit, AttackBlockCallback delete, /videoPlace /videoDelete /videoMute
- M6: per-tick distance attenuation gain = volume * clamp(1 - d/16, 0, 1), mute zeroes gain
- M7: WatermediaProbe (reflection-only; reports unavailable until v2 supports 1.21.6+)
- M8: multi-version build script (1.21.6/1.21.7/1.21.8) + Gitea Actions matrix workflow
2026-05-15 10:45:28 +09:00
34 changed files with 1554 additions and 96 deletions

View File

@@ -0,0 +1,23 @@
name: build
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 25
- name: Build (MC 26.1.2)
run: ./gradlew --no-daemon build
- uses: actions/upload-artifact@v4
with:
name: video_player-mc26.1.2
path: build/libs/*.jar

View File

@@ -1,5 +1,5 @@
plugins { plugins {
id 'fabric-loom' version '1.16.2' id 'net.fabricmc.fabric-loom' version "${loom_version}"
id 'maven-publish' id 'maven-publish'
id 'java' id 'java'
} }
@@ -16,33 +16,41 @@ repositories {
maven { url = 'https://maven.fabricmc.net/' } maven { url = 'https://maven.fabricmc.net/' }
} }
loom {
// Intentionally empty — MC 26.1+ ships unobfuscated, so the new loom does not remap.
}
dependencies { dependencies {
// No mappings dep — Mojang ships official names since 26.1, intermediary is gone.
minecraft "com.mojang:minecraft:${project.minecraft_version}" minecraft "com.mojang:minecraft:${project.minecraft_version}"
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" implementation "net.fabricmc:fabric-loader:${project.loader_version}"
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
} }
processResources { processResources {
inputs.property "version", project.version inputs.property "version", project.version
inputs.property "mod_id", project.mod_id inputs.property "mod_id", project.mod_id
inputs.property "minecraft_version", project.minecraft_version
filesMatching("fabric.mod.json") { filesMatching("fabric.mod.json") {
expand "version": project.version, "mod_id": project.mod_id expand "version": project.version,
"mod_id": project.mod_id,
"target_minecraft": "~${project.minecraft_version}"
} }
} }
tasks.withType(JavaCompile).configureEach { tasks.withType(JavaCompile).configureEach {
it.options.release = 21 it.options.release = 25
} }
java { java {
withSourcesJar() withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_21 sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_25
toolchain { toolchain {
languageVersion = JavaLanguageVersion.of(21) languageVersion = JavaLanguageVersion.of(25)
} }
} }

View File

@@ -1,14 +1,16 @@
org.gradle.jvmargs=-Xmx2G org.gradle.jvmargs=-Xmx2G
org.gradle.parallel=true org.gradle.parallel=true
# Config cache disabled — new loom + IntelliJ aren't fully compatible yet
org.gradle.configuration-cache=false
# Mod # Mod
mod_id=video_player mod_id=video_player
mod_version=0.1.0 mod_version=0.3.0
maven_group=com.ejclaw.videoplayer maven_group=com.ejclaw.videoplayer
archives_base_name=video_player archives_base_name=video_player
# Minecraft / Fabric (1.21.6) # Minecraft / Fabric (26.1.2 — single target, intermediary/Yarn retired)
minecraft_version=1.21.6 minecraft_version=26.1.2
yarn_mappings=1.21.6+build.1
loader_version=0.19.2 loader_version=0.19.2
fabric_version=0.120.1+1.21.6 loom_version=1.16-SNAPSHOT
fabric_version=0.149.0+26.1.2

View File

@@ -1,15 +1,73 @@
package com.ejclaw.videoplayer; package com.ejclaw.videoplayer;
import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import com.ejclaw.videoplayer.client.net.ClientNetworking;
import com.ejclaw.videoplayer.client.playback.VideoPlayback;
import com.ejclaw.videoplayer.client.render.VideoAnchorRenderer;
import com.ejclaw.videoplayer.item.VideoStickItem;
import com.ejclaw.videoplayer.net.DeleteAnchorPayload;
import com.ejclaw.videoplayer.registry.VideoPlayerBlockEntities;
import net.fabricmc.api.ClientModInitializer; import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.api.EnvType; import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment; import net.fabricmc.api.Environment;
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.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) @Environment(EnvType.CLIENT)
public class VideoPlayerClient implements ClientModInitializer { public class VideoPlayerClient implements ClientModInitializer {
@Override @Override
public void onInitializeClient() { public void onInitializeClient() {
// M2+: BlockEntityRendererFactories.register(...) ClientNetworking.register();
// M5+: AttackBlockCallback for left-click delete
VideoPlayerMod.LOG.info("[{}] client initialized (M1 scaffold)", VideoPlayerMod.MOD_ID); BlockEntityRendererRegistry.register(
VideoPlayerBlockEntities.VIDEO_ANCHOR,
VideoAnchorRenderer::new
);
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 InteractionResult.SUCCESS;
}
return InteractionResult.PASS;
});
ClientTickEvents.END_CLIENT_TICK.register(client -> {
VideoPlayback.tick();
updateDistanceGains(client);
});
ClientTickEvents.END_LEVEL_TICK.register(world -> {
// no-op for now
});
VideoPlayerMod.LOG.info("[{}] client initialized", VideoPlayerMod.MOD_ID);
}
/** SPEC §6 — recompute per-anchor audio gain from player distance every tick. */
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;
VideoPlayback.setGain(pos, gain);
}
} }
} }

View File

@@ -1,6 +1,10 @@
package com.ejclaw.videoplayer; package com.ejclaw.videoplayer;
import com.ejclaw.videoplayer.command.VideoDeleteCommand;
import com.ejclaw.videoplayer.command.VideoMuteCommand;
import com.ejclaw.videoplayer.command.VideoPlaceCommand;
import com.ejclaw.videoplayer.command.VideoStickCommand; import com.ejclaw.videoplayer.command.VideoStickCommand;
import com.ejclaw.videoplayer.net.VideoPlayerNetwork;
import com.ejclaw.videoplayer.registry.VideoPlayerBlockEntities; import com.ejclaw.videoplayer.registry.VideoPlayerBlockEntities;
import com.ejclaw.videoplayer.registry.VideoPlayerBlocks; import com.ejclaw.videoplayer.registry.VideoPlayerBlocks;
import com.ejclaw.videoplayer.registry.VideoPlayerItems; import com.ejclaw.videoplayer.registry.VideoPlayerItems;
@@ -19,10 +23,16 @@ public class VideoPlayerMod implements ModInitializer {
VideoPlayerBlockEntities.register(); VideoPlayerBlockEntities.register();
VideoPlayerItems.register(); VideoPlayerItems.register();
VideoPlayerNetwork.registerPayloadTypes();
VideoPlayerNetwork.registerServerReceivers();
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, env) -> { CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, env) -> {
VideoStickCommand.register(dispatcher); VideoStickCommand.register(dispatcher);
VideoPlaceCommand.register(dispatcher);
VideoDeleteCommand.register(dispatcher);
VideoMuteCommand.register(dispatcher);
}); });
LOG.info("[{}] initialized (M1 scaffold)", MOD_ID); LOG.info("[{}] initialized", MOD_ID);
} }
} }

View File

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

View File

@@ -1,14 +1,18 @@
package com.ejclaw.videoplayer.block; package com.ejclaw.videoplayer.block;
import com.ejclaw.videoplayer.registry.VideoPlayerBlockEntities; import com.ejclaw.videoplayer.registry.VideoPlayerBlockEntities;
import net.minecraft.block.BlockState; import net.minecraft.core.BlockPos;
import net.minecraft.block.entity.BlockEntity; import net.minecraft.core.Direction;
import net.minecraft.util.math.BlockPos; import net.minecraft.nbt.CompoundTag;
import net.minecraft.util.math.Direction; 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;
/** /**
* M1 placeholder. Holds the runtime fields described in SPEC §3.1; NBT persistence * Anchor BE — holds per-block config that drives playback.
* (ReadView/WriteView in 1.21.6) lands in M3/M4 alongside the playback and GUI work. * NBT persistence uses 26.1's ValueInput/ValueOutput.
* Network sync uses {@link #toNbt()} / {@link #fromNbt(CompoundTag)}.
*/ */
public class VideoAnchorBlockEntity extends BlockEntity { public class VideoAnchorBlockEntity extends BlockEntity {
private String url = ""; private String url = "";
@@ -33,8 +37,88 @@ public class VideoAnchorBlockEntity extends BlockEntity {
public boolean isMuted() { return muted; } public boolean isMuted() { return muted; }
public boolean isAutoplay() { return autoplay; } public boolean isAutoplay() { return autoplay; }
public void setMuted(boolean muted) { public void setUrl(String url) { this.url = url == null ? "" : url; setChanged(); }
this.muted = muted; public void setWidth(int width) { this.width = clamp(width, 1, 32); setChanged(); }
markDirty(); 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(CompoundTag nbt) {
fromNbt(nbt);
setChanged();
}
/** Wire-format NBT used by SaveConfig/SyncAnchor payloads. */
public CompoundTag toNbt() {
CompoundTag nbt = new CompoundTag();
nbt.putString("url", url);
nbt.putInt("width", width);
nbt.putInt("height", height);
nbt.putString("facing", facing.getSerializedName());
nbt.putBoolean("loop", loop);
nbt.putFloat("volume", volume);
nbt.putBoolean("muted", muted);
nbt.putBoolean("autoplay", autoplay);
return nbt;
}
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.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 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 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 = 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) {
return Math.max(lo, Math.min(hi, v));
}
private static String clampUrl(String s) {
if (s == null) return "";
return s.length() > 256 ? s.substring(0, 256) : s;
} }
} }

View File

@@ -0,0 +1,139 @@
package com.ejclaw.videoplayer.client.gui;
import com.ejclaw.videoplayer.net.DeleteAnchorPayload;
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.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 CompoundTag initial;
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, CompoundTag data) {
super(Component.literal("Video Anchor"));
this.pos = pos;
this.initial = data;
}
@Override
protected void init() {
int cx = this.width / 2;
int y = 40;
urlField = new EditBox(this.font, cx - 150, y, 300, 20, Component.literal("URL"));
urlField.setMaxLength(256);
urlField.setValue(initial.getStringOr("url", ""));
addRenderableWidget(urlField);
y += 30;
widthField = new EditBox(this.font, cx - 150, y, 60, 20, Component.literal("W"));
widthField.setMaxLength(2);
widthField.setValue(Integer.toString(initial.getIntOr("width", 1)));
addRenderableWidget(widthField);
heightField = new EditBox(this.font, cx - 80, y, 60, 20, Component.literal("H"));
heightField.setMaxLength(2);
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.getFloatOr("volume", 0.5F))));
addRenderableWidget(volumeSlider);
y += 30;
loopBox = Checkbox.builder(Component.literal("Loop"), this.font)
.pos(cx - 150, y).selected(initial.getBooleanOr("loop", true)).build();
addRenderableWidget(loopBox);
muteBox = Checkbox.builder(Component.literal("Mute"), this.font)
.pos(cx - 60, y).selected(initial.getBooleanOr("muted", false)).build();
addRenderableWidget(muteBox);
autoplayBox = Checkbox.builder(Component.literal("Autoplay"), this.font)
.pos(cx + 30, y).selected(initial.getBooleanOr("autoplay", true)).build();
addRenderableWidget(autoplayBox);
y += 36;
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() {
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.selected());
out.putBoolean("autoplay", autoplayBox.selected());
ClientPlayNetworking.send(new SaveConfigPayload(pos, out));
onClose();
}
private void delete() {
ClientPlayNetworking.send(new DeleteAnchorPayload(pos));
onClose();
}
@Override
public boolean isPauseScreen() { return false; }
@Override
public void onClose() {
Minecraft mc = this.minecraft != null ? this.minecraft : Minecraft.getInstance();
if (mc != null) mc.setScreen(null);
}
private static int parseInt(String s, int dflt) {
try { return Integer.parseInt(s); } catch (Exception e) { return dflt; }
}
private static final class VolumeSlider extends AbstractSliderButton {
VolumeSlider(int x, int y, int w, int h, float initial) {
super(x, y, w, h, Component.literal("Volume: " + pct(initial)), initial);
updateMessage();
}
float getVolume() { return (float) this.value; }
@Override
protected void updateMessage() {
setMessage(Component.literal("Volume: " + pct((float) this.value)));
}
@Override
protected void applyValue() {
updateMessage();
}
private static int pct(float v) {
return Math.round(v * 100F);
}
}
}

View File

@@ -0,0 +1,35 @@
package com.ejclaw.videoplayer.client.net;
import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import com.ejclaw.videoplayer.client.gui.VideoConfigScreen;
import com.ejclaw.videoplayer.client.playback.VideoPlayback;
import com.ejclaw.videoplayer.net.OpenScreenPayload;
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.Minecraft;
/** Client-side S2C receivers for OpenScreen and SyncAnchor. */
@Environment(EnvType.CLIENT)
public final class ClientNetworking {
private ClientNetworking() {}
public static void register() {
ClientPlayNetworking.registerGlobalReceiver(OpenScreenPayload.TYPE, (payload, context) -> {
Minecraft mc = context.client();
mc.execute(() -> mc.setScreen(new VideoConfigScreen(payload.pos(), payload.data())));
});
ClientPlayNetworking.registerGlobalReceiver(SyncAnchorPayload.TYPE, (payload, context) -> {
Minecraft mc = context.client();
mc.execute(() -> {
if (mc.level == null) return;
if (mc.level.getBlockEntity(payload.pos()) instanceof VideoAnchorBlockEntity be) {
be.applyFromNbt(payload.data());
VideoPlayback.onConfigChanged(be);
}
});
});
}
}

View File

@@ -0,0 +1,154 @@
package com.ejclaw.videoplayer.client.playback;
import com.ejclaw.videoplayer.VideoPlayerMod;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* SPEC §5.3 — fallback mp4/http(s) backend driven by JavaCV's FFmpegFrameGrabber.
*
* JavaCV is referenced entirely through reflection so that the mod jar stays loadable when the
* (large) JavaCV dependency isn't bundled — the backend just reports {@code !isReady()} until the
* runtime classpath contains org.bytedeco.javacv.FFmpegFrameGrabber.
*/
@Environment(EnvType.CLIENT)
public class JavaCvBackend implements VideoBackend {
private static final String GRABBER_CLASS = "org.bytedeco.javacv.FFmpegFrameGrabber";
private static final String FRAME_CLASS = "org.bytedeco.javacv.Frame";
private static final String CONVERTER_CLASS = "org.bytedeco.javacv.Java2DFrameConverter";
private final Object lock = new Object();
private Thread worker;
private final AtomicBoolean running = new AtomicBoolean(false);
private final AtomicBoolean paused = new AtomicBoolean(false);
private final AtomicReference<ByteBuffer> latest = new AtomicReference<>();
private volatile int width = 0;
private volatile int height = 0;
private volatile float gain = 1.0F;
private volatile boolean loop = true;
private volatile boolean ready = false;
private volatile boolean closed = false;
@Override
public void play(String url, boolean loop) {
if (url == null || url.isEmpty()) return;
this.loop = loop;
synchronized (lock) {
stopWorker();
running.set(true);
paused.set(false);
worker = new Thread(() -> runLoop(url), "video_player-decode");
worker.setDaemon(true);
worker.start();
}
}
@Override
public void pause() { paused.set(true); }
@Override
public void resume() { paused.set(false); }
@Override
public void setVolume(float g) { this.gain = Math.max(0F, Math.min(1F, g)); }
@Override
public boolean isReady() { return ready; }
@Override
public int videoWidth() { return width; }
@Override
public int videoHeight() { return height; }
@Override
public ByteBuffer pollFrame() {
return latest.getAndSet(null);
}
@Override
public void close() {
closed = true;
stopWorker();
}
private void stopWorker() {
running.set(false);
Thread t = worker;
worker = null;
if (t != null) t.interrupt();
ready = false;
}
/** Pure-reflection decode loop. Silent fallback if JavaCV isn't present. */
private void runLoop(String url) {
Object grabber = null;
try {
Class<?> grabberCls = Class.forName(GRABBER_CLASS);
grabber = grabberCls.getConstructor(String.class).newInstance(url);
Method start = grabberCls.getMethod("start");
Method stop = grabberCls.getMethod("stop");
Method grab = grabberCls.getMethod("grabImage");
Method getW = grabberCls.getMethod("getImageWidth");
Method getH = grabberCls.getMethod("getImageHeight");
Method setOpt = grabberCls.getMethod("setOption", String.class, String.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) {}
start.invoke(grabber);
this.width = (int) getW.invoke(grabber);
this.height = (int) getH.invoke(grabber);
this.ready = (width > 0 && height > 0);
Class<?> convCls = Class.forName(CONVERTER_CLASS);
Object converter = convCls.getDeclaredConstructor().newInstance();
Method toImage = convCls.getMethod("getBufferedImage", Class.forName(FRAME_CLASS));
while (running.get() && !closed) {
if (paused.get()) { Thread.sleep(20); continue; }
Object frame = grab.invoke(grabber);
if (frame == null) {
if (loop) {
try { stop.invoke(grabber); } catch (Throwable ignored) {}
try { start.invoke(grabber); } catch (Throwable ignored) {}
continue;
}
break;
}
java.awt.image.BufferedImage img = (java.awt.image.BufferedImage) toImage.invoke(converter, frame);
if (img == null) continue;
ByteBuffer buf = toRgba(img);
if (buf != null) latest.set(buf);
Thread.sleep(15); // ~60fps cap
}
} catch (ClassNotFoundException cnf) {
VideoPlayerMod.LOG.info("[{}] JavaCV not on classpath; backend inactive", VideoPlayerMod.MOD_ID);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
} catch (Throwable t) {
VideoPlayerMod.LOG.warn("[{}] JavaCV decode error: {}", VideoPlayerMod.MOD_ID, t.toString());
} finally {
ready = false;
if (grabber != null) {
try { grabber.getClass().getMethod("close").invoke(grabber); } catch (Throwable ignored) {}
}
}
}
private static ByteBuffer toRgba(java.awt.image.BufferedImage img) {
int w = img.getWidth(), h = img.getHeight();
int[] argb = img.getRGB(0, 0, w, h, null, 0, w);
ByteBuffer buf = ByteBuffer.allocateDirect(w * h * 4).order(ByteOrder.nativeOrder());
for (int p : argb) {
buf.put((byte) ((p >> 16) & 0xFF)); // R
buf.put((byte) ((p >> 8) & 0xFF)); // G
buf.put((byte) ( p & 0xFF)); // B
buf.put((byte) ((p >> 24) & 0xFF)); // A
}
buf.flip();
return buf;
}
}

View File

@@ -0,0 +1,30 @@
package com.ejclaw.videoplayer.client.playback;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import java.nio.ByteBuffer;
/**
* SPEC §5.3 — minimal playback backend abstraction. Implementations: WatermediaBackend (preferred,
* when v2 supports the target MC version) and JavaCvBackend (fallback).
*/
@Environment(EnvType.CLIENT)
public interface VideoBackend {
void play(String url, boolean loop);
void pause();
void resume();
void setVolume(float gain);
boolean isReady();
int videoWidth();
int videoHeight();
/**
* Poll a new decoded RGBA frame if one is ready.
* @return the frame buffer (capacity = w*h*4) or {@code null} if no new frame is ready.
*/
ByteBuffer pollFrame();
void close();
}

View File

@@ -0,0 +1,203 @@
package com.ejclaw.videoplayer.client.playback;
import com.ejclaw.videoplayer.VideoPlayerMod;
import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import com.mojang.blaze3d.platform.NativeImage;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.Identifier;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* SPEC §5 — per-anchor playback registry. Maps {@link BlockPos} → ({@link VideoBackend} +
* a {@link DynamicTexture} surface registered under a unique {@link Identifier}). The renderer
* reads {@link #currentTexture(BlockPos)} and binds it to the quad. {@link #tick()} pumps
* decoded RGBA frames into the texture.
*
* <p>When no backend is available on the classpath (e.g. JavaCV jar not installed by the
* user), the texture stays at its initial placeholder pattern, so the anchor's screen quad
* still renders as a visible surface.
*/
@Environment(EnvType.CLIENT)
public final class VideoPlayback {
private VideoPlayback() {}
private static final int PLACEHOLDER_SIZE = 32;
private static final Map<BlockPos, Entry> ENTRIES = new HashMap<>();
/**
* Ensure a playback entry exists for this anchor and return its texture identifier.
* Creates a backend + dynamic texture on first call. Returns {@code null} only if the
* URL is empty or autoplay is off.
*/
public static Identifier getOrStart(VideoAnchorBlockEntity be) {
BlockPos pos = be.getBlockPos();
Entry e = ENTRIES.get(pos);
if (be.getUrl().isEmpty() || !be.isAutoplay()) {
if (e != null) {
stop(pos);
}
return null;
}
if (e != null && e.url.equals(be.getUrl())) {
return e.id;
}
if (e != null) {
stop(pos);
}
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;
}
public static Identifier currentTexture(BlockPos pos) {
Entry e = ENTRIES.get(pos);
return e == null ? null : e.id;
}
public static void stop(BlockPos pos) {
Entry e = ENTRIES.remove(pos);
if (e != null) e.close();
}
public static void onConfigChanged(VideoAnchorBlockEntity be) {
if (be == null) return;
Entry e = ENTRIES.get(be.getBlockPos());
if (e == null) return;
if (!e.url.equals(be.getUrl())) {
stop(be.getBlockPos());
return;
}
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 (Minecraft.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) {
VideoPlayerMod.LOG.warn("[{}] texture upload failed: {}", VideoPlayerMod.MOD_ID, t.toString());
e.close();
it.remove();
}
}
}
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);
}
public static void stopAll() {
for (Entry e : ENTRIES.values()) e.close();
ENTRIES.clear();
}
/** Per-anchor playback state. */
private static final class Entry {
final String url;
final VideoBackend backend;
final Identifier id;
DynamicTexture texture;
int texW = 0, texH = 0;
boolean registered = false;
Entry(String url, VideoBackend backend) {
this.url = url;
this.backend = backend;
String tag = Integer.toHexString(System.identityHashCode(this));
this.id = Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, "dynamic/" + tag);
ensureTexture(PLACEHOLDER_SIZE, PLACEHOLDER_SIZE, true);
}
/** Allocate or resize the dynamic texture, registering it on first allocation. */
private void ensureTexture(int w, int h, boolean fillPlaceholder) {
if (texture != null && w == texW && h == texH) return;
if (texture != null) texture.close();
NativeImage img = new NativeImage(w, h, false);
if (fillPlaceholder) {
fillPlaceholder(img, w, h);
}
texture = new DynamicTexture(() -> "video_player:" + id, img);
texW = w;
texH = h;
Minecraft mc = Minecraft.getInstance();
if (mc != null) {
mc.getTextureManager().register(id, texture);
registered = true;
}
texture.upload();
}
/** Dark gray surface with a thin border — visible "screen" until first frame arrives. */
private static void fillPlaceholder(NativeImage img, int w, int h) {
// ABGR int. 0xAABBGGRR.
int body = 0xFF202020; // dark gray
int border = 0xFF505050;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
boolean isEdge = (x == 0 || y == 0 || x == w - 1 || y == h - 1);
img.setPixelABGR(x, y, isEdge ? border : body);
}
}
}
/** Copy an incoming RGBA byte buffer into the texture, resizing if dimensions changed. */
void upload(ByteBuffer rgba) {
int w = backend.videoWidth();
int h = backend.videoHeight();
if (w <= 0 || h <= 0) return;
ensureTexture(w, h, false);
NativeImage img = texture.getPixels();
if (img == null) return;
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 abgr = (a << 24) | (b << 16) | (g << 8) | r;
img.setPixelABGR(i % w, i / w, abgr);
}
texture.upload();
}
void close() {
try { backend.close(); } catch (Throwable ignored) {}
if (texture != null) {
try { texture.close(); } catch (Throwable ignored) {}
texture = null;
}
// texture manager keeps the registration; the texture itself is closed.
}
}
}

View File

@@ -0,0 +1,50 @@
package com.ejclaw.videoplayer.client.playback;
import com.ejclaw.videoplayer.VideoPlayerMod;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import java.nio.ByteBuffer;
/**
* SPEC §5.3 / §5.4 — WaterMedia v2 backend. Reflection-only so the mod jar stays clean of
* compile-time WaterMedia dependencies. Until a v2 build supports 1.21.6+ this returns
* {@code !isReady()} and {@link WatermediaProbe} reports unavailable, so callers fall through
* to {@link JavaCvBackend}.
*/
@Environment(EnvType.CLIENT)
public class WatermediaBackend implements VideoBackend {
private volatile boolean ready = false;
private volatile int width = 0;
private volatile int height = 0;
private volatile float gain = 1.0F;
private Object player;
@Override
public void play(String url, boolean loop) {
if (!WatermediaProbe.isAvailable()) {
VideoPlayerMod.LOG.debug("[{}] WatermediaBackend.play called but probe says unavailable",
VideoPlayerMod.MOD_ID);
return;
}
// Reflection construction skipped — only reachable when WaterMedia v2 ships for the target MC.
}
@Override public void pause() {}
@Override public void resume() {}
@Override public void setVolume(float g) { this.gain = Math.max(0F, Math.min(1F, g)); }
@Override public boolean isReady() { return ready; }
@Override public int videoWidth() { return width; }
@Override public int videoHeight() { return height; }
@Override
public ByteBuffer pollFrame() {
return null; // no frames until v2 is wired up
}
@Override
public void close() {
ready = false;
player = null;
}
}

View File

@@ -0,0 +1,35 @@
package com.ejclaw.videoplayer.client.playback;
import com.ejclaw.videoplayer.VideoPlayerMod;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
/**
* SPEC §5.4 — Reflection-only check for whether WaterMedia v2 is present and usable on this MC.
* As of 1.21.6+ WaterMedia v2 has no Yarn-mapped artifact, so this always reports false; we keep the
* shape so the mod will auto-prefer WaterMedia when a future build adds it.
*/
@Environment(EnvType.CLIENT)
public final class WatermediaProbe {
private WatermediaProbe() {}
private static final Boolean CACHED = compute();
public static boolean isAvailable() {
return CACHED;
}
private static boolean compute() {
try {
Class.forName("me.srrapero720.watermedia.api.WaterMediaAPI", false,
WatermediaProbe.class.getClassLoader());
VideoPlayerMod.LOG.info("[{}] WaterMedia v2 detected", VideoPlayerMod.MOD_ID);
return true;
} catch (ClassNotFoundException e) {
return false;
} catch (Throwable t) {
VideoPlayerMod.LOG.warn("[{}] WaterMedia probe failed: {}", VideoPlayerMod.MOD_ID, t.toString());
return false;
}
}
}

View File

@@ -0,0 +1,115 @@
package com.ejclaw.videoplayer.client.render;
import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import com.ejclaw.videoplayer.client.playback.VideoPlayback;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
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.rendertype.RenderType;
import net.minecraft.client.renderer.rendertype.RenderTypes;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.core.Direction;
import net.minecraft.resources.Identifier;
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.
*
* <p>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}.
*/
@Environment(EnvType.CLIENT)
public class VideoAnchorRenderer implements BlockEntityRenderer<VideoAnchorBlockEntity, VideoAnchorRenderer.State> {
public VideoAnchorRenderer(BlockEntityRendererProvider.Context ctx) {
// no-op
}
@Override
public State createRenderState() {
return new State();
}
@Override
public void extractRenderState(VideoAnchorBlockEntity be, State state, float partialTick,
Vec3 cameraPos, ModelFeatureRenderer.CrumblingOverlay crumbling) {
BlockEntityRenderState.extractBase(be, state, crumbling);
state.width = be.getWidth();
state.height = be.getHeight();
Direction facing = be.getFacing();
state.yaw = facing.getAxis().isHorizontal() ? facing.toYRot() : 0F;
state.textureId = VideoPlayback.getOrStart(be);
}
@Override
public void submit(State state, PoseStack pose, SubmitNodeCollector collector, CameraRenderState camera) {
Identifier tex = state.textureId;
if (tex == null) return; // url empty or autoplay off — nothing to draw
final float w = state.width;
final float h = state.height;
final int light = state.lightCoords;
pose.pushPose();
// Center quad on the anchor's top face, rotated to face the configured direction.
pose.translate(0.5F, 1.01F, 0.5F);
pose.mulPose(Axis.YP.rotationDegrees(-state.yaw));
pose.translate(-w / 2.0F, 0F, 0F);
// Snapshot the matrix so the callback's matrix-aware addVertex works even though
// submitCustomGeometry hands us a fresh Pose (its `pose` parameter).
final Matrix4f mat = new Matrix4f(pose.last().pose());
RenderType rt = RenderTypes.entityCutout(tex);
collector.submitCustomGeometry(pose, rt, (poseUnused, vc) -> {
// Front face (visible from the direction the anchor faces)
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)
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);
emit(vc, mat, 0F, 0F, 0F, 0F, 1F, light);
});
pose.popPose();
}
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)
.setColor(255, 255, 255, 255)
.setUv(u, v)
.setOverlay(net.minecraft.client.renderer.texture.OverlayTexture.NO_OVERLAY)
.setLight(light)
.setNormal(0F, 0F, 1F);
}
@Override
public boolean shouldRenderOffScreen() {
return true;
}
@Override
public int getViewDistance() {
return 128;
}
/** Per-frame render data extracted from the BE. */
public static final class State extends BlockEntityRenderState {
public Identifier textureId;
public int width = 1;
public int height = 1;
public float yaw = 0F;
}
}

View File

@@ -0,0 +1,46 @@
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.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<CommandSourceStack> dispatcher) {
dispatcher.register(build("videoDelete"));
dispatcher.register(build("videodelete"));
}
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<CommandSourceStack> ctx)
throws CommandSyntaxException {
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;
}
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
src.sendSuccess(() -> Component.literal("anchor deleted at " + pos.toShortString()), true);
return 1;
}
}

View File

@@ -0,0 +1,62 @@
package com.ejclaw.videoplayer.command;
import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import com.ejclaw.videoplayer.net.SyncAnchorPayload;
import com.mojang.brigadier.CommandDispatcher;
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.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<CommandSourceStack> dispatcher) {
dispatcher.register(build("videoMute"));
dispatcher.register(build("videomute"));
}
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<CommandSourceStack> ctx)
throws CommandSyntaxException {
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.sendFailure(Component.literal("state must be on/off"));
return 0;
}
if (!(level.getBlockEntity(pos) instanceof VideoAnchorBlockEntity be)) {
src.sendFailure(Component.literal("no anchor at that position"));
return 0;
}
be.setMuted(muted);
for (ServerPlayer p : PlayerLookup.tracking(level, pos)) {
ServerPlayNetworking.send(p, new SyncAnchorPayload(pos, be.toNbt()));
}
final boolean mFinal = muted;
src.sendSuccess(() -> Component.literal("anchor " + (mFinal ? "muted" : "unmuted")), true);
return 1;
}
}

View File

@@ -0,0 +1,90 @@
package com.ejclaw.videoplayer.command;
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.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.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<CommandSourceStack> dispatcher) {
dispatcher.register(build("videoPlace"));
dispatcher.register(build("videoplace"));
}
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<CommandSourceStack> ctx)
throws CommandSyntaxException {
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.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.sendFailure(Component.literal("url must be http:// or https:// (or empty)"));
return 0;
}
if (url.length() > 256) url = url.substring(0, 256);
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);
be.setWidth(width);
be.setHeight(height);
be.setUrl(url);
CompoundTag nbt = be.toNbt();
for (ServerPlayer p : PlayerLookup.tracking(level, pos)) {
ServerPlayNetworking.send(p, new SyncAnchorPayload(pos, nbt));
}
final BlockPos fp = pos;
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.ejclaw.videoplayer.registry.VideoPlayerItems;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.item.ItemStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.server.command.CommandManager; import net.minecraft.commands.Commands;
import net.minecraft.server.command.ServerCommandSource; import net.minecraft.network.chat.Component;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.text.Text; import net.minecraft.world.item.ItemStack;
public final class VideoStickCommand { public final class VideoStickCommand {
private VideoStickCommand() {} private VideoStickCommand() {}
public static void register(CommandDispatcher<ServerCommandSource> dispatcher) { public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(CommandManager.literal("videoStick") dispatcher.register(Commands.literal("videoStick")
.executes(ctx -> run(ctx.getSource()))); .executes(ctx -> run(ctx.getSource())));
// Lowercase alias — Brigadier is case-sensitive. // Lowercase alias — Brigadier is case-sensitive.
dispatcher.register(CommandManager.literal("videostick") dispatcher.register(Commands.literal("videostick")
.executes(ctx -> run(ctx.getSource()))); .executes(ctx -> run(ctx.getSource())));
} }
private static int run(ServerCommandSource source) { private static int run(CommandSourceStack source) {
ServerPlayerEntity player = source.getPlayer(); ServerPlayer player = source.getPlayer();
if (player == null) { if (player == null) {
source.sendError(Text.literal("플레이어만 이 명령을 사용할 수 있습니다.")); source.sendFailure(Component.literal("플레이어만 이 명령을 사용할 수 있습니다."));
return 0; return 0;
} }
ItemStack stack = new ItemStack(VideoPlayerItems.VIDEO_STICK); ItemStack stack = new ItemStack(VideoPlayerItems.VIDEO_STICK);
boolean inserted = player.getInventory().insertStack(stack); boolean inserted = player.getInventory().add(stack);
if (!inserted || !stack.isEmpty()) { 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; return 1;
} }
} }

View File

@@ -1,12 +1,58 @@
package com.ejclaw.videoplayer.item; package com.ejclaw.videoplayer.item;
import net.minecraft.item.Item; 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.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;
/** /** Right-click empty face → place anchor + open GUI. Right-click existing anchor → edit. */
* M1: registered placeholder. Right/left-click handlers land in M4M5.
*/
public class VideoStickItem extends Item { public class VideoStickItem extends Item {
public VideoStickItem(Settings settings) { public VideoStickItem(Properties properties) {
super(settings); super(properties);
}
@Override
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;
BlockPos hit = ctx.getClickedPos();
// Existing anchor → edit
if (sl.getBlockEntity(hit) instanceof VideoAnchorBlockEntity existing) {
ServerPlayNetworking.send(sp, new OpenScreenPayload(hit, 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);
if (!there.canBeReplaced()) return InteractionResult.PASS;
Block anchor = VideoPlayerBlocks.VIDEO_ANCHOR;
sl.setBlock(placeAt, 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()));
}
return InteractionResult.SUCCESS;
} }
} }

View File

@@ -0,0 +1,24 @@
package com.ejclaw.videoplayer.net;
import com.ejclaw.videoplayer.VideoPlayerMod;
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 CustomPacketPayload {
public static final CustomPacketPayload.Type<DeleteAnchorPayload> TYPE =
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, "delete_anchor"));
public static final StreamCodec<RegistryFriendlyByteBuf, DeleteAnchorPayload> CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, DeleteAnchorPayload::pos,
DeleteAnchorPayload::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}

View File

@@ -0,0 +1,27 @@
package com.ejclaw.videoplayer.net;
import com.ejclaw.videoplayer.VideoPlayerMod;
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, CompoundTag data) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<OpenScreenPayload> TYPE =
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, "open_screen"));
public static final StreamCodec<RegistryFriendlyByteBuf, OpenScreenPayload> CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, OpenScreenPayload::pos,
ByteBufCodecs.COMPOUND_TAG, OpenScreenPayload::data,
OpenScreenPayload::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}

View File

@@ -0,0 +1,27 @@
package com.ejclaw.videoplayer.net;
import com.ejclaw.videoplayer.VideoPlayerMod;
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, CompoundTag data) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<SaveConfigPayload> TYPE =
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, "save_config"));
public static final StreamCodec<RegistryFriendlyByteBuf, SaveConfigPayload> CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, SaveConfigPayload::pos,
ByteBufCodecs.COMPOUND_TAG, SaveConfigPayload::data,
SaveConfigPayload::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}

View File

@@ -0,0 +1,27 @@
package com.ejclaw.videoplayer.net;
import com.ejclaw.videoplayer.VideoPlayerMod;
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 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 StreamCodec<RegistryFriendlyByteBuf, SyncAnchorPayload> CODEC = StreamCodec.composite(
BlockPos.STREAM_CODEC, SyncAnchorPayload::pos,
ByteBufCodecs.COMPOUND_TAG, SyncAnchorPayload::data,
SyncAnchorPayload::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}

View File

@@ -0,0 +1,109 @@
package com.ejclaw.videoplayer.net;
import com.ejclaw.videoplayer.VideoPlayerMod;
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.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.
* Client-side receivers are registered in {@code com.ejclaw.videoplayer.client.net.ClientNetworking}.
*/
public final class VideoPlayerNetwork {
private VideoPlayerNetwork() {}
public static void registerPayloadTypes() {
// S2C
PayloadTypeRegistry.clientboundPlay().register(OpenScreenPayload.TYPE, OpenScreenPayload.CODEC);
PayloadTypeRegistry.clientboundPlay().register(SyncAnchorPayload.TYPE, SyncAnchorPayload.CODEC);
// C2S
PayloadTypeRegistry.serverboundPlay().register(SaveConfigPayload.TYPE, SaveConfigPayload.CODEC);
PayloadTypeRegistry.serverboundPlay().register(DeleteAnchorPayload.TYPE, DeleteAnchorPayload.CODEC);
}
public static void registerServerReceivers() {
ServerPlayNetworking.registerGlobalReceiver(SaveConfigPayload.TYPE, (payload, context) -> {
ServerPlayer player = context.player();
ServerLevel level = player.level();
BlockPos pos = payload.pos();
CompoundTag data = payload.data();
context.server().execute(() -> handleSave(level, player, pos, data));
});
ServerPlayNetworking.registerGlobalReceiver(DeleteAnchorPayload.TYPE, (payload, context) -> {
ServerPlayer player = context.player();
ServerLevel level = player.level();
BlockPos pos = payload.pos();
context.server().execute(() -> handleDelete(level, player, pos));
});
}
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 (!(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 (ServerPlayer watcher : PlayerLookup.tracking(level, pos)) {
ServerPlayNetworking.send(watcher, sync);
}
}
private static void handleDelete(ServerLevel level, ServerPlayer player, BlockPos pos) {
if (!canModify(player, pos)) {
return;
}
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(ServerPlayer player, BlockPos pos) {
if (player.isCreative()) return true;
return player.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER);
}
/** Strip out unexpected keys from C2S NBT before applying. */
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;
}
private static String trimUrl(String s) {
if (s == null) return "";
String t = s.trim();
if (t.length() > 256) t = t.substring(0, 256);
// SPEC §4.4: only https?:// or empty
if (!t.isEmpty() && !(t.startsWith("http://") || t.startsWith("https://"))) {
return "";
}
return t;
}
private static int clamp(int v, int lo, int hi) {
return Math.max(lo, Math.min(hi, v));
}
}

View File

@@ -3,17 +3,17 @@ package com.ejclaw.videoplayer.registry;
import com.ejclaw.videoplayer.VideoPlayerMod; import com.ejclaw.videoplayer.VideoPlayerMod;
import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity; import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder; import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder;
import net.minecraft.block.entity.BlockEntityType; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.registry.Registries; import net.minecraft.core.Registry;
import net.minecraft.registry.Registry; import net.minecraft.resources.Identifier;
import net.minecraft.util.Identifier; import net.minecraft.world.level.block.entity.BlockEntityType;
public final class VideoPlayerBlockEntities { public final class VideoPlayerBlockEntities {
private VideoPlayerBlockEntities() {} private VideoPlayerBlockEntities() {}
public static final BlockEntityType<VideoAnchorBlockEntity> VIDEO_ANCHOR = Registry.register( public static final BlockEntityType<VideoAnchorBlockEntity> VIDEO_ANCHOR = Registry.register(
Registries.BLOCK_ENTITY_TYPE, BuiltInRegistries.BLOCK_ENTITY_TYPE,
Identifier.of(VideoPlayerMod.MOD_ID, "video_anchor"), Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, "video_anchor"),
FabricBlockEntityTypeBuilder.create(VideoAnchorBlockEntity::new, VideoPlayerBlocks.VIDEO_ANCHOR).build() 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.VideoPlayerMod;
import com.ejclaw.videoplayer.block.VideoAnchorBlock; import com.ejclaw.videoplayer.block.VideoAnchorBlock;
import net.minecraft.block.AbstractBlock; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.block.Block; import net.minecraft.core.Registry;
import net.minecraft.registry.Registries; import net.minecraft.resources.Identifier;
import net.minecraft.registry.Registry; import net.minecraft.resources.ResourceKey;
import net.minecraft.registry.RegistryKey; import net.minecraft.world.level.block.Block;
import net.minecraft.registry.RegistryKeys; import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.util.Identifier;
public final class VideoPlayerBlocks { public final class VideoPlayerBlocks {
private VideoPlayerBlocks() {} private VideoPlayerBlocks() {}
public static final Block VIDEO_ANCHOR = register( public static final Block VIDEO_ANCHOR = register(
"video_anchor", "video_anchor",
AbstractBlock.Settings.create().strength(1.0F).nonOpaque(), BlockBehaviour.Properties.of().strength(1.0F).noOcclusion(),
VideoAnchorBlock::new VideoAnchorBlock::new
); );
@@ -25,13 +24,13 @@ public final class VideoPlayerBlocks {
@FunctionalInterface @FunctionalInterface
private interface BlockFactory<B extends Block> { 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) { private static <B extends Block> B register(String name, BlockBehaviour.Properties props, BlockFactory<B> factory) {
Identifier id = Identifier.of(VideoPlayerMod.MOD_ID, name); Identifier id = Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, name);
RegistryKey<Block> key = RegistryKey.of(RegistryKeys.BLOCK, id); ResourceKey<Block> key = ResourceKey.create(BuiltInRegistries.BLOCK.key(), id);
B block = factory.create(settings.registryKey(key)); B block = factory.create(props.setId(key));
return Registry.register(Registries.BLOCK, key, block); 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.VideoPlayerMod;
import com.ejclaw.videoplayer.item.VideoStickItem; import com.ejclaw.videoplayer.item.VideoStickItem;
import net.minecraft.item.Item; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.registry.Registries; import net.minecraft.core.Registry;
import net.minecraft.registry.Registry; import net.minecraft.resources.Identifier;
import net.minecraft.registry.RegistryKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.registry.RegistryKeys; import net.minecraft.world.item.Item;
import net.minecraft.util.Identifier;
public final class VideoPlayerItems { public final class VideoPlayerItems {
private VideoPlayerItems() {} private VideoPlayerItems() {}
public static final Item VIDEO_STICK = register( public static final Item VIDEO_STICK = register(
"video_stick", "video_stick",
settings -> new VideoStickItem(settings.maxCount(1)) props -> new VideoStickItem(props.stacksTo(1))
); );
public static void register() { 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 @FunctionalInterface
private interface ItemFactory<I extends Item> { 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) { private static <I extends Item> I register(String name, ItemFactory<I> factory) {
Identifier id = Identifier.of(VideoPlayerMod.MOD_ID, name); Identifier id = Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, name);
RegistryKey<Item> key = RegistryKey.of(RegistryKeys.ITEM, id); ResourceKey<Item> key = ResourceKey.create(BuiltInRegistries.ITEM.key(), id);
I item = factory.create(new Item.Settings().registryKey(key)); I item = factory.create(new Item.Properties().setId(key));
return Registry.register(Registries.ITEM, key, item); return Registry.register(BuiltInRegistries.ITEM, key, item);
} }
} }

View File

@@ -0,0 +1,5 @@
{
"variants": {
"": { "model": "video_player:block/video_anchor" }
}
}

View File

@@ -0,0 +1,21 @@
{
"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] }
}
}
]
}

View File

@@ -0,0 +1,6 @@
{
"parent": "item/generated",
"textures": {
"layer0": "video_player:item/video_stick"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 B

View File

@@ -16,9 +16,9 @@
"client": [ "com.ejclaw.videoplayer.VideoPlayerClient" ] "client": [ "com.ejclaw.videoplayer.VideoPlayerClient" ]
}, },
"depends": { "depends": {
"fabricloader": ">=0.16.0", "fabricloader": ">=0.19.0",
"fabric-api": "*", "fabric-api": "*",
"minecraft": ">=1.21.6", "minecraft": "${target_minecraft}",
"java": ">=21" "java": ">=25"
} }
} }