添加 look, jump, move 命令

This commit is contained in:
tanyaofei 2023-07-28 10:48:52 +08:00
parent 703b13515a
commit f9e19ae7f7
18 changed files with 413 additions and 185 deletions

View File

@ -28,6 +28,9 @@
+ /fp config get - 查看个性化配置 + /fp config get - 查看个性化配置
+ /fp drop - 丢弃手上物品 + /fp drop - 丢弃手上物品
+ /fp dropinv - 丢弃背包物品 + /fp dropinv - 丢弃背包物品
+ /fp look - 让假人看向指定位置
+ /fp move - 让家人移动
+ /fp jump - 让假人跳跃
+ /fp attack - 让假人点击鼠标左键 **(实验性)** + /fp attack - 让假人点击鼠标左键 **(实验性)**
+ /fp use - 让假人点击鼠标右键 **(实验性)** + /fp use - 让假人点击鼠标右键 **(实验性)**
+ /fp cmd - 让假人执行他有权限执行的命令 + /fp cmd - 让假人执行他有权限执行的命令

View File

@ -21,7 +21,7 @@ public abstract class AbstractCommand {
protected final FakeplayerManager fakeplayerManager = FakeplayerManager.instance; protected final FakeplayerManager fakeplayerManager = FakeplayerManager.instance;
public static Argument<Player> targetArgument(@NotNull String nodeName) { public static Argument<Player> target(@NotNull String nodeName) {
return new CustomArgument<>(new StringArgument(nodeName), info -> { return new CustomArgument<>(new StringArgument(nodeName), info -> {
var sender = info.sender(); var sender = info.sender();
return sender.isOp() return sender.isOp()
@ -44,7 +44,7 @@ public abstract class AbstractCommand {
})); }));
} }
public static Argument<List<Player>> multiTargetArgument(@NotNull String nodeName) { public static Argument<List<Player>> targets(@NotNull String nodeName) {
return new CustomArgument<List<Player>, String>(new StringArgument(nodeName), info -> { return new CustomArgument<List<Player>, String>(new StringArgument(nodeName), info -> {
var sender = info.sender(); var sender = info.sender();
var arg = info.currentInput(); var arg = info.currentInput();

View File

@ -2,10 +2,19 @@ package io.github.hello09x.fakeplayer.command;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException; import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.executors.CommandArguments; import dev.jorel.commandapi.executors.CommandArguments;
import dev.jorel.commandapi.executors.CommandExecutor;
import io.github.hello09x.fakeplayer.entity.action.Action; import io.github.hello09x.fakeplayer.entity.action.Action;
import io.github.hello09x.fakeplayer.entity.action.ActionSetting; import io.github.hello09x.fakeplayer.entity.action.ActionSetting;
import io.github.hello09x.fakeplayer.entity.action.PlayerActionManager; import io.github.hello09x.fakeplayer.entity.action.PlayerActionManager;
import io.github.hello09x.fakeplayer.util.MathUtils;
import io.github.hello09x.fakeplayer.util.Unwrapper;
import io.papermc.paper.entity.LookAnchor;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerPlayer;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Location;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.Component.text;
@ -19,9 +28,36 @@ public class ActionCommand extends AbstractCommand {
private final PlayerActionManager actionManager = PlayerActionManager.instance; private final PlayerActionManager actionManager = PlayerActionManager.instance;
private static String toLocationString(@NotNull Location location) {
return StringUtils.joinWith(", ",
MathUtils.round(location.getX(), 0.5),
MathUtils.round(location.getY(), 0.5),
MathUtils.round(location.getZ(), 0.5));
}
public CommandExecutor action(@NotNull Action action, @NotNull ActionSetting setting) {
return (sender, args) -> action(sender, args, action, setting.clone());
}
public void action(@NotNull CommandSender sender, @NotNull CommandArguments args, @NotNull Action action, @NotNull ActionSetting setting) throws WrapperCommandSyntaxException { public void action(@NotNull CommandSender sender, @NotNull CommandArguments args, @NotNull Action action, @NotNull ActionSetting setting) throws WrapperCommandSyntaxException {
var target = getTarget(sender, args); var target = getTarget(sender, args);
actionManager.setAction(target, action, setting); actionManager.setAction(target, action, setting);
String desc;
if (setting.equals(ActionSetting.stop())) {
desc = " 已停止";
} else if (setting.equals(ActionSetting.once())) {
desc = "";
} else {
desc = " 开始";
}
sender.sendMessage(textOfChildren(
text(target.getName()),
text(desc, GRAY),
text(" "),
text(action.name, GRAY)
));
} }
public void sneak(@NotNull CommandSender sender, @NotNull CommandArguments args) throws WrapperCommandSyntaxException { public void sneak(@NotNull CommandSender sender, @NotNull CommandArguments args) throws WrapperCommandSyntaxException {
@ -36,10 +72,77 @@ public class ActionCommand extends AbstractCommand {
sender.sendMessage(textOfChildren( sender.sendMessage(textOfChildren(
text(target.getName(), WHITE), text(target.getName(), WHITE),
text("现在", GRAY), text(" 现在", GRAY),
text(sneaking ? "潜行中" : "取消了潜行", GRAY) text(sneaking ? "潜行中" : "取消了潜行", GRAY)
)); ));
}
public void lookAt(@NotNull CommandSender sender, @NotNull CommandArguments args) throws WrapperCommandSyntaxException {
var target = getTarget(sender, args);
var location = (Location) args.get("location");
target.lookAt(location, LookAnchor.EYES);
sender.sendMessage(textOfChildren(
text(target.getName(), WHITE),
text(" 正在看向 ", GRAY),
text(toLocationString(location), GRAY)
));
}
public CommandExecutor look(@NotNull Direction direction) {
return (sender, args) -> {
var target = getTarget(sender, args);
look(target, direction);
sender.sendMessage(textOfChildren(
text(target.getName(), WHITE),
text(" 看向 ", GRAY),
text(switch (direction) {
case DOWN -> "下方";
case UP -> "上方";
case NORTH -> "北边";
case SOUTH -> "南边";
case WEST -> "西边";
case EAST -> "东边";
}, GRAY)
));
};
}
private void look(
@NotNull Player target,
@NotNull Direction direction
) {
var player = Unwrapper.getServerPlayer(target);
switch (direction) {
case NORTH -> look(player, 180, 0);
case SOUTH -> look(player, 0, 0);
case EAST -> look(player, -90, 0);
case WEST -> look(player, 90, 0);
case UP -> look(player, player.getYRot(), -90);
case DOWN -> look(player, player.getYRot(), 90);
}
}
private void look(@NotNull ServerPlayer player, float yaw, float pitch) {
player.setYRot(yaw % 360);
player.setXRot(MathUtils.clamp(pitch, -90, 90));
}
public CommandExecutor move(float forward, float strafing) {
return (sender, args) -> {
var target = getTarget(sender, args);
var player = Unwrapper.getServerPlayer(target);
float vel = target.isSneaking() ? 0.3F : 1.0F;
if (forward != 0.0F) {
player.zza = vel * forward;
}
if (strafing != 0.0F) {
player.xxa = vel * strafing;
}
sender.sendMessage(textOfChildren(
text(target.getName()),
text(" 动了一下", GRAY)
));
};
} }
} }

View File

@ -9,12 +9,17 @@ import org.jetbrains.annotations.NotNull;
import java.util.Objects; import java.util.Objects;
import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.format.NamedTextColor.RED; import static net.kyori.adventure.text.Component.textOfChildren;
import static net.kyori.adventure.text.format.NamedTextColor.*;
public class CmdCommand extends AbstractCommand { public class CmdCommand extends AbstractCommand {
public final static CmdCommand instance = new CmdCommand(); public final static CmdCommand instance = new CmdCommand();
private static String toCommandString(@NotNull CommandResult command) {
return "/" + command.command().getName() + String.join(" ", command.args());
}
public void cmd(@NotNull CommandSender sender, @NotNull CommandArguments args) throws WrapperCommandSyntaxException { public void cmd(@NotNull CommandSender sender, @NotNull CommandArguments args) throws WrapperCommandSyntaxException {
var target = getTarget(sender, args); var target = getTarget(sender, args);
var cmd = Objects.requireNonNull((CommandResult) args.get("command")); var cmd = Objects.requireNonNull((CommandResult) args.get("command"));
@ -25,7 +30,20 @@ public class CmdCommand extends AbstractCommand {
return; return;
} }
cmd.execute(target); if (!cmd.execute(target)) {
sender.sendMessage(textOfChildren(
text(target.getName(), WHITE),
text(" 执行命令失败: ", GRAY),
text(toCommandString(cmd), RED),
text(" , 请检查命令是否正确以及假人是否有权限", GRAY)
));
}
sender.sendMessage(textOfChildren(
text(target.getName(), WHITE),
text(" 成功执行了命令: ", GRAY),
text(toCommandString(cmd), YELLOW)
));
} }
} }

View File

@ -8,12 +8,15 @@ import dev.jorel.commandapi.arguments.MultiLiteralArgument;
import dev.jorel.commandapi.executors.CommandExecutor; import dev.jorel.commandapi.executors.CommandExecutor;
import io.github.hello09x.fakeplayer.entity.action.Action; import io.github.hello09x.fakeplayer.entity.action.Action;
import io.github.hello09x.fakeplayer.entity.action.ActionSetting; import io.github.hello09x.fakeplayer.entity.action.ActionSetting;
import net.minecraft.core.Direction;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.Arrays;
import static io.github.hello09x.fakeplayer.command.AbstractCommand.multiTargetArgument; import static io.github.hello09x.fakeplayer.command.AbstractCommand.target;
import static io.github.hello09x.fakeplayer.command.AbstractCommand.targetArgument; import static io.github.hello09x.fakeplayer.command.AbstractCommand.targets;
import static io.github.hello09x.fakeplayer.command.ConfigCommand.config;
import static io.github.hello09x.fakeplayer.command.ConfigCommand.configValue;
public class Commands { public class Commands {
@ -28,11 +31,11 @@ public class Commands {
private final static String PERMISSION_CMD = "fakeplayer.cmd"; private final static String PERMISSION_CMD = "fakeplayer.cmd";
public static void register() { public static void register() {
new CommandAPICommand("fakeplayer") command("fakeplayer")
.withAliases("fp") .withAliases("fp")
.withHelp( .withHelp(
"假人相关命令", "假人",
"fakeplayer 可以用来创建一个模拟玩家的假人, 能保持附近区块的刷新、触发怪物生成。同时还提供了一些操作命令让你控制假人的物品、动作等等。" "可以创建模拟玩家的假人, 能保持附近区块的刷新、触发怪物生成。同时还提供了一些操作命令让你控制假人的物品、动作等等。"
) )
.withUsage( .withUsage(
"§6/fp spawn §7- §f创建假人", "§6/fp spawn §7- §f创建假人",
@ -47,133 +50,179 @@ public class Commands {
"§6/fp health [假人] §7- §f查看生命值", "§6/fp health [假人] §7- §f查看生命值",
"§6/fp exp [假人] §7- §f查看经验值", "§6/fp exp [假人] §7- §f查看经验值",
"§6/fp expme [假人] §7- §f转移经验值", "§6/fp expme [假人] §7- §f转移经验值",
"§6/fp attack <once|continuous|interval|stop> [假人] §7- §攻击/破坏",
"§6/fp use <once|continuous|interval|stop> [假人] §7- §f使用/交互/放置",
"§6/fp jump <once|continuous|interval|stop> [假人] §7- §f跳跃",
"§6/fp drop [假人] [-a|--all] §7- §f丢弃手上物品", "§6/fp drop [假人] [-a|--all] §7- §f丢弃手上物品",
"§6/fp dropinv [假人] §7- §f丢弃背包物品", "§6/fp dropinv [假人] §7- §f丢弃背包物品",
"§6/fp sneak [假人] §7- §f开启/取消潜行", "§6/fp look <north|south|east|west|up|down|at> §7- §f看向指定位置",
"§6/fp attack <once|continuous|interval|stop> [假人] §7- §f模拟鼠标左键", "§6/fp move <forward|backward|left|right> §7- §f移动假人",
"§6/fp use <once|continuous|interval|stop> [假人] §7- §f模拟鼠标右键", "§6/fp cmd §7- §f执行命令",
"§6/fp cmd §7- §f让假人执行命令",
"§6/fp reload §7- §f重载配置文件" "§6/fp reload §7- §f重载配置文件"
) )
.withSubcommands( .withSubcommands(
new CommandAPICommand("help") command("help")
.withAliases("?") .withAliases("?")
.withOptionalArguments(new IntegerArgument("page", 1)) .withOptionalArguments(new IntegerArgument("page", 1))
.executesPlayer(HelpCommand.instance::help), .executesPlayer(HelpCommand.instance::help),
new CommandAPICommand("spawn") command("spawn")
.withPermission(PERMISSION_SPAWN) .withPermission(PERMISSION_SPAWN)
.withOptionalArguments(new LocationArgument("location").withPermission(PERMISSION_SPAWN_LOCATION)) .withOptionalArguments(location("location").withPermission(PERMISSION_SPAWN_LOCATION))
.executes(SpawnCommand.instance::spawn), .executes(SpawnCommand.instance::spawn),
new CommandAPICommand("kill") command("kill")
.withPermission(PERMISSION_SPAWN) .withPermission(PERMISSION_SPAWN)
.withOptionalArguments(multiTargetArgument("targets")) .withOptionalArguments(targets("targets"))
.executes(SpawnCommand.instance::kill), .executes(SpawnCommand.instance::kill),
new CommandAPICommand("list") command("list")
.withPermission(PERMISSION_SPAWN) .withPermission(PERMISSION_SPAWN)
.withOptionalArguments(new IntegerArgument("page", 1), new IntegerArgument("size", 1)) .withOptionalArguments(integer("page", 1), integer("size", 1))
.executes(SpawnCommand.instance::list), .executes(SpawnCommand.instance::list),
new CommandAPICommand("distance") command("distance")
.withPermission(PERMISSION_SPAWN) .withPermission(PERMISSION_SPAWN)
.withOptionalArguments(targetArgument("target")) .withOptionalArguments(target("target"))
.executesPlayer(SpawnCommand.instance::distance), .executesPlayer(SpawnCommand.instance::distance),
new CommandAPICommand("exp") command("exp")
.withPermission(PERMISSION_PROFILE) .withPermission(PERMISSION_PROFILE)
.withOptionalArguments(targetArgument("target")) .withOptionalArguments(target("target"))
.executes(ProfileCommand.instance::exp), .executes(ProfileCommand.instance::exp),
new CommandAPICommand("health") command("health")
.withPermission(PERMISSION_PROFILE) .withPermission(PERMISSION_PROFILE)
.withOptionalArguments(targetArgument("target")) .withOptionalArguments(target("target"))
.executes(ProfileCommand.instance::health), .executes(ProfileCommand.instance::health),
new CommandAPICommand("tp") command("tp")
.withPermission(PERMISSION_TP) .withPermission(PERMISSION_TP)
.withOptionalArguments(targetArgument("target")) .withOptionalArguments(target("target"))
.executesPlayer(TpCommand.instance::tp), .executesPlayer(TpCommand.instance::tp),
new CommandAPICommand("tphere") command("tphere")
.withPermission(PERMISSION_TP) .withPermission(PERMISSION_TP)
.withOptionalArguments(targetArgument("target")) .withOptionalArguments(target("target"))
.executesPlayer(TpCommand.instance::tphere), .executesPlayer(TpCommand.instance::tphere),
new CommandAPICommand("tps") command("tps")
.withPermission(PERMISSION_TP) .withPermission(PERMISSION_TP)
.withOptionalArguments(targetArgument("target")) .withOptionalArguments(target("target"))
.executesPlayer(TpCommand.instance::tps), .executesPlayer(TpCommand.instance::tps),
new CommandAPICommand("config") command("config")
.withSubcommands( .withSubcommands(
new CommandAPICommand("get") command("get")
.withArguments(ConfigCommand.configArgument("config")) .withArguments(config("config"))
.executesPlayer(ConfigCommand.instance::getConfig), .executesPlayer(ConfigCommand.instance::getConfig),
new CommandAPICommand("set") command("set")
.withArguments( .withArguments(
ConfigCommand.configArgument("config"), config("config"),
ConfigCommand.configValueArgument("config", "value")) configValue("config", "value"))
.executesPlayer(ConfigCommand.instance::setConfig) .executesPlayer(ConfigCommand.instance::setConfig)
), ),
new CommandAPICommand("attack") command("attack")
.withPermission(PERMISSION_EXPERIMENTAL_ACTION) .withPermission(PERMISSION_EXPERIMENTAL_ACTION)
.withSubcommands(buildActionCommand(Action.ATTACK)), .withSubcommands(action(Action.ATTACK)),
new CommandAPICommand("use") command("use")
.withPermission(PERMISSION_EXPERIMENTAL_ACTION) .withPermission(PERMISSION_EXPERIMENTAL_ACTION)
.withSubcommands(buildActionCommand(Action.USE)), .withSubcommands(action(Action.USE)),
new CommandAPICommand("drop") command("jump")
.withPermission(PERMISSION_ACTION)
.withSubcommands(action(Action.JUMP)),
command("drop")
.withPermission(PERMISSION_ACTION) .withPermission(PERMISSION_ACTION)
.withOptionalArguments( .withOptionalArguments(
targetArgument("target"), target("target"),
new MultiLiteralArgument("all", List.of("-a", "--all"))) literals("all", "-a", "--all"))
.executes((CommandExecutor) (sender, args) -> ActionCommand.instance.action( .executes((CommandExecutor) (sender, args) -> ActionCommand.instance.action(
sender, sender,
args, args,
args.getOptional("all").isPresent() ? Action.DROP_STACK : Action.DROP_ITEM, args.getOptional("all").isPresent() ? Action.DROP_STACK : Action.DROP_ITEM,
ActionSetting.once())), ActionSetting.once())),
new CommandAPICommand("dropinv") command("dropinv")
.withPermission(PERMISSION_ACTION) .withPermission(PERMISSION_ACTION)
.withOptionalArguments(targetArgument("target")) .withOptionalArguments(target("target"))
.executes((CommandExecutor) (sender, args) -> ActionCommand.instance.action(sender, args, Action.DROP_INVENTORY, ActionSetting.once())), .executes(ActionCommand.instance.action(Action.DROP_INVENTORY, ActionSetting.once())),
new CommandAPICommand("sneak") command("sneak")
.withPermission(PERMISSION_ACTION) .withPermission(PERMISSION_ACTION)
.withOptionalArguments(targetArgument("target")) .withOptionalArguments(target("target"))
.withOptionalArguments(new MultiLiteralArgument("sneaking", List.of("true", "false"))) .withOptionalArguments(literals("sneaking", "true", "false"))
.executes(ActionCommand.instance::sneak), .executes(ActionCommand.instance::sneak),
command("look")
.withPermission(PERMISSION_ACTION)
.withSubcommands(
command("north")
.withOptionalArguments(target("target"))
.executes(ActionCommand.instance.look(Direction.NORTH)),
command("south")
.withOptionalArguments(target("target"))
.executes(ActionCommand.instance.look(Direction.SOUTH)),
command("west")
.withOptionalArguments(target("target"))
.executes(ActionCommand.instance.look(Direction.WEST)),
command("east")
.withOptionalArguments(target("target"))
.executes(ActionCommand.instance.look(Direction.EAST)),
command("up")
.withOptionalArguments(target("target"))
.executes(ActionCommand.instance.look(Direction.UP)),
command("down")
.withOptionalArguments(target("target"))
.executes(ActionCommand.instance.look(Direction.DOWN)),
command("at")
.withArguments(new LocationArgument("location"))
.withOptionalArguments(target("target"))
.executes(ActionCommand.instance::lookAt)
),
command("move")
.withPermission(PERMISSION_ACTION)
.withSubcommands(
command("forward")
.withOptionalArguments(target("target"))
.executes(ActionCommand.instance.move(1, 0)),
command("backward")
.withOptionalArguments(target("target"))
.executes(ActionCommand.instance.move(-1, 0)),
command("left")
.withOptionalArguments(target("target"))
.executes(ActionCommand.instance.move(0, 1)),
command("right")
.withOptionalArguments(target("target"))
.executes(ActionCommand.instance.move(0, -1))
),
new CommandAPICommand("expme") command("expme")
.withPermission(PERMISSION_EXP) .withPermission(PERMISSION_EXP)
.withOptionalArguments(targetArgument("target")) .withOptionalArguments(target("target"))
.executesPlayer(ExpCommand.instance::expme), .executesPlayer(ExpCommand.instance::expme),
new CommandAPICommand("cmd") command("cmd")
.withPermission(PERMISSION_CMD) .withPermission(PERMISSION_CMD)
.withArguments( .withArguments(
targetArgument("target"), target("target"),
new CommandArgument("command")) new CommandArgument("command"))
.executes(CmdCommand.instance::cmd), .executes(CmdCommand.instance::cmd),
new CommandAPICommand("reload") command("reload")
.withPermission(PERMISSION_ADMIN) .withPermission(PERMISSION_ADMIN)
.executes(ReloadCommand.instance::reload) .executes(ReloadCommand.instance::reload)
).register(); ).register();
} }
private static CommandAPICommand[] buildActionCommand(@NotNull Action action) { private static CommandAPICommand[] action(@NotNull Action action) {
return new CommandAPICommand[]{ return new CommandAPICommand[]{
new CommandAPICommand("once") command("once")
.withOptionalArguments(targetArgument("target")) .withOptionalArguments(target("target"))
.executes((CommandExecutor) (sender, args) -> ActionCommand.instance.action(sender, args, action, ActionSetting.once()) .executes(ActionCommand.instance.action(action, ActionSetting.once())
), ),
new CommandAPICommand("continuous") command("continuous")
.withOptionalArguments(targetArgument("target")) .withOptionalArguments(target("target"))
.executes((CommandExecutor) (sender, args) -> ActionCommand.instance.action(sender, args, action, ActionSetting.continuous()) .executes(ActionCommand.instance.action(action, ActionSetting.continuous())),
command("stop")
.withOptionalArguments(target("target"))
.executes(ActionCommand.instance.action(action, ActionSetting.stop())
), ),
new CommandAPICommand("stop") command("interval")
.withOptionalArguments(targetArgument("target"))
.executes((CommandExecutor) (sender, args) -> ActionCommand.instance.action(sender, args, action, ActionSetting.stop())
),
new CommandAPICommand("interval")
.withOptionalArguments( .withOptionalArguments(
new IntegerArgument("interval", 1), integer("interval", 1),
targetArgument("target")) target("target"))
.executes((sender, args) -> { .executes((sender, args) -> {
int interval = (int) args.getOptional("interval").orElse(1); int interval = (int) args.getOptional("interval").orElse(1);
ActionCommand.instance.action(sender, args, action, ActionSetting.interval(interval)); ActionCommand.instance.action(sender, args, action, ActionSetting.interval(interval));
@ -181,5 +230,21 @@ public class Commands {
}; };
} }
private static CommandAPICommand command(@NotNull String name) {
return new CommandAPICommand(name);
}
public static IntegerArgument integer(String name, int min) {
return new IntegerArgument(name, min);
}
public static LocationArgument location(String name) {
return new LocationArgument(name);
}
public static MultiLiteralArgument literals(String name, String... literals) {
return new MultiLiteralArgument(name, Arrays.asList(literals));
}
} }

View File

@ -24,7 +24,7 @@ public class ConfigCommand extends AbstractCommand {
private final UserConfigRepository repository = UserConfigRepository.instance; private final UserConfigRepository repository = UserConfigRepository.instance;
public static Argument<Config<Object>> configArgument(String nodeName) { public static Argument<Config<Object>> config(String nodeName) {
return new CustomArgument<>(new StringArgument(nodeName), info -> { return new CustomArgument<>(new StringArgument(nodeName), info -> {
var arg = info.currentInput(); var arg = info.currentInput();
try { try {
@ -35,7 +35,7 @@ public class ConfigCommand extends AbstractCommand {
}).replaceSuggestions(ArgumentSuggestions.strings(Arrays.stream(Configs.values()).map(Config::name).toList())); }).replaceSuggestions(ArgumentSuggestions.strings(Arrays.stream(Configs.values()).map(Config::name).toList()));
} }
public static Argument<Object> configValueArgument(String configNodeName, String nodeName) { public static Argument<Object> configValue(String configNodeName, String nodeName) {
return new CustomArgument<>(new StringArgument(nodeName), info -> { return new CustomArgument<>(new StringArgument(nodeName), info -> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
var config = Objects.requireNonNull((Config<Object>) info.previousArgs().get(configNodeName)); var config = Objects.requireNonNull((Config<Object>) info.previousArgs().get(configNodeName));

View File

@ -26,12 +26,11 @@ public class SpawnCommand extends AbstractCommand {
private static String toLocationString(@NotNull Location location) { private static String toLocationString(@NotNull Location location) {
return location.getWorld().getName() return location.getWorld().getName()
+ " 世界: " + ": "
+ StringUtils.joinWith(", ", + StringUtils.joinWith(", ",
MathUtils.round(location.getX(), 0.5), MathUtils.round(location.getX(), 0.5),
MathUtils.round(location.getY(), 0.5), MathUtils.round(location.getY(), 0.5),
MathUtils.round(location.getZ(), 0.5)); MathUtils.round(location.getZ(), 0.5));
} }
public void spawn(@NotNull CommandSender sender, CommandArguments args) { public void spawn(@NotNull CommandSender sender, CommandArguments args) {
@ -131,9 +130,9 @@ public class SpawnCommand extends AbstractCommand {
var distance = location1.distance(location2); var distance = location1.distance(location2);
sender.sendMessage(textOfChildren( sender.sendMessage(textOfChildren(
text("你与 "), text("你与 ", GRAY),
text(target.getName()), text(target.getName()),
text(" 相距 "), text(" 相距 ", GRAY),
text(MathUtils.round(distance, 0.5), WHITE) text(MathUtils.round(distance, 0.5), WHITE)
)); ));
} }

View File

@ -130,6 +130,7 @@ public class FakePlayer {
bukkitPlayer.setInvulnerable(invulnerable); bukkitPlayer.setInvulnerable(invulnerable);
bukkitPlayer.setCollidable(collidable); bukkitPlayer.setCollidable(collidable);
bukkitPlayer.setCanPickupItems(pickupItems); bukkitPlayer.setCanPickupItems(pickupItems);
bukkitPlayer.getInventory().clear(); // 一些同步背包的插件可能在 JOIN 的时候恢复背包, 这个时候清空掉防止物品被复制了
new BukkitRunnable() { new BukkitRunnable() {
@Override @Override
public void run() { public void run() {

View File

@ -1,7 +1,7 @@
package io.github.hello09x.fakeplayer.entity.action; package io.github.hello09x.fakeplayer.entity.action;
import io.github.hello09x.fakeplayer.util.Tracer; import io.github.hello09x.fakeplayer.util.Tracer;
import net.minecraft.core.BlockPos; import lombok.AllArgsConstructor;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
@ -13,14 +13,15 @@ import org.jetbrains.annotations.NotNull;
import static net.minecraft.network.protocol.game.ServerboundPlayerActionPacket.Action.*; import static net.minecraft.network.protocol.game.ServerboundPlayerActionPacket.Action.*;
@AllArgsConstructor
public enum Action { public enum Action {
USE { USE("交互/使用/放置") {
@Override @Override
@SuppressWarnings("resource") @SuppressWarnings("resource")
public boolean tick(@NotNull ActionPack ap) { public boolean tick(@NotNull ActionPack ap, @NotNull ActionSetting setting) {
if (ap.itemUseFreeze > 0) { if (ap.use.freeze > 0) {
ap.itemUseFreeze--; ap.use.freeze--;
return false; return false;
} }
@ -42,7 +43,7 @@ public enum Action {
var result = player.gameMode.useItemOn(player, world, player.getItemInHand(hand), hand, blockHit); var result = player.gameMode.useItemOn(player, world, player.getItemInHand(hand), hand, blockHit);
if (result.consumesAction()) { if (result.consumesAction()) {
player.swing(hand); player.swing(hand);
ap.itemUseFreeze = 3; ap.use.freeze = 3;
return true; return true;
} }
} }
@ -55,18 +56,18 @@ public enum Action {
boolean itemFrameEmpty = (entity instanceof ItemFrame) && ((ItemFrame) entity).getItem().isEmpty(); boolean itemFrameEmpty = (entity instanceof ItemFrame) && ((ItemFrame) entity).getItem().isEmpty();
var pos = entityHit.getLocation().subtract(entity.getX(), entity.getY(), entity.getZ()); var pos = entityHit.getLocation().subtract(entity.getX(), entity.getY(), entity.getZ());
if (entity.interactAt(player, pos, hand).consumesAction()) { if (entity.interactAt(player, pos, hand).consumesAction()) {
ap.itemUseFreeze = 3; ap.use.freeze = 3;
return true; return true;
} }
if (player.interactOn(entity, hand).consumesAction() && !(handWasEmpty && itemFrameEmpty)) { if (player.interactOn(entity, hand).consumesAction() && !(handWasEmpty && itemFrameEmpty)) {
ap.itemUseFreeze = 3; ap.use.freeze = 3;
return true; return true;
} }
} }
} }
var handItem = player.getItemInHand(hand); var handItem = player.getItemInHand(hand);
if (player.gameMode.useItem(player, player.level(), handItem, hand).consumesAction()) { if (player.gameMode.useItem(player, player.level(), handItem, hand).consumesAction()) {
ap.itemUseFreeze = 3; ap.use.freeze = 3;
return true; return true;
} }
} }
@ -74,16 +75,16 @@ public enum Action {
} }
@Override @Override
public void stop(@NotNull ActionPack ap) { public void stop(@NotNull ActionPack ap, @NotNull ActionSetting setting) {
ap.itemUseFreeze = 0; ap.use.freeze = 0;
ap.player.releaseUsingItem(); ap.player.releaseUsingItem();
} }
}, },
ATTACK { ATTACK("攻击/破坏") {
@Override @Override
@SuppressWarnings("resource") @SuppressWarnings("resource")
public boolean tick(@NotNull ActionPack ap) { public boolean tick(@NotNull ActionPack ap, @NotNull ActionSetting setting) {
var player = ap.player; var player = ap.player;
var hit = getTarget(player); var hit = getTarget(player);
switch (hit.getType()) { switch (hit.getType()) {
@ -96,8 +97,8 @@ public enum Action {
return true; return true;
} }
case BLOCK -> { case BLOCK -> {
if (ap.blockHitFreeze > 0) { if (ap.attack.freeze > 0) {
ap.blockHitFreeze--; ap.attack.freeze--;
return false; return false;
} }
@ -109,8 +110,8 @@ public enum Action {
return false; return false;
} }
if (ap.curBlockPos != null && player.level().getBlockState(ap.curBlockPos).isAir()) { if (ap.attack.pos != null && player.level().getBlockState(ap.attack.pos).isAir()) {
ap.curBlockPos = null; ap.attack.pos = null;
return false; return false;
} }
@ -124,11 +125,11 @@ public enum Action {
player.level().getMaxBuildHeight(), player.level().getMaxBuildHeight(),
-1 -1
); );
ap.blockHitFreeze = 5; ap.attack.freeze = 5;
} else if (ap.curBlockPos == null || !ap.curBlockPos.equals(pos)) { } else if (ap.attack.pos == null || !ap.attack.pos.equals(pos)) {
if (ap.curBlockPos != null) { if (ap.attack.pos != null) {
player.gameMode.handleBlockBreakAction( player.gameMode.handleBlockBreakAction(
ap.curBlockPos, ap.attack.pos,
ABORT_DESTROY_BLOCK, ABORT_DESTROY_BLOCK,
side, side,
player.level().getMaxBuildHeight(), player.level().getMaxBuildHeight(),
@ -144,20 +145,20 @@ public enum Action {
-1 -1
); );
if (!state.isAir() && ap.curBlockPgs == 0) { if (!state.isAir() && ap.attack.progress == 0) {
state.attack(player.level(), pos, player); state.attack(player.level(), pos, player);
} }
if (!state.isAir() && state.getDestroyProgress(player, player.level(), pos) >= 1) { if (!state.isAir() && state.getDestroyProgress(player, player.level(), pos) >= 1) {
ap.curBlockPos = null; ap.attack.pos = null;
broken = true; broken = true;
} else { } else {
ap.curBlockPos = pos; ap.attack.pos = pos;
ap.curBlockPgs = 0; ap.attack.progress = 0;
} }
} else { } else {
ap.curBlockPgs += state.getDestroyProgress(player, player.level(), pos); ap.attack.progress += state.getDestroyProgress(player, player.level(), pos);
if (ap.curBlockPgs >= 1) { if (ap.attack.progress >= 1) {
player.gameMode.handleBlockBreakAction( player.gameMode.handleBlockBreakAction(
pos, pos,
STOP_DESTROY_BLOCK, STOP_DESTROY_BLOCK,
@ -165,11 +166,11 @@ public enum Action {
player.level().getMaxBuildHeight(), player.level().getMaxBuildHeight(),
-1 -1
); );
ap.curBlockPos = null; ap.attack.pos = null;
ap.blockHitFreeze = 5; ap.attack.freeze = 5;
broken = true; broken = true;
} }
player.level().destroyBlockProgress(-1, pos, (int) (ap.curBlockPgs * 10)); player.level().destroyBlockProgress(-1, pos, (int) (ap.attack.progress * 10));
} }
player.resetLastActionTime(); player.resetLastActionTime();
@ -181,30 +182,52 @@ public enum Action {
} }
@Override @Override
public void stop(@NotNull ActionPack ap) { @SuppressWarnings("resource")
if (ap.curBlockPos == null) { public void stop(@NotNull ActionPack ap, @NotNull ActionSetting setting) {
if (ap.attack.pos == null) {
return; return;
} }
var player = ap.player; var player = ap.player;
player.level().destroyBlockProgress(-1, ap.curBlockPos, -1); player.level().destroyBlockProgress(-1, ap.attack.pos, -1);
player.gameMode.handleBlockBreakAction( player.gameMode.handleBlockBreakAction(
ap.curBlockPos, ap.attack.pos,
ABORT_DESTROY_BLOCK, ABORT_DESTROY_BLOCK,
Direction.DOWN, Direction.DOWN,
player.level().getMaxBuildHeight(), player.level().getMaxBuildHeight(),
-1 -1
); );
ap.curBlockPos = null; ap.attack.pos = null;
ap.blockHitFreeze = 0; ap.attack.freeze = 0;
ap.curBlockPgs = 0; ap.attack.progress = 0;
}
},
JUMP("") {
@Override
public boolean tick(@NotNull ActionPack ap, @NotNull ActionSetting setting) {
var player = ap.player;
if (setting.limit == 1) {
if (player.onGround()) {
player.jumpFromGround();
return true;
}
}
player.setJumping(true);
return true;
}
@Override
public void inactiveTick(@NotNull ActionPack ap, @NotNull ActionSetting setting) {
ap.player.setJumping(false);
} }
}, },
DROP_ITEM { DROP_ITEM("丢弃手上物品") {
@Override @Override
public boolean tick(@NotNull ActionPack ap) { public boolean tick(@NotNull ActionPack ap, @NotNull ActionSetting setting) {
var player = ap.player; var player = ap.player;
player.resetLastActionTime(); player.resetLastActionTime();
player.drop(false); player.drop(false);
@ -212,9 +235,9 @@ public enum Action {
} }
}, },
DROP_STACK { DROP_STACK("丢弃手上整组物品") {
@Override @Override
public boolean tick(@NotNull ActionPack ap) { public boolean tick(@NotNull ActionPack ap, @NotNull ActionSetting setting) {
var player = ap.player; var player = ap.player;
player.resetLastActionTime(); player.resetLastActionTime();
player.drop(true); player.drop(true);
@ -222,9 +245,9 @@ public enum Action {
} }
}, },
DROP_INVENTORY { DROP_INVENTORY("丢弃背包物品") {
@Override @Override
public boolean tick(@NotNull ActionPack ap) { public boolean tick(@NotNull ActionPack ap, @NotNull ActionSetting setting) {
var player = ap.player; var player = ap.player;
dropInventory(player); dropInventory(player);
return true; return true;
@ -232,6 +255,8 @@ public enum Action {
}; };
public final String name;
static HitResult getTarget(ServerPlayer player) { static HitResult getTarget(ServerPlayer player) {
double reach = player.gameMode.isCreative() ? 5 : 4.5f; double reach = player.gameMode.isCreative() ? 5 : 4.5f;
return Tracer.rayTrace(player, 1, reach, false); return Tracer.rayTrace(player, 1, reach, false);
@ -244,31 +269,13 @@ public enum Action {
} }
} }
public abstract boolean tick(@NotNull ActionPack ap);
public void stop(@NotNull ActionPack ap) {} public abstract boolean tick(@NotNull ActionPack ap, @NotNull ActionSetting setting);
public void inactiveTick(@NotNull ActionPack ap) { public void stop(@NotNull ActionPack ap, @NotNull ActionSetting setting) {}
this.stop(ap);
}
public static class ActionPack {
public final ServerPlayer player;
// attack
public BlockPos curBlockPos;
public float curBlockPgs;
public int blockHitFreeze;
// use
public int itemUseFreeze;
public ActionPack(ServerPlayer player) {
this.player = player;
}
public void inactiveTick(@NotNull ActionPack ap, @NotNull ActionSetting setting) {
this.stop(ap, setting);
} }

View File

@ -5,13 +5,13 @@ import net.minecraft.server.level.ServerPlayer;
public class ActionManager { public class ActionManager {
public final Action action; public final Action action;
public final Action.ActionPack actionPack; public final ActionPack actionPack;
public ActionSetting setting; public ActionSetting setting;
public ActionManager(ServerPlayer player, Action action, ActionSetting setting) { public ActionManager(ServerPlayer player, Action action, ActionSetting setting) {
this.action = action; this.action = action;
this.setting = setting; this.setting = setting;
this.actionPack = new Action.ActionPack(player); this.actionPack = new ActionPack(player);
} }
public void tick() { public void tick() {
@ -26,7 +26,7 @@ public class ActionManager {
return; return;
} }
var done = action.tick(this.actionPack); var done = action.tick(this.actionPack, this.setting);
if (done) { if (done) {
if (setting.times > 0) { if (setting.times > 0) {
setting.times--; setting.times--;
@ -36,11 +36,11 @@ public class ActionManager {
} }
public void inactiveTick() { public void inactiveTick() {
action.inactiveTick(this.actionPack); action.inactiveTick(this.actionPack, this.setting);
} }
public void stop() { public void stop() {
action.stop(this.actionPack); action.stop(this.actionPack, this.setting);
} }

View File

@ -0,0 +1,28 @@
package io.github.hello09x.fakeplayer.entity.action;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerPlayer;
public class ActionPack {
public final ServerPlayer player;
public final AttackActionPack attack = new AttackActionPack();
public final UseActionPack use = new UseActionPack();
public ActionPack(ServerPlayer player) {
this.player = player;
}
public final static class AttackActionPack {
public BlockPos pos;
public float progress;
public int freeze;
}
public final static class UseActionPack {
public int freeze;
}
}

View File

@ -1,9 +1,17 @@
package io.github.hello09x.fakeplayer.entity.action; package io.github.hello09x.fakeplayer.entity.action;
public class ActionSetting { import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public class ActionSetting implements Cloneable {
/** /**
* 次数 * 总次数
*/
public final int limit;
/**
* 剩余次数
*/ */
public int times; public int times;
@ -18,31 +26,38 @@ public class ActionSetting {
public int wait; public int wait;
public ActionSetting(int times, int interval) { public ActionSetting(int times, int interval) {
this.times = times; this(times, interval, 0);
this.interval = interval;
this.wait = 0;
} }
public ActionSetting(int times, int interval, int wait) { public ActionSetting(int times, int interval, int wait) {
this.limit = times;
this.times = times; this.times = times;
this.interval = interval; this.interval = interval;
this.wait = wait; this.wait = wait;
} }
public static ActionSetting once() { public static ActionSetting once() {
return new ActionSetting(1, 1, 0); return new ActionSetting(1, 1 );
} }
public static ActionSetting stop() { public static ActionSetting stop() {
return new ActionSetting(0, 1, 0); return new ActionSetting(0, 1);
} }
public static ActionSetting interval(int interval) { public static ActionSetting interval(int interval) {
return new ActionSetting(-1, interval, 0); return new ActionSetting(-1, interval);
} }
public static ActionSetting continuous() { public static ActionSetting continuous() {
return new ActionSetting(-1, 1, 0); return new ActionSetting(-1, 1);
} }
@Override
public ActionSetting clone() {
return new ActionSetting(
this.times,
this.interval,
this.wait
);
}
} }

View File

@ -32,7 +32,7 @@ public class PlayerListeners implements Listener {
* 拒绝假人用过的 ID 上线 * 拒绝假人用过的 ID 上线
*/ */
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void handleUsedIdLogin(@NotNull AsyncPlayerPreLoginEvent event) { public void onLogin(@NotNull AsyncPlayerPreLoginEvent event) {
if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { if (event.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) {
return; return;
} }
@ -49,7 +49,7 @@ public class PlayerListeners implements Listener {
* 死亡退出游戏 * 死亡退出游戏
*/ */
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void handlePlayerDeath(@NotNull PlayerDeathEvent event) { public void onDead(@NotNull PlayerDeathEvent event) {
var player = event.getPlayer(); var player = event.getPlayer();
if (!manager.isFake(player)) { if (!manager.isFake(player)) {
return; return;
@ -63,7 +63,7 @@ public class PlayerListeners implements Listener {
* 退出游戏掉落背包 * 退出游戏掉落背包
*/ */
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
public void handlePlayerQuit(@NotNull PlayerQuitEvent event) { public void onQuit(@NotNull PlayerQuitEvent event) {
var player = event.getPlayer(); var player = event.getPlayer();
if (!manager.isFake(player)) { if (!manager.isFake(player)) {
return; return;

View File

@ -122,7 +122,7 @@ public class FakeplayerManager {
usedIdRepository.add(bukkitPlayer.getUniqueId()); usedIdRepository.add(bukkitPlayer.getUniqueId());
dispatchCommands(bukkitPlayer, properties.getPreparingCommands()); dispatchCommands(bukkitPlayer, properties.getPreparingCommands());
performCommands(bukkitPlayer); performCommands(bukkitPlayer, properties.getSelfCommands());
bukkitPlayer.teleport(spawnAt); // 当前 tick 必须传到出生点否则无法触发区块刷新 bukkitPlayer.teleport(spawnAt); // 当前 tick 必须传到出生点否则无法触发区块刷新
spawnAt.getWorld().playSound(spawnAt, Sound.ENTITY_ENDERMAN_TELEPORT, 1.0F, 1.0F); spawnAt.getWorld().playSound(spawnAt, Sound.ENTITY_ENDERMAN_TELEPORT, 1.0F, 1.0F);
@ -239,9 +239,7 @@ public class FakeplayerManager {
Metadatas.NAME_SEQUENCE.get(fakePlayer).asInt() Metadatas.NAME_SEQUENCE.get(fakePlayer).asInt()
); );
Arrays.stream(Metadatas.values()).forEach(meta -> meta.remove(fakePlayer)); Arrays.stream(Metadatas.values()).forEach(meta -> meta.remove(fakePlayer));
if (properties.isDropInventoryOnQuiting()) { Action.dropInventory(Unwrapper.getServerPlayer(fakePlayer));
Action.dropInventory(Unwrapper.getServerPlayer(fakePlayer));
}
} }
/** /**
@ -298,12 +296,15 @@ public class FakeplayerManager {
return uuid; return uuid;
} }
public void performCommands(@NotNull Player player) { public void performCommands(@NotNull Player player, @NotNull List<String> commands) {
if (commands.isEmpty()) {
return;
}
if (!isFake(player)) { if (!isFake(player)) {
return; return;
} }
for (var cmd : properties.getSelfCommands()) { for (var cmd : commands) {
cmd = cmd.trim(); cmd = cmd.trim();
if (cmd.startsWith("/")) { if (cmd.startsWith("/")) {
cmd = cmd.substring(1); cmd = cmd.substring(1);
@ -327,7 +328,7 @@ public class FakeplayerManager {
var server = Bukkit.getServer(); var server = Bukkit.getServer();
var sender = Bukkit.getConsoleSender(); var sender = Bukkit.getConsoleSender();
for (var cmd : properties.getPreparingCommands()) { for (var cmd : commands) {
cmd = cmd.trim(); cmd = cmd.trim();
if (cmd.startsWith("/")) { if (cmd.startsWith("/")) {
cmd = cmd.substring(1); cmd = cmd.substring(1);

View File

@ -77,11 +77,6 @@ public class FakeplayerProperties extends AbstractProperties<FakeplayerPropertie
*/ */
private boolean simulateLogin; private boolean simulateLogin;
/**
* 下线时是否丢弃背包
*/
private boolean dropInventoryOnQuiting;
public FakeplayerProperties(@NotNull JavaPlugin plugin, @NotNull String version) { public FakeplayerProperties(@NotNull JavaPlugin plugin, @NotNull String version) {
super(plugin, version); super(plugin, version);
} }
@ -102,7 +97,6 @@ public class FakeplayerProperties extends AbstractProperties<FakeplayerPropertie
this.destroyCommands = file.getStringList("destroy-commands"); this.destroyCommands = file.getStringList("destroy-commands");
this.nameTemplate = file.getString("name-template", ""); this.nameTemplate = file.getString("name-template", "");
this.simulateLogin = file.getBoolean("simulate-login", false); this.simulateLogin = file.getBoolean("simulate-login", false);
this.dropInventoryOnQuiting = file.getBoolean("drop-inventory-on-quiting", true);
if (this.nameTemplate.startsWith("-")) { if (this.nameTemplate.startsWith("-")) {
log.warning("假人名称模版不能以 - 开头, 该配置不会生效: " + this.nameTemplate); log.warning("假人名称模版不能以 - 开头, 该配置不会生效: " + this.nameTemplate);

View File

@ -10,5 +10,9 @@ public class MathUtils {
return Math.floor(num / base) * base; return Math.floor(num / base) * base;
} }
public static float clamp(float value, float min, float max) {
return value < min ? min : Math.min(value, max);
}
} }

View File

@ -24,16 +24,6 @@ name-template: ''
# 如果玩家只是切换服务器, 那么不会触发跟随下线 # 如果玩家只是切换服务器, 那么不会触发跟随下线
follow-quiting: true follow-quiting: true
# 下线时是否丢弃背包
# 如果服务器有背包同步之类的插件,请验证是否会有复制 bug
# 这个 bug 原因是先保存了背包再丢弃背包的物品导致地上和背包各一份
# 验证方法如下:
# 1. 创建假人,并给他一点东西
# 2. kick 掉假人, 此时他会丢弃背包的物品
# 3. 重新创建假人, 看看他背包的物品是否多了一份
drop-inventory-on-quiting: true
# 是否检测 IP
# 如果启用, 则一个 IP 只能创建 `maximum` 个假人 # 如果启用, 则一个 IP 只能创建 `maximum` 个假人
# 能够避免玩家开小号疯狂创建假人 # 能够避免玩家开小号疯狂创建假人
detect-ip: true detect-ip: true
@ -75,7 +65,7 @@ destroy-commands:
# 自执行命令 # 自执行命令
# 假人在诞生时会以自己的身份按顺序执行命令 # 假人在诞生时会以自己的身份按顺序执行命令
# 你可以在这里做一些 /register 之类的命令 # 你可以在这里做添加 /register 和 /login 命令来防止 `AuthMe` 等插件踢掉超时未登陆的玩家
self-commands: self-commands:
- '' - ''
- '' - ''

View File

@ -32,7 +32,7 @@ permissions:
default: op default: op
fakeplayer.action: fakeplayer.action:
description: '拥有 drop, dropinv, sneak 命令权限' description: '拥有 drop, dropinv, sneak, look, move, jump 命令权限'
default: op default: op
fakeplayer.experimental.action: fakeplayer.experimental.action: