Skip to content
Merged
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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/):*提示:您必须理解其运作原理!*
Expand Down Expand Up @@ -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 <code>` | `confirm` | `easybot.command.confirm`,同时要求 `bind` 权限 | 0 |
| `/easybot bind status`(绑定状态) | `bindStatus` | `easybot.command.bindstatus` | 0 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class EasyBotCommands {
new StatusCommandHandler(),
new SayCommandHandler(),
new ReloadCommandHandler(),
new ConfigCommandHandler(),
new BindCommandHandler()
);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<CommandSourceStack> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ private int sendGetHelp(CommandContext<CommandSourceStack> 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", "绑定账号"));
Expand Down
52 changes: 49 additions & 3 deletions src/main/java/com/springwater/easybot/config/ConfigLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,6 +30,7 @@ public class ConfigLoader {

// 当前正在使用的配置实例
private static volatile EasyBotConfig currentConfig;
public static final List<String> COOLDOWN_KEYS = List.of("joinCooldownSeconds", "quitCooldownSeconds", "deathCooldownSeconds");
// 配置内容哈希值,用于检测文件是否真正变化
private static long lastConfigHash = 0;

Expand Down Expand Up @@ -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();
Expand All @@ -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("配置文件不存在,继续使用上一个有效配置");
Expand Down Expand Up @@ -263,7 +265,7 @@ public static void startAutoReloadWatcher() {
/**
* 保存当前内存中的配置到磁盘。
*/
public static void save() {
public static synchronized void save() {
if (currentConfig == null) {
currentConfig = new EasyBotConfig();
}
Expand All @@ -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)));
}
}

/**
* 注册当配置发生变化(重载成功)时的回调函数。
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) -> {
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) -> {
Expand All @@ -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), "消息同步-进服");
});
Expand All @@ -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), "消息同步-退服");
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
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;

import static com.springwater.easybot.utils.DamageTypeMappings.DEATH_MESSAGES;

public class PlayerDeathSyncFeature implements IEasyBotFeatures {
private final SyncCooldown deathCooldown = new SyncCooldown();

@Override
public void register() {
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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), "消息同步-进服");
Expand All @@ -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), "消息同步-退服");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() {
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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), "消息同步-进服");
}
Expand All @@ -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), "消息同步-退服");
}
Expand Down
Loading
Loading