From 796372f8c44c1323300ec5966339ff8373128ebb Mon Sep 17 00:00:00 2001 From: iceBear67 Date: Wed, 22 Jul 2026 17:16:31 +0800 Subject: [PATCH] feat: enhance anti-spam logic to allow messages between player interactions --- .../sfcraft/module/chat/AntiSpamModule.java | 25 +++++++++++++----- .../module/chat/decorator/ChatPingModule.java | 2 +- .../chat/decorator/ChatShowoffModule.java | 26 ++++++++++++++----- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/main/java/io/ib67/sfcraft/module/chat/AntiSpamModule.java b/src/main/java/io/ib67/sfcraft/module/chat/AntiSpamModule.java index a5cfcfe..2aa6c26 100644 --- a/src/main/java/io/ib67/sfcraft/module/chat/AntiSpamModule.java +++ b/src/main/java/io/ib67/sfcraft/module/chat/AntiSpamModule.java @@ -22,7 +22,8 @@ import java.util.UUID; * * 判定完全在服务端进行,走 Fabric {@code ALLOW_CHAT_MESSAGE} 钩子——返回 false 即取消该条消息。 * 只影响公屏聊天;命令(如 /msg)走的是 {@code ALLOW_COMMAND_MESSAGE},不受影响。 @@ -39,6 +40,8 @@ public class AntiSpamModule extends ServerModule { // 仅在服务端主线程访问(聊天与断线回调都在主线程),无需并发容器。 private final Map trackers = new HashMap<>(); + // 全局放行消息计数器,每放行一条公屏消息 +1。用于判断某玩家两次发言之间是否有其他人插话。 + private long messageSeq; @Override public void onInitialize() { @@ -53,13 +56,14 @@ public class AntiSpamModule extends ServerModule { var tracker = trackers.computeIfAbsent(sender.getUUID(), ignored -> new Tracker()); var content = message.signedContent().strip(); - var reasonKey = tracker.check(content, now); + var reasonKey = tracker.check(content, now, messageSeq); if (reasonKey != null) { // 被拦截的消息不计入放行窗口、也不刷新「上一条」,以免刷屏者借此绕过判定。 warn(sender, tracker, now, reasonKey); return false; } - tracker.accept(content, now); + // 先自增再记录:tracker 存下的是「本条消息之后」的全局序号。 + tracker.accept(content, now, ++messageSeq); return true; } @@ -74,13 +78,19 @@ public class AntiSpamModule extends ServerModule { private final Deque recent = new ArrayDeque<>(); private String lastContent = ""; private long lastContentTime; + /** 上一条放行消息时的全局序号;若之后全局序号增长,说明有其他人插过话。 */ + private long lastSeq; private long lastWarnTime; - /** @return 拦截原因的翻译键,{@code null} 表示放行。 */ - String check(String content, long now) { + /** + * @param seq 当前全局放行序号;等于 {@link #lastSeq} 表示自本玩家上次发言以来无人插话。 + * @return 拦截原因的翻译键,{@code null} 表示放行。 + */ + String check(String content, long now, long seq) { if (!content.isEmpty() && content.equalsIgnoreCase(lastContent) - && now - lastContentTime < DUPLICATE_WINDOW_MILLIS) { + && now - lastContentTime < DUPLICATE_WINDOW_MILLIS + && seq == lastSeq) { return "message.sfcraft.antispam.duplicate"; } while (!recent.isEmpty() && now - recent.peekFirst() >= WINDOW_MILLIS) { @@ -92,10 +102,11 @@ public class AntiSpamModule extends ServerModule { return null; } - void accept(String content, long now) { + void accept(String content, long now, long seq) { recent.addLast(now); lastContent = content; lastContentTime = now; + lastSeq = seq; } boolean canWarn(long now) { diff --git a/src/main/java/io/ib67/sfcraft/module/chat/decorator/ChatPingModule.java b/src/main/java/io/ib67/sfcraft/module/chat/decorator/ChatPingModule.java index 428acf6..580366a 100644 --- a/src/main/java/io/ib67/sfcraft/module/chat/decorator/ChatPingModule.java +++ b/src/main/java/io/ib67/sfcraft/module/chat/decorator/ChatPingModule.java @@ -71,7 +71,7 @@ public class ChatPingModule extends ServerModule implements ChatDecorator { var r = match.group(); if (r.isEmpty()) continue; final var d = r.substring(1).toLowerCase(); - return playerNames.stream() + return "@"+playerNames.stream() .filter(it -> it.toLowerCase().startsWith(d)) .peek(it -> foundPlayers.add(serverSupplier.get().getPlayerList().getPlayerByName(it))) .findFirst() diff --git a/src/main/java/io/ib67/sfcraft/module/chat/decorator/ChatShowoffModule.java b/src/main/java/io/ib67/sfcraft/module/chat/decorator/ChatShowoffModule.java index 42c6f84..5d4fa93 100644 --- a/src/main/java/io/ib67/sfcraft/module/chat/decorator/ChatShowoffModule.java +++ b/src/main/java/io/ib67/sfcraft/module/chat/decorator/ChatShowoffModule.java @@ -1,15 +1,21 @@ package io.ib67.sfcraft.module.chat.decorator; import com.google.inject.Inject; +import io.ib67.kiwi.routine.Fail; +import io.ib67.kiwi.routine.Result; +import io.ib67.kiwi.routine.Some; import io.ib67.sfcraft.ServerModule; import io.ib67.sfcraft.registry.chat.SimpleMessageDecorator; import net.minecraft.network.chat.ChatDecorator; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; +import java.util.Optional; + public class ChatShowoffModule extends ServerModule implements ChatDecorator { @Inject SimpleMessageDecorator messageDecorator; @@ -24,15 +30,21 @@ public class ChatShowoffModule extends ServerModule implements ChatDecorator { if (!isEnabled()) { return message; } - if(player == null || player.getMainHandItem().is(Items.AIR)) { return message; } + if (player == null) { + return message; + } - return ReplaceHelper.replace(player, message, ".item", this::generateShowoffText); + var comp = generateShowoffText(player.getMainHandItem(), player) + .orElseGet(generateShowoffText(player.getOffhandItem(), player)::result); + + if (comp == null) return message; + return ReplaceHelper.replace(player, message, ".item", p -> comp); } - private Component generateShowoffText(ServerPlayer serverPlayer) { - var item = serverPlayer.getMainHandItem(); - return item.count() > 1 ? - Component.literal(" "+item.count()+"x ").append(item.getDisplayName()) - : Component.literal(" ").append(item.getDisplayName()); + private Result generateShowoffText(@Nullable ItemStack item, ServerPlayer serverPlayer) { + if (item == null || item.is(Items.AIR)) return Fail.none(); + return new Some<>(item.count() > 1 ? + Component.literal(" " + item.count() + "x ").append(item.getDisplayName()) + : Component.literal(" ").append(item.getDisplayName())); } }