第十六批:雪球菜单SN校验+游戏内轮询封禁播报(BanBroadcastManager)
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.bukkit.BanEntry;
|
||||
import org.bukkit.BanList;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* ★ 第十六批:游戏内轮询封禁播报(耻辱台)
|
||||
*
|
||||
* 配置文件(plugins/Sdf1_login/ban/ 目录):
|
||||
* - *.json 直接作为配置文件;
|
||||
* - 规则配置.md 优先解 ```json 代码块,解不出再解 markdown 表格;
|
||||
* - 解码全失败 → 备份原文件 + 释放一份新的。
|
||||
* 配置丢失 → 自动补发(saveResource 自愈)。
|
||||
*
|
||||
* 键值:轮播最低间隔(秒) / 轮播最大间隔(秒) / 播报模式
|
||||
* 播报模式:true=由近到远、false=由远到近、0=关闭;
|
||||
* 表格值:由近到远 / 由远到近 / 0。
|
||||
* 间隔或模式为 0 → 关闭播报。
|
||||
*/
|
||||
public class BanBroadcastManager {
|
||||
|
||||
private final Main plugin;
|
||||
private final Random rnd = new Random();
|
||||
|
||||
private File banDir;
|
||||
private File mdFile;
|
||||
private File jsonFile; // *.json 配置(存在则优先)
|
||||
|
||||
private int minInterval = 60;
|
||||
private int maxInterval = 180;
|
||||
private int mode = 1; // 0=关闭 1=由近到远 2=由远到近
|
||||
private int pageSize = 9; // 每轮播报条数(耻辱台一页)
|
||||
|
||||
public BanBroadcastManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
public void start() {
|
||||
banDir = new File(plugin.getDataFolder(), "ban");
|
||||
mdFile = new File(banDir, "规则配置.md");
|
||||
jsonFile = findJsonConfig();
|
||||
ensureConfigExists();
|
||||
reloadConfig();
|
||||
scheduleNext();
|
||||
plugin.getLogger().info("[封禁播报] 已启动,模式=" + modeName()
|
||||
+ " 间隔=" + minInterval + "~" + maxInterval + "s 每页" + pageSize + "条");
|
||||
}
|
||||
|
||||
public void shutdown() { /* BukkitRunnable 随插件卸载自动取消 */ }
|
||||
|
||||
// ==================== 配置:自愈 + 双模式解析 ====================
|
||||
|
||||
/** 找 ban/ 目录下任意 *.json 配置文件 */
|
||||
private File findJsonConfig() {
|
||||
File[] fs = banDir == null ? null : banDir.listFiles(
|
||||
(d, n) -> n.toLowerCase(Locale.ROOT).endsWith(".json"));
|
||||
return (fs != null && fs.length > 0) ? fs[0] : null;
|
||||
}
|
||||
|
||||
/** 配置文件丢失就补一份(自愈) */
|
||||
private void ensureConfigExists() {
|
||||
try {
|
||||
if (jsonFile != null) {
|
||||
if (!banDir.exists()) banDir.mkdirs();
|
||||
if (!jsonFile.exists()) {
|
||||
plugin.saveResource("ban/" + jsonFile.getName(), false);
|
||||
log("配置文件丢失,已自愈补发: " + jsonFile.getName());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!mdFile.exists()) {
|
||||
banDir.mkdirs();
|
||||
plugin.saveResource("ban/规则配置.md", false);
|
||||
log("配置文件丢失,已自愈补发: 规则配置.md");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
warn("自愈补发失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 重新加载配置(json 优先 → md json块 → md 表格 → 备份重置) */
|
||||
public void reloadConfig() {
|
||||
try {
|
||||
if (jsonFile == null) jsonFile = findJsonConfig();
|
||||
if (jsonFile != null && jsonFile.exists()) {
|
||||
String txt = readFile(jsonFile);
|
||||
if (parseJsonText(txt)) { log("已加载 json 配置: " + jsonFile.getName()); return; }
|
||||
backupAndReset(jsonFile, jsonFile.getName());
|
||||
return;
|
||||
}
|
||||
if (!mdFile.exists()) { ensureConfigExists(); }
|
||||
String md = readFile(mdFile);
|
||||
if (md != null) {
|
||||
String jsonBlock = extractJsonBlock(md);
|
||||
if (jsonBlock != null && parseJsonText(jsonBlock)) {
|
||||
log("已加载 md 内嵌 json 配置: 规则配置.md"); return;
|
||||
}
|
||||
if (parseTableText(md)) { log("已加载 md 表格配置: 规则配置.md"); return; }
|
||||
}
|
||||
// 全部解码失败 → 备份原文件 + 释放一份新的
|
||||
backupAndReset(mdFile, "规则配置.md");
|
||||
} catch (Exception e) {
|
||||
warn("reloadConfig 异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 md 提取 ```json 代码块内容(不含围栏) */
|
||||
private String extractJsonBlock(String md) {
|
||||
Pattern p = Pattern.compile("```json\\s*\\n(.*?)\\n\\s*```", Pattern.DOTALL);
|
||||
Matcher m = p.matcher(md);
|
||||
return m.find() ? m.group(1).trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 json 文本(键:最小间隔/最大间隔/播报模式)。
|
||||
* 出厂 规则配置.md 的 json 块值为空字符串而表格有真值(文档语义:二者选一填),
|
||||
* 因此仅当 json 里出现「非空间隔值」才认 json 生效;
|
||||
* 只有播报模式、没有任何间隔键的纯 json 也认生效(间隔走默认)。
|
||||
* @return true = json 配置生效;false = 视为未配置,调用方回退表格
|
||||
*/
|
||||
private boolean parseJsonText(String txt) {
|
||||
try {
|
||||
JsonObject o = JsonParser.parseString(txt).getAsJsonObject();
|
||||
boolean hasIntervalKey = o.has("最小间隔") || o.has("最大间隔")
|
||||
|| o.has("轮播最低间隔") || o.has("轮播最大间隔");
|
||||
boolean anyInterval = false;
|
||||
Integer v;
|
||||
if ((v = jsonInt(o, "最小间隔")) != null) { minInterval = v; anyInterval = true; }
|
||||
if ((v = jsonInt(o, "最大间隔")) != null) { maxInterval = v; anyInterval = true; }
|
||||
if ((v = jsonInt(o, "轮播最低间隔")) != null) { minInterval = v; anyInterval = true; }
|
||||
if ((v = jsonInt(o, "轮播最大间隔")) != null) { maxInterval = v; anyInterval = true; }
|
||||
boolean modePresent = false;
|
||||
if (o.has("播报模式") && !o.get("播报模式").isJsonNull()) {
|
||||
mode = parseMode(o.get("播报模式").getAsString());
|
||||
modePresent = true;
|
||||
}
|
||||
if (anyInterval) { normalize(); return true; }
|
||||
// 没有可用间隔值:纯「只有播报模式且无间隔键」的 json 也算生效
|
||||
if (modePresent && !hasIntervalKey) { normalize(); return true; }
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** json 取间隔值:键存在且非空才返回(空串/null → null,留给表格或默认值) */
|
||||
private Integer jsonInt(JsonObject o, String key) {
|
||||
try {
|
||||
if (!o.has(key) || o.get(key).isJsonNull()) return null;
|
||||
String s = o.get(key).getAsString();
|
||||
if (s == null || s.trim().isEmpty()) return null;
|
||||
int r = toSec(s);
|
||||
return r >= 0 ? r : null;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析 md 表格(| 名字 | 说明 | 参数值 |) */
|
||||
private boolean parseTableText(String md) {
|
||||
boolean any = false;
|
||||
for (String line : md.split("\n")) {
|
||||
String t = line.trim();
|
||||
if (!t.startsWith("|") || t.startsWith("| ---") || t.startsWith("|---")) continue;
|
||||
String[] cells = t.split("\\|");
|
||||
if (cells.length < 4) continue;
|
||||
String key = cells[1].trim();
|
||||
String val = cells[cells.length - 2].trim();
|
||||
if (key.equals("轮播最低间隔")) { minInterval = toSec(val); any = true; }
|
||||
else if (key.equals("轮播最大间隔")) { maxInterval = toSec(val); any = true; }
|
||||
else if (key.equals("播报模式")) { mode = parseMode(val); any = true; }
|
||||
}
|
||||
if (any) normalize();
|
||||
return any;
|
||||
}
|
||||
|
||||
/** 播报模式文本/布尔/数字 → 0/1/2 */
|
||||
private int parseMode(String s) {
|
||||
if (s == null) return 1;
|
||||
String v = s.trim();
|
||||
if (v.equals("0") || v.equalsIgnoreCase("false")) return 2; // false=由远到近
|
||||
if (v.equals("1") || v.equalsIgnoreCase("true")) return 1; // true=由近到远
|
||||
if (v.contains("近到远")) return 1; // 由近到远
|
||||
if (v.contains("远到近")) return 2; // 由远到近
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** 秒数文本 → int(容错"60s"/"60秒"/"60") */
|
||||
private int toSec(String s) {
|
||||
String v = s == null ? "" : s.trim().replaceAll("[^0-9]", "");
|
||||
if (v.isEmpty()) return 0;
|
||||
try { return Integer.parseInt(v); } catch (Exception e) { return 0; }
|
||||
}
|
||||
|
||||
/** 归一化:模式为0 或 最大最小间隔都为0 → 关闭;最大<最低 → 补齐(对齐配置说明语义) */
|
||||
private void normalize() {
|
||||
if (minInterval < 0) minInterval = 0;
|
||||
if (maxInterval < 0) maxInterval = 0;
|
||||
if (maxInterval > 0 && minInterval > maxInterval) maxInterval = minInterval;
|
||||
if (mode != 0 && mode != 1 && mode != 2) mode = 1;
|
||||
if (mode == 0 || (minInterval == 0 && maxInterval == 0)) mode = 0;
|
||||
}
|
||||
|
||||
// ==================== 调度 ====================
|
||||
|
||||
/** 排下一次窗口期:随机 [min,max] 秒后播报,再自我reschedule */
|
||||
private void scheduleNext() {
|
||||
int delaySec;
|
||||
if (mode == 0) {
|
||||
// 播报关闭时仍每 60 秒自检一次:配置文件被删则自愈补发、
|
||||
// 运行中把 0 改回正常值可自动恢复播报(无需重启)
|
||||
delaySec = 60;
|
||||
} else {
|
||||
int span = maxInterval - minInterval;
|
||||
delaySec = minInterval + (span > 0 ? rnd.nextInt(span + 1) : 0);
|
||||
}
|
||||
long delayTicks = Math.max(20L, delaySec * 20L);
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!plugin.isEnabled()) return;
|
||||
// ★ 自愈 + 热重载:每次窗口期前检查配置存在性并重读
|
||||
ensureConfigExists();
|
||||
reloadConfig();
|
||||
if (mode != 0) {
|
||||
try { broadcastOnce(); }
|
||||
catch (Exception e) { warn("播报异常: " + e.getMessage()); }
|
||||
}
|
||||
scheduleNext();
|
||||
}
|
||||
}.runTaskLater(plugin, delayTicks);
|
||||
}
|
||||
|
||||
// ==================== 播报 ====================
|
||||
|
||||
/** 取当前生效封禁条目(未过期),按创建时间排序 */
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<BanEntry<?>> collectBans() {
|
||||
List<BanEntry<?>> list = new ArrayList<>();
|
||||
try {
|
||||
BanList nb = Bukkit.getBanList(BanList.Type.NAME);
|
||||
// getEntries() 实际返回 Set,按 Collection 中转强转(防 ClassCastException)
|
||||
for (BanEntry<?> e : (Collection<BanEntry<?>>) (Collection<?>) nb.getEntries()) {
|
||||
Date exp = e.getExpiration();
|
||||
if (exp != null && exp.getTime() <= System.currentTimeMillis()) continue;
|
||||
list.add(e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
warn("读取封禁名单失败: " + e.getMessage());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 轮播当前页并广播给全服 */
|
||||
private void broadcastOnce() {
|
||||
List<BanEntry<?>> bans = collectBans();
|
||||
if (bans.isEmpty()) { log("当前无生效封禁,跳过本轮"); return; }
|
||||
|
||||
// 由近到远:最新封禁在前;由远到近:最早封禁在前
|
||||
bans.sort((a, b) -> {
|
||||
long ta = a.getCreated() != null ? a.getCreated().getTime() : 0L;
|
||||
long tb = b.getCreated() != null ? b.getCreated().getTime() : 0L;
|
||||
return mode == 1 ? Long.compare(tb, ta) : Long.compare(ta, tb);
|
||||
});
|
||||
|
||||
int total = bans.size();
|
||||
int start = cursor % total;
|
||||
int end = Math.min(start + pageSize, total);
|
||||
List<BanEntry<?>> page = bans.subList(start, end);
|
||||
cursor = end % total;
|
||||
|
||||
String head = "§c§l⚠ §6违规封禁公示 §7(共§f" + total + "§7人, 第§f"
|
||||
+ (start / pageSize + 1) + "§7/" + ((total + pageSize - 1) / pageSize) + "§7页)";
|
||||
Bukkit.broadcastMessage(head);
|
||||
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
for (BanEntry<?> e : page) {
|
||||
String target = e.getTarget();
|
||||
String reason = e.getReason() != null && !e.getReason().isEmpty()
|
||||
? e.getReason() : "违反服务器规则";
|
||||
String when;
|
||||
Date created = e.getCreated();
|
||||
when = created != null ? df.format(created) : "未知时间";
|
||||
Date exp = e.getExpiration();
|
||||
String expStr = exp != null ? ("§7至§f" + df.format(exp)) : "§c永久";
|
||||
Bukkit.broadcastMessage("§c☠ §f" + target + " §7- " + reason
|
||||
+ " §8| §7封于§f" + when + " §8| " + expStr);
|
||||
}
|
||||
Bukkit.broadcastMessage("§7§o请各位玩家引以为戒,遵守服务器规则。");
|
||||
log("本轮已播报 " + page.size() + "/" + total + " 条封禁");
|
||||
}
|
||||
|
||||
/** 轮播游标(下一页起点) */
|
||||
private int cursor = 0;
|
||||
|
||||
// ==================== 管理员命令 ====================
|
||||
|
||||
/** 手动重载配置并汇报当前生效值 */
|
||||
public void report(CommandSender to) {
|
||||
reloadConfig();
|
||||
to.sendMessage("§a[封禁播报] §7模式=§f" + modeName()
|
||||
+ " §7间隔=§f" + minInterval + "~" + maxInterval + "s"
|
||||
+ " §7每页=§f" + pageSize + "条"
|
||||
+ (mode == 0 ? " §c(已关闭)" : ""));
|
||||
}
|
||||
|
||||
private String modeName() {
|
||||
return mode == 0 ? "关闭" : (mode == 1 ? "由近到远" : "由远到近");
|
||||
}
|
||||
|
||||
// ==================== 工具 ====================
|
||||
|
||||
private String readFile(File f) {
|
||||
if (f == null || !f.exists()) return null;
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(
|
||||
new FileInputStream(f), StandardCharsets.UTF_8))) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) sb.append(line).append('\n');
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
warn("读取失败 " + f.getName() + ": " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 备份原配置文件 + 释放一份新的(自愈) */
|
||||
private void backupAndReset(File f, String resourceName) {
|
||||
try {
|
||||
if (f != null && f.exists()) {
|
||||
String bak = f.getAbsolutePath() + ".bak." + System.currentTimeMillis();
|
||||
copyFile(f, new File(bak));
|
||||
warn("配置解码失败,已备份原文件: " + new File(bak).getName());
|
||||
}
|
||||
// 先删旧文件再从 jar 释放新的
|
||||
if (f != null && f.exists()) f.delete();
|
||||
plugin.saveResource("ban/" + resourceName, false);
|
||||
log("已释放一份新配置: " + resourceName);
|
||||
} catch (Exception e) {
|
||||
warn("备份/重置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void copyFile(File src, File dst) throws IOException {
|
||||
try (FileInputStream in = new FileInputStream(src);
|
||||
FileOutputStream out = new FileOutputStream(dst)) {
|
||||
byte[] buf = new byte[8192];
|
||||
int n;
|
||||
while ((n = in.read(buf)) > 0) out.write(buf, 0, n);
|
||||
}
|
||||
}
|
||||
|
||||
private void log(String s) { plugin.getLogger().info("[封禁播报] " + s); }
|
||||
private void warn(String s) { plugin.getLogger().warning("[封禁播报] " + s); }
|
||||
}
|
||||
@@ -557,6 +557,9 @@ public class Main extends JavaPlugin
|
||||
// 全程无 ERROR 只有 SSL 断路器 WARN —— 日志抓不到卡点栈,靠它下次复现
|
||||
// 直接把主线程栈打进日志定位)
|
||||
new MainThreadWatchdog(this).start();
|
||||
// ★ 第十六批:游戏内轮询封禁播报(耻辱台)
|
||||
// 配置 ban/规则配置.md 或 ban/*.json,丢失自愈补发、解码失败备份重置
|
||||
new BanBroadcastManager(this).start();
|
||||
afk.startCheck();
|
||||
startLoginReminder();
|
||||
startTimeoutCheck();
|
||||
@@ -3388,6 +3391,51 @@ public class Main extends JavaPlugin
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 第十六批:雪球菜单 SN 校验(仅雪球开 GUI 路径调用)
|
||||
* 规则:
|
||||
* - 无 SN(历史雪球)→ 放行;
|
||||
* - SN 未登记 → 拒绝(数据异常/伪造);
|
||||
* - 状态 illegal(锁定)→ 拒绝;
|
||||
* - 登记持有者 ≠ 实际持有者 → 拒绝;
|
||||
* - active/lost 且持有者一致 → 放行。
|
||||
* 校验异常时放行(防 DB 抖动误伤),只记日志。
|
||||
* @return true=放行可打开主 GUI;false=已提示并拒绝
|
||||
*/
|
||||
private boolean verifyMenuSnowballSn(Player p) {
|
||||
try {
|
||||
if (snManager == null) return true;
|
||||
ItemStack it = p.getInventory().getItemInMainHand();
|
||||
if (!isMenuSnowball(it)) it = p.getInventory().getItemInOffHand();
|
||||
String sn = snManager.readSn(it);
|
||||
if (sn == null || sn.isEmpty()) return true; // 历史雪球无 SN,放行
|
||||
java.util.Map<String, Object> row = snManager.getSn(sn);
|
||||
if (row == null) {
|
||||
p.sendMessage("§c§l[菜单] §f菜单雪球校验失败:SN 未登记,请联系管理员");
|
||||
getLogger().info("[菜单] 雪球SN校验拒绝: " + p.getName() + " SN=" + sn + " 未登记");
|
||||
return false;
|
||||
}
|
||||
String st = row.get("status") == null ? "" : String.valueOf(row.get("status"));
|
||||
if (SnManager.ST_ILLEGAL.equals(st)) {
|
||||
p.sendMessage("§c§l[菜单] §f该菜单雪球已被锁定,无法打开菜单");
|
||||
getLogger().info("[菜单] 雪球SN校验拒绝: " + p.getName() + " SN=" + sn + " 已锁定");
|
||||
return false;
|
||||
}
|
||||
String owner = row.get("owner") == null ? "" : String.valueOf(row.get("owner"));
|
||||
String me = p.getName();
|
||||
// 持有者精确匹配(离线模式大小写不同=不同玩家)
|
||||
if (!owner.isEmpty() && !owner.equals(me)) {
|
||||
p.sendMessage("§c§l[菜单] §f该菜单雪球不属于你,无法打开菜单");
|
||||
getLogger().info("[菜单] 雪球SN校验拒绝: " + me + " 持有 " + owner + " 的 SN=" + sn);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("[菜单] 雪球SN校验异常(放行): " + e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMenuSnowball(ItemStack item) {
|
||||
if (item == null
|
||||
|| item.getType()
|
||||
@@ -3544,6 +3592,12 @@ public class Main extends JavaPlugin
|
||||
if (isMenuSnowball(hand)
|
||||
|| isMenuSnowball(offhand)) {
|
||||
e.setCancelled(true);
|
||||
// ★ 第十六批:雪球菜单打开 GUI 前校验 SN
|
||||
// 被锁定 / SN登记持有者与实际持有者不符 → 拒绝打开主 GUI
|
||||
// 主指令 /sdf1_login 不走这里,不受影响
|
||||
if (!verifyMenuSnowballSn(p)) {
|
||||
return;
|
||||
}
|
||||
gui.openMain(p);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
## ----配置区开始----
|
||||
|
||||
| 名字 | 功能说明 | 参数值 |
|
||||
| --- | --- | --- |
|
||||
| 轮播最低间隔 | 最短是多少秒一次(建议阈值60,太低很吵) | 60 |
|
||||
| 轮播最大间隔 | 最长多少秒一次(建议阈值300以内,太长起不到效果) | 180 |
|
||||
| 播报模式 | 是由远到近(即从第一个封禁的玩家开始报到最后一个),还是又近到远(从最新一个被封禁的玩家播报到第一个被封禁的玩家)。此项填0或最大最小间隔都为0表示关闭播报 | 由近到远 |
|
||||
|
||||
```json
|
||||
{
|
||||
"最小间隔":"",
|
||||
"最大间隔":"",
|
||||
"播报模式":true,
|
||||
"解释说明":{
|
||||
"播报模式":"true表示开启播报又近到远,false表示开启播报由远到近。0为关闭",
|
||||
"最小间隔":"最短是多少秒一次(建议阈值60,太低很吵)",
|
||||
"最大间隔":"最长多少秒一次(建议阈值300以内,太长起不到效果)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ----配置区域结束----
|
||||
|
||||
## 功能说明
|
||||
- 此功能为服务器打造一个不可拆解的耻辱台,展示所有被封禁的玩家。json代码块的参数值和md表格是2选1,你可以删了表格,留json则表示用json作为配置值,也可以删了json用表格。小白用户推荐使用md,因为最简单,专业人员推荐使用json,因为最清晰,没有注释(注释是另起一行,不在配置块)。<br>
|
||||
- 同时兼容.json直接作为配置文件
|
||||
Reference in New Issue
Block a user