Author SHA1 Message Date
AxTps b2d09b241c feat: /botall spawn list一键召唤名单内所有假人
build / build (push) Has been cancelled
2026-10-05 11:19:24 +08:00
AxTps f0fac64e84 feat: /botall支持批量召唤(spawn)和批量移除(kill)
spawn <prefix> <count>生成prefix1~prefixN;kill移除名单内假人。createBot改为public供复用。
2026-10-05 11:04:32 +08:00
AxTps ccbf442413 feat: 新增/botlist假人名单管理,/botall按名单过滤
名单持久化到config/slsa-bot-list.json。名单非空时/botall只操作名单内假人,为空时操作所有。支持add/remove/clear/show/addall。
2026-10-05 10:57:55 +08:00
AxTps 541ffe5ed2 feat: /botall支持全套假人操作(移动/看向/动作/骑乘等)
镜像BotCommand的action结构,对isBot()假人批量执行。修复权限判断用Commands.LEVEL_OWNERS.check。
2026-10-05 10:45:29 +08:00
AxTps ea93b9dcdf feat: 新增/botall批量控制假人命令(sit/stand)
遍历所有isBot()为true的EntityPlayerMPFake,批量坐下/站起。重构SitCommand抽出sitPlayer公共方法。
2026-10-05 10:39:42 +08:00
6 changed files with 459 additions and 14 deletions

No files matched your search

