diff --git a/gradle.properties b/gradle.properties
index 324dbcb..a4f01a9 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -5,7 +5,7 @@ org.gradle.configuration-cache=false
# Mod
mod_id=video_player
-mod_version=0.4.30
+mod_version=0.4.31
maven_group=com.ejclaw.videoplayer
archives_base_name=video_player
diff --git a/src/main/java/com/ejclaw/videoplayer/command/CommandPermissions.java b/src/main/java/com/ejclaw/videoplayer/command/CommandPermissions.java
new file mode 100644
index 0000000..c7c446a
--- /dev/null
+++ b/src/main/java/com/ejclaw/videoplayer/command/CommandPermissions.java
@@ -0,0 +1,45 @@
+package com.ejclaw.videoplayer.command;
+
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.server.permissions.Permissions;
+import net.minecraft.world.entity.player.Player;
+
+/**
+ * Shared {@code .requires(...)} predicate for all {@code /video*} commands.
+ *
+ *
Semantics:
+ *
+ * - Player source — must be OP (permission level ≥ 2, via
+ * {@link Permissions#COMMANDS_GAMEMASTER}). Non-OP players don't even see the command
+ * in tab-completion.
+ * - Non-player source — server console, command block, and datapack
+ * {@code /function} are always allowed regardless of any permission level
+ * or gamerule. This means admins don't need to bump {@code functionPermissionLevel}
+ * just to drive {@code /videoPlace} etc. from a datapack function.
+ *
+ *
+ * The bypass for non-player sources is safe because reaching one of those execution
+ * contexts already requires server-operator trust:
+ *
+ * - Console — physical/admin access to the server.
+ * - Command block — placing one requires OP + {@code /gamerule sendCommandFeedback}
+ * privileges, and {@code /execute as} preserves the underlying source's permissions
+ * (so a non-OP player can't smuggle commands through one).
+ * - Datapack function — installed by the server admin.
+ *
+ */
+public final class CommandPermissions {
+ private CommandPermissions() {}
+
+ /**
+ * Returns {@code true} when the source is allowed to run a {@code /video*} command.
+ *
+ * Player → OP only. Anything else → always allowed.
+ */
+ public static boolean opOrServer(CommandSourceStack s) {
+ if (s.getEntity() instanceof Player) {
+ return s.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER);
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/ejclaw/videoplayer/command/VideoCacheCommand.java b/src/main/java/com/ejclaw/videoplayer/command/VideoCacheCommand.java
index a44fb3e..6d552df 100644
--- a/src/main/java/com/ejclaw/videoplayer/command/VideoCacheCommand.java
+++ b/src/main/java/com/ejclaw/videoplayer/command/VideoCacheCommand.java
@@ -20,7 +20,6 @@ import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.chat.Style;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
-import net.minecraft.server.permissions.Permissions;
import java.net.URI;
import java.util.Map;
@@ -32,8 +31,9 @@ import java.util.Map;
*
{@code /videocache remove } — drop the entry from server config and tell every client
* to delete the matching cache file.
*
- * Replaces the old {@code /videopreload}. Same permission gate
- * ({@link Permissions#COMMANDS_GAMEMASTER}) so command blocks can drive it.
+ *
Replaces the old {@code /videopreload}. Permission gate via
+ * {@link CommandPermissions#opOrServer(CommandSourceStack)} so command blocks and datapack
+ * functions can drive it without touching {@code functionPermissionLevel}.
*/
public final class VideoCacheCommand {
private VideoCacheCommand() {}
@@ -44,7 +44,7 @@ public final class VideoCacheCommand {
private static LiteralArgumentBuilder build(String root) {
return Commands.literal(root)
- .requires(s -> s.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER))
+ .requires(CommandPermissions::opOrServer)
.then(Commands.literal("add")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("url", StringArgumentType.greedyString())
diff --git a/src/main/java/com/ejclaw/videoplayer/command/VideoDeleteCommand.java b/src/main/java/com/ejclaw/videoplayer/command/VideoDeleteCommand.java
index b2fe6b7..9370fbf 100644
--- a/src/main/java/com/ejclaw/videoplayer/command/VideoDeleteCommand.java
+++ b/src/main/java/com/ejclaw/videoplayer/command/VideoDeleteCommand.java
@@ -9,7 +9,6 @@ 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;
@@ -24,7 +23,7 @@ public final class VideoDeleteCommand {
private static com.mojang.brigadier.builder.LiteralArgumentBuilder
build(String name) {
return Commands.literal(name)
- .requires(s -> s.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER))
+ .requires(CommandPermissions::opOrServer)
.then(Commands.argument("pos", BlockPosArgument.blockPos())
.executes(VideoDeleteCommand::run));
}
diff --git a/src/main/java/com/ejclaw/videoplayer/command/VideoMuteCommand.java b/src/main/java/com/ejclaw/videoplayer/command/VideoMuteCommand.java
index 873de63..bc87d02 100644
--- a/src/main/java/com/ejclaw/videoplayer/command/VideoMuteCommand.java
+++ b/src/main/java/com/ejclaw/videoplayer/command/VideoMuteCommand.java
@@ -14,7 +14,6 @@ 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 } */
public final class VideoMuteCommand {
@@ -27,7 +26,7 @@ public final class VideoMuteCommand {
private static com.mojang.brigadier.builder.LiteralArgumentBuilder
build(String name) {
return Commands.literal(name)
- .requires(s -> s.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER))
+ .requires(CommandPermissions::opOrServer)
.then(Commands.argument("pos", BlockPosArgument.blockPos())
.then(Commands.argument("state", StringArgumentType.word())
.executes(VideoMuteCommand::run)));
diff --git a/src/main/java/com/ejclaw/videoplayer/command/VideoPlaceCommand.java b/src/main/java/com/ejclaw/videoplayer/command/VideoPlaceCommand.java
index 0a6ea76..173981f 100644
--- a/src/main/java/com/ejclaw/videoplayer/command/VideoPlaceCommand.java
+++ b/src/main/java/com/ejclaw/videoplayer/command/VideoPlaceCommand.java
@@ -19,7 +19,6 @@ 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;
/**
@@ -53,7 +52,7 @@ public final class VideoPlaceCommand {
private static com.mojang.brigadier.builder.LiteralArgumentBuilder
build(String name) {
return Commands.literal(name)
- .requires(s -> s.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER))
+ .requires(CommandPermissions::opOrServer)
.then(Commands.argument("pos", BlockPosArgument.blockPos())
.then(Commands.argument("facing", StringArgumentType.word())
.then(Commands.argument("width", IntegerArgumentType.integer(1, 32))
diff --git a/src/main/java/com/ejclaw/videoplayer/command/VideoStickCommand.java b/src/main/java/com/ejclaw/videoplayer/command/VideoStickCommand.java
index 2f84b68..1f74046 100644
--- a/src/main/java/com/ejclaw/videoplayer/command/VideoStickCommand.java
+++ b/src/main/java/com/ejclaw/videoplayer/command/VideoStickCommand.java
@@ -6,18 +6,17 @@ 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.server.permissions.Permissions;
import net.minecraft.world.item.ItemStack;
public final class VideoStickCommand {
private VideoStickCommand() {}
public static void register(CommandDispatcher dispatcher) {
- // OP/console/command-block 만 사용 가능. Permissions.COMMANDS_GAMEMASTER = level 2,
- // 즉 /op 받은 플레이어(level 4) 와 콘솔(level 4), command block(default level 2) 통과.
+ // 플레이어는 OP(level 2+) 만, 콘솔/커맨드블럭/함수(/function) 는 무조건 통과.
+ // 따라서 functionPermissionLevel 같은 gamerule 을 만질 필요가 없다.
// 일반 플레이어(level 0) 는 탭 자동완성에도 안 떠야 정상.
dispatcher.register(Commands.literal("videoStick")
- .requires(s -> s.permissions().hasPermission(Permissions.COMMANDS_GAMEMASTER))
+ .requires(CommandPermissions::opOrServer)
.executes(ctx -> run(ctx.getSource())));
}