fix: carry-over fixes — per-type cooldowns, PM cooldown, nickname persistence, thread-safe deques

- PlayerServiceImpl: separate cooldown map per chat type (fixes cross-blocking between local chat and RP commands)
- PrivateMessageServiceImpl: enforce chat.pm.cooldown (honors chat.bypass.cooldown)
- NickServiceImpl: persist nicknames async on every change/reset
- FloodFilter/SpamFilter: ConcurrentLinkedDeque instead of ArrayDeque
- tests: PlayerServiceCooldownTest
This commit is contained in:
loki5512344 2026-08-09 21:08:58 +02:00
parent b1b0c9082b
commit cda89c78e4
Signed by: boba
GPG key ID: 253067914055423B
8 changed files with 114 additions and 28 deletions

View file

@ -5,18 +5,17 @@ import com.loki.lochat.core.filter.FilterResult;
import org.bukkit.entity.Player;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
public class FloodFilter {
private static final String FLOOD_BLOCK_MESSAGE = "&#CF6679Не флудите!";
private final int maxMessages;
private final int timePeriod;
private final Map<UUID, Deque<Long>> floodTracker = new ConcurrentHashMap<>();
private final Map<UUID, ConcurrentLinkedDeque<Long>> floodTracker = new ConcurrentHashMap<>();
public FloodFilter(FiltersConfig filters) {
this.maxMessages = filters.getFloodMaxMessages();
@ -31,8 +30,8 @@ public class FloodFilter {
long now = System.currentTimeMillis();
long windowMs = timePeriod * 1000L;
Deque<Long> timestamps = floodTracker.computeIfAbsent(
player.getUniqueId(), k -> new ArrayDeque<>());
ConcurrentLinkedDeque<Long> timestamps = floodTracker.computeIfAbsent(
player.getUniqueId(), k -> new ConcurrentLinkedDeque<>());
// Удаляем старые timestamp'ы
while (!timestamps.isEmpty() && now - timestamps.peekFirst() > windowMs) {

View file

@ -5,17 +5,16 @@ import com.loki.lochat.core.filter.FilterResult;
import org.bukkit.entity.Player;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
public class SpamFilter {
private final int maxSimilar;
private final int threshold;
private final String blockMessage;
private final Map<UUID, Deque<String>> spamTracker = new ConcurrentHashMap<>();
private final Map<UUID, ConcurrentLinkedDeque<String>> spamTracker = new ConcurrentHashMap<>();
public SpamFilter(FiltersConfig filters) {
this(filters.getSpamMaxSimilarMessages(), filters.getSpamSimilarityThreshold(),
@ -33,8 +32,8 @@ public class SpamFilter {
return FilterResult.ok(message);
}
Deque<String> history = spamTracker.computeIfAbsent(
player.getUniqueId(), k -> new ArrayDeque<>());
ConcurrentLinkedDeque<String> history = spamTracker.computeIfAbsent(
player.getUniqueId(), k -> new ConcurrentLinkedDeque<>());
// Считаем похожие сообщения
long similarCount = history.stream()

View file

@ -52,7 +52,7 @@ public class ServiceRegistry {
register(ChatService.class, new ChatServiceImpl(plugin, messageService));
PrivateMessageServiceImpl pmService = new PrivateMessageServiceImpl((LoChat) plugin);
PrivateMessageServiceImpl pmService = new PrivateMessageServiceImpl((LoChat) plugin, playerService);
MessagingService messagingService = ServiceFactory.createMessagingService(plugin, messageConfig, pmService);
pmService.init(messagingService);
register(PrivateMessageService.class, pmService);

View file

@ -4,6 +4,7 @@ import com.loki.lochat.api.service.NickService;
import com.loki.lochat.config.RatConfig;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.persistence.FilePersistence;
import com.loki.lochat.utils.platform.FoliaUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
@ -76,6 +77,9 @@ public class NickServiceImpl implements NickService {
// Устанавливаем ник
nicknames.put(player, nickname);
// Персистим сразу — не теряем ники при падении сервера
FoliaUtil.runAsync(plugin, this::save);
// Обновляем display
Player onlinePlayer = Bukkit.getPlayer(player);
if (onlinePlayer != null) {
@ -89,6 +93,9 @@ public class NickServiceImpl implements NickService {
public void resetNickname(UUID player) {
nicknames.remove(player);
// Персистим сразу
FoliaUtil.runAsync(plugin, this::save);
// Сбрасываем display
Player onlinePlayer = Bukkit.getPlayer(player);
if (onlinePlayer != null) {

View file

@ -20,9 +20,8 @@ public class PlayerServiceImpl implements PlayerService {
private final JavaPlugin plugin;
// Cooldown state
private final Map<UUID, Long> globalCooldowns = new ConcurrentHashMap<>();
private final Map<UUID, Long> localCooldowns = new ConcurrentHashMap<>();
// Cooldown state — separate map per chat type (global, local, rp_me, rp_do, rp_try, pm...)
private final Map<String, Map<UUID, Long>> cooldowns = new ConcurrentHashMap<>();
// Statistics state
private final Map<UUID, Long> playerMessages = new ConcurrentHashMap<>();
@ -74,12 +73,11 @@ public class PlayerServiceImpl implements PlayerService {
@Override
public void removeCooldown(UUID player) {
globalCooldowns.remove(player);
localCooldowns.remove(player);
cooldowns.values().forEach(map -> map.remove(player));
}
private Map<UUID, Long> getCooldownMap(String type) {
return "global".equals(type) ? globalCooldowns : localCooldowns;
return cooldowns.computeIfAbsent(type, k -> new ConcurrentHashMap<>());
}
// ========== Statistics Implementation ==========

View file

@ -2,6 +2,7 @@ package com.loki.lochat.core.service.messaging;
import com.loki.lochat.LoChat;
import com.loki.lochat.api.service.MessagingService;
import com.loki.lochat.api.service.PlayerService;
import com.loki.lochat.api.service.pm.PrivateMessageService;
import com.loki.lochat.utils.format.ChatFormatter;
import com.loki.lochat.utils.player.PlayerUtil;
@ -23,10 +24,12 @@ public class PrivateMessageServiceImpl implements PrivateMessageService {
private final Map<UUID, UUID> lastConversation = new ConcurrentHashMap<>();
private final LoChat plugin;
private final PlayerService playerService;
private MessagingService messagingService;
public PrivateMessageServiceImpl(LoChat plugin) {
public PrivateMessageServiceImpl(LoChat plugin, PlayerService playerService) {
this.plugin = plugin;
this.playerService = playerService;
}
public void init(MessagingService messagingService) {
@ -56,6 +59,19 @@ public class PrivateMessageServiceImpl implements PrivateMessageService {
@Override
public void sendPrivateMessage(CommandSender sender, Player target, String message) {
Player playerSender = (Player) sender;
int cooldown = plugin.getConfig().getInt("chat.pm.cooldown", 2);
if (cooldown > 0 && !playerSender.hasPermission("chat.bypass.cooldown")
&& playerService.isOnCooldown(playerSender.getUniqueId(), "pm", cooldown)) {
int remaining = playerService.getRemainingCooldown(
playerSender.getUniqueId(), "pm", cooldown);
String cooldownMessage = plugin.getConfigManager().getMessagesConfig()
.getCooldownMessage().replace("{remaining}", String.valueOf(remaining));
sender.sendMessage(ChatFormatter.parse(cooldownMessage));
return;
}
playerService.setCooldown(playerSender.getUniqueId(), "pm");
sender.sendMessage(ChatFormatter.formatPmSentNew(
plugin.getMessageConfig().getPmFormatSent(), playerSender, target, message));
target.sendMessage(ChatFormatter.formatPmReceivedNew(