Author SHA1 Message Date
AxTps f985a22d8a fix: 补齐缺失的 5 条规则中文名,修正 en_us 中的中文描述
build / build (push) Canceled after 0s
- 补 elytraCraftable / armadilloImmediateDespawns / offlineFakePlayers / netherPortalSize / trialSpawnerCD 的 name(此前 /list 里显示为驼峰字段名)
- en_us 的 offlineFakePlayers.desc 由中文改为英文
2026-10-17 15:10:14 +08:00
AxTps 61ce9891b9 fix: 碎冰规则改为只保留水源变碎冰,流动水不再中断水流
原实现直接拦截 setBlockAndUpdate 并返回 false,导致邻接蓝冰的流动水被 shouldSpreadLiquid 的 return false 短路,水流扩散停止。
改为在 BlockState#is(BLUE_ICE) 判断处做手脚:规则开启且当前水非水源时让 is 返回 false,原版整个变碎冰分支被跳过,shouldSpreadLiquid 正常返回 true,水流照常扩散。
2026-10-17 15:10:07 +08:00
AxTps 94fafe972c fix: 适配 26.4 快照的 API 变更
build / build (push) Canceled after 0s
- 升级 Gradle wrapper 到 9.8.1(Loom 1.19.1 要求 Gradle >= 9.8.0)
- accesswidener:EnderMan$EndermanTakeBlockGoal -> Enderman$EndermanTakeBlockGoal(大小写变更)
- MixinRedstoneWireBlock:RedStoneWireBlock -> RedstoneWireBlock(类名大小写变更)
- MixinSpawnHelper:EntityType.BAT -> EntityTypes.BAT(实体常量迁移到 EntityTypes)
- BotCommand:CommonListenerCookie 构造器新增 ClientDebugInfo 参数
- MixinEnderDragonFight:EndPodiumFeature.getLocation(BlockPos) 移除,改用 EnderDragonFight.getPodiumLocation(BlockPos) 并 Shadow origin 字段
- ShulkerBoxItemUtil:ItemContainerContents.allItemsCopyStream() -> nonEmptyItemCopyStream()
2026-10-17 14:41:56 +08:00
AxTps f9a198a892 feat: 适配 26.4 并新增横向流动水不产生碎冰规则
build / build (push) Canceled after 0s
- gradle.properties 切换到 26.4-snapshot-4 / Loom 1.19-SNAPSHOT / Loader 0.20.0 / Fabric API 0.163.0+26.4 / Carpet 26.4-beta-3
- 新增规则 noBrittleIceFromFlowingWater,默认关闭
- LiquidBlock#shouldSpreadLiquid 用 WrapOperation 拦截碎冰替换:只有竖直下落的水(FlowingFluid#isFalling)才允许把邻接蓝冰转成碎冰,横向流动的水跳过
- 补 zh_cn/en_us 规则名与描述
2026-10-17 14:26:21 +08:00
AxTps 0df145927f chore: 版本号升至0.25.0,去掉榜单诊断日志 2026-10-06 21:46:38 +08:00
AxTps 5cfe2f1694 fix: onClientRequest先REMOVE再ADD objective+补分数包(两榜都显示且不崩) 2026-10-06 21:43:15 +08:00
AxTps ea7720b7cd fix: 回退重发分数包(导致客户端unknown objective,两榜都不显示) 2026-10-06 21:41:48 +08:00
AxTps 1c77347dc2 fix: onClientRequest重发该objective的分数包(客户端空榜不显示)
sls_deaths分数值不变时服务端不发分数包,客户端收到空objective不渲染。改成请求时重发所有分数包(只发Score包避免重复创建)。
2026-10-06 21:38:27 +08:00
AxTps 35b05eb044 debug: onClientRequest打印obj是否null 2026-10-06 21:34:58 +08:00
AxTps a434f12749 fix: 回退syncScores里的startTrackingObjective(不幂等导致客户端重复建objective崩溃) 2026-10-06 21:30:55 +08:00
AxTps 93e7416056 fix: syncScores每次确保objective在追踪(分数才同步给客户端) 2026-10-06 21:28:10 +08:00
AxTps 62397479be fix: 修复BoardManager末尾字面量\\n编译错误 2026-10-06 21:23:50 +08:00
AxTps 5001b2f5a2 feat: /slsboard改发请求包,服务端给该玩家单独发display包(per-player显示)
客户端本地setDisplayObjective会被服务端覆盖,改用C2S请求包,服务端单独发ClientboundSetDisplayObjectivePacket给请求者。
2026-10-06 21:22:42 +08:00
AxTps d3bd73f7c5 fix: 调用startTrackingObjective让客户端收到objective
load进来的objective未在trackedObjectives集合,客户端收不到。SERVER_STARTED时显式startTrackingObjective。
2026-10-06 21:04:18 +08:00
AxTps d44ea0f4a7 fix: objective改在SERVER_STARTED创建(scoreboard load完成后)
轮询线程比scoreboard.load更早跑,仍会撞名崩溃。改用ServerLifecycleEvents.SERVER_STARTED。
2026-10-06 21:01:03 +08:00
AxTps de966ac511 fix: 修复objective重复创建崩溃(ensure推迟到scoreboard load后)
onServerLoaded时机太早,scoreboard.dat还没load完,getObjective返回null导致重复addObjective崩溃。改为syncScores首次调用时ensure。
2026-10-06 20:52:24 +08:00
AxTps 33542f9678 debug: ensureObjectives加日志 2026-10-06 20:47:17 +08:00
AxTps 870223bc1e refactor: 榜单改用原版计分板侧边栏显示
服务端读stats定期写入scoreboard objective;客户端/slsboard设本地sidebar display。删掉自绘HUD和网络包(GuiGraphicsExtractor渲染不走)。
2026-10-06 20:42:40 +08:00
AxTps 2097a241aa fix: HUD画之前调nextStratum(原版计分板也这么做,否则不渲染) 2026-10-06 20:36:22 +08:00
AxTps 8576395126 debug: totalMined打印stats map的size/挖掘条目数/总和 2026-10-06 20:32:15 +08:00
AxTps 72fbc95874 fix: totalMined改用遍历stats map(之前遍历所有方块读不到数据)
AW暴露StatsCounter.stats字段。之前遍历BuiltInRegistries.BLOCK用getValue(StatType,block)大多返回0,导致挖掘数偏低。
2026-10-06 20:25:40 +08:00
AxTps 1ee529eb5b debug: 加客户端收包日志,去服务端刷屏日志 2026-10-06 20:11:40 +08:00
AxTps bdac8233a6 debug: 服务端加榜单同步/分数诊断日志 2026-10-06 20:07:32 +08:00
AxTps 4cc1b64fc3 fix: HUD去日志+空列表return,服务端每5秒定期同步榜单数据 2026-10-06 20:06:37 +08:00
AxTps 664d2a39cf debug: HUD加诊断日志 + 改为右侧居中 2026-10-06 20:03:18 +08:00
AxTps 99b431145f fix: 修复CustomPacketPayload.createType导致的启动崩溃
createType(String)的参数是纯路径不能含冒号,改用new Type(Identifier.fromNamespaceAndPath(namespace,path))。
2026-10-06 19:58:42 +08:00
AxTps 3fde7e2c1b feat: 加slsBoardEnabled规则作榜单总开关(服务端规则控制客户端命令)
BoardTogglePayload同步开关状态;客户端/slsboard检查enabled,未开启则拒绝。ServerMain加轮询线程检测规则变化。
2026-10-06 19:49:26 +08:00
AxTps 7366a8b244 feat: 玩家加入时同步榜单数据(ServerPlayConnectionEvents.JOIN) 2026-10-06 19:39:35 +08:00
AxTps 9e32c4dbfa fix: 客户端命令类名改用ClientCommands(26.1.2无ClientCommandManager) 2026-10-06 19:37:52 +08:00
AxTps fa526b0839 fix: 客户端命令改用ClientCommandManager(FabricClientCommandSource) 2026-10-06 19:36:33 +08:00
AxTps ac6cd5c1b5 feat: 榜单客户端(接收包+/slsboard命令+右侧HUD)
BoardClient注册客户端入口:接收BoardSyncPayload、/slsboard deaths/mined/off切换显示、HudElementRegistry注册右侧HUD。各玩家本地状态独立,不同步。
2026-10-06 19:34:48 +08:00
AxTps f63a59495e fix: PayloadTypeRegistry改用clientboundPlay()注册 2026-10-06 19:27:35 +08:00
AxTps fba85271f4 feat: 榜单同步服务端(读stats+网络包下发)
BoardSyncPayload自定义包,BoardSyncManager读原版stats(挖掘总数/死亡数)构建Top10排行。顺带补回之前漏掉的BotListManager.load()。
2026-10-06 19:22:34 +08:00
AxTps 993001c4ef chore: 版本号升至0.24.0 2026-10-06 06:24:16 +08:00
AxTps 9e03c076ae fix: 载具堆叠改为Slot层,仅玩家背包可叠
删除全局ItemInstanceMixin(会让所有容器都叠)。新增SlotMixin:仅当container是玩家Inventory且物品是矿车/船时返回64。
2026-10-05 19:23:34 +08:00
AxTps c647189f77 fix: 修ItemInstanceMixin的instanceof编译错误(用(Object)强转) 2026-10-05 19:19:18 +08:00
AxTps 022b2144d3 feat: 新增载具可堆叠规则(矿车/船堆叠至64)
mixin ItemInstance.getMaxStackSize,规则开启时矿车/船返回64,实现自动堆叠。新增item mixin分组。
2026-10-05 19:11:12 +08:00
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
30 changed files with 866 additions and 566 deletions

