Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f985a22d8a | ||
|
|
61ce9891b9 | ||
|
|
94fafe972c | ||
|
|
f9a198a892 | ||
|
|
0df145927f | ||
|
|
5cfe2f1694 | ||
|
|
ea7720b7cd | ||
|
|
1c77347dc2 | ||
|
|
35b05eb044 | ||
|
|
a434f12749 | ||
|
|
93e7416056 | ||
|
|
62397479be | ||
|
|
5001b2f5a2 | ||
|
|
d3bd73f7c5 | ||
|
|
d44ea0f4a7 | ||
|
|
de966ac511 | ||
|
|
33542f9678 | ||
|
|
870223bc1e | ||
|
|
2097a241aa | ||
|
|
8576395126 | ||
|
|
72fbc95874 | ||
|
|
1ee529eb5b | ||
|
|
bdac8233a6 | ||
|
|
4cc1b64fc3 | ||
|
|
664d2a39cf | ||
|
|
99b431145f | ||
|
|
3fde7e2c1b | ||
|
|
7366a8b244 | ||
|
|
9e32c4dbfa | ||
|
|
fa526b0839 | ||
|
|
ac6cd5c1b5 | ||
|
|
f63a59495e | ||
|
|
fba85271f4 | ||
|
|
993001c4ef | ||
|
|
9e03c076ae | ||
|
|
c647189f77 | ||
|
|
022b2144d3 | ||
|
|
b2d09b241c | ||
|
|
f0fac64e84 | ||
|
|
ccbf442413 | ||
|
|
541ffe5ed2 | ||
|
|
ea93b9dcdf |
No files matched your search
+6
-6
@@ -5,13 +5,13 @@ org.gradle.configuration-cache=true
|
||||
|
||||
# Fabric Properties
|
||||
# check these on https://fabricmc.net/develop
|
||||
minecraft_version=26.1.2
|
||||
loader_version=0.19.5
|
||||
loom_version=1.18-SNAPSHOT
|
||||
minecraft_version=26.4-snapshot-4
|
||||
loader_version=0.20.0
|
||||
loom_version=1.19-SNAPSHOT
|
||||
# Mod Properties
|
||||
mod_version = 0.23
|
||||
mod_version = 0.26.0-26.4
|
||||
maven_group = com.github.zly2006
|
||||
archives_base_name = carpet-sls-addition
|
||||
# Dependencies
|
||||
fabric_version=0.155.3+26.1.2
|
||||
carpet_core_version=26.1+v260401
|
||||
fabric_version=0.163.0+26.4
|
||||
carpet_core_version=26.4-beta-3+v261013
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
|
||||
@@ -77,4 +77,13 @@ public class SLSCarpetSettings {
|
||||
|
||||
@Rule(categories = {SLSA, RuleCategory.FEATURE})
|
||||
public static boolean unbreakableDecoratedPot = false;
|
||||
|
||||
@Rule(categories = {SLSA, RuleCategory.FEATURE})
|
||||
public static boolean stackableVehicles = false;
|
||||
|
||||
@Rule(categories = {SLSA, RuleCategory.FEATURE})
|
||||
public static boolean slsBoardEnabled = false;
|
||||
|
||||
@Rule(categories = {SLSA, RuleCategory.FEATURE})
|
||||
public static boolean noBrittleIceFromFlowingWater = false;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -41,6 +43,13 @@ public class ServerMain implements ModInitializer, CarpetExtension {
|
||||
LOGGER.info("[SLSA] onInitialize: managing Carpet extension");
|
||||
INSTANCE = this;
|
||||
CarpetServer.manageExtension(this);
|
||||
com.github.zly2006.carpetslsaddition.net.BoardRequestPayload.register();
|
||||
// 客户端请求显示榜单 → 给该玩家单独发 display 包
|
||||
net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.registerGlobalReceiver(
|
||||
com.github.zly2006.carpetslsaddition.net.BoardRequestPayload.TYPE,
|
||||
(payload, context) -> context.server().execute(() ->
|
||||
com.github.zly2006.carpetslsaddition.util.BoardManager.onClientRequest(
|
||||
context.player(), payload.objectiveName())));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,6 +60,32 @@ public class ServerMain implements ModInitializer, CarpetExtension {
|
||||
@Override
|
||||
public void onServerLoaded(MinecraftServer server) {
|
||||
ServerMain.server = server;
|
||||
com.github.zly2006.carpetslsaddition.util.BotListManager.load();
|
||||
startBoardWatcher(server);
|
||||
}
|
||||
|
||||
/** 服务器完全启动后创建 objective(此时 scoreboard 已 load 完,避免重名崩溃)。 */
|
||||
static {
|
||||
net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents.SERVER_STARTED.register(server ->
|
||||
com.github.zly2006.carpetslsaddition.util.BoardManager.ensureObjectives(server));
|
||||
}
|
||||
|
||||
/** 每秒检测 slsBoardEnabled 规则变化并同步给客户端。 */
|
||||
private void startBoardWatcher(MinecraftServer server) {
|
||||
Thread t = new Thread(() -> {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
com.github.zly2006.carpetslsaddition.util.BoardManager.syncScores(server);
|
||||
Thread.sleep(1000L);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
} catch (Throwable ex) {
|
||||
LOGGER.debug("[SLSA] board watcher error: {}", ex.toString());
|
||||
}
|
||||
}
|
||||
}, "SLSA-BoardWatcher");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,6 +116,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,49 @@
|
||||
package com.github.zly2006.carpetslsaddition.client;
|
||||
|
||||
import com.github.zly2006.carpetslsaddition.net.BoardRequestPayload;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.api.EnvType;
|
||||
import net.fabricmc.api.Environment;
|
||||
import net.fabricmc.fabric.api.client.command.v2.ClientCommands;
|
||||
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/**
|
||||
* 客户端入口:/slsboard 命令,请求服务端给自己显示某个榜单。
|
||||
*
|
||||
* <p>服务端收到请求后,给该玩家单独发 display 包,实现各玩家各自显示、互不影响。
|
||||
* 数据由服务端从 stats 同步到 scoreboard objective(见 BoardManager)。
|
||||
*/
|
||||
@Environment(EnvType.CLIENT)
|
||||
public class BoardClient implements ClientModInitializer {
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
|
||||
dispatcher.register(
|
||||
ClientCommands.literal("slsboard")
|
||||
.then(ClientCommands.literal("deaths").executes(c -> request(c.getSource(), "sls_deaths")))
|
||||
.then(ClientCommands.literal("mined").executes(c -> request(c.getSource(), "sls_mined")))
|
||||
.then(ClientCommands.literal("off").executes(c -> request(c.getSource(), "")))
|
||||
.then(ClientCommands.argument("board", StringArgumentType.word())
|
||||
.suggests((c, b) -> SharedSuggestionProvider.suggest(
|
||||
new String[]{"deaths", "mined", "off"}, b))
|
||||
.executes(c -> {
|
||||
String v = StringArgumentType.getString(c, "board");
|
||||
return request(c.getSource(), "off".equals(v) ? "" : "sls_" + v);
|
||||
}))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private static int request(FabricClientCommandSource source, String objectiveName) {
|
||||
ClientPlayNetworking.send(new BoardRequestPayload(objectiveName));
|
||||
source.sendFeedback(Component.literal(
|
||||
objectiveName.isEmpty() ? "[SLSA] 榜单已关闭" : "[SLSA] 请求显示:" + objectiveName));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import net.minecraft.server.level.ClientInformation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.network.CommonListenerCookie;
|
||||
import net.minecraft.server.network.ClientDebugInfo;
|
||||
import net.minecraft.server.players.NameAndId;
|
||||
import net.minecraft.server.players.PlayerList;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
@@ -352,7 +353,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;
|
||||
@@ -386,7 +387,7 @@ public class BotCommand {
|
||||
EntityPlayerMPFake bot = constructor.newInstance(server, worldIn, gameprofile, ClientInformation.createDefault(), false);
|
||||
|
||||
bot.fixStartingPosition = () -> bot.snapTo(pos.x, pos.y, pos.z, (float) yaw, (float) pitch);
|
||||
server.getPlayerList().placeNewPlayer(new FakeClientConnection(PacketFlow.SERVERBOUND), bot, new CommonListenerCookie(gameprofile, 0, bot.clientInformation(), false));
|
||||
server.getPlayerList().placeNewPlayer(new FakeClientConnection(PacketFlow.SERVERBOUND), bot, new CommonListenerCookie(gameprofile, 0, bot.clientInformation(), false, new ClientDebugInfo()));
|
||||
|
||||
bot.teleportTo(worldIn, pos.x, pos.y, pos.z, Set.of(), (float) yaw, (float) pitch, true);
|
||||
bot.setHealth(20.0F);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package com.github.zly2006.carpetslsaddition.mixin.block;
|
||||
|
||||
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LiquidBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.material.FluidState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/**
|
||||
* 26.4 快照加入的“碎冰”(Brittle Ice)机制:
|
||||
* LiquidBlock#shouldSpreadLiquid 中,水流方块任意水平方向的邻居是蓝冰时,
|
||||
* 会把当前水流方块替换为碎冰。
|
||||
*
|
||||
* 开启 noBrittleIceFromFlowingWater 规则后:
|
||||
* 只有「水源方块」贴蓝冰时照常变碎冰;所有「流动的水」(横向、下落)都不再变碎冰,
|
||||
* 且水流扩散不受影响。
|
||||
*
|
||||
* 实现:原版判断邻居是不是蓝冰用 BlockState#is(Blocks.BLUE_ICE),一旦命中就
|
||||
* 变碎冰并 return false(中断扩散)。本 mixin 让流动水贴蓝冰时这次 is() 返回 false,
|
||||
* 整个“变碎冰”分支被跳过,shouldSpreadLiquid 正常返回 true,水流照常扩散。
|
||||
* 位置信息通过 ThreadLocal 从方法入口传入。
|
||||
*/
|
||||
@Mixin(LiquidBlock.class)
|
||||
public class MixinFlowingWaterBrittleIce {
|
||||
@Unique
|
||||
private static final ThreadLocal<Level> CURRENT_LEVEL = new ThreadLocal<>();
|
||||
@Unique
|
||||
private static final ThreadLocal<BlockPos> CURRENT_POS = new ThreadLocal<>();
|
||||
|
||||
@Inject(method = "shouldSpreadLiquid", at = @At("HEAD"))
|
||||
private void captureArgs(Level level, BlockPos pos, BlockState state, CallbackInfoReturnable<Boolean> cir) {
|
||||
CURRENT_LEVEL.set(level);
|
||||
CURRENT_POS.set(pos);
|
||||
}
|
||||
|
||||
@WrapOperation(
|
||||
method = "shouldSpreadLiquid",
|
||||
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;is(Ljava/lang/Object;)Z")
|
||||
)
|
||||
private boolean wrapIsBlueIce(BlockState instance, Object target, Operation<Boolean> original) {
|
||||
Boolean result = original.call(instance, target);
|
||||
if (!result) return false;
|
||||
// 只有判断目标确实是 BLUE_ICE 时才可能干预
|
||||
if (!(target instanceof net.minecraft.world.level.block.Block b) || !b.equals(Blocks.BLUE_ICE)) {
|
||||
return true;
|
||||
}
|
||||
if (!SLSCarpetSettings.noBrittleIceFromFlowingWater) return true;
|
||||
|
||||
Level level = CURRENT_LEVEL.get();
|
||||
BlockPos pos = CURRENT_POS.get();
|
||||
if (level == null || pos == null) return true;
|
||||
|
||||
FluidState current = level.getFluidState(pos);
|
||||
// 流动水(非水源)不让它触发碎冰替换
|
||||
if (!current.isSource()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -3,13 +3,13 @@ package com.github.zly2006.carpetslsaddition.mixin.block;
|
||||
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.RedStoneWireBlock;
|
||||
import net.minecraft.world.level.block.RedstoneWireBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
@Mixin(value = RedStoneWireBlock.class, priority = 1)
|
||||
@Mixin(value = RedstoneWireBlock.class, priority = 1)
|
||||
public class MixinRedstoneWireBlock {
|
||||
// oldRedstoneConnectionLogic:让红石能连到活板门上的红石线。
|
||||
// 新版在 getConnectingSide 里用 `blockState.getBlock() instanceof TrapDoorBlock` 判断。
|
||||
|
||||
+7
-4
@@ -15,7 +15,7 @@ import net.minecraft.world.level.block.state.pattern.BlockInWorld;
|
||||
import net.minecraft.world.level.block.state.pattern.BlockPattern;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.feature.EndPodiumFeature;
|
||||
import net.minecraft.world.level.dimension.end.EnderDragonFight;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.*;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
@@ -37,6 +37,9 @@ public class MixinEnderDragonFight {
|
||||
@Shadow
|
||||
private BlockPos exitPortalLocation;
|
||||
|
||||
@Shadow
|
||||
private BlockPos origin;
|
||||
|
||||
@Unique
|
||||
private int defaultChunkX = -8;
|
||||
@Unique
|
||||
@@ -79,13 +82,13 @@ public class MixinEnderDragonFight {
|
||||
if (SLSCarpetSettings.optimizedOnDragonRespawn && defaultOriginY != -1) {
|
||||
i = defaultOriginY;
|
||||
} else {
|
||||
i = this.level.getHeight(Heightmap.Types.MOTION_BLOCKING, EndPodiumFeature.getLocation(BlockPos.ZERO).getX(), EndPodiumFeature.getLocation(BlockPos.ZERO).getZ());
|
||||
i = this.level.getHeight(Heightmap.Types.MOTION_BLOCKING, EnderDragonFight.getPodiumLocation(this.origin).getX(), EnderDragonFight.getPodiumLocation(this.origin).getZ());
|
||||
}
|
||||
boolean notFirstSearch = false;
|
||||
for (j = i; j >= 0; --j) {
|
||||
BlockPattern.BlockPatternMatch result2;
|
||||
BlockPos origin = EndPodiumFeature.getLocation(BlockPos.ZERO);
|
||||
BlockPos searchPos = new BlockPos(origin.getX(), j, origin.getZ());
|
||||
BlockPos podiumPos = EnderDragonFight.getPodiumLocation(this.origin);
|
||||
BlockPos searchPos = new BlockPos(podiumPos.getX(), j, podiumPos.getZ());
|
||||
if (SLSCarpetSettings.optimizedOnDragonRespawn && notFirstSearch) {
|
||||
result2 = partialSearchAround(this.exitPortalPattern, this.level, searchPos);
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.github.zly2006.carpetslsaddition.mixin.item;
|
||||
|
||||
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.BoatItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.MinecartItem;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/**
|
||||
* 载具堆叠(仅玩家背包):矿车/船在【玩家背包】内可堆叠到 64,其他容器不堆叠。
|
||||
*
|
||||
* <p>原理:{@code Slot.getMaxStackSize(ItemStack)} = min(容器上限, 物品上限)。
|
||||
* 载具的物品上限全局为 1(默认不叠)。本 mixin 在【容器是玩家背包 Inventory】且物品是载具时,
|
||||
* 直接返回 64,绕过 min 限制。
|
||||
*
|
||||
* <p>这样只有玩家背包(物品栏 36 格)内能叠,箱子/漏斗/潜影盒等其他容器不受影响。
|
||||
*/
|
||||
@Mixin(Slot.class)
|
||||
public abstract class SlotMixin {
|
||||
|
||||
@Inject(method = "getMaxStackSize(Lnet/minecraft/world/item/ItemStack;)I", at = @At("HEAD"), cancellable = true)
|
||||
private void stackableVehiclesInPlayerInventory(ItemStack stack, CallbackInfoReturnable<Integer> cir) {
|
||||
if (!SLSCarpetSettings.stackableVehicles) {
|
||||
return;
|
||||
}
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Slot self = (Slot) (Object) this;
|
||||
// 仅玩家背包
|
||||
if (!(self.container instanceof Inventory)) {
|
||||
return;
|
||||
}
|
||||
Item item = stack.getItem();
|
||||
if (item instanceof MinecartItem || item instanceof BoatItem) {
|
||||
cir.setReturnValue(64);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -3,6 +3,7 @@ package com.github.zly2006.carpetslsaddition.mixin.world;
|
||||
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.level.NaturalSpawner;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
@@ -16,7 +17,7 @@ public class MixinSpawnHelper {
|
||||
// 改为在 getMobForSpawn 返回处拦截。
|
||||
@Inject(method = "getMobForSpawn", at = @At("HEAD"), cancellable = true)
|
||||
private static void onCreateMob(ServerLevel level, EntityType<?> type, CallbackInfoReturnable<Mob> cir) {
|
||||
if (SLSCarpetSettings.noBatSpawning && type == EntityType.BAT) {
|
||||
if (SLSCarpetSettings.noBatSpawning && type == EntityTypes.BAT) {
|
||||
cir.setReturnValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.github.zly2006.carpetslsaddition.net;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 客户端 → 服务端:请求显示某个榜单(或关闭)。
|
||||
*
|
||||
* <p>服务端收到后,给该玩家单独发送 {@code ClientboundSetDisplayObjectivePacket},
|
||||
* 实现"各玩家各自显示、互不影响"。
|
||||
*
|
||||
* <p>{@code objectiveName} 为空字符串表示关闭。
|
||||
*/
|
||||
public record BoardRequestPayload(String objectiveName) implements CustomPacketPayload {
|
||||
|
||||
public static final CustomPacketPayload.Type<BoardRequestPayload> TYPE =
|
||||
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath("slsa", "board_request"));
|
||||
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, BoardRequestPayload> CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.STRING_UTF8, BoardRequestPayload::objectiveName,
|
||||
BoardRequestPayload::new
|
||||
);
|
||||
|
||||
@Override
|
||||
public CustomPacketPayload.Type<? extends CustomPacketPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.serverboundPlay().register(TYPE, CODEC);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.github.zly2006.carpetslsaddition.util;
|
||||
|
||||
import com.github.zly2006.carpetslsaddition.ServerMain;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.stats.ServerStatsCounter;
|
||||
import net.minecraft.stats.Stat;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.scores.Objective;
|
||||
import net.minecraft.world.scores.ScoreAccess;
|
||||
import net.minecraft.world.scores.ScoreHolder;
|
||||
import net.minecraft.world.scores.Scoreboard;
|
||||
import net.minecraft.world.scores.criteria.ObjectiveCriteria;
|
||||
|
||||
/**
|
||||
* 死亡榜 / 挖掘榜的 scoreboard 管理器(服务端)。
|
||||
*
|
||||
* <p>数据源是 vanilla stats,但 stats 不会自动同步给其他客户端。
|
||||
* 因此本类定期把 stats 的聚合值写入 scoreboard objective,
|
||||
* 客户端再用原版计分板侧边栏显示(scoreboard 会自动同步给所有客户端)。
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code sls_deaths}:死亡次数(DUMMY 判据,从 stats 的 {@link Stats#DEATHS} 写入)。</li>
|
||||
* <li>{@code sls_mined}:挖掘总数(DUMMY 判据,从 stats 的 {@link Stats#BLOCK_MINED} 聚合)。</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class BoardManager {
|
||||
|
||||
private BoardManager() {}
|
||||
|
||||
public static final String OBJ_DEATHS = "sls_deaths";
|
||||
public static final String OBJ_MINED = "sls_mined";
|
||||
|
||||
/** 确保两个 objective 存在(服务器启动/重载时调用)。 */
|
||||
public static void ensureObjectives(MinecraftServer server) {
|
||||
Scoreboard sb = server.getScoreboard();
|
||||
ServerMain.LOGGER.info("[SLSA] ensureObjectives called, deaths={} mined={}",
|
||||
sb.getObjective(OBJ_DEATHS) != null, sb.getObjective(OBJ_MINED) != null);
|
||||
if (sb.getObjective(OBJ_DEATHS) == null) {
|
||||
sb.addObjective(OBJ_DEATHS, ObjectiveCriteria.DUMMY,
|
||||
Component.literal("死亡榜"), ObjectiveCriteria.RenderType.INTEGER, true, null);
|
||||
ServerMain.LOGGER.info("[SLSA] created scoreboard objective {}", OBJ_DEATHS);
|
||||
}
|
||||
if (sb.getObjective(OBJ_MINED) == null) {
|
||||
sb.addObjective(OBJ_MINED, ObjectiveCriteria.DUMMY,
|
||||
Component.literal("挖掘榜"), ObjectiveCriteria.RenderType.INTEGER, true, null);
|
||||
ServerMain.LOGGER.info("[SLSA] created scoreboard objective {}", OBJ_MINED);
|
||||
}
|
||||
// 让客户端能收到这两个 objective(load 进来的 objective 可能未被追踪)
|
||||
if (sb instanceof net.minecraft.server.ServerScoreboard ssb) {
|
||||
Objective d = sb.getObjective(OBJ_DEATHS);
|
||||
if (d != null) ssb.startTrackingObjective(d);
|
||||
Objective m = sb.getObjective(OBJ_MINED);
|
||||
if (m != null) ssb.startTrackingObjective(m);
|
||||
}
|
||||
}
|
||||
|
||||
/** 读取某玩家的挖掘总数(遍历 stats map 中 BLOCK_MINED 类型条目)。 */
|
||||
private static int totalMined(ServerStatsCounter counter) {
|
||||
int sum = 0;
|
||||
for (Stat<?> stat : counter.stats.keySet()) {
|
||||
if (stat.getType() == Stats.BLOCK_MINED) {
|
||||
sum += counter.getValue(stat);
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/** 读取某玩家的死亡次数。 */
|
||||
private static int totalDeaths(ServerStatsCounter counter) {
|
||||
return counter.getValue(Stats.CUSTOM, Stats.DEATHS);
|
||||
}
|
||||
|
||||
/** 把一个分数写入 objective(若不存在则创建/更新)。 */
|
||||
private static void setScore(Scoreboard sb, Objective obj, ServerPlayer player, int value) {
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
ScoreAccess access = sb.getOrCreatePlayerScore((ScoreHolder) player, obj);
|
||||
access.set(value);
|
||||
}
|
||||
|
||||
/** 从 stats 同步所有在线玩家的分数到 scoreboard(定期调用)。 */
|
||||
public static void syncScores(MinecraftServer server) {
|
||||
Scoreboard sb = server.getScoreboard();
|
||||
Objective deaths = sb.getObjective(OBJ_DEATHS);
|
||||
Objective mined = sb.getObjective(OBJ_MINED);
|
||||
if (deaths == null && mined == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
ServerStatsCounter counter = player.getStats();
|
||||
setScore(sb, deaths, player, totalDeaths(counter));
|
||||
setScore(sb, mined, player, totalMined(counter));
|
||||
}
|
||||
}
|
||||
|
||||
/** 客户端请求显示某榜单 → 给该玩家单独发 display 包(不影响其他玩家)。 */
|
||||
public static void onClientRequest(ServerPlayer player, String objectiveName) {
|
||||
MinecraftServer server = ServerMain.server;
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
Objective obj = objectiveName == null || objectiveName.isEmpty()
|
||||
? null
|
||||
: server.getScoreboard().getObjective(objectiveName);
|
||||
// 干净重建客户端该 objective:先 REMOVE(客户端没有则忽略),再 ADD + 分数包
|
||||
if (obj != null) {
|
||||
player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetObjectivePacket(
|
||||
obj, net.minecraft.network.protocol.game.ClientboundSetObjectivePacket.METHOD_REMOVE));
|
||||
player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetObjectivePacket(
|
||||
obj, net.minecraft.network.protocol.game.ClientboundSetObjectivePacket.METHOD_ADD));
|
||||
if (server.getScoreboard() instanceof net.minecraft.server.ServerScoreboard ssb) {
|
||||
for (var pkt : ssb.getStartTrackingPackets(obj)) {
|
||||
if (pkt instanceof net.minecraft.network.protocol.game.ClientboundSetScorePacket) {
|
||||
player.connection.send(pkt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 发 display 包(slot=SIDEBAR,objective 可为 null 表示清除)
|
||||
player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetDisplayObjectivePacket(
|
||||
net.minecraft.world.scores.DisplaySlot.SIDEBAR, obj));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public class ShulkerBoxItemUtil {
|
||||
if (itemStack.getItem() instanceof BlockItem blockItem
|
||||
&& blockItem.getBlock() instanceof ShulkerBoxBlock) {
|
||||
return itemStack.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY)
|
||||
.allItemsCopyStream().findAny().isEmpty();
|
||||
.nonEmptyItemCopyStream().findAny().isEmpty();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"carpet.rule.elytraCraftable.desc": "Make elytra craftable for Minecraft 1.20.6+ (eltra wont disappear when crafting)",
|
||||
"carpet.rule.botMaxOnlineTime.desc": "Set the maximum living time of the bot which create by carpet sls addition.",
|
||||
"carpet.rule.spectatorCannotUseLeash.desc": "If a player is a spectator, they cannot use the leash.",
|
||||
"carpet.rule.offlineFakePlayers.desc": "假人强制离线uuid",
|
||||
"carpet.rule.offlineFakePlayers.desc": "Force fake players to use offline UUIDs",
|
||||
"carpet.rule.armadilloImmediateDespawns.desc": "Armadillos will despawn immediately if they are 128 blocks away from the player.",
|
||||
"carpet.rule.turtleLocalNesting.name": "Turtle Local Nesting",
|
||||
"carpet.rule.turtleLocalNesting.desc": "Turtles carrying eggs will lay them on reachable sand near the closest local water source instead of returning to their birthplace.",
|
||||
@@ -72,5 +72,16 @@
|
||||
"carpet.slsa.bot.respawned": "The bot %s's online time has been reset.",
|
||||
"carpet.slsa.bot.not_a_bot": "The player %s is not created by carpet sls addition, so you can't reset the online time.",
|
||||
|
||||
"carpet.slsa.block.deep_slate_core": "Deepslate Core"
|
||||
"carpet.slsa.block.deep_slate_core": "Deepslate Core",
|
||||
"carpet.rule.stackableVehicles.name": "Stackable vehicles",
|
||||
"carpet.rule.stackableVehicles.desc": "Minecarts and boats can stack up to 64, auto-merging on pickup",
|
||||
"carpet.rule.slsBoardEnabled.name": "Enable boards",
|
||||
"carpet.rule.slsBoardEnabled.desc": "Allow players to use /slsboard to show death/mining leaderboards",
|
||||
"carpet.rule.noBrittleIceFromFlowingWater.name": "No brittle ice from flowing water",
|
||||
"carpet.rule.noBrittleIceFromFlowingWater.desc": "Only water flowing straight down onto blue ice forms brittle ice. Horizontally flowing water will not.",
|
||||
"carpet.rule.elytraCraftable.name": "Craftable elytra",
|
||||
"carpet.rule.armadilloImmediateDespawns.name": "Armadillos despawn immediately",
|
||||
"carpet.rule.offlineFakePlayers.name": "Force offline UUID for fake players",
|
||||
"carpet.rule.netherPortalSize.name": "Max nether portal size",
|
||||
"carpet.rule.trialSpawnerCD.name": "Trial spawner cooldown"
|
||||
}
|
||||
@@ -72,5 +72,16 @@
|
||||
"carpet.slsa.bot.respawned": "假人%s的在线时长已重置!",
|
||||
"carpet.slsa.bot.not_a_bot": "假人%s不是由Carpet SLS Addition生成的,所以你不能重置它的在线时间!",
|
||||
|
||||
"carpet.slsa.block.deep_slate_core": "深板岩核心"
|
||||
"carpet.slsa.block.deep_slate_core": "深板岩核心",
|
||||
"carpet.rule.stackableVehicles.name": "载具可堆叠",
|
||||
"carpet.rule.stackableVehicles.desc": "矿车和船的物品可以堆叠到64,拾取时自动合并",
|
||||
"carpet.rule.slsBoardEnabled.name": "启用榜单",
|
||||
"carpet.rule.slsBoardEnabled.desc": "允许玩家用 /slsboard 显示死亡榜/挖掘榜",
|
||||
"carpet.rule.noBrittleIceFromFlowingWater.name": "横向流动水不产生碎冰",
|
||||
"carpet.rule.noBrittleIceFromFlowingWater.desc": "只有竖直向下的水流接触蓝冰时才会产生碎冰,横向流动的水不再转化",
|
||||
"carpet.rule.elytraCraftable.name": "鞘翅可合成",
|
||||
"carpet.rule.armadilloImmediateDespawns.name": "犰狳立即消失",
|
||||
"carpet.rule.offlineFakePlayers.name": "假人强制离线UUID",
|
||||
"carpet.rule.netherPortalSize.name": "地狱门最大尺寸",
|
||||
"carpet.rule.trialSpawnerCD.name": "试炼刷怪笼冷却"
|
||||
}
|
||||
@@ -19,6 +19,9 @@
|
||||
"entrypoints": {
|
||||
"main": [
|
||||
"com.github.zly2006.carpetslsaddition.ServerMain"
|
||||
],
|
||||
"client": [
|
||||
"com.github.zly2006.carpetslsaddition.client.BoardClient"
|
||||
]
|
||||
},
|
||||
"accessWidener": "slsa.accesswidener",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
accessWidener v1 official
|
||||
|
||||
accessible class net/minecraft/world/entity/monster/EnderMan$EndermanTakeBlockGoal
|
||||
accessible class net/minecraft/world/entity/monster/Enderman$EndermanTakeBlockGoal
|
||||
accessible method net/minecraft/world/entity/Entity unsetRemoved ()V
|
||||
accessible field net/minecraft/world/entity/Entity entityData Lnet/minecraft/network/syncher/SynchedEntityData;
|
||||
accessible field net/minecraft/world/entity/Avatar DATA_PLAYER_MODE_CUSTOMISATION Lnet/minecraft/network/syncher/EntityDataAccessor;
|
||||
|
||||
accessible field net/minecraft/stats/StatsCounter stats Lit/unimi/dsi/fastutil/objects/Object2IntMap;
|
||||
@@ -12,6 +12,8 @@
|
||||
"block.MixinDecoratedPotBlock",
|
||||
"block.MixinTrialSpawnerData",
|
||||
"block.MixinTrialSpawnerState",
|
||||
"block.MixinFlowingWaterBrittleIce",
|
||||
"item.SlotMixin",
|
||||
"carpet.MixinEntityPlayerMPFake",
|
||||
"carpet.MixinPlayerCommand",
|
||||
"elytra.ItemAccessor",
|
||||
|
||||
Reference in new issue
Block a user