@@ -2,7 +2,9 @@ package com.github.zly2006.carpetslsaddition;
import carpet.CarpetExtension;
import carpet.CarpetServer;
import com.github.zly2006.carpetslsaddition.command.BotAllCommand;
import com.github.zly2006.carpetslsaddition.command.BotCommand;
import com.github.zly2006.carpetslsaddition.command.BotListCommand;
import com.github.zly2006.carpetslsaddition.command.HatCommand;
import com.github.zly2006.carpetslsaddition.command.SitCommand;
import com.google.common.collect.Maps;
@@ -81,6 +83,8 @@ public class ServerMain implements ModInitializer, CarpetExtension {
HatCommand.register(dispatcher);
SitCommand.register(dispatcher);
BotCommand.register(dispatcher);
BotAllCommand.register(dispatcher);
BotListCommand.register(dispatcher);
LOGGER.info("[SLSA] commands registered: hat/sit/bot");
} catch (Throwable t) {
LOGGER.error("[SLSA] failed to register commands", t);
@@ -0,0 +1,234 @@
package com.github.zly2006.carpetslsaddition.command;
import carpet.fakes.ServerPlayerInterface;
import carpet.helpers.EntityPlayerActionPack;
import carpet.patches.EntityPlayerMPFake;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.util.access.SLSBotAccessor;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.coordinates.RotationArgument;
import net.minecraft.commands.arguments.coordinates.Vec3Argument;
import net.minecraft.core.Direction;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import static net.minecraft.commands.Commands.argument;
import static net.minecraft.commands.Commands.literal;
/**
* /botall —— 批量控制本模组生成的所有假人。
*
* <p>与 {@link BotCommand} 的区别:{@code /bot <player>} 操作单个指定玩家,
* 而 {@code /botall} 对其余命令中出现的所有本模组假人(isBot()==true)批量执行同一操作。
*/
public class BotAllCommand {
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
LiteralArgumentBuilder<CommandSourceStack> command = literal("botall")
.requires(source -> Commands.LEVEL_OWNERS.check(source.permissions()) || me.lucko.fabric.api.permissions.v0.Permissions.check(source, "slsaddition.command.bot"))
.then(literal("stop").executes(batch(EntityPlayerActionPack::stopAll)))
.then(actionCommand("use", EntityPlayerActionPack.ActionType.USE))
.then(actionCommand("jump", EntityPlayerActionPack.ActionType.JUMP))
.then(actionCommand("attack", EntityPlayerActionPack.ActionType.ATTACK))
.then(actionCommand("drop", EntityPlayerActionPack.ActionType.DROP_ITEM))
.then(dropCommand("dropStack", true))
.then(actionCommand("swapHands", EntityPlayerActionPack.ActionType.SWAP_HANDS))
.then(literal("hotbar").then(argument("slot", IntegerArgumentType.integer(1, 9))
.executes(c -> batchAp(c, ap -> ap.setSlot(IntegerArgumentType.getInteger(c, "slot"))))))
.then(literal("mount").executes(batch(ap -> ap.mount(true)))
.then(literal("anything").executes(batch(ap -> ap.mount(false)))))
.then(literal("dismount").executes(batch(EntityPlayerActionPack::dismount)))
.then(literal("sneak").executes(batch(ap -> ap.setSneaking(true))))
.then(literal("unsneak").executes(batch(ap -> ap.setSneaking(false))))
.then(literal("sprint").executes(batch(ap -> ap.setSprinting(true))))
.then(literal("unsprint").executes(batch(ap -> ap.setSprinting(false))))
.then(literal("look")
.then(literal("north").executes(batch(ap -> ap.look(Direction.NORTH))))
.then(literal("south").executes(batch(ap -> ap.look(Direction.SOUTH))))
.then(literal("east").executes(batch(ap -> ap.look(Direction.EAST))))
.then(literal("west").executes(batch(ap -> ap.look(Direction.WEST))))
.then(literal("up").executes(batch(ap -> ap.look(Direction.UP))))
.then(literal("down").executes(batch(ap -> ap.look(Direction.DOWN))))
.then(literal("at").then(argument("position", Vec3Argument.vec3())
.executes(c -> batchAp(c, ap -> ap.lookAt(Vec3Argument.getVec3(c, "position"))))))
.then(argument("direction", RotationArgument.rotation())
.executes(c -> batchAp(c, ap -> ap.look(RotationArgument.getRotation(c, "direction").getRotation(c.getSource()))))))
.then(literal("turn")
.then(literal("left").executes(batch(ap -> ap.turn(-90, 0))))
.then(literal("right").executes(batch(ap -> ap.turn(90, 0))))
.then(literal("back").executes(batch(ap -> ap.turn(180, 0))))
.then(argument("rotation", RotationArgument.rotation())
.executes(c -> batchAp(c, ap -> ap.turn(RotationArgument.getRotation(c, "rotation").getRotation(c.getSource()))))))
.then(literal("move").executes(batch(EntityPlayerActionPack::stopMovement))
.then(literal("forward").executes(batch(ap -> ap.setForward(1))))
.then(literal("backward").executes(batch(ap -> ap.setForward(-1))))
.then(literal("left").executes(batch(ap -> ap.setStrafing(1))))
.then(literal("right").executes(batch(ap -> ap.setStrafing(-1)))))
.then(literal("sit").executes(BotAllCommand::sitAll))
.then(literal("stand").executes(BotAllCommand::standAll))
.then(literal("spawn")
.then(literal("list").executes(BotAllCommand::spawnFromList))
.then(argument("prefix", com.mojang.brigadier.arguments.StringArgumentType.word())
.then(argument("count", IntegerArgumentType.integer(1, 1000))
.executes(BotAllCommand::spawnBatch))))
.then(literal("kill").executes(BotAllCommand::killBatch));
dispatcher.register(command);
}
private static LiteralArgumentBuilder<CommandSourceStack> actionCommand(String name, EntityPlayerActionPack.ActionType type) {
return literal(name)
.executes(batch(ap -> ap.start(type, EntityPlayerActionPack.Action.once())))
.then(literal("once").executes(batch(ap -> ap.start(type, EntityPlayerActionPack.Action.once()))))
.then(literal("continuous").executes(batch(ap -> ap.start(type, EntityPlayerActionPack.Action.continuous()))))
.then(literal("interval").then(argument("ticks", IntegerArgumentType.integer(1))
.executes(c -> batchAp(c, ap -> ap.start(type, EntityPlayerActionPack.Action.interval(IntegerArgumentType.getInteger(c, "ticks")))))));
}
private static LiteralArgumentBuilder<CommandSourceStack> dropCommand(String name, boolean dropAll) {
return literal(name)
.then(literal("all").executes(batch(ap -> ap.drop(-2, dropAll))))
.then(literal("mainhand").executes(batch(ap -> ap.drop(-1, dropAll))))
.then(literal("offhand").executes(batch(ap -> ap.drop(40, dropAll))))
.then(argument("slot", IntegerArgumentType.integer(0, 40))
.executes(c -> batchAp(c, ap -> ap.drop(IntegerArgumentType.getInteger(c, "slot"), dropAll))));
}
/** 批量执行一个 action pack 操作。 */
private static int batchAp(CommandContext<CommandSourceStack> context, Consumer<EntityPlayerActionPack> action) {
List<ServerPlayer> bots = collectBots(context.getSource());
for (ServerPlayer bot : bots) {
action.accept(((ServerPlayerInterface) bot).getActionPack());
}
final int n = bots.size();
context.getSource().sendSuccess(
() -> net.minecraft.network.chat.Component.literal("[SLSA] 已对 " + n + " 个假人执行操作"), false);
return n;
}
private static com.mojang.brigadier.Command<CommandSourceStack> batch(Consumer<EntityPlayerActionPack> action) {
return c -> batchAp(c, action);
}
/** 收集所有本模组生成的假人(受名单限制:名单非空时只取名单内的)。 */
private static List<ServerPlayer> collectBots(CommandSourceStack source) {
MinecraftServer server = source.getServer();
List<ServerPlayer> bots = new ArrayList<>();
boolean filterByList = !com.github.zly2006.carpetslsaddition.util.BotListManager.isEmpty();
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
if (player instanceof EntityPlayerMPFake && ((SLSBotAccessor) player).carpet_SLS_Addition$isBot()) {
if (filterByList && !com.github.zly2006.carpetslsaddition.util.BotListManager.contains(player.getScoreboardName())) {
continue;
}
bots.add(player);
}
}
return bots;
}
private static int sitAll(CommandContext<CommandSourceStack> context) {
if (!SLSCarpetSettings.canUseSitCommand) {
context.getSource().sendSuccess(() -> net.minecraft.network.chat.Component.literal(
"[SLSA] 坐下功能未启用(canUseSitCommand)"), false);
return 0;
}
List<ServerPlayer> bots = collectBots(context.getSource());
int ok = 0;
for (ServerPlayer bot : bots) {
if (SitCommand.sitPlayer(bot)) ok++;
}
final int done = ok;
context.getSource().sendSuccess(() -> net.minecraft.network.chat.Component.literal(
"[SLSA] 已让 " + done + "/" + bots.size() + " 个假人坐下"), false);
return ok;
}
private static int standAll(CommandContext<CommandSourceStack> context) {
List<ServerPlayer> bots = collectBots(context.getSource());
int ok = 0;
for (ServerPlayer bot : bots) {
if (bot.getVehicle() != null) {
bot.stopRiding();
ok++;
}
}
final int done = ok;
context.getSource().sendSuccess(() -> net.minecraft.network.chat.Component.literal(
"[SLSA] 已让 " + done + "/" + bots.size() + " 个假人站起"), false);
return ok;
}
/** 一键召唤名单里的所有假人(执行者位置)。 */
private static int spawnFromList(CommandContext<CommandSourceStack> context) {
CommandSourceStack source = context.getSource();
java.util.Set<String> names = com.github.zly2006.carpetslsaddition.util.BotListManager.getNames();
if (names.isEmpty()) {
source.sendSuccess(() -> net.minecraft.network.chat.Component.literal(
"[SLSA] 名单为空,先用 /botlist add 添加名字"), false);
return 0;
}
net.minecraft.world.phys.Vec3 pos = source.getPosition();
net.minecraft.world.phys.Vec2 rot = source.getRotation();
net.minecraft.resources.ResourceKey<net.minecraft.world.level.Level> dim = source.getLevel().dimension();
int ok = 0;
for (String name : names) {
// 已在线的跳过
if (source.getServer().getPlayerList().getPlayerByName(name) != null) continue;
try {
var bot = BotCommand.createBot(name, source.getServer(), pos, rot.y, rot.x, dim);
if (bot != null) ok++;
} catch (Throwable t) {
com.github.zly2006.carpetslsaddition.ServerMain.LOGGER.warn("[SLSA] spawn list bot {} failed: {}", name, t.toString());
}
}
final int done = ok;
source.sendSuccess(() -> net.minecraft.network.chat.Component.literal(
"[SLSA] 已召唤名单中的 " + done + "/" + names.size() + " 个假人"), false);
return ok;
}
/** 批量召唤:生成 前缀1 ~ 前缀N。 */
private static int spawnBatch(CommandContext<CommandSourceStack> context) {
CommandSourceStack source = context.getSource();
String prefix = com.mojang.brigadier.arguments.StringArgumentType.getString(context, "prefix");
int count = IntegerArgumentType.getInteger(context, "count");
net.minecraft.world.phys.Vec3 pos = source.getPosition();
net.minecraft.world.phys.Vec2 rot = source.getRotation();
net.minecraft.resources.ResourceKey<net.minecraft.world.level.Level> dim = source.getLevel().dimension();
int ok = 0;
for (int i = 1; i <= count; i++) {
String name = prefix + i;
try {
var bot = BotCommand.createBot(name, source.getServer(), pos, rot.y, rot.x, dim);
if (bot != null) ok++;
} catch (Throwable t) {
com.github.zly2006.carpetslsaddition.ServerMain.LOGGER.warn("[SLSA] spawn bot {} failed: {}", name, t.toString());
}
}
final int done = ok;
source.sendSuccess(() -> net.minecraft.network.chat.Component.literal(
"[SLSA] 已召唤 " + done + "/" + count + " 个假人(前缀 " + prefix + ")"), false);
return ok;
}
/** 批量移除:杀死受名单限制的假人(名单空则全部)。 */
private static int killBatch(CommandContext<CommandSourceStack> context) {
List<ServerPlayer> bots = collectBots(context.getSource());
int ok = 0;
for (ServerPlayer bot : bots) {
bot.kill((net.minecraft.server.level.ServerLevel) bot.level());
ok++;
}
final int done = ok;
context.getSource().sendSuccess(() -> net.minecraft.network.chat.Component.literal(
"[SLSA] 已移除 " + done + " 个假人"), false);
return ok;
}
}
@@ -352,7 +352,7 @@ public class BotCommand {
}
@SuppressWarnings("unchecked")
private static EntityPlayerMPFake createBot(String username, MinecraftServer server, Vec3 pos, double yaw, double pitch, ResourceKey<Level> dimensionId) {
public static EntityPlayerMPFake createBot(String username, MinecraftServer server, Vec3 pos, double yaw, double pitch, ResourceKey<Level> dimensionId) {
ServerLevel worldIn = server.getLevel(dimensionId);
server.services().nameToIdCache().resolveOfflineUsers(false);
GameProfile gameprofile;
@@ -0,0 +1,100 @@
package com.github.zly2006.carpetslsaddition.command;
import carpet.patches.EntityPlayerMPFake;
import com.github.zly2006.carpetslsaddition.util.BotListManager;
import com.github.zly2006.carpetslsaddition.util.access.SLSBotAccessor;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import java.util.Set;
import static net.minecraft.commands.Commands.argument;
import static net.minecraft.commands.Commands.literal;
/**
* /botlist —— 维护"受控假人名单",供 /botall 限定操作范围。
*
* <p>名单持久化在 config/slsa-bot-list.json,重启保留。
*/
public class BotListCommand {
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
LiteralArgumentBuilder<CommandSourceStack> command = literal("botlist")
.requires(source -> Commands.LEVEL_OWNERS.check(source.permissions())
|| me.lucko.fabric.api.permissions.v0.Permissions.check(source, "slsaddition.command.bot"))
.then(literal("show").executes(BotListCommand::show))
.then(literal("clear").executes(c -> {
BotListManager.clear();
c.getSource().sendSuccess(() -> Component.literal("[SLSA] 已清空假人名单"), false);
return 1;
}))
.then(literal("add")
.then(argument("names", StringArgumentType.greedyString())
.executes(c -> add(c.getSource(), StringArgumentType.getString(c, "names")))))
.then(literal("remove")
.then(argument("names", StringArgumentType.greedyString())
.executes(c -> remove(c.getSource(), StringArgumentType.getString(c, "names")))))
.then(literal("addall").executes(BotListCommand::addAll));
dispatcher.register(command);
}
private static int show(com.mojang.brigadier.context.CommandContext<CommandSourceStack> c) {
Set<String> names = BotListManager.getNames();
if (names.isEmpty()) {
c.getSource().sendSuccess(() -> Component.literal("[SLSA] 假人名单为空(此时 /botall 操作所有假人)"), false);
} else {
c.getSource().sendSuccess(() -> Component.literal(
"[SLSA] 假人名单(" + names.size() + "):" + String.join(", ", names)), false);
}
return names.size();
}
private static int add(CommandSourceStack source, String raw) {
String[] parts = raw.trim().split("\\s+");
int n = 0;
for (String p : parts) {
if (!p.isEmpty() && BotListManager.add(p)) {
n++;
}
}
final int added = n;
source.sendSuccess(() -> Component.literal(
"[SLSA] 已添加 " + added + " 个名字,名单共 " + BotListManager.getNames().size() + " 个"), false);
return n;
}
private static int remove(CommandSourceStack source, String raw) {
String[] parts = raw.trim().split("\\s+");
int n = 0;
for (String p : parts) {
if (!p.isEmpty() && BotListManager.remove(p)) {
n++;
}
}
final int removed = n;
source.sendSuccess(() -> Component.literal(
"[SLSA] 已移除 " + removed + " 个名字,名单共 " + BotListManager.getNames().size() + " 个"), false);
return n;
}
/** 把当前所有在线 SLS 假人加入名单。 */
private static int addAll(com.mojang.brigadier.context.CommandContext<CommandSourceStack> c) {
int n = 0;
for (ServerPlayer player : c.getSource().getServer().getPlayerList().getPlayers()) {
if (player instanceof EntityPlayerMPFake && ((SLSBotAccessor) player).carpet_SLS_Addition$isBot()) {
if (BotListManager.add(player.getScoreboardName())) {
n++;
}
}
}
final int added = n;
c.getSource().sendSuccess(() -> Component.literal(
"[SLSA] 已把 " + added + " 个在线假人加入名单,名单共 " + BotListManager.getNames().size() + " 个"), false);
return n;
}
}
@@ -20,21 +20,27 @@ public class SitCommand {
if (player == null) {
return 0;
}
if (player.getVehicle() != null || !player.onGround()) { // 防止错误的坐下行为
return 1;
}
ServerLevel world = (ServerLevel) player.level();
ArmorStand armorStandEntity = new ArmorStand(world, player.getX(), player.getY(), player.getZ());
((SitEntity) armorStandEntity).setSitEntity(true);
world.addFreshEntity(armorStandEntity);
player.setShiftKeyDown(false);
player.startRiding(armorStandEntity);
sitPlayer(player);
return 1;
})
);
}
/**
* 让指定玩家原地坐下(生成一个标记为 SitEntity 的 ArmorStand 并骑乘)。
*
* @return 若成功坐下返回 true,否则返回 false(如已在骑乘/不在地面)。
*/
public static boolean sitPlayer(ServerPlayer player) {
if (player.getVehicle() != null || !player.onGround()) { // 防止错误的坐下行为
return false;
}
ServerLevel world = (ServerLevel) player.level();
ArmorStand armorStandEntity = new ArmorStand(world, player.getX(), player.getY(), player.getZ());
((SitEntity) armorStandEntity).setSitEntity(true);
world.addFreshEntity(armorStandEntity);
player.setShiftKeyDown(false);
player.startRiding(armorStandEntity);
return true;
}
}
@@ -0,0 +1,101 @@
package com.github.zly2006.carpetslsaddition.util;
import com.github.zly2006.carpetslsaddition.ServerMain;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import net.fabricmc.loader.api.FabricLoader;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 假人名单管理器:维护一份"受控假人名字"列表,持久化到 config/slsa-bot-list.json。
*
* <p>用途:{@code /botlist add cute wazi} 把名字加入名单后,{@code /botall} 的操作范围
* 就限定为名单内的假人(名单为空时操作所有 SLS 假人)。
*/
public final class BotListManager {
private BotListManager() {}
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
/** 名字列表(保序、去重)。 */
private static final Set<String> NAMES = new LinkedHashSet<>();
private static Path configPath;
private static Path path() {
if (configPath == null) {
configPath = FabricLoader.getInstance().getConfigDir().resolve("slsa-bot-list.json");
}
return configPath;
}
/** 加载名单(服务器启动时调用)。 */
public static void load() {
NAMES.clear();
Path p = path();
if (!Files.exists(p)) {
return;
}
try (Reader r = Files.newBufferedReader(p)) {
Set<String> loaded = GSON.fromJson(r, new TypeToken<LinkedHashSet<String>>() {}.getType());
if (loaded != null) {
NAMES.addAll(loaded);
}
ServerMain.LOGGER.info("[SLSA] loaded bot list: {} entries", NAMES.size());
} catch (Exception e) {
ServerMain.LOGGER.error("[SLSA] failed to load bot list", e);
}
}
/** 保存名单。 */
public static void save() {
Path p = path();
try {
Files.createDirectories(p.getParent());
try (Writer w = Files.newBufferedWriter(p)) {
GSON.toJson(NAMES, w);
}
} catch (IOException e) {
ServerMain.LOGGER.error("[SLSA] failed to save bot list", e);
}
}
public static boolean add(String name) {
boolean added = NAMES.add(name);
if (added) save();
return added;
}
public static boolean remove(String name) {
boolean removed = NAMES.remove(name);
if (removed) save();
return removed;
}
public static void clear() {
NAMES.clear();
save();
}
/** 返回名单快照。 */
public static Set<String> getNames() {
return new LinkedHashSet<>(NAMES);
}
public static boolean contains(String name) {
return NAMES.contains(name);
}
public static boolean isEmpty() {
return NAMES.isEmpty();
}
}