新增OP白名单功能(opwl):读OP白名单目录json授OP,读失败一次性卸载全部OP,15秒增量热重载,命令仅控台
Check Source / check (push) Successful in 3s
Auto Release / build (push) Successful in 2m12s
Simple Test / test (push) Successful in 1s
Test Env / test (push) Successful in 1s

This commit is contained in:
youpaishidifu
2026-10-07 22:09:01 +08:00
parent e745010220
commit d19ecc5ce0
5 changed files with 797 additions and 2 deletions
-2
View File
@@ -119,5 +119,3 @@ test.*
*.md
# 过滤readme
!readme.md
# 临时屏蔽未开发功能的配置文件
opwl.json
+19
View File
@@ -195,6 +195,8 @@ public class Main extends JavaPlugin
private HomeManager homeMgr;
private DeathReport deathReport;
private QuickBack quickBack;
/** OP白名单(opwl 命令 + 15 秒增量热重载,仅控制台可用) */
private OpWhiteListManager opWhiteListMgr;
// 钱包流水查看目标(玩家名 → 查看目标)
@@ -776,6 +778,13 @@ public class Main extends JavaPlugin
// ===== 维护模式(白名单)=====
// 配置文件 plugins/Sdf1_login/白名单.txt,登录/加入/退出时热重载
getServer().getPluginManager().registerEvents(new MaintenanceManager(this), this);
// ===== OP白名单(opwl,仅控制台;读不出配置时一次性卸载全部 OP)=====
opWhiteListMgr = new OpWhiteListManager(this);
getServer().getPluginManager().registerEvents(opWhiteListMgr, this);
if (getCommand("opwl") != null) {
getCommand("opwl").setExecutor(this);
}
getCommand("back").setTabCompleter(quickBack);
// ===== 药水类成就防作弊 =====
@@ -4730,6 +4739,8 @@ public class Main extends JavaPlugin
String cmd = en.getKey();
// conmsg 永不下发给玩家
if ("conmsg".equals(cmd)) continue;
// opwl 仅控制台,永不下发给玩家
if ("opwl".equals(cmd)) continue;
// 管理类指令仅管理员可见
if (!admin && ADMIN_ONLY.contains(cmd)) continue;
out.put(en.getKey(), en.getValue());
@@ -7235,6 +7246,14 @@ public class Main extends JavaPlugin
String cmdName = cmd.getName().toLowerCase();
// ===== /opwl:OP白名单(仅控制台;玩家调用一律静默无响应)=====
if (cmdName.equals("opwl")) {
if (!(sender instanceof Player) && opWhiteListMgr != null) {
opWhiteListMgr.onCommand(sender, args);
}
return true;
}
// ===== /conmsg <名字>:控制台发言署名(仅控制台可用)=====
if (cmdName.equals("conmsg")) {
if (!(sender instanceof Player)) {
@@ -0,0 +1,705 @@
package Sdf1_login;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* OP 白名单(2026-10-07)
*
* 配置目录:plugins/Sdf1_login/OP白名单/ —— 里面【任意一个 .json 文件】就是白名单
* 配置(文件名随意,按文件名排序只读首个;模板 opwl.json 随 jar 释放)。
* 该 json 同时是配置文件与说明书,除四个真实配置项外的其它字段一律忽略、写回时原样保留。
*
* 行为(与 opwl.json 说明书一致):
* - 读得出规则 → 白名单内玩家授 OP;【只授权不主动撤】:白名单外的玩家一律不碰
* (原生 OP / 服主不受影响);本插件授过 OP 的玩家记入 granted(持久化到
* .granted.txt),到期时自动撤销(说明书「到期自动撤销」)。
* - 读不出规则(无 json / JSON 语法错 / 必需字段类型错)→ 【一次性】卸载全部
* 已知玩家(含离线)的 OP,只执行一次,修复配置后自动恢复;
* - 启用状态=false → 功能休眠,不授权也不卸载;
* - 15 秒一次增量热重载:文件指纹(路径+lastModified+长度)有变化才重新解析并打
* 日志,没变化只做静默的授权应用(新上线玩家 / 到期撤销),保持安静;
* - opwl 命令仅限控台(Main 侧对玩家直接静默 return,连帮助都不给)。
*
* 授权应用只在主线程跑(scheduler 主线程 + 命令 + join 事件),无并发问题。
*/
public class OpWhiteListManager implements Listener {
private final Main plugin;
private final File dir;
private final File grantedFile;
private final Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
/** 当前被读的 json(pickJson 的结果,命令写回也写它) */
private File watched;
/** 解析成功后的完整配置(null = 解析失败态) */
private JsonObject cfg;
/** 解析失败标志:驱动「一次性全量卸载」,持续失败不重复卸 */
private boolean parseFailed;
/** 文件指纹(路径 + lastModified + 长度),变化才重载打日志 */
private String lastPath = "";
private long lastStamp = Long.MIN_VALUE;
/** 本插件授过 OP 的玩家(小写名)——兑现「到期自动撤销」的凭据,持久化 */
private final Set<String> granted = new HashSet<>();
public OpWhiteListManager(Main plugin) {
this.plugin = plugin;
this.dir = new File(plugin.getDataFolder(), "OP白名单");
this.grantedFile = new File(dir, ".granted.txt");
if (!dir.exists()) {
dir.mkdirs();
}
// 模板随 jar 释放(只在不存在时复制,绝不覆盖用户改过的文件)
File tpl = new File(dir, "opwl.json");
if (!tpl.exists()) {
try (InputStream in = plugin.getResource("OP白名单/opwl.json")) {
if (in != null) {
Files.copy(in, tpl.toPath());
plugin.getLogger().info("[OP白名单] 已释放模板配置: OP白名单/" + tpl.getName());
} else {
plugin.getLogger().warning("[OPWl白名单] jar 内未找到模板 OP白名单/opwl.json,请手工在目录里放一个 .json");
}
} catch (Exception e) {
plugin.getLogger().warning("[OP白名单] 模板释放失败: " + e.getMessage());
}
}
loadGranted();
reloadInternal(true);
// 15 秒增量热重载 + 静默授权应用
Bukkit.getScheduler().runTaskTimer(plugin, this::tick, 300L, 300L);
}
// ================= 配置读取 =================
/** 目录里任意一个 .json = 白名单文件;文件名按字典序取首个(确定性) */
private File pickJson() {
File[] fs = dir.listFiles((d, n) -> n.toLowerCase(Locale.ROOT).endsWith(".json"));
if (fs == null || fs.length == 0) {
return null;
}
Arrays.sort(fs, (a, b) -> a.getName().compareToIgnoreCase(b.getName()));
return fs[0];
}
private void reloadInternal(boolean log) {
watched = pickJson();
loadFrom(watched, log);
updateStamp();
}
private void loadFrom(File f, boolean log) {
if (f == null) {
fail("OP白名单目录里没有任何 .json 文件");
return;
}
try {
String text = new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8).trim();
JsonObject o = JsonParser.parseString(text).getAsJsonObject();
JsonElement en = o.get("启用状态");
if (en == null || !en.isJsonPrimitive() || !en.getAsJsonPrimitive().isBoolean()) {
throw new IllegalArgumentException("缺少布尔字段「启用状态」");
}
JsonElement pu = o.get("无固定期限授权列表");
if (pu == null || !pu.isJsonArray()) {
throw new IllegalArgumentException("「无固定期限授权列表」必须是数组 []");
}
JsonElement td = o.get("有固定期限授权列表");
if (td == null || !td.isJsonObject()) {
throw new IllegalArgumentException("「有固定期限授权列表」必须是对象 {}");
}
cfg = o;
if (parseFailed) {
plugin.getLogger().info("[OP白名单] 配置已恢复正常,恢复授权管理");
}
parseFailed = false;
if (log) {
plugin.getLogger().info("[OP白名单] 已重载 " + f.getName() + ":"
+ (isEnabled() ? "启用" : "停用") + ",无固定期限 "
+ permanentList().size() + " 人,有固定期限 " + timedObject().size() + " 人");
}
} catch (Exception e) {
fail(f.getName() + " 解析失败:" + e.getMessage());
}
}
/** 解析失败 → 一次性卸载全部已知 OP(含离线),持续失败不重复执行 */
private void fail(String why) {
cfg = null;
if (parseFailed) {
return;
}
parseFailed = true;
int n = 0;
for (OfflinePlayer op : Bukkit.getOperators()) {
if (op.isOp()) {
op.setOp(false);
n++;
}
}
granted.clear();
saveGranted();
plugin.getLogger().warning("[OP白名单] " + why
+ " —— 已一次性卸载全部 " + n + " 名玩家的 OP;修复 json 后自动恢复授权。");
}
/** 文件指纹:路径 + lastModified + 长度(任一变化都视为变更) */
private void updateStamp() {
File f = (watched != null && watched.exists()) ? watched : pickJson();
if (f == null) {
lastPath = "";
lastStamp = 0L;
return;
}
lastPath = f.getAbsolutePath();
lastStamp = f.lastModified() + f.length();
}
// ================= 15 秒 tick =================
private void tick() {
File f = pickJson();
String p = f == null ? "" : f.getAbsolutePath();
long st = f == null ? 0L : f.lastModified() + f.length();
if (!p.equals(lastPath) || st != lastStamp) {
// 有变化:重新解析 + 打日志(含解析失败的告警)
watched = f;
loadFrom(f, true);
updateStamp();
}
// 无论有无变化都做一次静默的授权应用(不打无变化日志,保持安静)
apply();
}
/** 玩家上线:立即应用一次(白名单内马上拿到 OP,不用等下个 15 秒) */
@EventHandler
public void onJoin(PlayerJoinEvent e) {
apply();
}
// ================= 授权应用(只授不撤,例外仅两处:到期、解析失败) =================
private void apply() {
if (cfg == null || !isEnabled()) {
return;
}
long now = System.currentTimeMillis();
// 1) 到期自动撤销:只动本插件授过 OP 且当前在「有固定期限」表里、已过期的玩家。
// 白名单外的其他人(原生 OP 等)一律不碰。
boolean dirty = false;
Iterator<String> it = granted.iterator();
while (it.hasNext()) {
String key = it.next();
String timed = findTimed(key);
if (timed == null) {
continue;
}
long exp = expireOf(timed);
if (exp > 0 && exp <= now) {
setOpByName(key, false);
it.remove();
dirty = true;
plugin.getLogger().info("[OP白名单] " + key + " 授权到期,已自动撤销 OP");
}
}
if (dirty) {
saveGranted();
}
// 2) 白名单内在线玩家授权(已是 OP 的跳过;新授的记入 granted)
for (Player p : Bukkit.getOnlinePlayers()) {
if (p.isOp()) {
continue;
}
if (!isWhitelisted(p.getName())) {
continue;
}
p.setOp(true);
granted.add(p.getName().toLowerCase(Locale.ROOT));
saveGranted();
plugin.getLogger().info("[OP白名单] 已授权 " + p.getName() + " 获得 OP");
}
}
private void setOpByName(String name, boolean op) {
Player p = Bukkit.getPlayerExact(name);
if (p != null) {
p.setOp(op);
return;
}
Bukkit.getOfflinePlayer(name).setOp(op);
}
// ================= 配置字段辅助 =================
private boolean isEnabled() {
return cfg != null && cfg.get("启用状态").getAsBoolean();
}
private JsonArray permanentList() {
return cfg.getAsJsonArray("无固定期限授权列表");
}
private JsonObject timedObject() {
return cfg.getAsJsonObject("有固定期限授权列表");
}
/** 大小写不敏感地找无固定期限条目 */
private boolean hasPermanent(String name) {
for (JsonElement e : permanentList()) {
if (e.isJsonPrimitive() && name.equalsIgnoreCase(e.getAsString())) {
return true;
}
}
return false;
}
/** 大小写不敏感地找有固定期限条目,返回其值(时长/到期时间原文),无则 null */
private String findTimed(String name) {
for (Map.Entry<String, JsonElement> en : timedObject().entrySet()) {
if (name.equalsIgnoreCase(en.getKey()) && en.getValue().isJsonPrimitive()) {
return en.getValue().getAsString();
}
}
return null;
}
/** 有固定期限条目里实际存储的规范 key(大小写不敏感),无则返回入参 */
private String timedKeyOf(String name) {
for (String k : timedObject().keySet()) {
if (name.equalsIgnoreCase(k)) {
return k;
}
}
return name;
}
private boolean removePermanent(String name) {
Iterator<JsonElement> it = permanentList().iterator();
while (it.hasNext()) {
JsonElement e = it.next();
if (e.isJsonPrimitive() && name.equalsIgnoreCase(e.getAsString())) {
it.remove();
return true;
}
}
return false;
}
private boolean removeTimed(String name) {
String k = timedKeyOf(name);
if (timedObject().has(k) && !k.equalsIgnoreCase(name) && timedObject().get(k) != null) {
// key 原样存在
}
for (String key : new ArrayList<>(timedObject().keySet())) {
if (name.equalsIgnoreCase(key)) {
timedObject().remove(key);
return true;
}
}
return false;
}
/** 是否白名单有效成员:永久 或 有期限且未过期(坏时长视为无效,不授) */
private boolean isWhitelisted(String name) {
if (cfg == null) {
return false;
}
if (hasPermanent(name)) {
return true;
}
String t = findTimed(name);
if (t == null) {
return false;
}
long e = expireOf(t);
return e > 0 && e > System.currentTimeMillis();
}
/** 解析为绝对到期毫秒;解析失败返回 -1(调用方按「无效/不过期」保守处理) */
private long expireOf(String v) {
try {
return DurationParser.parseToExpireMs(v);
} catch (Exception e) {
return -1L;
}
}
private String defaultDuration() {
if (cfg != null && cfg.has("默认时长") && cfg.get("默认时长").isJsonPrimitive()) {
String s = cfg.get("默认时长").getAsString().trim();
if (!s.isEmpty()) {
return s;
}
}
return "24h";
}
// ================= 持久化 =================
/** 写回 json:保留用户在配置项之外塞的所有字段(说明书/混淆内容),只动配置项 */
private void saveConfig() {
try {
File target = watched;
if (target == null || !target.exists()) {
target = new File(dir, "opwl.json");
watched = target;
}
Files.write(target.toPath(), gson.toJson(cfg).getBytes(StandardCharsets.UTF_8));
updateStamp(); // 命令自己有回馈,抑制下个 tick 再打一次「已重载」
} catch (Exception e) {
plugin.getLogger().warning("[OP白名单] 配置写回失败: " + e.getMessage());
}
}
private void loadGranted() {
granted.clear();
if (!grantedFile.exists()) {
return;
}
try {
for (String line : Files.readAllLines(grantedFile.toPath(), StandardCharsets.UTF_8)) {
String s = line.trim().toLowerCase(Locale.ROOT);
if (!s.isEmpty()) {
granted.add(s);
}
}
} catch (Exception e) {
plugin.getLogger().warning("[OP白名单] 授权痕迹读取失败: " + e.getMessage());
}
}
private void saveGranted() {
try {
StringBuilder sb = new StringBuilder();
for (String s : granted) {
sb.append(s).append('\n');
}
Files.write(grantedFile.toPath(), sb.toString().getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
plugin.getLogger().warning("[OP白名单] 授权痕迹写入失败: " + e.getMessage());
}
}
// ================= opwl 命令(仅控台;Main 已拦玩家) =================
public void onCommand(org.bukkit.command.CommandSender sender, String[] args) {
String sub = args.length > 0 ? args[0].trim().toLowerCase(Locale.ROOT) : "help";
switch (sub) {
case "help":
case "?":
sendHelp(sender);
return;
case "reload": {
reloadInternal(true);
apply();
sender.sendMessage(cfg == null
? "§c重载完成,但配置解析失败(详见控制台告警)"
: "§a重载完成:§f" + (isEnabled() ? "启用" : "停用")
+ ",无固定期限 " + permanentList().size()
+ " 人,有固定期限 " + timedObject().size() + " 人" +
"§d欢迎来到草原探险服务器,ip: mc2.ypshidifu.cn 端口(基岩版需要):30679");
return;
}
default:
break;
}
// 以下子命令需要配置可读
if (cfg == null) {
sender.sendMessage("§c配置解析失败,先修复 OP白名单 目录里的 json 再操作(或 /opwl reload)");
return;
}
boolean readOnly = "list".equals(sub);
if (!isEnabled() && !readOnly) {
sender.sendMessage("§cOP白名单已停用(「启用状态」= false),先启用再操作");
return;
}
switch (sub) {
case "add": {
if (args.length < 2) {
sender.sendMessage("§e用法: §f/opwl add <玩家>");
return;
}
String name = args[1].trim();
boolean moved = removeTimed(name); // 已有临时授权 → 转永久
if (!hasPermanent(name)) {
permanentList().add(new com.google.gson.JsonPrimitive(name));
}
saveConfig();
boolean ok = grantNow(name, true);
sender.sendMessage("§a已添加 §f" + name + " §a为无固定期限授权"
+ (moved ? "(已把原临时授权转为永久)" : "")
+ (ok ? ",OP 已授予" : "(玩家不在本机档案中,上线时自动授予)"));
return;
}
case "addtime": {
if (args.length < 2) {
sender.sendMessage("§e用法: §f/opwl addtime <玩家> [时长] §7(缺省用默认时长 "
+ defaultDuration() + ")");
return;
}
String name = args[1].trim();
String dur = args.length >= 3 ? joinFrom(args, 2) : defaultDuration();
long exp;
try {
exp = DurationParser.parseToExpireMs(dur);
} catch (Exception e) {
sender.sendMessage("§c时长无法解析: " + dur + " §7(" + e.getMessage() + ")");
return;
}
if (exp <= System.currentTimeMillis()) {
sender.sendMessage("§c该时长的到期时间已在过去: " + dur);
return;
}
removePermanent(name); // 临时授权优先:从永久表移出
timedObject().addProperty(timedKeyOf(name), dur);
saveConfig();
boolean ok = grantNow(name, true);
sender.sendMessage("§a已给 §f" + name + " §a添加临时授权 §7(时长 " + dur
+ ",到期 " + fmt(exp) + ")" + (ok ? ",OP 已授予" : ",上线时自动授予"));
return;
}
case "removetime": {
if (args.length < 3) {
sender.sendMessage("§e用法: §f/opwl removetime <玩家> <扣减时长> §7(例: /opwl removetime "
+ (args.length > 1 ? args[1] : "user") + " 1h)");
return;
}
String name = args[1].trim();
if (hasPermanent(name)) {
sender.sendMessage("§c" + name + " 是永久授权,没有时长可扣;取消请用 /opwl remove " + name);
return;
}
String cur = findTimed(name);
if (cur == null) {
sender.sendMessage("§c" + name + " 没有临时授权,无时长可扣");
return;
}
long oldExp = expireOf(cur);
if (oldExp <= 0) {
sender.sendMessage("§c当前授权的到期时间无法解析: " + cur + "(请手工修正 json)");
return;
}
String cut = joinFrom(args, 2);
long delta;
try {
delta = DurationParser.parseToExpireMs(cut) - System.currentTimeMillis();
} catch (Exception e) {
sender.sendMessage("§c扣减时长无法解析: " + cut);
return;
}
if (delta <= 0) {
sender.sendMessage("§c扣减时长无法解析(需要相对时长,如 30m / 1h / 两小时): " + cut);
return;
}
long newExp = oldExp - delta;
if (newExp <= System.currentTimeMillis()) {
removeTimed(name);
saveConfig();
boolean revoked = revokeNow(name);
sender.sendMessage("§a扣减后 " + name + " 的授权已到期,条目已移除"
+ (revoked ? ",OP 已撤销" : ""));
} else {
String s = fmt(newExp);
timedObject().addProperty(timedKeyOf(name), s);
saveConfig();
sender.sendMessage("§a" + name + " 到期时间已调整为 §f" + s
+ " §7(剩余 " + human(newExp - System.currentTimeMillis()) + ")");
}
return;
}
case "remove": {
if (args.length < 2) {
sender.sendMessage("§e用法: §f/opwl remove <玩家>");
return;
}
String name = args[1].trim();
boolean inPerm = removePermanent(name);
boolean inTimed = removeTimed(name);
saveConfig();
boolean revoked = revokeNow(name);
if (!inPerm && !inTimed && !revoked) {
sender.sendMessage("§e" + name + " 本就不在白名单中");
} else {
sender.sendMessage("§a已移除 §f" + name + " §a的授权"
+ (revoked ? ",OP 已撤销"
: ((inPerm || inTimed) ? "(该玩家 OP 非本插件授予,OP 状态未动)" : "")));
}
return;
}
case "list": {
if (args.length >= 2) {
listOne(sender, args[1].trim());
return;
}
sender.sendMessage("§e===== OP白名单 §7(" + (watched != null ? watched.getName() : "?") + ") =====");
sender.sendMessage("§7状态: " + (isEnabled() ? "§a启用" : "§c停用")
+ " §7| 解析: " + (parseFailed ? "§c失败" : "§a正常"));
JsonArray pu = permanentList();
StringBuilder sb = new StringBuilder("§e无固定期限(" + pu.size() + "): ");
boolean first = true;
for (JsonElement e : pu) {
if (!e.isJsonPrimitive()) {
continue;
}
sb.append(first ? "§f" : "§7, §f").append(e.getAsString());
first = false;
}
sender.sendMessage(sb.toString());
JsonObject td = timedObject();
sender.sendMessage("§e有固定期限(" + td.size() + "):");
long now = System.currentTimeMillis();
if (td.size() == 0) {
sender.sendMessage("§7 (空)");
}
for (Map.Entry<String, JsonElement> en : td.entrySet()) {
String v = en.getValue().isJsonPrimitive() ? en.getValue().getAsString() : "?";
long e = expireOf(v);
String state = e <= 0 ? "§c到期时间无法解析: " + v
: (e > now ? "§7到期 §f" + fmt(e) + " §7(剩 " + human(e - now) + ")"
: "§c已过期 " + fmt(e));
sender.sendMessage("§7 " + en.getKey() + " → " + state);
}
return;
}
default:
sendHelp(sender);
return;
}
}
// ================= 命令辅助 =================
/** 立即授予/撤销并记录;返回 false = 档案未知(上线时由 join 自动补授) */
private boolean grantNow(String name, boolean op) {
if (!op) {
return revokeNow(name);
}
Player p = Bukkit.getPlayerExact(name);
if (p != null) {
p.setOp(true);
granted.add(name.toLowerCase(Locale.ROOT));
saveGranted();
return true;
}
OfflinePlayer o = Bukkit.getOfflinePlayer(name);
if (!o.hasPlayedBefore()) {
// 档案未知:留到 join 兜底(isWhitelisted && !isOp → 授)
return false;
}
o.setOp(true);
granted.add(name.toLowerCase(Locale.ROOT));
saveGranted();
return true;
}
/** 仅撤销本插件授过 OP 的玩家;返回是否真的撤了 */
private boolean revokeNow(String name) {
String key = name.toLowerCase(Locale.ROOT);
if (!granted.contains(key)) {
return false;
}
setOpByName(name, false);
granted.remove(key);
saveGranted();
return true;
}
private void listOne(org.bukkit.command.CommandSender s, String name) {
s.sendMessage("§e" + name + " 授权状态:");
String t = findTimed(name);
if (hasPermanent(name)) {
s.sendMessage("§7 类型: §f无固定期限");
} else if (t != null) {
long e = expireOf(t);
long now = System.currentTimeMillis();
if (e > 0) {
s.sendMessage("§7 类型: §f临时 §7到期 §f" + fmt(e)
+ (e > now ? " §7(剩 " + human(e - now) + ")" : " §c(已过期)"));
} else {
s.sendMessage("§7 类型: §f临时 §c(到期时间无法解析: " + t + ")");
}
} else {
s.sendMessage("§7 类型: §c不在白名单");
}
Player online = Bukkit.getPlayerExact(name);
boolean isOp = online != null ? online.isOp() : Bukkit.getOfflinePlayer(name).isOp();
s.sendMessage("§7 当前OP: " + (isOp ? "§f是" : "§f否")
+ " §7| 本插件授OP记录: " + (granted.contains(name.toLowerCase(Locale.ROOT)) ? "§f是" : "§f否"));
}
private void sendHelp(org.bukkit.command.CommandSender s) {
s.sendMessage("§e===== OP白名单(仅控制台可用) =====");
s.sendMessage("§f/opwl §7- 本帮助");
s.sendMessage("§f/opwl add <玩家> §7- 添加无固定期限授权");
s.sendMessage("§f/opwl addtime <玩家> [时长] §7- 添加临时授权(缺省用默认时长 "
+ defaultDuration() + ")");
s.sendMessage("§f/opwl removetime <玩家> <扣减时长> §7- 扣减授权时长(如 1h)");
s.sendMessage("§f/opwl remove <玩家> §7- 移除授权");
s.sendMessage("§f/opwl list [玩家] §7- 查看白名单 / 单个玩家状态");
s.sendMessage("§f/opwl reload §7- 重载配置");
s.sendMessage("§7配置目录: plugins/Sdf1_login/OP白名单/(任意 *.json),15 秒自动热重载");
s.sendMessage("§d欢迎来到草原探险服务器,ip: mc2.ypshidifu.cn 端口(基岩版需要):30679");
}
/** args[idx..] 用空格拼回(兼容 "one hour" 这类带空格时长) */
private String joinFrom(String[] args, int idx) {
StringBuilder sb = new StringBuilder();
for (int i = idx; i < args.length; i++) {
if (sb.length() > 0) {
sb.append(' ');
}
sb.append(args[i]);
}
return sb.toString().trim();
}
private static String fmt(long ms) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date(ms));
}
/** 剩余时长人性化:X天X小时X分 */
private static String human(long ms) {
long min = ms / 60000L;
if (min < 1) {
return "不到1分钟";
}
long d = min / 1440L;
long h = (min % 1440L) / 60L;
long m = min % 60L;
StringBuilder sb = new StringBuilder();
if (d > 0) {
sb.append(d).append("天");
}
if (h > 0) {
sb.append(h).append("小时");
}
if (m > 0 && d == 0) {
sb.append(m).append("分");
}
return sb.length() == 0 ? "不到1分钟" : sb.toString();
}
}
+67
View File
@@ -0,0 +1,67 @@
{
"OP白名单": "服务器的好管家",
"为什么要OP白名单": {
"根因有3点": {
"第一点": "已有OP乱给权限防不住",
"第二点": "有人利用服务器漏洞拿到OP权限",
"第三点": "不信任OP"
}
},
"我们做了什么": {
"第一层": "临时授权,允许你给OP设置到期时间,到期自动撤销",
"第二层": "防止你的OP乱给别人OP引起人人都是OP"
},
"你需要做什么": [
"共3步",
"第一步:安装sdf1_login插件",
"第二步改这个文件",
"第三步按参考格式或后台指令添加授权"
],
"----": "----",
"启用状态": true,
"无固定期限授权列表": [],
"有固定期限授权列表": {},
"默认时长": "24h",
"时长支持格式": [
"可以是中文口语:一个小时",
"可以是英文one hour",
"可以是标准写法:24H",
"格式很宽松,只要是标准json字符串格式就行。至于你怎么写,插件会解析"
],
"以下是模板文件": {
"# 启用状态": true,
"# 无固定期限授权列表": [
"user1",
"user2"
],
"# 有固定期限授权列表": {
"user1": "2026-10-06 23:00",
"user2": "1小时",
"user3": "two hour"
},
"# 默认时长": "24小时"
},
"为什么这次选择json": {
"根因依旧2点": {
"1": "防止非专业人士修改本文件",
"2": "使用专业格式,让小白试探着劝退(一旦解析失败,所有玩家OP全部卸载)"
}
},
"指令参数": {
"opwl": "帮助菜单",
"opwl add user": "添加user用户为无固定期限用户",
"opwl addtime user": "按默认时长添加临时授权用户",
"opwl addtime user 1h": "给user用户价1小时授权",
"opwl removetime user 1h": "给user用户扣1小时授权",
"opwl remove user": "移除user的授权",
"opwl list user": "获取user的授权状态",
"opwl list":"列出OP白名单",
"opwl reload":"重载OP白名单",
"温馨提示": "opwl命令仅限控台执行,普通用户无权",
"加授权完全无感": "因为用户本身没有op时,被授权后,插件立即给予OP并记录授权状态。删除也是。不需要先添加用户在添加授权。"
},
"安全升级": [
"不追求一定是config.json,你可以把文件改成任意.json,这样你可以在目录放一些其它文件,提升伪装。",
"此文件即是配置文件也是说明书。这就意味着,你可以把其它混淆内容一起打包进本json文件,只要保留配置项。其它的你随便删改"
]
}
+6
View File
@@ -190,3 +190,9 @@ commands:
cleartake:
description: 清空DB队列中未回应的web同步任务(管理员)
usage: /cleartake
opwl:
description: OP白名单管理(仅控制台可用,15秒自动热重载)
usage: /opwl <add|addtime|removetime|remove|list|reload|help>
opwl:
description: op白名单
usage: /opwl