No files matched your search

+6 -6
View File
@@ -5,13 +5,13 @@ org.gradle.configuration-cache=true
# Fabric Properties # Fabric Properties
# check these on https://fabricmc.net/develop # check these on https://fabricmc.net/develop
minecraft_version=26.1.2 minecraft_version=26.4-snapshot-4
loader_version=0.19.5 loader_version=0.20.0
loom_version=1.18-SNAPSHOT loom_version=1.19-SNAPSHOT
# Mod Properties # Mod Properties
mod_version = 0.23 mod_version = 0.26.0-26.4
maven_group = com.github.zly2006 maven_group = com.github.zly2006
archives_base_name = carpet-sls-addition archives_base_name = carpet-sls-addition
# Dependencies # Dependencies
fabric_version=0.155.3+26.1.2 fabric_version=0.163.0+26.4
carpet_core_version=26.1+v260401 carpet_core_version=26.4-beta-3+v261013
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
retries=0 retries=0
retryBackOffMs=500 retryBackOffMs=500
@@ -7,7 +7,6 @@ public class SLSCarpetSettings {
public static final String NEED_CLIENT = "needClient"; // 需要客户端安装SLS-Addition或实现相关支持 public static final String NEED_CLIENT = "needClient"; // 需要客户端安装SLS-Addition或实现相关支持
public static final String FROM_AMS = "AMS"; public static final String FROM_AMS = "AMS";
public static final String SLSA = "SLS"; public static final String SLSA = "SLS";
public static final String LEGACY = "legacy";
@Rule(categories = {SLSA, RuleCategory.SURVIVAL}) @Rule(categories = {SLSA, RuleCategory.SURVIVAL})
public static boolean obtainableReinforcedDeepSlate = false; public static boolean obtainableReinforcedDeepSlate = false;
@@ -78,32 +77,13 @@ public class SLSCarpetSettings {
@Rule(categories = {SLSA, RuleCategory.FEATURE}) @Rule(categories = {SLSA, RuleCategory.FEATURE})
public static boolean unbreakableDecoratedPot = false; public static boolean unbreakableDecoratedPot = false;
// ===== [旧版] 遗留漏洞机制复刻 =====
// 总开关:开启后,其余子规则才生效
@Rule(categories = {LEGACY}, strict = false)
public static boolean legacyExploitMode = false;
// 等价 instantTick:计划刻(scheduleTick)立即执行,不等延迟 @Rule(categories = {SLSA, RuleCategory.FEATURE})
@Rule(categories = {LEGACY}) public static boolean stackableVehicles = false;
public static boolean legacyInstantTick = false;
// 等价 instantFall:重力方块被计划刻触发时立即下落 @Rule(categories = {SLSA, RuleCategory.FEATURE})
@Rule(categories = {LEGACY}) public static boolean slsBoardEnabled = false;
public static boolean legacyInstantFall = false;
// 自建后台线程,模拟 1.12 信标线程并发访问 chunk @Rule(categories = {SLSA, RuleCategory.FEATURE})
@Rule(categories = {LEGACY}) public static boolean noBrittleIceFromFlowingWater = false;
public static boolean legacyAsyncChunkAccess = false;
// 允许异步线程在 chunk 表竞态时把"已加载"当成"未加载",触发重新加载/装饰
@Rule(categories = {LEGACY})
public static boolean legacyChunkSwap = false;
// 落沙替换:在 FallingBlock 转实体窗口内把方块替换成目标方块
@Rule(categories = {LEGACY})
public static boolean legacyFallingBlockReplace = false;
// 目标方块(替换结果),默认末地传送门框架
@Rule(categories = {LEGACY}, strict = false, options = {"#none", "end_portal_frame", "command_block", "spawner", "barrier", "nether_portal"})
public static String legacyFallingBlockTarget = "#none";
} }
@@ -2,7 +2,9 @@ package com.github.zly2006.carpetslsaddition;
import carpet.CarpetExtension; import carpet.CarpetExtension;
import carpet.CarpetServer; import carpet.CarpetServer;
import com.github.zly2006.carpetslsaddition.command.BotAllCommand;
import com.github.zly2006.carpetslsaddition.command.BotCommand; 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.HatCommand;
import com.github.zly2006.carpetslsaddition.command.SitCommand; import com.github.zly2006.carpetslsaddition.command.SitCommand;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
@@ -41,6 +43,13 @@ public class ServerMain implements ModInitializer, CarpetExtension {
LOGGER.info("[SLSA] onInitialize: managing Carpet extension"); LOGGER.info("[SLSA] onInitialize: managing Carpet extension");
INSTANCE = this; INSTANCE = this;
CarpetServer.manageExtension(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 @Override
@@ -51,34 +60,32 @@ public class ServerMain implements ModInitializer, CarpetExtension {
@Override @Override
public void onServerLoaded(MinecraftServer server) { public void onServerLoaded(MinecraftServer server) {
ServerMain.server = server; ServerMain.server = server;
startLegacyRuleWatcher(); com.github.zly2006.carpetslsaddition.util.BotListManager.load();
startBoardWatcher(server);
} }
/** [旧版] 轮询规则状态,按需启停异步 chunk 线程。 */ /** 服务器完全启动后创建 objective(此时 scoreboard 已 load 完,避免重名崩溃)。 */
private void startLegacyRuleWatcher() { static {
Thread watcher = new Thread(() -> { net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents.SERVER_STARTED.register(server ->
boolean running = false; com.github.zly2006.carpetslsaddition.util.BoardManager.ensureObjectives(server));
}
/** 每秒检测 slsBoardEnabled 规则变化并同步给客户端。 */
private void startBoardWatcher(MinecraftServer server) {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) { while (!Thread.currentThread().isInterrupted()) {
try { try {
boolean want = SLSCarpetSettings.legacyExploitMode com.github.zly2006.carpetslsaddition.util.BoardManager.syncScores(server);
&& SLSCarpetSettings.legacyAsyncChunkAccess;
if (want && !running) {
com.github.zly2006.carpetslsaddition.util.LegacyAsyncExecutor.start();
running = true;
} else if (!want && running) {
com.github.zly2006.carpetslsaddition.util.LegacyAsyncExecutor.stop();
running = false;
}
Thread.sleep(1000L); Thread.sleep(1000L);
} catch (InterruptedException e) { } catch (InterruptedException e) {
return; return;
} catch (Throwable t) { } catch (Throwable ex) {
LOGGER.debug("[SLSA] legacy rule watcher error: {}", t.toString()); LOGGER.debug("[SLSA] board watcher error: {}", ex.toString());
} }
} }
}, "SLSA-Legacy-RuleWatcher"); }, "SLSA-BoardWatcher");
watcher.setDaemon(true); t.setDaemon(true);
watcher.start(); t.start();
} }
@Override @Override
@@ -109,6 +116,8 @@ public class ServerMain implements ModInitializer, CarpetExtension {
HatCommand.register(dispatcher); HatCommand.register(dispatcher);
SitCommand.register(dispatcher); SitCommand.register(dispatcher);
BotCommand.register(dispatcher); BotCommand.register(dispatcher);
BotAllCommand.register(dispatcher);
BotListCommand.register(dispatcher);
LOGGER.info("[SLSA] commands registered: hat/sit/bot"); LOGGER.info("[SLSA] commands registered: hat/sit/bot");
} catch (Throwable t) { } catch (Throwable t) {
LOGGER.error("[SLSA] failed to register commands", 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.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.CommonListenerCookie; import net.minecraft.server.network.CommonListenerCookie;
import net.minecraft.server.network.ClientDebugInfo;
import net.minecraft.server.players.NameAndId; import net.minecraft.server.players.NameAndId;
import net.minecraft.server.players.PlayerList; import net.minecraft.server.players.PlayerList;
import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.attributes.Attributes;
@@ -352,7 +353,7 @@ public class BotCommand {
} }
@SuppressWarnings("unchecked") @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); ServerLevel worldIn = server.getLevel(dimensionId);
server.services().nameToIdCache().resolveOfflineUsers(false); server.services().nameToIdCache().resolveOfflineUsers(false);
GameProfile gameprofile; GameProfile gameprofile;
@@ -386,7 +387,7 @@ public class BotCommand {
EntityPlayerMPFake bot = constructor.newInstance(server, worldIn, gameprofile, ClientInformation.createDefault(), false); EntityPlayerMPFake bot = constructor.newInstance(server, worldIn, gameprofile, ClientInformation.createDefault(), false);
bot.fixStartingPosition = () -> bot.snapTo(pos.x, pos.y, pos.z, (float) yaw, (float) pitch); 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.teleportTo(worldIn, pos.x, pos.y, pos.z, Set.of(), (float) yaw, (float) pitch, true);
bot.setHealth(20.0F); 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) { if (player == null) {
return 0; return 0;
} }
sitPlayer(player);
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);
return 1; 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,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;
}
}
@@ -3,13 +3,13 @@ package com.github.zly2006.carpetslsaddition.mixin.block;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings; import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks; 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 net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(value = RedStoneWireBlock.class, priority = 1) @Mixin(value = RedstoneWireBlock.class, priority = 1)
public class MixinRedstoneWireBlock { public class MixinRedstoneWireBlock {
// oldRedstoneConnectionLogic:让红石能连到活板门上的红石线。 // oldRedstoneConnectionLogic:让红石能连到活板门上的红石线。
// 新版在 getConnectingSide 里用 `blockState.getBlock() instanceof TrapDoorBlock` 判断。 // 新版在 getConnectingSide 里用 `blockState.getBlock() instanceof TrapDoorBlock` 判断。
@@ -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.block.state.pattern.BlockPattern;
import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.levelgen.Heightmap; 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.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.*; import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
@@ -37,6 +37,9 @@ public class MixinEnderDragonFight {
@Shadow @Shadow
private BlockPos exitPortalLocation; private BlockPos exitPortalLocation;
@Shadow
private BlockPos origin;
@Unique @Unique
private int defaultChunkX = -8; private int defaultChunkX = -8;
@Unique @Unique
@@ -79,13 +82,13 @@ public class MixinEnderDragonFight {
if (SLSCarpetSettings.optimizedOnDragonRespawn && defaultOriginY != -1) { if (SLSCarpetSettings.optimizedOnDragonRespawn && defaultOriginY != -1) {
i = defaultOriginY; i = defaultOriginY;
} else { } 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; boolean notFirstSearch = false;
for (j = i; j >= 0; --j) { for (j = i; j >= 0; --j) {
BlockPattern.BlockPatternMatch result2; BlockPattern.BlockPatternMatch result2;
BlockPos origin = EndPodiumFeature.getLocation(BlockPos.ZERO); BlockPos podiumPos = EnderDragonFight.getPodiumLocation(this.origin);
BlockPos searchPos = new BlockPos(origin.getX(), j, origin.getZ()); BlockPos searchPos = new BlockPos(podiumPos.getX(), j, podiumPos.getZ());
if (SLSCarpetSettings.optimizedOnDragonRespawn && notFirstSearch) { if (SLSCarpetSettings.optimizedOnDragonRespawn && notFirstSearch) {
result2 = partialSearchAround(this.exitPortalPattern, this.level, searchPos); result2 = partialSearchAround(this.exitPortalPattern, this.level, searchPos);
} else { } 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);
}
}
}
@@ -1,56 +0,0 @@
package com.github.zly2006.carpetslsaddition.mixin.legacy;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.ServerMain;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ChunkMap;
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;
/**
* [旧版] 区块替换(chunk swap)——race condition 的等价实现。
*
* <p>1.12 的漏洞:{@code ChunkProviderServer.id2ChunkMap}(非线程安全的
* {@code Long2ObjectOpenHashMap})被主线程 {@code remove}(卸载)与异步线程 {@code get}
* 并发访问,导致 {@code get} 错过已加载的区块,进而触发"重新加载 + 世界装饰"。
*
* <p>26.1.2 已用 {@code Long2ObjectLinkedOpenHashMap} + {@code mainThreadExecutor} 约束修复。
* 直接并发改 map 风险极高(可能崩溃/损坏存档),故本 mixin 采用<b>低风险等价方案</b>:
* hook {@link ChunkMap#getVisibleChunkIfPresent(long)},当满足以下条件时对<b>非主线程</b>调用返回 {@code null}:
* <ul>
* <li>{@code legacyExploitMode} + {@code legacyChunkSwap} 均开启</li>
* <li>当前不是服务器主线程(即异步线程在访问)</li>
* </ul>
* 这样异步线程会"误以为区块未加载",后续逻辑会尝试重新加载并触发世界装饰,
* 从而在异步线程上产生方块更新——与 1.12 的效果等价,但不触碰共享数据结构。
*
* <p><b>⚠ 风险</b>:仍可能引起区块状态不一致。仅在明确开启时生效。
*/
@Mixin(ChunkMap.class)
public abstract class ChunkMapMixin {
@Inject(method = "getVisibleChunkIfPresent", at = @At("RETURN"), cancellable = true)
private void legacyChunkSwap(long chunkPos, CallbackInfoReturnable<ChunkHolder> cir) {
if (cir.getReturnValue() == null) {
return;
}
if (!SLSCarpetSettings.legacyExploitMode || !SLSCarpetSettings.legacyChunkSwap) {
return;
}
// 仅在异步线程访问时伪装"区块未加载"
if (!isServerMainThread()) {
cir.setReturnValue(null);
}
}
/** 判断当前是否在服务器主线程上。 */
private static boolean isServerMainThread() {
net.minecraft.server.MinecraftServer server = ServerMain.server;
if (server == null) {
return true;
}
return server.isSameThread();
}
}
@@ -1,46 +0,0 @@
package com.github.zly2006.carpetslsaddition.mixin.legacy;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.ServerMain;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
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.ModifyVariable;
/**
* [旧版] 落沙替换——固定目标版。
*
* <p>26.1.2 单线程下 1.12 的"替换窗口"物理不存在,本 mixin 采用直接方案:
* 当 {@code legacyFallingBlockReplace} 开启且配置了 {@code legacyFallingBlockTarget} 时,
* 把下落实体的方块状态替换为目标方块的默认状态——沙子下落即变目标方块。
*/
@Mixin(net.minecraft.world.entity.item.FallingBlockEntity.class)
public class FallingBlockEntityMixin {
@ModifyVariable(
method = "fall",
at = @At("HEAD"),
argsOnly = true,
index = 2
)
private static BlockState legacyReplaceFallingState(BlockState original, Level level, BlockPos pos, BlockState state) {
if (!SLSCarpetSettings.legacyExploitMode || !SLSCarpetSettings.legacyFallingBlockReplace) {
return original;
}
String target = SLSCarpetSettings.legacyFallingBlockTarget;
if (target == null || target.isEmpty() || "#none".equals(target)) {
return original;
}
Block block = BuiltInRegistries.BLOCK.getOptional(Identifier.withDefaultNamespace(target)).orElse(null);
if (block == null) {
return original;
}
ServerMain.LOGGER.info("[SLSA-DEBUG] >>> REPLACE at {}: {} -> {}", pos, original.getBlock(), block);
return block.defaultBlockState();
}
}
@@ -1,78 +0,0 @@
package com.github.zly2006.carpetslsaddition.mixin.legacy;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import com.github.zly2006.carpetslsaddition.util.LegacyExploitState;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.FallingBlock;
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.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* [旧版] 瞬间下落(等价 1.12 的 instantFall flag)。
*
* <p>1.12:世界装饰时短暂开启 instantFall,使重力方块在被更新时【立即】转为下落实体,
* 而不等待 2gt 的计划刻延迟。26.1.2 已移除该 flag。
*
* <p>实现方式(双保险):
* <ul>
* <li>{@link #legacyDelayAfterPlace}:把 {@code getDelayAfterPlace()} 的返回值改为 0。</li>
* <li>{@link #legacyScheduleDelay}:直接拦截 {@code onPlace}/{@code updateShape} 里
* 对 {@code scheduleTick} 的 delay 参数,强制改为 0(更直接可靠)。</li>
* </ul>
*/
@Mixin(FallingBlock.class)
public abstract class FallingBlockMixin {
/** instantFall:把"放置后延迟"改为 0。 */
@Inject(method = "getDelayAfterPlace", at = @At("HEAD"), cancellable = true)
private void legacyDelayAfterPlace(CallbackInfoReturnable<Integer> cir) {
if (SLSCarpetSettings.legacyExploitMode && SLSCarpetSettings.legacyInstantFall) {
cir.setReturnValue(0);
}
}
/**
* instantFall:拦截 onPlace / updateShape 里 scheduleTick 的 delay 参数,强制为 0。
* 第 3 个参数(index=2)是延迟 tick 数。
*/
@ModifyArg(
method = "onPlace",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/level/Level;scheduleTick(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/Block;I)V"),
index = 2
)
private int legacyScheduleDelayLevel(int delay) {
if (SLSCarpetSettings.legacyExploitMode && SLSCarpetSettings.legacyInstantFall) {
return 0;
}
return delay;
}
@ModifyArg(
method = "updateShape",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/level/ScheduledTickAccess;scheduleTick(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/Block;I)V"),
index = 2
)
private int legacyScheduleDelayAccess(int delay) {
if (SLSCarpetSettings.legacyExploitMode && SLSCarpetSettings.legacyInstantFall) {
return 0;
}
return delay;
}
/** instantFall:tick 入口标记(供其他机制判断窗口)。 */
@Inject(method = "tick", at = @At("HEAD"))
private void legacyTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource rand, CallbackInfo ci) {
if (SLSCarpetSettings.legacyExploitMode && SLSCarpetSettings.legacyInstantFall) {
LegacyExploitState.markInstantFall(pos);
}
}
}
@@ -1,32 +0,0 @@
package com.github.zly2006.carpetslsaddition.mixin.legacy;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.world.ticks.ScheduledTick;
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;
/**
* [旧版] 计划刻立即执行(等价 1.12 的 instantTick flag)。
*
* <p>1.12 的 instantTick:世界装饰时会短暂开启,使计划刻(中继器、发射器、沙子等)
* 立即执行而不等待延迟。26.1.2 已移除该 flag。
*
* <p>本 mixin 用 {@link Inject} 拦截 {@link ScheduledTick#triggerTick()}:
* 规则开启时返回 0,使该计划刻在"当前时刻"被触发(等价立即执行)。
*
* <p><b>注意</b>:这会强制所有计划刻立即触发,可能造成大量连锁更新,
* 仅应在 {@code legacyExploitMode} + {@code legacyInstantTick} 同时开启时使用。
*/
@Mixin(ScheduledTick.class)
public abstract class ScheduledTickMixin {
@Inject(method = "triggerTick", at = @At("HEAD"), cancellable = true)
private void legacyInstantTriggerTick(CallbackInfoReturnable<Long> cir) {
if (SLSCarpetSettings.legacyExploitMode && SLSCarpetSettings.legacyInstantTick) {
// 返回 0 表示"在当前时刻立即触发"
cir.setReturnValue(0L);
}
}
}
@@ -3,6 +3,7 @@ package com.github.zly2006.carpetslsaddition.mixin.world;
import com.github.zly2006.carpetslsaddition.SLSCarpetSettings; import com.github.zly2006.carpetslsaddition.SLSCarpetSettings;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.EntityTypes;
import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.Mob;
import net.minecraft.world.level.NaturalSpawner; import net.minecraft.world.level.NaturalSpawner;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
@@ -16,7 +17,7 @@ public class MixinSpawnHelper {
// 改为在 getMobForSpawn 返回处拦截。 // 改为在 getMobForSpawn 返回处拦截。
@Inject(method = "getMobForSpawn", at = @At("HEAD"), cancellable = true) @Inject(method = "getMobForSpawn", at = @At("HEAD"), cancellable = true)
private static void onCreateMob(ServerLevel level, EntityType<?> type, CallbackInfoReturnable<Mob> cir) { 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); 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();
}
}
@@ -1,105 +0,0 @@
package com.github.zly2006.carpetslsaddition.util;
import com.github.zly2006.carpetslsaddition.ServerMain;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.chunk.LevelChunk;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* [旧版] 自建异步线程,模拟 1.12 的"染色玻璃→信标搜索线程"。
*
* <p>1.12 的漏洞依赖一个独立的异步线程,与主线程并发访问区块哈希表
* ({@code Long2ObjectOpenHashMap}),从而制造 race condition,让 chunk 被误判为"未加载",
* 触发重新加载与世界装饰。
*
* <p>26.1.2 的信标已无独立线程,故本类自建一个单线程调度器:
* 周期性在<b>主线程之外</b>调用 {@link ServerChunkCache#getChunkNow} 与读取区块,
* 尽最大可能复现 1.12 的并发访问模式。
*
* <p><b>⚠ 风险提示</b>:26.1.2 的区块访问有 {@code mainThreadExecutor} 约束,
* 跨线程访问可能触发断言或未定义行为,极端情况下可导致服务器崩溃或存档损坏。
* 仅在 {@code legacyExploitMode} + {@code legacyAsyncChunkAccess} 同时开启时启动。
*/
public final class LegacyAsyncExecutor {
private LegacyAsyncExecutor() {}
private static ScheduledExecutorService executor;
private static volatile boolean started = false;
/** 目标区块坐标(由玩家装置或命令设置)。 */
private static volatile int targetChunkX = Integer.MIN_VALUE;
private static volatile int targetChunkZ = Integer.MIN_VALUE;
public static void setTargetChunk(int x, int z) {
targetChunkX = x;
targetChunkZ = z;
}
public static synchronized void start() {
if (started) {
return;
}
executor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "SLSA-Legacy-AsyncChunk");
t.setDaemon(true);
return t;
});
executor.scheduleAtFixedRate(LegacyAsyncExecutor::tick, 0, 1, TimeUnit.MILLISECONDS);
started = true;
LegacyExploitState.setAsyncThreadRunning(true);
ServerMain.LOGGER.info("[SLSA] legacy async chunk thread started");
}
public static synchronized void stop() {
if (!started || executor == null) {
return;
}
executor.shutdownNow();
executor = null;
started = false;
LegacyExploitState.setAsyncThreadRunning(false);
ServerMain.LOGGER.info("[SLSA] legacy async chunk thread stopped");
}
/** 异步线程主体:并发访问目标区块,制造与主线程的竞态。 */
private static void tick() {
try {
if (!SLSCarpetSettingsBridge.shouldRun()) {
return;
}
MinecraftServer server = ServerMain.server;
if (server == null) {
return;
}
for (ServerLevel level : server.getAllLevels()) {
ServerChunkCache cache = level.getChunkSource();
if (targetChunkX != Integer.MIN_VALUE) {
// 故意在主线程之外读取区块表——制造 race condition
LevelChunk chunk = cache.getChunkNow(targetChunkX, targetChunkZ);
if (chunk != null) {
// 触发一次方块状态读取,模拟信标向下搜索
chunk.getBlockState(new net.minecraft.core.BlockPos(
targetChunkX << 4, level.getMinY(), targetChunkZ << 4));
}
}
}
} catch (Throwable t) {
// 异步线程绝不能让异常冒泡导致调度器静默停止
ServerMain.LOGGER.debug("[SLSA] legacy async tick error: {}", t.toString());
}
}
/** 桥接类:避免直接依赖 SLSCarpetSettings 的静态初始化顺序问题。 */
private static final class SLSCarpetSettingsBridge {
static boolean shouldRun() {
return com.github.zly2006.carpetslsaddition.SLSCarpetSettings.legacyExploitMode
&& com.github.zly2006.carpetslsaddition.SLSCarpetSettings.legacyAsyncChunkAccess;
}
}
}
@@ -1,70 +0,0 @@
package com.github.zly2006.carpetslsaddition.util;
import com.github.zly2006.carpetslsaddition.ServerMain;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.block.state.BlockState;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* [旧版] 方块变更记录器——用于重建 1.12 的"替换窗口"。
*
* <p>1.12 的窗口依赖"异步线程在主线程 tick 中途改块",26.1.2 单线程模型下物理不存在。
* 本类改用"记录+回放":把最近发生的方块变更记录在此,
* 当重力方块触发下落时,若该位置"刚被改过",就用记录的新方块作为下落实体的内容——
* 等价于 1.12 的窗口替换效果(只是窗口由 mod 主动开启)。
*
* <p>记录以"服务器 tick 数"为界:仅保留最近 {@link #WINDOW_TICKS} 个 tick 内的变更,
* 超过即失效,避免记录无限增长或误伤无关方块。
*/
public final class LegacyBlockChangeRecorder {
private LegacyBlockChangeRecorder() {}
/** 窗口大小:变更与下落之间允许的最大 tick 间隔。 */
public static final long WINDOW_TICKS = 2L;
private record Change(BlockState state, long tick) {}
private static final Map<Long, Change> RECENT = new ConcurrentHashMap<>();
/** 获取当前服务器 tick(拿不到时回退为 0)。 */
private static long now() {
MinecraftServer server = ServerMain.server;
return server != null ? server.getTickCount() : 0L;
}
/** 记录一次方块变更。 */
public static void record(BlockPos pos, BlockState newState) {
RECENT.put(pos.asLong(), new Change(newState, now()));
}
/**
* 查询某位置是否在窗口期内有变更记录。
*
* @return 若命中窗口,返回记录的新状态;否则返回 null。
*/
public static BlockState getRecentChange(BlockPos pos) {
Change c = RECENT.get(pos.asLong());
if (c == null) {
return null;
}
if (now() - c.tick() > WINDOW_TICKS) {
RECENT.remove(pos.asLong());
return null;
}
return c.state();
}
/** 清理过期记录。 */
public static void cleanup() {
long t = now();
RECENT.entrySet().removeIf(e -> t - e.getValue().tick() > WINDOW_TICKS);
}
public static void clear() {
RECENT.clear();
}
}
@@ -1,66 +0,0 @@
package com.github.zly2006.carpetslsaddition.util;
import net.minecraft.core.BlockPos;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
/**
* [旧版] 漏洞机制的全局状态管理(线程安全)。
*
* <p>承载:
* <ul>
* <li>当前"落沙替换目标方块"ID(由 FallingBlockMixin 设置,FallingBlockEntityMixin 消费)</li>
* <li>被标记为 instantFall 触发的位置(跨线程共享,故用并发容器)</li>
* <li>异步 chunk 访问线程的运行状态</li>
* </ul>
*
* <p><b>线程安全说明</b>:本类会被主线程与自建的异步线程同时访问,
* 所有可变状态均使用并发容器或原子引用。
*/
public final class LegacyExploitState {
private LegacyExploitState() {}
/** 当前落沙替换目标(方块 ID 字符串),"#none" 表示禁用。 */
private static final AtomicReference<String> REPLACE_TARGET = new AtomicReference<>("#none");
/** 被标记为 instantFall 触发的位置集合(异步线程可读)。 */
private static final ConcurrentHashMap<Long, Boolean> INSTANT_FALL_POS = new ConcurrentHashMap<>();
/** 异步 chunk 访问线程是否正在运行。 */
private static volatile boolean asyncThreadRunning = false;
public static void setReplaceTarget(String target) {
REPLACE_TARGET.set(target == null ? "#none" : target);
}
public static String getReplaceTarget() {
return REPLACE_TARGET.get();
}
public static boolean hasReplaceTarget() {
String t = REPLACE_TARGET.get();
return t != null && !"#none".equals(t) && !t.isEmpty();
}
public static void markInstantFall(BlockPos pos) {
INSTANT_FALL_POS.put(pos.asLong(), Boolean.TRUE);
}
public static boolean isInstantFall(BlockPos pos) {
return INSTANT_FALL_POS.containsKey(pos.asLong());
}
public static void clearInstantFall(BlockPos pos) {
INSTANT_FALL_POS.remove(pos.asLong());
}
public static boolean isAsyncThreadRunning() {
return asyncThreadRunning;
}
public static void setAsyncThreadRunning(boolean running) {
asyncThreadRunning = running;
}
}
@@ -23,7 +23,7 @@ public class ShulkerBoxItemUtil {
if (itemStack.getItem() instanceof BlockItem blockItem if (itemStack.getItem() instanceof BlockItem blockItem
&& blockItem.getBlock() instanceof ShulkerBoxBlock) { && blockItem.getBlock() instanceof ShulkerBoxBlock) {
return itemStack.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY) return itemStack.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY)
.allItemsCopyStream().findAny().isEmpty(); .nonEmptyItemCopyStream().findAny().isEmpty();
} }
return false; return false;
} }
@@ -2,7 +2,6 @@
"carpet.category.pca": "PCA Adaption", "carpet.category.pca": "PCA Adaption",
"carpet.category.needClient":"Need client support", "carpet.category.needClient":"Need client support",
"carpet.category.protocol":"Protocol", "carpet.category.protocol":"Protocol",
"carpet.category.legacy": "Legacy",
"carpet.rule.obtainableReinforcedDeepSlate.name": "Obtainable reinforced deep slate", "carpet.rule.obtainableReinforcedDeepSlate.name": "Obtainable reinforced deep slate",
@@ -49,7 +48,7 @@
"carpet.rule.elytraCraftable.desc": "Make elytra craftable for Minecraft 1.20.6+ (eltra wont disappear when crafting)", "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.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.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.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.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.", "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.",
@@ -74,19 +73,15 @@
"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.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.legacyExploitMode.name": "Legacy exploit mode", "carpet.rule.stackableVehicles.desc": "Minecarts and boats can stack up to 64, auto-merging on pickup",
"carpet.rule.legacyExploitMode.desc": "Master switch: other rules under the [Legacy] category only take effect when enabled", "carpet.rule.slsBoardEnabled.name": "Enable boards",
"carpet.rule.legacyInstantTick.name": "Instant scheduled tick", "carpet.rule.slsBoardEnabled.desc": "Allow players to use /slsboard to show death/mining leaderboards",
"carpet.rule.legacyInstantTick.desc": "Equivalent to 1.12 instantTick: scheduled ticks execute immediately without delay", "carpet.rule.noBrittleIceFromFlowingWater.name": "No brittle ice from flowing water",
"carpet.rule.legacyInstantFall.name": "Instant falling blocks", "carpet.rule.noBrittleIceFromFlowingWater.desc": "Only water flowing straight down onto blue ice forms brittle ice. Horizontally flowing water will not.",
"carpet.rule.legacyInstantFall.desc": "Equivalent to 1.12 instantFall: falling blocks fall immediately when scheduled", "carpet.rule.elytraCraftable.name": "Craftable elytra",
"carpet.rule.legacyAsyncChunkAccess.name": "Async chunk access thread", "carpet.rule.armadilloImmediateDespawns.name": "Armadillos despawn immediately",
"carpet.rule.legacyAsyncChunkAccess.desc": "Spawns a background thread mimicking 1.12 beacon thread to access chunks concurrently", "carpet.rule.offlineFakePlayers.name": "Force offline UUID for fake players",
"carpet.rule.legacyChunkSwap.name": "Chunk swap", "carpet.rule.netherPortalSize.name": "Max nether portal size",
"carpet.rule.legacyChunkSwap.desc": "Allows async thread to treat a loaded chunk as unloaded via race condition, triggering reload and world population", "carpet.rule.trialSpawnerCD.name": "Trial spawner cooldown"
"carpet.rule.legacyFallingBlockReplace.name": "Falling block replace",
"carpet.rule.legacyFallingBlockReplace.desc": "Replaces the block with a target block during the falling block to entity window",
"carpet.rule.legacyFallingBlockTarget.name": "Falling replace target",
"carpet.rule.legacyFallingBlockTarget.desc": "Target block ID for falling block replace, #none to disable"
} }
@@ -2,7 +2,6 @@
"carpet.category.pca": "PCA适配", "carpet.category.pca": "PCA适配",
"carpet.category.needClient": "需要客户端支持", "carpet.category.needClient": "需要客户端支持",
"carpet.category.protocol": "协议", "carpet.category.protocol": "协议",
"carpet.category.legacy": "旧版",
"carpet.rule.obtainableReinforcedDeepSlate.name": "强化深板岩可获取", "carpet.rule.obtainableReinforcedDeepSlate.name": "强化深板岩可获取",
@@ -74,19 +73,15 @@
"carpet.slsa.bot.not_a_bot": "假人%s不是由Carpet SLS Addition生成的,所以你不能重置它的在线时间!", "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.legacyExploitMode.name": "旧版漏洞模式", "carpet.rule.stackableVehicles.desc": "矿车和船的物品可以堆叠到64,拾取时自动合并",
"carpet.rule.legacyExploitMode.desc": "总开关:开启后[旧版]分类下的其余机制规则才会生效", "carpet.rule.slsBoardEnabled.name": "启用榜单",
"carpet.rule.legacyInstantTick.name": "计划刻立即执行", "carpet.rule.slsBoardEnabled.desc": "允许玩家用 /slsboard 显示死亡榜/挖掘榜",
"carpet.rule.legacyInstantTick.desc": "等价1.12的instantTick:计划刻(scheduleTick)立即执行而不等待延迟", "carpet.rule.noBrittleIceFromFlowingWater.name": "横向流动水不产生碎冰",
"carpet.rule.legacyInstantFall.name": "重力方块瞬间下落", "carpet.rule.noBrittleIceFromFlowingWater.desc": "只有竖直向下的水流接触蓝冰时才会产生碎冰,横向流动的水不再转化",
"carpet.rule.legacyInstantFall.desc": "等价1.12的instantFall:重力方块被计划刻触发时立即下落", "carpet.rule.elytraCraftable.name": "鞘翅可合成",
"carpet.rule.legacyAsyncChunkAccess.name": "异步区块访问线程", "carpet.rule.armadilloImmediateDespawns.name": "犰狳立即消失",
"carpet.rule.legacyAsyncChunkAccess.desc": "自建后台线程模拟1.12的信标线程,并发访问区块", "carpet.rule.offlineFakePlayers.name": "假人强制离线UUID",
"carpet.rule.legacyChunkSwap.name": "区块替换", "carpet.rule.netherPortalSize.name": "地狱门最大尺寸",
"carpet.rule.legacyChunkSwap.desc": "允许异步线程在区块表竞态时把已加载区块当成未加载,触发重新加载与世界装饰", "carpet.rule.trialSpawnerCD.name": "试炼刷怪笼冷却"
"carpet.rule.legacyFallingBlockReplace.name": "落沙替换",
"carpet.rule.legacyFallingBlockReplace.desc": "在重力方块转为下落实体的窗口内,把方块替换为指定目标方块",
"carpet.rule.legacyFallingBlockTarget.name": "落沙替换目标方块",
"carpet.rule.legacyFallingBlockTarget.desc": "落沙替换的目标方块ID,#none为禁用"
} }
+3
View File
@@ -19,6 +19,9 @@
"entrypoints": { "entrypoints": {
"main": [ "main": [
"com.github.zly2006.carpetslsaddition.ServerMain" "com.github.zly2006.carpetslsaddition.ServerMain"
],
"client": [
"com.github.zly2006.carpetslsaddition.client.BoardClient"
] ]
}, },
"accessWidener": "slsa.accesswidener", "accessWidener": "slsa.accesswidener",
+3 -1
View File
@@ -1,6 +1,8 @@
accessWidener v1 official 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 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/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/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;
+2 -4
View File
@@ -12,14 +12,12 @@
"block.MixinDecoratedPotBlock", "block.MixinDecoratedPotBlock",
"block.MixinTrialSpawnerData", "block.MixinTrialSpawnerData",
"block.MixinTrialSpawnerState", "block.MixinTrialSpawnerState",
"block.MixinFlowingWaterBrittleIce",
"item.SlotMixin",
"carpet.MixinEntityPlayerMPFake", "carpet.MixinEntityPlayerMPFake",
"carpet.MixinPlayerCommand", "carpet.MixinPlayerCommand",
"elytra.ItemAccessor", "elytra.ItemAccessor",
"elytra.MixinItem", "elytra.MixinItem",
"legacy.FallingBlockMixin",
"legacy.FallingBlockEntityMixin",
"legacy.ScheduledTickMixin",
"legacy.ChunkMapMixin",
"entity.MixinArmadilloEntity", "entity.MixinArmadilloEntity",
"entity.MixinArmorStandEntity", "entity.MixinArmorStandEntity",
"entity.MixinBoatEntity", "entity.MixinBoatEntity",