feat(M1): Fabric scaffold for MC 1.21.6 with /videoStick

- video_player mod id, 영상재생모드 display name
- VideoAnchorBlock + VideoAnchorBlockEntity (placeholder)
- VideoStickItem
- /videoStick (+ /videostick alias) command gives the stick
- gradle 9.5.1 wrapper, fabric-loom 1.16.2, Java 21 toolchain
- works in both singleplayer and dedicated server (environment: *)
This commit is contained in:
tkrmagid
2026-05-15 00:56:35 +09:00
parent 0d46208f01
commit 4094e492b9
22 changed files with 762 additions and 1 deletions

View File

@@ -0,0 +1,15 @@
package com.ejclaw.videoplayer;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
@Environment(EnvType.CLIENT)
public class VideoPlayerClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
// M2+: BlockEntityRendererFactories.register(...)
// M5+: AttackBlockCallback for left-click delete
VideoPlayerMod.LOG.info("[{}] client initialized (M1 scaffold)", VideoPlayerMod.MOD_ID);
}
}

View File

@@ -0,0 +1,28 @@
package com.ejclaw.videoplayer;
import com.ejclaw.videoplayer.command.VideoStickCommand;
import com.ejclaw.videoplayer.registry.VideoPlayerBlockEntities;
import com.ejclaw.videoplayer.registry.VideoPlayerBlocks;
import com.ejclaw.videoplayer.registry.VideoPlayerItems;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class VideoPlayerMod implements ModInitializer {
public static final String MOD_ID = "video_player";
public static final Logger LOG = LoggerFactory.getLogger(MOD_ID);
@Override
public void onInitialize() {
VideoPlayerBlocks.register();
VideoPlayerBlockEntities.register();
VideoPlayerItems.register();
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, env) -> {
VideoStickCommand.register(dispatcher);
});
LOG.info("[{}] initialized (M1 scaffold)", MOD_ID);
}
}

View File

@@ -0,0 +1,28 @@
package com.ejclaw.videoplayer.block;
import com.mojang.serialization.MapCodec;
import net.minecraft.block.AbstractBlock;
import net.minecraft.block.Block;
import net.minecraft.block.BlockEntityProvider;
import net.minecraft.block.BlockState;
import net.minecraft.block.BlockWithEntity;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.util.math.BlockPos;
public class VideoAnchorBlock extends BlockWithEntity implements BlockEntityProvider {
public static final MapCodec<VideoAnchorBlock> CODEC = createCodec(VideoAnchorBlock::new);
public VideoAnchorBlock(AbstractBlock.Settings settings) {
super(settings);
}
@Override
protected MapCodec<? extends BlockWithEntity> getCodec() {
return CODEC;
}
@Override
public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return new VideoAnchorBlockEntity(pos, state);
}
}

View File

@@ -0,0 +1,40 @@
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.util.math.BlockPos;
import net.minecraft.util.math.Direction;
/**
* M1 placeholder. Holds the runtime fields described in SPEC §3.1; NBT persistence
* (ReadView/WriteView in 1.21.6) lands in M3/M4 alongside the playback and GUI work.
*/
public class VideoAnchorBlockEntity extends BlockEntity {
private String url = "";
private int width = 1;
private int height = 1;
private Direction facing = Direction.NORTH;
private boolean loop = true;
private float volume = 0.5F;
private boolean muted = false;
private boolean autoplay = true;
public VideoAnchorBlockEntity(BlockPos pos, BlockState state) {
super(VideoPlayerBlockEntities.VIDEO_ANCHOR, pos, state);
}
public String getUrl() { return url; }
public int getWidth() { return width; }
public int getHeight() { return height; }
public Direction getFacing() { return facing; }
public boolean isLoop() { return loop; }
public float getVolume() { return volume; }
public boolean isMuted() { return muted; }
public boolean isAutoplay() { return autoplay; }
public void setMuted(boolean muted) {
this.muted = muted;
markDirty();
}
}

