diff --git a/README.md b/README.md index 65c7ea0..a6d7ee9 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,35 @@ EasyBot的Minecraft模组实现端,基于[Stonecutter](https://stonecutter.kikug - 使用 Gradle 任务中的 `"Set active project to ..."` 来更新 `src/` 目录下类文件可用的 Minecraft 版本。 - 使用 `buildAndCollect` Gradle 任务将模组发布文件存储在 `build/libs/` 目录。 +## 播报冷却 + +管理员可用 `/easybot config` 查看冷却配置,或执行以下命令保存并即时生效。默认权限等级为 3,与 reload 相同;支持下文的命令开关、权限等级和 LuckPerms 节点配置。 + +```text +/easybot config sync.joinCooldownSeconds 60 +/easybot config sync.quitCooldownSeconds 60 +/easybot config sync.deathCooldownSeconds 30 +``` + +省略秒数时查看该项;支持配置项和常用秒数补全。秒数须为 32 位整数,`0` 或负数关闭冷却。命令只开放上述三个冷却项,保存失败时保留原配置。 + +在 `config/easybot/config.json` 的 `sync` 中配置冷却秒数,例如: + +```json +"sync": { + "chatImageSupport": true, + "joinCooldownSeconds": 60, + "quitCooldownSeconds": 60, + "deathCooldownSeconds": 60 +} +``` + +- 三项默认均为 `0`,`0` 或负数表示关闭;旧配置无需修改。 +- 进服、退服按玩家 UUID 分别计时;死亡按玩家 UUID 和伤害类型分别计时。 +- 首次正常播报,冷却中的重复事件不延长时间;切换死因不会清除原死因的冷却。 +- 只跳过发往主程序的对应播报,不取消游戏事件或改变登录验证。`skipOptions` 仍优先生效。 +- 修改某项时长并重载后,该类事件下次发生时清空原冷却;冷却不写入磁盘,重启后重新计时。 + ## 实用链接 - [Stonecutter 新手指南](https://stonecutter.kikugie.dev/wiki/start/):*提示:您必须理解其运作原理!* @@ -62,6 +91,7 @@ EasyBot的Minecraft模组实现端,基于[Stonecutter](https://stonecutter.kikug | `/easybot say <消息>` | `say` | `easybot.command.say` | 0 | | `/easybot status`(服务状态) | `status` | `easybot.command.status` | 3 | | `/easybot reload` | `reload` | `easybot.command.reload` | 3 | +| `/easybot config <配置项> [秒数]` | `config` | `easybot.command.config` | 3 | | `/easybot bind`、`/easybot bind confirm` | `bind` | `easybot.command.bind` | 0 | | `/easybot confirm ` | `confirm` | `easybot.command.confirm`,同时要求 `bind` 权限 | 0 | | `/easybot bind status`(绑定状态) | `bindStatus` | `easybot.command.bindstatus` | 0 | diff --git a/src/main/java/com/springwater/easybot/commands/EasyBotCommands.java b/src/main/java/com/springwater/easybot/commands/EasyBotCommands.java index 0f1a149..72b4126 100644 --- a/src/main/java/com/springwater/easybot/commands/EasyBotCommands.java +++ b/src/main/java/com/springwater/easybot/commands/EasyBotCommands.java @@ -13,6 +13,7 @@ public class EasyBotCommands { new StatusCommandHandler(), new SayCommandHandler(), new ReloadCommandHandler(), + new ConfigCommandHandler(), new BindCommandHandler() ); diff --git a/src/main/java/com/springwater/easybot/commands/handlers/ConfigCommandHandler.java b/src/main/java/com/springwater/easybot/commands/handlers/ConfigCommandHandler.java new file mode 100644 index 0000000..7d3fd15 --- /dev/null +++ b/src/main/java/com/springwater/easybot/commands/handlers/ConfigCommandHandler.java @@ -0,0 +1,53 @@ +package com.springwater.easybot.commands.handlers; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.springwater.easybot.commands.ICommandHandler; +import com.springwater.easybot.config.ConfigLoader; +import com.springwater.easybot.platforms.ModData; +import com.springwater.easybot.utils.PermissionUtils; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; + +public class ConfigCommandHandler implements ICommandHandler { + @Override + public void register(LiteralArgumentBuilder stack) { + var config = Commands.literal("config") + .requires(source -> PermissionUtils.canUse(source, "config")) + .executes(context -> { + for (String key : ConfigLoader.COOLDOWN_KEYS) show(context.getSource(), key); + context.getSource().sendSystemMessage(Component.literal("用法: /easybot config <配置项> [秒数],0 或负数关闭冷却")); + return 1; + }); + for (String key : ConfigLoader.COOLDOWN_KEYS) { + config.then(Commands.literal("sync." + key) + .executes(context -> show(context.getSource(), key)) + .then(Commands.argument("seconds", IntegerArgumentType.integer()) + .suggests((context, builder) -> { + for (String value : new String[]{"0", "30", "60"}) { + if (value.startsWith(builder.getRemaining())) builder.suggest(value); + } + return builder.buildFuture(); + }) + .executes(context -> { + int seconds = IntegerArgumentType.getInteger(context, "seconds"); + try { + ConfigLoader.setCooldown(key, seconds); + context.getSource().sendSuccess(() -> Component.literal("已保存并生效: sync." + key + " = " + seconds + " 秒"), false); + return 1; + } catch (Exception e) { + ModData.LOGGER.error("保存播报冷却配置失败", e); + context.getSource().sendFailure(Component.literal("保存失败,冷却配置未更改,请检查服务器日志")); + return 0; + } + }))); + } + stack.then(config); + } + + private int show(CommandSourceStack source, String key) { + source.sendSystemMessage(Component.literal("sync." + key + " = " + ConfigLoader.getCooldown(key) + " 秒")); + return 1; + } +} diff --git a/src/main/java/com/springwater/easybot/commands/handlers/HelpCommandHandler.java b/src/main/java/com/springwater/easybot/commands/handlers/HelpCommandHandler.java index 8779ea3..ff7f228 100644 --- a/src/main/java/com/springwater/easybot/commands/handlers/HelpCommandHandler.java +++ b/src/main/java/com/springwater/easybot/commands/handlers/HelpCommandHandler.java @@ -37,6 +37,7 @@ private int sendGetHelp(CommandContext context) { if (PermissionUtils.canUse(source, "say")) root.append(buildCommandLine("/easybot say <消息>", "主动消息同步")); if (PermissionUtils.canUse(source, "status")) root.append(buildCommandLine("/easybot status", "获取状态")); if (PermissionUtils.canUse(source, "reload")) root.append(buildCommandLine("/easybot reload", "重载配置")); + if (PermissionUtils.canUse(source, "config")) root.append(buildCommandLine("/easybot config <配置项> [秒数]", "查看或修改播报冷却")); root.append(Component.literal("\n--------------------------------------------------") .withStyle(ChatFormatting.GRAY)); if (PermissionUtils.canUse(source, "bind")) root.append(buildCommandLine("/easybot bind", "绑定账号")); diff --git a/src/main/java/com/springwater/easybot/config/ConfigLoader.java b/src/main/java/com/springwater/easybot/config/ConfigLoader.java index bbea344..c9a80a7 100644 --- a/src/main/java/com/springwater/easybot/config/ConfigLoader.java +++ b/src/main/java/com/springwater/easybot/config/ConfigLoader.java @@ -3,6 +3,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; +import com.google.gson.JsonObject; import com.springwater.easybot.platforms.EasyBotModImpl; import com.springwater.easybot.platforms.ModData; import org.slf4j.Logger; @@ -29,6 +30,7 @@ public class ConfigLoader { // 当前正在使用的配置实例 private static volatile EasyBotConfig currentConfig; + public static final List COOLDOWN_KEYS = List.of("joinCooldownSeconds", "quitCooldownSeconds", "deathCooldownSeconds"); // 配置内容哈希值,用于检测文件是否真正变化 private static long lastConfigHash = 0; @@ -121,7 +123,7 @@ private static boolean isValidToken(EasyBotConfig config) { * @throws IOException 读取配置文件时发生 IO 错误 * @throws JsonSyntaxException 配置文件格式错误 */ - public static void load() throws IOException { + public static synchronized void load() throws IOException { if (!Files.exists(CONFIG_PATH)) { LOGGER.info("未找到配置文件,正在创建默认配置..."); createDefaultConfigFromResources(); @@ -140,7 +142,7 @@ public static void load() throws IOException { /** * 热重载配置 */ - public static boolean reload() { + public static synchronized boolean reload() { LOGGER.info("正在热重载配置..."); if (!Files.exists(CONFIG_PATH)) { LOGGER.warn("配置文件不存在,继续使用上一个有效配置"); @@ -263,7 +265,7 @@ public static void startAutoReloadWatcher() { /** * 保存当前内存中的配置到磁盘。 */ - public static void save() { + public static synchronized void save() { if (currentConfig == null) { currentConfig = new EasyBotConfig(); } @@ -279,6 +281,50 @@ public static void save() { } } + public static int getCooldown(String key) { + var sync = get().getSync(); + return switch (key) { + case "joinCooldownSeconds" -> sync.getJoinCooldownSeconds(); + case "quitCooldownSeconds" -> sync.getQuitCooldownSeconds(); + case "deathCooldownSeconds" -> sync.getDeathCooldownSeconds(); + default -> throw new IllegalArgumentException("未知冷却配置项: " + key); + }; + } + + public static synchronized void setCooldown(String key, int seconds) throws IOException { + if (!COOLDOWN_KEYS.contains(key)) throw new IllegalArgumentException("未知冷却配置项: " + key); + // Preserve fields unknown to this version, and reject malformed files before writing. + JsonObject document = GSON.fromJson(Files.readString(CONFIG_PATH), JsonObject.class); + if (document == null) throw new JsonSyntaxException("配置不能为 null"); + if (!document.has("sync")) document.add("sync", new JsonObject()); + document.getAsJsonObject("sync").addProperty(key, seconds); + String content = GSON.toJson(document); + EasyBotConfig next = GSON.fromJson(content, EasyBotConfig.class); + next.validate(); + JsonObject expected = GSON.toJsonTree(get()).getAsJsonObject(); + expected.getAsJsonObject("sync").addProperty(key, seconds); + boolean otherSettingsChanged = !expected.equals(GSON.toJsonTree(next)); + Path temporary = Files.createTempFile(CONFIG_PATH.getParent(), "easybot-config-", ".tmp"); + try { + Files.writeString(temporary, content); + try { + Files.move(temporary, CONFIG_PATH, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(temporary, CONFIG_PATH, StandardCopyOption.REPLACE_EXISTING); + } + } finally { + Files.deleteIfExists(temporary); + } + currentConfig = next; + lastConfigHash = content.hashCode(); + // Cooldown listeners read the current config directly; no Bridge reconnect is needed. + if (otherSettingsChanged) { + notifyListeners(); + var server = EasyBotModImpl.INSTANCE.getServer(); + if (server != null) server.execute(() -> server.getPlayerList().getPlayers().forEach(player -> server.getCommands().sendCommands(player))); + } + } + /** * 注册当配置发生变化(重载成功)时的回调函数。 * diff --git a/src/main/java/com/springwater/easybot/config/EasyBotConfig.java b/src/main/java/com/springwater/easybot/config/EasyBotConfig.java index ce70265..9cf95c4 100644 --- a/src/main/java/com/springwater/easybot/config/EasyBotConfig.java +++ b/src/main/java/com/springwater/easybot/config/EasyBotConfig.java @@ -100,7 +100,7 @@ public void validate() { chatFilter.groupToGame.validate(); if (!chatFilter.groupToGame.blockedPlayerNames.isEmpty() || !chatFilter.groupToGame.blockedPlayerUuids.isEmpty()) throw new IllegalArgumentException("groupToGame 不支持玩家名单,主程序未提供发言人身份"); - var commands = List.of("help", "say", "status", "reload", "bind", "confirm", "bindStatus"); + var commands = List.of("help", "say", "status", "reload", "config", "bind", "confirm", "bindStatus"); if (command.enabled == null || command.permissionLevels == null) throw new IllegalArgumentException("命令配置不能为 null"); command.enabled.forEach((name, value) -> { @@ -143,5 +143,8 @@ public static class Event{ @ToString public static class Sync { private boolean chatImageSupport = true; + private int joinCooldownSeconds = 0; + private int quitCooldownSeconds = 0; + private int deathCooldownSeconds = 0; } } diff --git a/src/main/java/com/springwater/easybot/platforms/fabric/features/LoginEventSyncFeature.java b/src/main/java/com/springwater/easybot/platforms/fabric/features/LoginEventSyncFeature.java index 4c90e1e..0ec214b 100644 --- a/src/main/java/com/springwater/easybot/platforms/fabric/features/LoginEventSyncFeature.java +++ b/src/main/java/com/springwater/easybot/platforms/fabric/features/LoginEventSyncFeature.java @@ -8,9 +8,12 @@ import com.springwater.easybot.utils.CarpetUtils; import com.springwater.easybot.threading.EasyBotNetworkingThreadPool; import com.springwater.easybot.utils.PlayerUtils; +import com.springwater.easybot.utils.SyncCooldown; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; public class LoginEventSyncFeature implements IEasyBotFeatures { + private final SyncCooldown joinCooldown = new SyncCooldown(); + private final SyncCooldown quitCooldown = new SyncCooldown(); @Override public void register() { ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { @@ -27,6 +30,7 @@ public void register() { ModData.LOGGER.warn("玩家取消玩家上线同步,因为服务器未连接主程序"); return; } + if (!joinCooldown.allow(player.getUUID().toString(), ConfigLoader.get().getSync().getJoinCooldownSeconds())) return; var playerInfo = PlayerUtils.getPlayerInfo(player); // 注意,最好不要在别的线程获取,你永远不知道下一个tick数据是否可用 EasyBotNetworkingThreadPool.getInstance().addTask(() -> EasyBotModImpl.INSTANCE.getBridgeClient().syncEnterExit(playerInfo, true), "消息同步-进服"); }); @@ -45,6 +49,7 @@ public void register() { ModData.LOGGER.warn("玩家取消玩家下线同步,因为服务器未连接主程序"); return; } + if (!quitCooldown.allow(player.getUUID().toString(), ConfigLoader.get().getSync().getQuitCooldownSeconds())) return; var playerInfo = PlayerUtils.getPlayerInfo(player); // 注意,最好不要在别的线程获取,你永远不知道下一个tick数据是否可用 EasyBotNetworkingThreadPool.getInstance().addTask(() -> EasyBotModImpl.INSTANCE.getBridgeClient().syncEnterExit(playerInfo, false), "消息同步-退服"); }); diff --git a/src/main/java/com/springwater/easybot/platforms/fabric/features/PlayerDeathSyncFeature.java b/src/main/java/com/springwater/easybot/platforms/fabric/features/PlayerDeathSyncFeature.java index ad62e92..8ccf81b 100644 --- a/src/main/java/com/springwater/easybot/platforms/fabric/features/PlayerDeathSyncFeature.java +++ b/src/main/java/com/springwater/easybot/platforms/fabric/features/PlayerDeathSyncFeature.java @@ -8,6 +8,7 @@ import com.springwater.easybot.utils.CarpetUtils; import com.springwater.easybot.threading.EasyBotNetworkingThreadPool; import com.springwater.easybot.utils.PlayerUtils; +import com.springwater.easybot.utils.SyncCooldown; import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.LivingEntity; @@ -15,6 +16,7 @@ import static com.springwater.easybot.utils.DamageTypeMappings.DEATH_MESSAGES; public class PlayerDeathSyncFeature implements IEasyBotFeatures { + private final SyncCooldown deathCooldown = new SyncCooldown(); @Override public void register() { @@ -28,6 +30,9 @@ public void register() { return; } + if (EasyBotModImpl.INSTANCE.getBridgeClient() == null || !EasyBotModImpl.INSTANCE.getBridgeClient().isReady()) return; + String cause = source.typeHolder().unwrapKey().map(Object::toString).orElse(source.getMsgId()); + if (!deathCooldown.allow(player.getUUID() + ":" + cause, ConfigLoader.get().getSync().getDeathCooldownSeconds())) return; var profile = PlayerUtils.getPlayerInfo(player); var killer = new StringBuilder(); var deathReason = new StringBuilder(); diff --git a/src/main/java/com/springwater/easybot/platforms/legacyforge/features/LoginEventSyncFeature.java b/src/main/java/com/springwater/easybot/platforms/legacyforge/features/LoginEventSyncFeature.java index 77709ea..cedfd19 100644 --- a/src/main/java/com/springwater/easybot/platforms/legacyforge/features/LoginEventSyncFeature.java +++ b/src/main/java/com/springwater/easybot/platforms/legacyforge/features/LoginEventSyncFeature.java @@ -8,12 +8,15 @@ import com.springwater.easybot.threading.EasyBotNetworkingThreadPool; import com.springwater.easybot.utils.CarpetUtils; import com.springwater.easybot.utils.PlayerUtils; +import com.springwater.easybot.utils.SyncCooldown; import net.minecraft.server.level.ServerPlayer; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.entity.player.PlayerEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; public class LoginEventSyncFeature implements IEasyBotFeatures { + private final SyncCooldown joinCooldown = new SyncCooldown(); + private final SyncCooldown quitCooldown = new SyncCooldown(); @Override public void register() { MinecraftForge.EVENT_BUS.register(this); @@ -34,6 +37,7 @@ public void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { return; } + if (!joinCooldown.allow(player.getUUID().toString(), ConfigLoader.get().getSync().getJoinCooldownSeconds())) return; var playerInfo = PlayerUtils.getPlayerInfo(player); EasyBotNetworkingThreadPool.getInstance().addTask(() -> LegacyForgeEntry.getBridgeClient().syncEnterExit(playerInfo, true), "消息同步-进服"); @@ -54,6 +58,7 @@ public void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) { return; } + if (!quitCooldown.allow(player.getUUID().toString(), ConfigLoader.get().getSync().getQuitCooldownSeconds())) return; var playerInfo = PlayerUtils.getPlayerInfo(player); EasyBotNetworkingThreadPool.getInstance().addTask(() -> LegacyForgeEntry.getBridgeClient().syncEnterExit(playerInfo, false), "消息同步-退服"); diff --git a/src/main/java/com/springwater/easybot/platforms/legacyforge/features/PlayerDeathSyncFeature.java b/src/main/java/com/springwater/easybot/platforms/legacyforge/features/PlayerDeathSyncFeature.java index 6ce3c6d..c5f32eb 100644 --- a/src/main/java/com/springwater/easybot/platforms/legacyforge/features/PlayerDeathSyncFeature.java +++ b/src/main/java/com/springwater/easybot/platforms/legacyforge/features/PlayerDeathSyncFeature.java @@ -8,6 +8,7 @@ import com.springwater.easybot.threading.EasyBotNetworkingThreadPool; import com.springwater.easybot.utils.CarpetUtils; import com.springwater.easybot.utils.PlayerUtils; +import com.springwater.easybot.utils.SyncCooldown; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.LivingEntity; import net.minecraftforge.common.MinecraftForge; @@ -17,6 +18,7 @@ import static com.springwater.easybot.utils.DamageTypeMappings.DEATH_MESSAGES; public class PlayerDeathSyncFeature implements IEasyBotFeatures { + private final SyncCooldown deathCooldown = new SyncCooldown(); @Override public void register() { @@ -40,6 +42,8 @@ public void onLivingDeath(LivingDeathEvent event) { } var source = event.getSource(); + String cause = source.typeHolder().unwrapKey().map(Object::toString).orElse(source.getMsgId()); + if (!deathCooldown.allow(player.getUUID() + ":" + cause, ConfigLoader.get().getSync().getDeathCooldownSeconds())) return; var profile = PlayerUtils.getPlayerInfo(player); var killer = new StringBuilder(); var deathReason = new StringBuilder(); diff --git a/src/main/java/com/springwater/easybot/platforms/neoforge/features/LoginEventSyncFeature.java b/src/main/java/com/springwater/easybot/platforms/neoforge/features/LoginEventSyncFeature.java index 659e1dd..3fe6cdf 100644 --- a/src/main/java/com/springwater/easybot/platforms/neoforge/features/LoginEventSyncFeature.java +++ b/src/main/java/com/springwater/easybot/platforms/neoforge/features/LoginEventSyncFeature.java @@ -8,12 +8,15 @@ import com.springwater.easybot.threading.EasyBotNetworkingThreadPool; import com.springwater.easybot.utils.CarpetUtils; import com.springwater.easybot.utils.PlayerUtils; +import com.springwater.easybot.utils.SyncCooldown; import net.minecraft.server.level.ServerPlayer; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.entity.player.PlayerEvent; public class LoginEventSyncFeature implements IEasyBotFeatures { + private final SyncCooldown joinCooldown = new SyncCooldown(); + private final SyncCooldown quitCooldown = new SyncCooldown(); @Override public void register() { @@ -35,6 +38,7 @@ public void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event) { return; } // 注意,最好不要在别的线程获取,你永远不知道下一个tick数据是否可用 + if (!joinCooldown.allow(player.getUUID().toString(), ConfigLoader.get().getSync().getJoinCooldownSeconds())) return; var playerInfo = PlayerUtils.getPlayerInfo(player); EasyBotNetworkingThreadPool.getInstance().addTask(() -> EasyBotModImpl.INSTANCE.getBridgeClient().syncEnterExit(playerInfo, true), "消息同步-进服"); } @@ -54,6 +58,7 @@ public void onPlayerQuit(PlayerEvent.PlayerLoggedOutEvent event) { return; } // 注意,最好不要在别的线程获取,你永远不知道下一个tick数据是否可用 + if (!quitCooldown.allow(player.getUUID().toString(), ConfigLoader.get().getSync().getQuitCooldownSeconds())) return; var playerInfo = PlayerUtils.getPlayerInfo(player); EasyBotNetworkingThreadPool.getInstance().addTask(() -> EasyBotModImpl.INSTANCE.getBridgeClient().syncEnterExit(playerInfo, false), "消息同步-退服"); } diff --git a/src/main/java/com/springwater/easybot/platforms/neoforge/features/PlayerDeathSyncFeature.java b/src/main/java/com/springwater/easybot/platforms/neoforge/features/PlayerDeathSyncFeature.java index 8727d45..560e6dd 100644 --- a/src/main/java/com/springwater/easybot/platforms/neoforge/features/PlayerDeathSyncFeature.java +++ b/src/main/java/com/springwater/easybot/platforms/neoforge/features/PlayerDeathSyncFeature.java @@ -8,6 +8,7 @@ import com.springwater.easybot.threading.EasyBotNetworkingThreadPool; import com.springwater.easybot.utils.CarpetUtils; import com.springwater.easybot.utils.PlayerUtils; +import com.springwater.easybot.utils.SyncCooldown; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.LivingEntity; import net.neoforged.bus.api.SubscribeEvent; @@ -17,6 +18,7 @@ import static com.springwater.easybot.utils.DamageTypeMappings.DEATH_MESSAGES; public class PlayerDeathSyncFeature implements IEasyBotFeatures { + private final SyncCooldown deathCooldown = new SyncCooldown(); @Override public void register() { @@ -38,6 +40,9 @@ public void onLivingDeath(LivingDeathEvent event) { var source = event.getSource(); + if (EasyBotModImpl.INSTANCE.getBridgeClient() == null || !EasyBotModImpl.INSTANCE.getBridgeClient().isReady()) return; + String cause = source.typeHolder().unwrapKey().map(Object::toString).orElse(source.getMsgId()); + if (!deathCooldown.allow(player.getUUID() + ":" + cause, ConfigLoader.get().getSync().getDeathCooldownSeconds())) return; var profile = PlayerUtils.getPlayerInfo(player); var killer = new StringBuilder(); var deathReason = new StringBuilder(); diff --git a/src/main/java/com/springwater/easybot/utils/PermissionUtils.java b/src/main/java/com/springwater/easybot/utils/PermissionUtils.java index 6261713..e5ee51f 100644 --- a/src/main/java/com/springwater/easybot/utils/PermissionUtils.java +++ b/src/main/java/com/springwater/easybot/utils/PermissionUtils.java @@ -19,7 +19,7 @@ public static boolean canUse(CommandSourceStack source, String command) { Boolean allowed = LuckPermsPermissions.check(source, "easybot.command." + command.toLowerCase(java.util.Locale.ROOT)); if (allowed != null) return allowed; } - int defaultLevel = command.equals("reload") || command.equals("status") ? 3 : 0; + int defaultLevel = command.equals("reload") || command.equals("status") || command.equals("config") ? 3 : 0; return hasPermission(source, config.getPermissionLevels().getOrDefault(command, defaultLevel)); } diff --git a/src/main/java/com/springwater/easybot/utils/SyncCooldown.java b/src/main/java/com/springwater/easybot/utils/SyncCooldown.java new file mode 100644 index 0000000..4929fef --- /dev/null +++ b/src/main/java/com/springwater/easybot/utils/SyncCooldown.java @@ -0,0 +1,22 @@ +package com.springwater.easybot.utils; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import java.util.concurrent.TimeUnit; + +/** Each instance tracks one event type. Suppressed events do not extend the cooldown. */ +public final class SyncCooldown { + private int seconds; + private Cache sent; + + public synchronized boolean allow(String key, int cooldownSeconds) { + if (seconds != cooldownSeconds) { + seconds = cooldownSeconds; + sent = seconds > 0 + ? CacheBuilder.newBuilder().expireAfterWrite(seconds, TimeUnit.SECONDS).build() + : null; + } + return sent == null || sent.asMap().putIfAbsent(key, Boolean.TRUE) == null; + } +} diff --git a/src/main/resources/config.json b/src/main/resources/config.json index dbc0cf3..209fe65 100644 --- a/src/main/resources/config.json +++ b/src/main/resources/config.json @@ -16,13 +16,15 @@ "say": true, "status": true, "reload": true, + "config": true, "bind": true, "confirm": true, "bindStatus": true }, "permissionLevels": { "status": 3, - "reload": 3 + "reload": 3, + "config": 3 } }, "skipOptions": { @@ -45,7 +47,10 @@ "useMixinReport1201": false }, "sync": { - "chatImageSupport": true + "chatImageSupport": true, + "joinCooldownSeconds": 0, + "quitCooldownSeconds": 0, + "deathCooldownSeconds": 0 }, "chatFilter": { "gameToGroup": {