第十四批:控制台execute链@s拦截与单斜杠中文化、二手批量上架成功跳过计数、OP白名单定时器墙钟自对齐与ops.json先备份后撤销、撤销提示病句与最小化信息披露
This commit is contained in:
@@ -3831,8 +3831,48 @@ public class Main extends JavaPlugin
|
||||
String[] toks = raw.split("\\s+");
|
||||
String head = toks[0].toLowerCase(java.util.Locale.ROOT);
|
||||
if (head.startsWith("minecraft:")) head = head.substring(10);
|
||||
// execute ... as ... run ... 链里的 @s 指向被 as 的实体,合法,不拦
|
||||
if (head.equals("execute")) return;
|
||||
// execute 链静态扫描:@s 出现在任何 as 之前 = 指向控制台自身 → 拦截提示;
|
||||
// 出现在 as 之后 = 指向被 as 的实体,合法放行。
|
||||
// ★ 问题1(2026-10-14):原实现对 head=execute 一刀切 return,
|
||||
// execute at @s run say 123 这类从控制台发出原版【静默无输出】,
|
||||
// 插件完全没监管 —— 现扫到就拦,给中文改法提示。
|
||||
if (head.equals("execute")) {
|
||||
int eIdx = -1;
|
||||
boolean asSeen = false;
|
||||
for (int i = 1; i < toks.length; i++) {
|
||||
String t = toks[i].toLowerCase(java.util.Locale.ROOT);
|
||||
if (t.equals("as")) {
|
||||
asSeen = true; // execute as / positioned as / rotated as 的 as 都算
|
||||
continue;
|
||||
}
|
||||
if (t.equals("run")) {
|
||||
if (i + 1 >= toks.length) break;
|
||||
String nx = toks[i + 1].toLowerCase(java.util.Locale.ROOT);
|
||||
if (nx.startsWith("minecraft:")) nx = nx.substring(10);
|
||||
// run 后只有仍嵌套 execute 才继续扫;say/me/help 等文本命令里
|
||||
// 的字面 @s 不是选择器,直接停(防误伤)
|
||||
if (!nx.equals("execute")) break;
|
||||
i++; // 跳过内层 execute 本身,继续扫下一段链
|
||||
continue;
|
||||
}
|
||||
if (!asSeen && (toks[i].equals("@s") || toks[i].startsWith("@s["))) {
|
||||
eIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (eIdx < 0) return;
|
||||
e.setCancelled(true);
|
||||
cs.sendMessage("§c§l[命令未执行] §f/" + raw);
|
||||
cs.sendMessage("§c原因: §f第 " + eIdx + " 个参数 §e" + toks[eIdx]
|
||||
+ " §f是 @s(=执行者自己),但控制台不是玩家,没有“自己”可指。");
|
||||
cs.sendMessage("§7原版此情况【静默无输出】:execute 的 @s 解析不到就直接中止,不报任何错。");
|
||||
cs.sendMessage("§7改法三选一:");
|
||||
cs.sendMessage("§7 · 想对该玩家执行 → 直接写玩家名,如 §e/execute at Steve run say 123");
|
||||
cs.sendMessage("§7 · 想对全体在线玩家 → 换成 §e@a");
|
||||
cs.sendMessage("§7 · 想让某个玩家自己执行 → 用 §e/execute as <玩家> run §7加原命令");
|
||||
getLogger().info("[命令] 控制台 execute 链含 @s 被拦: " + raw + "(@s 出现在 as 之前)");
|
||||
return;
|
||||
}
|
||||
// 纯文本广播命令:@s 是要发出去的字面文字,不是选择器,不拦
|
||||
if (head.equals("say") || head.equals("me") || head.equals("teammsg")
|
||||
|| head.equals("tm") || head.equals("help") || head.equals("?")) {
|
||||
@@ -4158,7 +4198,22 @@ public class Main extends JavaPlugin
|
||||
body = body.trim();
|
||||
if (body.isEmpty()) return;
|
||||
if (body.charAt(0) == '/') body = body.substring(1).trim();
|
||||
if (body.isEmpty()) return;
|
||||
if (body.isEmpty()) {
|
||||
// ★ 问题2(2026-10-14):只输入了 "/"(去头后为空)—— 原实现直接
|
||||
// return 且不压制,Paper 原生 "Unknown or incomplete command" 就此
|
||||
// 溜给运维(生产日志实证)。现压掉原生报错 + 中文提示正确写法:
|
||||
// 控制台直接输入指令名(无需斜杠),除非指令本身就以斜杠开头。
|
||||
suppressUnknown(e);
|
||||
if (sender instanceof Player) {
|
||||
sender.sendMessage("§c§l[命令未执行] §7你只输入了 §f/ §7,后面没有指令名,"
|
||||
+ "请输入完整指令,如 §f/list§7、§f/tpa Steve");
|
||||
} else {
|
||||
sender.sendMessage("§c§l[命令未执行] §7控制台直接输入指令名即可(无需斜杠),"
|
||||
+ "如 §flist§7、§fsay 你好");
|
||||
}
|
||||
getLogger().info("[无感切换] 单斜杠空指令已拦下并中文化: " + e.getCommandLine());
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 0) 诊断日志:确认本处理器是否真的被触发 ----
|
||||
boolean isPlayer = sender instanceof Player;
|
||||
|
||||
@@ -149,14 +149,47 @@ public class OpWhiteListManager implements Listener {
|
||||
// ★ 2026-10-13 问题3:初始延迟原来写死 300 tick=15s,从插件启动时刻起算,
|
||||
// 于是轮询点整体漂移固定偏移(实际 6/21/36/51 秒,期望 0/15/30/45 秒)。
|
||||
// 改为对齐到下一个 15 秒墙钟边界后,再按 15s 周期跑(同 startShCfgReloadTask 范式)。
|
||||
long now15 = System.currentTimeMillis();
|
||||
long next15 = (now15 / 15000L + 1) * 15000L;
|
||||
long delay15Ticks = Math.max(1L, (next15 - now15) / 50L);
|
||||
Bukkit.getScheduler().runTaskTimer(plugin, this::checkAndReload,
|
||||
delay15Ticks, 300L);
|
||||
long delayMs = millisToNextQuarterPoint();
|
||||
Bukkit.getScheduler().runTaskTimer(plugin, this::onQuarterPoint,
|
||||
(long) delayMs / 50L, 18000L);
|
||||
// ★ 2026-10-14 问题4(第十三批复发根因):上一版只对齐了【初始延迟】,
|
||||
// 固定 period(300/18000 tick)在 TPS≠20 时逐轮累积漂移,运行几小时后
|
||||
// 轮询点又飘离 0/15/30/45 秒整点。改为墙钟自对齐、每轮执行完重排的
|
||||
// runTaskLater 调度(见 scheduleAligned),无论 TPS 多少都精确落点。
|
||||
scheduleAligned(this::checkAndReload, 15000L, "15s热重载");
|
||||
scheduleAligned(this::onQuarterPoint, 900000L, "整点扫描");
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 2026-10-14 问题4:墙钟自对齐、每轮重排的调度器(替代固定 period 的 runTaskTimer)。
|
||||
* 每轮任务跑完后按【执行时刻的墙钟】重新计算下一个 periodMs 边界再 runTaskLater,
|
||||
* 漂移不累积 —— TPS 高低、卡顿都影响不了落点;任务异常也不打断重排(finally)。
|
||||
*
|
||||
* @param task 每轮要跑的任务
|
||||
* @param periodMs 墙钟周期毫秒(15000=对齐 :00/:15/:30/:45 秒;900000=对齐 :00/:15/:30/:45 分)
|
||||
* @param tag 日志标签(异常时打印用)
|
||||
*/
|
||||
private void scheduleAligned(Runnable task, long periodMs, String tag) {
|
||||
Runnable self = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!plugin.isEnabled()) {
|
||||
return; // 插件已禁用,不再重排
|
||||
}
|
||||
try {
|
||||
task.run();
|
||||
} catch (Throwable t) {
|
||||
plugin.getLogger().warning("[OP白名单] " + tag + "任务异常(下轮照常): "
|
||||
+ t.getMessage());
|
||||
} finally {
|
||||
long now = System.currentTimeMillis();
|
||||
long next = (now / periodMs + 1) * periodMs;
|
||||
long delayTicks = Math.max(1L, (next - now + 49L) / 50L);
|
||||
Bukkit.getScheduler().runTaskLater(plugin, this, delayTicks);
|
||||
}
|
||||
}
|
||||
};
|
||||
long now = System.currentTimeMillis();
|
||||
long next = (now / periodMs + 1) * periodMs;
|
||||
long delayTicks = Math.max(1L, (next - now + 49L) / 50L);
|
||||
Bukkit.getScheduler().runTaskLater(plugin, self, delayTicks);
|
||||
}
|
||||
|
||||
// ================= 配置读取 =================
|
||||
@@ -385,8 +418,15 @@ public class OpWhiteListManager implements Listener {
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs();
|
||||
}
|
||||
// 原版 ops.json 不存在或为空 = 没有内容可丢;已有历史备份则原样保留(绝不用空文件覆盖)
|
||||
if (!src.exists() || src.length() == 0) {
|
||||
// 原版 ops.json 不存在 / 空白 / 只剩 "[]" 空壳 = 没有内容可丢;
|
||||
// 一律保留已有历史备份(★ 2026-10-14 问题4:fail() 清空后 ops.json 是
|
||||
// 2 字节的 "[]",旧判空 length()==0 挡不住 → REPLACE_EXISTING 会用空壳
|
||||
// 覆盖掉唯一一份好备份,管理员就只能手动逐个恢复 OP)
|
||||
if (!src.exists() || isDegenerateOps(src)) {
|
||||
if (dst.exists() && dst.length() > 0) {
|
||||
plugin.getLogger().info("[OP白名单] 原版 ops.json 实质为空(空壳),"
|
||||
+ "已有历史备份原样保留: OP白名单/" + NATIVE_OPS_BACKUP);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Files.copy(src.toPath(), dst.toPath(),
|
||||
@@ -406,6 +446,19 @@ public class OpWhiteListManager implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
/** ops.json 是否实质为空(不存在 / 空白 / 只有 "[]" 空壳)——空壳绝不允许覆盖历史备份 */
|
||||
private static boolean isDegenerateOps(File f) {
|
||||
try {
|
||||
if (!f.exists() || f.length() == 0) {
|
||||
return true;
|
||||
}
|
||||
String s = new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8).trim();
|
||||
return s.isEmpty() || s.equals("[]");
|
||||
} catch (Exception e) {
|
||||
return false; // 读不了 → 当作有内容,走正常备份流程(失败则按铁律不卸载)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 从留底备份复原原版 OP(返回复原到的玩家名,供 adoptToWhitelist 写回白名单)。
|
||||
*
|
||||
@@ -740,6 +793,12 @@ public class OpWhiteListManager implements Listener {
|
||||
+ String.join(", ", justRestored));
|
||||
}
|
||||
// 1) 非法持有扫描:持 OP 但不在白名单 → 撤销 + 报警
|
||||
// ★ 问题4 铁律(2026-10-14 用户原话):「先备份原生 ops.json 到插件自己的
|
||||
// json 文件夹下面来,再卸载删除」。本循环是 fail() 之外唯一的自动撤销入口
|
||||
// (配置自愈后的全量接管也走这里)—— 此前该路径一次备份都没做过,
|
||||
// 用户没提前备份就只能手动逐个恢复。备份失败 → 数据第一优先级,
|
||||
// 本轮中止 apply(不撤也不授),下轮自动重试。
|
||||
boolean opsBackedUp = false;
|
||||
for (OfflinePlayer op : new ArrayList<>(Bukkit.getOperators())) {
|
||||
String n = op.getName();
|
||||
if (n == null || n.isEmpty() || !op.isOp()) {
|
||||
@@ -751,10 +810,19 @@ public class OpWhiteListManager implements Listener {
|
||||
if (justRestored.contains(n.toLowerCase(Locale.ROOT))) {
|
||||
continue;
|
||||
}
|
||||
if (!opsBackedUp) {
|
||||
if (!backupNativeOps()) {
|
||||
plugin.getLogger().warning("[OP白名单] 撤销前备份原版 ops.json 失败 → "
|
||||
+ "本轮【跳过全部撤销】(数据第一优先级),下轮自动重试");
|
||||
return;
|
||||
}
|
||||
opsBackedUp = true;
|
||||
}
|
||||
setOpByName(n, false);
|
||||
warnIllegal(n, now, describeIllegal(n));
|
||||
// 卸任提示当事人(在线直发,离线暂存)
|
||||
notifyOpChange(n, false, "自动撤销(不在白名单:" + describeIllegal(n) + ")");
|
||||
// ★ 问题5(2026-10-14):玩家侧最小化信息披露 —— 只说结果,
|
||||
// 不点名白名单/判定细节(细节只进控制台 warnIllegal 日志)
|
||||
notifyAutoRevoked(n);
|
||||
}
|
||||
// 2) 白名单内玩家补授(已是 OP 的跳过)
|
||||
// ★ 2026-10-08 现场修复:原实现只遍历【在线】玩家,导致离线授权人
|
||||
@@ -1177,7 +1245,8 @@ public class OpWhiteListManager implements Listener {
|
||||
saveConfig();
|
||||
if (isOpNow(name)) {
|
||||
setOpByName(name, false);
|
||||
notifyOpChange(name, false, "临时OP被扣减至到期,已撤销");
|
||||
// ★ 问题6:旧文案与模板拼成病句「你的 临时OP被扣减至到期,已撤销 已撤销」
|
||||
notifyAutoRevoked(name);
|
||||
}
|
||||
sender.sendMessage("§a扣减后 " + name + " 的临时OP已到期,条目已移除,OP 已撤销");
|
||||
} else {
|
||||
@@ -1201,7 +1270,9 @@ public class OpWhiteListManager implements Listener {
|
||||
boolean wasOp = isOpNow(name);
|
||||
setOpByName(name, false);
|
||||
if (wasOp) {
|
||||
notifyOpChange(name, false, inTimed ? "临时OP被移除" : "长期OP被移除");
|
||||
// ★ 问题6(2026-10-14):旧文案「你的 长期OP被移除 已撤销」是病句,
|
||||
// 改走固定文案「管理员已撤销你的OP」(最小化信息披露)
|
||||
notifyAutoRevoked(name);
|
||||
}
|
||||
lastWarn.remove(name.toLowerCase(Locale.ROOT));
|
||||
if (!inPerm && !inTimed && !wasOp) {
|
||||
@@ -1282,10 +1353,22 @@ public class OpWhiteListManager implements Listener {
|
||||
/** 提示当事人 OP 上任/卸任。在线直发;离线暂存 pendingMsg,上线时补发。 */
|
||||
public void notifyOpChange(String name, boolean isGrant, String what) {
|
||||
if (name == null || name.isEmpty()) return;
|
||||
Player p = Bukkit.getPlayerExact(name);
|
||||
String msg = isGrant
|
||||
? "§a[OP白名单] 你已获得 §f" + what + "§a。"
|
||||
: "§c[OP白名单] 你的 §f" + what + " §c已撤销,OP 权限已收回。";
|
||||
notifyMsg(name, msg);
|
||||
}
|
||||
|
||||
/** ★ 问题5(2026-10-14):自动撤销的玩家侧固定文案 —— 最小化信息披露,
|
||||
* 只说结果「管理员已撤销你的OP」;白名单/判定细节只留控制台 warnIllegal 日志。 */
|
||||
public void notifyAutoRevoked(String name) {
|
||||
if (name == null || name.isEmpty()) return;
|
||||
notifyMsg(name, "§c[OP白名单] §f管理员已撤销你的OP");
|
||||
}
|
||||
|
||||
/** 提示投递:在线直发,离线暂存(每人最多 2 条,上线由 flushPendingMsgs 投递) */
|
||||
private void notifyMsg(String name, String msg) {
|
||||
Player p = Bukkit.getPlayerExact(name);
|
||||
if (p != null) {
|
||||
p.sendMessage(msg);
|
||||
} else {
|
||||
|
||||
@@ -84,6 +84,9 @@ public class ShopManager implements Listener {
|
||||
new HashMap<>();
|
||||
// 批量总件数(用于进度提示)
|
||||
private final Map<UUID, Integer> publishQueueTotal = new HashMap<>();
|
||||
// ★ 问题3(2026-10-14):批量成功/跳过计数(与 publishQueue 同生命周期)
|
||||
private final Map<UUID, Integer> publishQueueOk = new HashMap<>();
|
||||
private final Map<UUID, Integer> publishQueueSkip = new HashMap<>();
|
||||
// ===== 编辑模式(2026-10-11 任务1):UUID → 正在编辑的商品ID =====
|
||||
private final Map<UUID, Long> editListingId = new HashMap<>();
|
||||
// ★ 会话令牌(2026-10-11 第五批):进入新会话/新步骤时 +1,
|
||||
@@ -4753,6 +4756,8 @@ public class ShopManager implements Listener {
|
||||
}
|
||||
publishQueue.put(uuid, items);
|
||||
publishQueueTotal.put(uuid, items.size());
|
||||
publishQueueOk.put(uuid, 0);
|
||||
publishQueueSkip.put(uuid, 0);
|
||||
p.sendMessage("§6§l[二手商店] §e开始批量上架,共 §f" + items.size() + " §e件");
|
||||
beginNextPublishItem(p, uuid);
|
||||
}
|
||||
@@ -4761,9 +4766,7 @@ public class ShopManager implements Listener {
|
||||
private void beginNextPublishItem(Player p, UUID uuid) {
|
||||
java.util.LinkedList<ItemStack> q = publishQueue.get(uuid);
|
||||
if (q == null || q.isEmpty()) {
|
||||
publishQueue.remove(uuid);
|
||||
publishQueueTotal.remove(uuid);
|
||||
p.sendMessage("§a§l[二手商店] §f全部商品已上架完成");
|
||||
finishPublishQueue(p, uuid);
|
||||
if (p.isOnline()) openSecondHandShop(p, 0);
|
||||
return;
|
||||
}
|
||||
@@ -4786,6 +4789,7 @@ public class ShopManager implements Listener {
|
||||
returnItem(p, it);
|
||||
p.sendMessage("§c[二手商店] 跳过不可上架物品 §e"
|
||||
+ ItemNameResolver.toChinese(it.getType()) + " §c(" + blocked + "),已退还");
|
||||
publishQueueSkip.merge(uuid, 1, Integer::sum); // ★ 问题3:跳过计数
|
||||
beginNextPublishItem(p, uuid);
|
||||
return;
|
||||
}
|
||||
@@ -4862,6 +4866,8 @@ public class ShopManager implements Listener {
|
||||
ItemStack back = pendingPublishItem.remove(uuid);
|
||||
java.util.LinkedList<ItemStack> q = publishQueue.remove(uuid);
|
||||
publishQueueTotal.remove(uuid);
|
||||
publishQueueOk.remove(uuid);
|
||||
publishQueueSkip.remove(uuid);
|
||||
publishListening.remove(uuid);
|
||||
publishStep.remove(uuid);
|
||||
pendingPublishDesc.remove(uuid);
|
||||
@@ -4906,6 +4912,8 @@ public class ShopManager implements Listener {
|
||||
// ★ 批量队列剩余一并退还(2026-10-11 任务2)
|
||||
java.util.LinkedList<ItemStack> q = publishQueue.remove(uuid);
|
||||
publishQueueTotal.remove(uuid);
|
||||
publishQueueOk.remove(uuid);
|
||||
publishQueueSkip.remove(uuid);
|
||||
int extra = 0;
|
||||
if (q != null) {
|
||||
extra = q.size();
|
||||
@@ -5074,6 +5082,7 @@ public class ShopManager implements Listener {
|
||||
}
|
||||
secondHandListings.add(l);
|
||||
saveSecondHand();
|
||||
publishQueueOk.merge(uuid, 1, Integer::sum); // ★ 问题3:成功计数
|
||||
// ★ 上架成功立刻触发一次增量热重载,把配置数据对齐(2026-10-11 问题4 例外)
|
||||
reloadSecondHandConfigIncremental("上架触发");
|
||||
p.sendMessage("§a§l[二手商店] §f上架成功: §e" + ItemNameResolver.toChinese(item.getType())
|
||||
@@ -5095,13 +5104,40 @@ public class ShopManager implements Listener {
|
||||
p.sendMessage("§7进度: 还剩 §f" + q.size() + " §7件待定价");
|
||||
beginNextPublishItem(p, uuid);
|
||||
} else {
|
||||
publishQueue.remove(uuid);
|
||||
publishQueueTotal.remove(uuid);
|
||||
p.sendMessage("§a§l[二手商店] §f全部商品已上架完成");
|
||||
finishPublishQueue(p, uuid);
|
||||
if (p.isOnline()) openSecondHandShop(p, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ 批量上架统一收尾(2026-10-14 问题3):按成功/跳过计数给出准确语义。
|
||||
* 此前两处收尾无条件打「全部商品已上架完成」——物品被退回(禁售跳过/全禁)
|
||||
* 时也这么说,语义误导。计数与 publishQueue 同生命周期:
|
||||
* startBatchPublish 置 0 → 成功 add(l) 时 ok++、禁售跳过 skip++ →
|
||||
* abortPublish / 超时 / 退出一并清理。
|
||||
*/
|
||||
private void finishPublishQueue(Player p, UUID uuid) {
|
||||
Integer ok = publishQueueOk.remove(uuid);
|
||||
Integer skip = publishQueueSkip.remove(uuid);
|
||||
publishQueue.remove(uuid);
|
||||
publishQueueTotal.remove(uuid);
|
||||
if (ok == null && skip == null) {
|
||||
// 防御:计数缺失(理论不可达)→ 退回旧文案
|
||||
p.sendMessage("§a§l[二手商店] §f全部商品已上架完成");
|
||||
return;
|
||||
}
|
||||
int okN = ok == null ? 0 : ok;
|
||||
int skipN = skip == null ? 0 : skip;
|
||||
if (skipN == 0) {
|
||||
p.sendMessage("§a§l[二手商店] §f全部商品已上架完成(共 " + okN + " 件)");
|
||||
} else if (okN == 0) {
|
||||
p.sendMessage("§c§l[二手商店] §f" + skipN + " 件物品全部不可上架、已退还,本次未上架任何商品");
|
||||
} else {
|
||||
p.sendMessage("§a§l[二手商店] §f批量上架结束:成功上架 §a" + okN
|
||||
+ " §f件、跳过退还 §c" + skipN + " §f件");
|
||||
}
|
||||
}
|
||||
|
||||
/** 取消上架:清全部暂存状态并退还物品(批量模式下连同队列剩余一并退还) */
|
||||
private void abortPublish(Player p, UUID uuid, ItemStack item) {
|
||||
publishListening.remove(uuid);
|
||||
@@ -5113,6 +5149,8 @@ public class ShopManager implements Listener {
|
||||
// ★ 批量上架(2026-10-11 任务2):中止时把队列里未定价的物品全部退还
|
||||
java.util.LinkedList<ItemStack> q = publishQueue.remove(uuid);
|
||||
publishQueueTotal.remove(uuid);
|
||||
publishQueueOk.remove(uuid);
|
||||
publishQueueSkip.remove(uuid);
|
||||
if (q != null && !q.isEmpty()) {
|
||||
int back = q.size() + (item != null ? 1 : 0);
|
||||
for (ItemStack it : q) returnItem(p, it);
|
||||
|
||||
@@ -41,4 +41,5 @@
|
||||
| 100债券 | 买家出 | 5‰ | 100 | 101 | 100(原价)×5‰(手续费)=0.5(4舍5入),100(原价)+1(手续费取整)=101 |
|
||||
|
||||
- 改完多久生效: 自动增量重载,平时安安静静,有人下单了,发布新商品了自动拉起增量重载,有变更就对齐。每个整点(每分钟的第0秒、15秒、30秒、45秒)自动拉起增量热重载,有变更就对齐,没有则跳过<br>
|
||||
- 默认理由最大支持8个,第9号位为自定义下架理由,故默认内置理由仅支持8个
|
||||
- 默认理由最大支持8个,第9号位为返回。<br>
|
||||
- 谁是管理员这一栏填单格式:`player+example`→指定example为管理员,`op`→指定全服OP为管理员,`tag`→指定插件的tag玩家是管理员
|
||||
Reference in New Issue
Block a user