Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import to.itsme.itsmyconfig.util.IMCSerializer;
import to.itsme.itsmyconfig.util.Strings;
import to.itsme.itsmyconfig.util.Utilities;
import to.itsme.itsmyconfig.util.ChatResendDetector;

import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -52,14 +53,21 @@ public void onPacketSend(final PacketSendEvent event) {
if (!(type instanceof PacketType.Play.Server server)) {
return;
}

// Record EVERY packet for burst detection (including blank lines)
final Player player = event.getPlayer();
final String playerIdentifier = player.getUniqueId().toString();
ChatResendDetector.recordPacket(playerIdentifier);

final boolean isInBurst = ChatResendDetector.isInBurst(playerIdentifier);
Utilities.debug(() -> "################# CHAT PACKET #################\nProcessing packet " + server.name() +
(isInBurst ? " (RESEND DETECTED)" : ""));

final PacketProcessor<?> processor = packetTypeMap.get(server);
if (processor == null) {
return;
}

Comment on lines +57 to 70
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect packet recording: The detector records packets before checking if the processor exists (line 66-68) and before verifying the packet type is actually a chat packet. This means ALL server packets (not just chat-related ones) are recorded, and DISCONNECT packets (line 34) are also recorded. Disconnect packets should not contribute to chat resend detection. Move the recordPacket call after confirming it's a chat-related packet.

Suggested change
// Record EVERY packet for burst detection (including blank lines)
final Player player = event.getPlayer();
final String playerIdentifier = player.getUniqueId().toString();
ChatResendDetector.recordPacket(playerIdentifier);
final boolean isInBurst = ChatResendDetector.isInBurst(playerIdentifier);
Utilities.debug(() -> "################# CHAT PACKET #################\nProcessing packet " + server.name() +
(isInBurst ? " (RESEND DETECTED)" : ""));
final PacketProcessor<?> processor = packetTypeMap.get(server);
if (processor == null) {
return;
}
final Player player = event.getPlayer();
final String playerIdentifier = player.getUniqueId().toString();
final PacketProcessor<?> processor = packetTypeMap.get(server);
if (processor == null) {
return;
}
// Record only chat-related packets for burst detection
ChatResendDetector.recordPacket(playerIdentifier);
final boolean isInBurst = ChatResendDetector.isInBurst(playerIdentifier);
Utilities.debug(() -> "################# CHAT PACKET #################\nProcessing packet " + server.name() +
(isInBurst ? " (RESEND DETECTED)" : ""));

Copilot uses AI. Check for mistakes.
Utilities.debug(() -> "################# CHAT PACKET #################\nProcessing packet " + server.name());

// Convert to wrapped packet only once
Object wrappedPacket = switch (server) {
case CHAT_MESSAGE -> new WrapperPlayServerChatMessage(event);
Expand Down Expand Up @@ -89,7 +97,12 @@ public void onPacketSend(final PacketSendEvent event) {
}

final String message = packet.message();
Utilities.debug(() -> "Found message: " + message);
final boolean hasInvisibleUnicode = ChatResendDetector.containsInvisibleUnicode(message);
final boolean isChatClear = isInBurst || hasInvisibleUnicode;

Utilities.debug(() -> "Found message: " + message +
(hasInvisibleUnicode ? " [INVISIBLE UNICODE]" : "") +
(isChatClear ? " [CHAT CLEAR DETECTED]" : ""));
Comment on lines +101 to +105
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable: The variable 'isChatClear' is computed but never used. It appears to be calculated for debugging purposes but isn't referenced anywhere in the method logic. Either use it for some purpose or remove it to avoid confusion.

Suggested change
final boolean isChatClear = isInBurst || hasInvisibleUnicode;
Utilities.debug(() -> "Found message: " + message +
(hasInvisibleUnicode ? " [INVISIBLE UNICODE]" : "") +
(isChatClear ? " [CHAT CLEAR DETECTED]" : ""));
Utilities.debug(() -> "Found message: " + message +
(hasInvisibleUnicode ? " [INVISIBLE UNICODE]" : "") +
((isInBurst || hasInvisibleUnicode) ? " [CHAT CLEAR DETECTED]" : ""));

Copilot uses AI. Check for mistakes.

if (message.startsWith(FAIL_MESSAGE_PREFIX)) {
Utilities.debug(() -> "Message send failure message, cancelling...");
Expand All @@ -98,13 +111,18 @@ public void onPacketSend(final PacketSendEvent event) {
}

final Optional<String> parsed = Strings.parsePrefixedMessage(message);
if (parsed.isEmpty()) {
Utilities.debug(() -> "Message doesn't start w/ the symbol-prefix: " + message + "\n" + Strings.DEBUG_HYPHEN);

// Also check if message contains ItsMyConfig placeholders even without prefix (like console does)
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation: The comment mentions checking for placeholders "like console does" but there's no reference to where this console behavior is implemented or why this change is needed for chat clear detection. This makes it unclear if this change is related to the PR's purpose or is an unrelated feature addition.

Suggested change
// Also check if message contains ItsMyConfig placeholders even without prefix (like console does)
// Also check if the message contains ItsMyConfig placeholders even without the symbol-prefix.
// This mirrors the behavior of the server console, which allows sending messages with placeholders
// (e.g., "<p:...>") directly, without requiring the prefix. See ConsoleCommandSender handling in
// PacketProcessor#processConsoleMessage for details. This ensures that placeholder messages sent
// from the console are processed correctly, and is also relevant for chat clear detection logic.

Copilot uses AI. Check for mistakes.
final boolean hasPlaceholders = message.contains("<p:");
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The placeholder check using message.contains("<p:") is fragile and inconsistent with the tag detection pattern used elsewhere in the codebase. This hard-coded string check doesn't account for escaped tags or tags within quotes, and doesn't match the ARG_TAG_PATTERN regex used in TagManager. Consider using a consistent approach for detecting placeholders across the codebase.

Copilot uses AI. Check for mistakes.

if (parsed.isEmpty() && !hasPlaceholders) {
Utilities.debug(() -> "Message doesn't start w/ the symbol-prefix and has no <p: placeholders: " + message + "\n" + Strings.DEBUG_HYPHEN);
return;
}

final Player player = event.getPlayer();
final Component translated = Utilities.translate(parsed.get(), player);
// Use the parsed message if available, otherwise use original message
final String messageToProcess = parsed.isPresent() ? parsed.get() : message;
final Component translated = Utilities.translate(messageToProcess, player);
if (translated.equals(Component.empty())) {
event.setCancelled(true);
Utilities.debug(() -> "Component is empty, cancelling...\n" + Strings.DEBUG_HYPHEN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import to.itsme.itsmyconfig.util.IMCSerializer;
import to.itsme.itsmyconfig.util.Strings;
import to.itsme.itsmyconfig.util.Utilities;
import to.itsme.itsmyconfig.util.ChatResendDetector;

import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -53,24 +54,35 @@ public void load() {
public void onPacketSending(final PacketEvent event) {
final PacketContainer container = event.getPacket();
final PacketType type = container.getType();
Utilities.debug(() -> "################# CHAT PACKET #################\nProccessing packet " + type.name());

// Record EVERY packet for burst detection (including blank lines)
final String playerIdentifier = event.getPlayer().getUniqueId().toString();
ChatResendDetector.recordPacket(playerIdentifier);
Comment on lines +58 to +60
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect packet recording: The detector records ALL packets processed by this listener, including KICK_DISCONNECT packets (line 38). Kick packets should not contribute to chat resend detection as they're not part of chat history floods. Move the recordPacket call after verifying the packet type is actually a chat packet, not a kick packet.

Copilot uses AI. Check for mistakes.

final boolean isInBurst = ChatResendDetector.isInBurst(playerIdentifier);
Utilities.debug(() -> "################# CHAT PACKET #################\nProccessing packet " + type.name() +
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in debug message: "Proccessing" should be "Processing" (missing one 'c').

Suggested change
Utilities.debug(() -> "################# CHAT PACKET #################\nProccessing packet " + type.name() +
Utilities.debug(() -> "################# CHAT PACKET #################\nProcessing packet " + type.name() +

Copilot uses AI. Check for mistakes.
(isInBurst ? " (RESEND DETECTED)" : ""));

final PacketContent<PacketContainer> packet = this.processPacket(container);
if (packet == null || packet.isEmpty()) {
Utilities.debug(() -> "Packet is null or empty\n" + Strings.DEBUG_HYPHEN);
return;
}

final String message = packet.message();
Utilities.debug(() -> "Found message: " + message);
final boolean hasInvisibleUnicode = ChatResendDetector.containsInvisibleUnicode(message);
final boolean isChatClear = isInBurst || hasInvisibleUnicode;

Utilities.debug(() -> "Found message: " + message +
(hasInvisibleUnicode ? " [INVISIBLE UNICODE]" : "") +
(isChatClear ? " [CHAT CLEAR DETECTED]" : ""));
Comment on lines +74 to +78
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable: The variable 'isChatClear' is computed but never used. It appears to be calculated for debugging purposes but isn't referenced anywhere in the method logic. Either use it for some purpose or remove it to avoid confusion.

Suggested change
final boolean isChatClear = isInBurst || hasInvisibleUnicode;
Utilities.debug(() -> "Found message: " + message +
(hasInvisibleUnicode ? " [INVISIBLE UNICODE]" : "") +
(isChatClear ? " [CHAT CLEAR DETECTED]" : ""));
Utilities.debug(() -> "Found message: " + message +
(hasInvisibleUnicode ? " [INVISIBLE UNICODE]" : "") +
((isInBurst || hasInvisibleUnicode) ? " [CHAT CLEAR DETECTED]" : ""));

Copilot uses AI. Check for mistakes.

final Optional<String> parsed = Strings.parsePrefixedMessage(message);
if (parsed.isEmpty()) {
Utilities.debug(() -> "Message doesn't start w/ the symbol-prefix: " + message + "\n" + Strings.DEBUG_HYPHEN);
return;
}


final Player player = event.getPlayer();
final Component translated = Utilities.translate(parsed.get(), player);
// Use the parsed message if available, otherwise use original message
final String messageToProcess = parsed.isPresent() ? parsed.get() : message;
final Component translated = Utilities.translate(messageToProcess, player);
Comment on lines 80 to +85
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic error: The code now processes ALL messages (even those without the symbol prefix) by falling back to the original message when parsed.isEmpty(). This changes the original behavior which would return early if the message didn't have the prefix. Messages without the prefix should not be processed for placeholder replacement unless there's a specific reason documented in the PR description. This could lead to unintended processing of regular chat messages.

Copilot uses AI. Check for mistakes.
if (translated.equals(Component.empty())) {
event.setCancelled(true);
Utilities.debug(() -> "Component is empty, cancelling...\n" + Strings.DEBUG_HYPHEN);
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/java/to/itsme/itsmyconfig/tag/TagManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import to.itsme.itsmyconfig.util.ChatResendDetector;

public final class TagManager {

Expand Down Expand Up @@ -51,6 +52,10 @@ public static String processArgumentTags(
final Player player,
@NotNull String text
) {
// Check if player is currently in burst mode (packet flood already detected by listener)
final String playerIdentifier = player.getUniqueId().toString();
final boolean isChatClearBurst = ChatResendDetector.isInBurst(playerIdentifier);

Matcher matcher = ARG_TAG_PATTERN.matcher(text);
while (matcher.find()) {
final int start = matcher.start();
Expand All @@ -67,6 +72,16 @@ public static String processArgumentTags(
continue; // unknown tag — skip safely, do NOT replace
}

// During chat clear bursts, filter out sound and actionbar tags
if (isChatClearBurst && (tagName.equals("sound") || tagName.equals("actionbar"))) {
// For both actionbar and sound during burst: remove entirely
// - Actionbar messages should be cancelled completely (if message becomes empty, packet is cancelled)
// - Sound tags should be stripped so sound doesn't play
text = text.substring(0, start) + text.substring(end);
matcher = ARG_TAG_PATTERN.matcher(text);
continue;
}

final String arguments = matcher.group(2);
final String[] args = extractArguments(arguments);
if (args.length == 1 && "cancel".equals(args[0])) {
Expand Down
122 changes: 122 additions & 0 deletions core/src/main/java/to/itsme/itsmyconfig/util/ChatResendDetector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package to.itsme.itsmyconfig.util;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;

/**
* Detects rapid chat resend patterns used by chat deletion/moderation plugins.
* When plugins delete messages from chat, they typically flood blank lines to clear
* the chat window, then resend the chat history. This detector identifies that pattern
* to prevent actionbar/sound effects from firing during resends.
*/
public final class ChatResendDetector {

private static final long BURST_WINDOW_MS = 100; // 100ms window for detecting rapid packet bursts
private static final int BURST_THRESHOLD = 6; // 6+ packets in 100ms = resend pattern detected

private static final Map<String, BurstTracker> trackers = new ConcurrentHashMap<>();
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory leak: The trackers Map accumulates entries for every player UUID but never removes expired entries automatically. Only endBurst() removes entries, but it's only called in tests. Over time, this will grow unbounded as players join and leave. Add automatic cleanup in isInBurst() or recordPacket() to remove trackers that have been inactive beyond a reasonable timeout (e.g., remove entries older than 1 second), or implement a scheduled cleanup task.

Copilot uses AI. Check for mistakes.

// Invisible unicode characters commonly used to prevent chat condensing
// Includes: Zero Width Space, Zero Width Non-Joiner, Zero Width Joiner,
// Word Joiner, Zero Width No-Break Space (BOM), Mongolian Vowel Separator
Comment on lines +23 to +24
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete documentation: The comment lists 6 specific invisible unicode characters but the regex pattern includes many more (at least 20+ characters including ranges like \u2061-\u2064 and \u206A-\u206F). Update the comment to accurately reflect all characters in the pattern, or clarify that it's a partial list by adding "including but not limited to" or similar wording.

Suggested change
// Includes: Zero Width Space, Zero Width Non-Joiner, Zero Width Joiner,
// Word Joiner, Zero Width No-Break Space (BOM), Mongolian Vowel Separator
// Includes (but is not limited to): Zero Width Space, Zero Width Non-Joiner, Zero Width Joiner,
// Word Joiner, Zero Width No-Break Space (BOM), Mongolian Vowel Separator, and other invisible or formatting Unicode characters.

Copilot uses AI. Check for mistakes.
private static final Pattern INVISIBLE_UNICODE_PATTERN = Pattern.compile(
"[\u200B\u200C\u200D\u2060\uFEFF\u180E\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u2061-\u2064\u206A-\u206F]"
);

/**
* Records a packet for resend detection. Call this for EVERY chat packet to track
* the "blank line flood" pattern used by chat deletion plugins.
* @param identifier Unique identifier (usually player UUID)
*/
public static void recordPacket(String identifier) {
final long currentTime = System.currentTimeMillis();
final BurstTracker tracker = trackers.computeIfAbsent(identifier, k -> new BurstTracker());
tracker.addPacket(currentTime);
}

/**
* Checks if player is currently in a resend burst state.
* Call this when processing messages with actionbar/sound tags.
* @param identifier Unique identifier (usually player UUID)
* @return true if player is receiving a chat resend (rapid packet flood detected)
*/
public static boolean isInBurst(String identifier) {
final BurstTracker tracker = trackers.get(identifier);
if (tracker == null) {
return false;
}
return tracker.isInBurst();
}

/**
* Forcefully ends burst tracking for testing or manual intervention.
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete documentation: The Javadoc comment for endBurst mentions "testing or manual intervention" but doesn't warn about the memory leak implications if this method is not called in production. Given that this is the only way to clean up tracker entries, the documentation should clarify when/how this should be called in production environments (e.g., on player disconnect).

Suggested change
* Forcefully ends burst tracking for testing or manual intervention.
* Forcefully ends burst tracking for the given identifier.
* <p>
* <b>Warning:</b> If this method is not called when a player (or other identifier) disconnects or is no longer being tracked,
* the internal tracker map will retain entries indefinitely, leading to a memory leak.
* <p>
* This method <b>must</b> be called in production environments (e.g., on player disconnect) to clean up tracker entries.
* While it can also be used for testing or manual intervention, proper cleanup is required to prevent unbounded memory growth.
*
* @param identifier Unique identifier (usually player UUID)

Copilot uses AI. Check for mistakes.
*/
public static void endBurst(String identifier) {
trackers.remove(identifier);
}

/**
* Checks if a message contains invisible unicode characters.
* These are commonly used by chat clear plugins (like Allium) to prevent
* chat mods from condensing repeated blank lines.
* @param message The message to check
* @return true if the message contains invisible unicode characters
*/
public static boolean containsInvisibleUnicode(String message) {
if (message == null || message.isEmpty()) {
return false;
}
return INVISIBLE_UNICODE_PATTERN.matcher(message).find();
}

/**
* Checks if a message appears to be part of a chat clear operation.
* Returns true if the message contains invisible unicode OR if player is in burst.
* @param identifier Unique identifier (usually player UUID)
* @param message The message content to check
* @return true if this appears to be a chat clear/resend operation
*/
public static boolean isChatClearPacket(String identifier, String message) {
return isInBurst(identifier) || containsInvisibleUnicode(message);
}

/**
* Tracks packet patterns to identify chat resend behavior.
* Uses pure timing-based detection - counts ALL packets including blank lines.
*/
private static class BurstTracker {
private final AtomicInteger count = new AtomicInteger(0);
private final AtomicLong windowStart = new AtomicLong(0);
private volatile boolean inBurst = false;

public void addPacket(long currentTime) {
// If window expired, start new window
if ((currentTime - windowStart.get()) > BURST_WINDOW_MS) {
windowStart.set(currentTime);
count.set(1);
inBurst = false;
return;
Comment on lines +95 to +101
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: The window expiration check and state reset are not atomic. Between checking if the window has expired (line 97) and resetting the state (lines 98-100), another thread could call isInBurst and read stale values. This could cause inconsistent behavior where a burst is reported as active when it should have expired. Consider synchronizing both addPacket and isInBurst methods.

Copilot uses AI. Check for mistakes.
}

// Add packet to current window
final int newCount = count.incrementAndGet();

// Enter burst mode on high frequency (6+ packets in 100ms)
if (newCount >= BURST_THRESHOLD) {
inBurst = true;
}
}

public boolean isInBurst() {
// Check if window has expired
if ((System.currentTimeMillis() - windowStart.get()) > BURST_WINDOW_MS) {
inBurst = false;
count.set(0);
Comment on lines +115 to +117
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: The window expiration check and state reset are not atomic. Between checking the window expiration (line 115) and resetting the state (lines 116-117), another thread could call addPacket and modify the count or windowStart. This could lead to inconsistent state where inBurst is reset but count remains non-zero, or vice versa. Consider using synchronized methods or AtomicReference to wrap the BurstTracker state.

Copilot uses AI. Check for mistakes.
}
return inBurst;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package to.itsme.itsmyconfig.util;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class ChatResendDetectorTest {

@Test
public void testNoBurstOnSinglePacket() {
final String testId = "test-player-single";

// Clean up any existing state
ChatResendDetector.endBurst(testId);

// Single packet should not trigger burst
ChatResendDetector.recordPacket(testId);
assertFalse(ChatResendDetector.isInBurst(testId));
}

@Test
public void testNoBurstOnFewPackets() {
final String testId = "test-player-few";

// Clean up any existing state
ChatResendDetector.endBurst(testId);

// Few packets (under threshold of 6) should not trigger burst
for (int i = 0; i < 4; i++) {
ChatResendDetector.recordPacket(testId);
assertFalse(ChatResendDetector.isInBurst(testId));
}
}

@Test
public void testBurstOnManyRapidPackets() {
final String testId = "test-player-burst";

// Clean up any existing state
ChatResendDetector.endBurst(testId);

// 6+ packets in rapid succession should trigger burst
for (int i = 0; i < 5; i++) {
ChatResendDetector.recordPacket(testId);
assertFalse(ChatResendDetector.isInBurst(testId)); // 1-5
}
ChatResendDetector.recordPacket(testId); // 6th
assertTrue(ChatResendDetector.isInBurst(testId)); // Should now be in burst
}

@Test
public void testBurstResetAfterDelay() throws InterruptedException {
final String testId = "test-player-reset";

// Clean up any existing state
ChatResendDetector.endBurst(testId);

// Send rapid packets to trigger burst
for (int i = 0; i < 6; i++) {
ChatResendDetector.recordPacket(testId);
}
assertTrue(ChatResendDetector.isInBurst(testId));

// Wait for window to expire
Thread.sleep(120); // Longer than 100ms threshold
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky test: The test uses Thread.sleep(120) with a threshold of 100ms. This hardcoded 20ms buffer may not be sufficient on slower systems or under load, potentially causing intermittent test failures. Consider using a larger buffer (e.g., 150ms or 200ms) or making the window threshold configurable for testing.

Suggested change
Thread.sleep(120); // Longer than 100ms threshold
Thread.sleep(200); // Increased buffer to avoid flakiness (threshold is 100ms)

Copilot uses AI. Check for mistakes.

// Should no longer be in burst
assertFalse(ChatResendDetector.isInBurst(testId));
}

@Test
public void testIndependentPlayerTracking() {
final String player1 = "test-player-1";
final String player2 = "test-player-2";

// Clean up
ChatResendDetector.endBurst(player1);
ChatResendDetector.endBurst(player2);

// Trigger burst for player1
for (int i = 0; i < 6; i++) {
ChatResendDetector.recordPacket(player1);
}

// Player1 should be in burst, player2 should not
assertTrue(ChatResendDetector.isInBurst(player1));
assertFalse(ChatResendDetector.isInBurst(player2));
}
}
Comment on lines +1 to +88
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for containsInvisibleUnicode method: The tests only cover the timing-based burst detection logic but don't test the invisible unicode detection feature, which is a key part of the chat clear detection mechanism mentioned in the PR description. Add tests that verify containsInvisibleUnicode returns true for messages with characters like Zero Width Space (U+200B), Zero Width Non-Joiner (U+200C), etc., and false for normal messages.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +88
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for isChatClearPacket method: This public method combines burst detection with invisible unicode detection and is used by the listeners, but there are no tests covering it. Add tests to verify it correctly returns true when either condition is met (burst OR invisible unicode), and false when neither is present.

Copilot uses AI. Check for mistakes.
Loading