View File

@@ -0,0 +1,36 @@
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;
public final class VideoStickCommand {
private VideoStickCommand() {}
public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
dispatcher.register(CommandManager.literal("videoStick")
.executes(ctx -> run(ctx.getSource())));
// Lowercase alias — Brigadier is case-sensitive.
dispatcher.register(CommandManager.literal("videostick")
.executes(ctx -> run(ctx.getSource())));
}
private static int run(ServerCommandSource source) {
ServerPlayerEntity player = source.getPlayer();
if (player == null) {
source.sendError(Text.literal("플레이어만 이 명령을 사용할 수 있습니다."));
return 0;
}
ItemStack stack = new ItemStack(VideoPlayerItems.VIDEO_STICK);
boolean inserted = player.getInventory().insertStack(stack);
if (!inserted || !stack.isEmpty()) {
player.dropItem(stack, false);
}
source.sendFeedback(() -> Text.literal("비디오 스틱을 지급했습니다."), false);
return 1;
}
}

View File

@@ -0,0 +1,12 @@
package com.ejclaw.videoplayer.item;
import net.minecraft.item.Item;
/**
* M1: registered placeholder. Right/left-click handlers land in M4M5.
*/
public class VideoStickItem extends Item {
public VideoStickItem(Settings settings) {
super(settings);
}
}

View File

@@ -0,0 +1,23 @@
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;
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"),
FabricBlockEntityTypeBuilder.create(VideoAnchorBlockEntity::new, VideoPlayerBlocks.VIDEO_ANCHOR).build()
);
public static void register() {
// class-load side effect triggers registration
}
}

View File

@@ -0,0 +1,37 @@
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;
public final class VideoPlayerBlocks {
private VideoPlayerBlocks() {}
public static final Block VIDEO_ANCHOR = register(
"video_anchor",
AbstractBlock.Settings.create().strength(1.0F).nonOpaque(),
VideoAnchorBlock::new
);
public static void register() {
// class-load side effect triggers registration
}
@FunctionalInterface
private interface BlockFactory<B extends Block> {
B create(AbstractBlock.Settings settings);
}
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);
}
}

View File

@@ -0,0 +1,35 @@
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;
public final class VideoPlayerItems {
private VideoPlayerItems() {}
public static final Item VIDEO_STICK = register(
"video_stick",
settings -> new VideoStickItem(settings.maxCount(1))
);
public static void register() {
// For M1 we don't add to a vanilla item group; players get the stick via /videoStick.
}
@FunctionalInterface
private interface ItemFactory<I extends Item> {
I create(Item.Settings settings);
}
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);
}
}

View File

@@ -0,0 +1,4 @@
{
"block.video_player.video_anchor": "Video Anchor",
"item.video_player.video_stick": "Video Stick"
}

View File

@@ -0,0 +1,4 @@
{
"block.video_player.video_anchor": "영상 앵커",
"item.video_player.video_stick": "비디오 스틱"
}

View File

@@ -0,0 +1,24 @@
{
"schemaVersion": 1,
"id": "${mod_id}",
"version": "${version}",
"name": "영상재생모드",
"description": "Play arbitrary mp4 URLs on block surfaces in Minecraft (Fabric).",
"authors": [ "tkrmagid" ],
"contact": {
"homepage": "https://git.tkrmagid.kr/tkrmagid/mc_video_player_mod",
"sources": "https://git.tkrmagid.kr/tkrmagid/mc_video_player_mod"
},
"license": "MIT",
"environment": "*",
"entrypoints": {
"main": [ "com.ejclaw.videoplayer.VideoPlayerMod" ],
"client": [ "com.ejclaw.videoplayer.VideoPlayerClient" ]
},
"depends": {
"fabricloader": ">=0.16.0",
"fabric-api": "*",
"minecraft": ">=1.21.6",
"java": ">=21"
}
}