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;
*
* - 频率限制:{@link #WINDOW_MILLIS} 毫秒内最多放行 {@link #MAX_MESSAGES} 条。
* - 重复拦截:与上一条内容相同(忽略大小写与首尾空白)且间隔不足
- * {@link #DUPLICATE_WINDOW_MILLIS} 毫秒的消息被拦截。
+ * {@link #DUPLICATE_WINDOW_MILLIS} 毫秒的消息被拦截。
+ * 但若在这两条之间有其他人发过公屏消息(对话已经推进),则重复内容放行。
*
* 判定完全在服务端进行,走 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()));
}
}