diff --git a/plugin.yml b/plugin.yml index e69de29..fb7836a 100644 --- a/plugin.yml +++ b/plugin.yml @@ -0,0 +1,10 @@ +name: sdf1 +version: 1.3 +author: Sdf1_team +main: com.sdf1.Main +api-version: 1.21 +softdepend: [CY_beibao] +commands: + sdf1: + description: 主控激活口令指令 + usage: /sdf1 \ No newline at end of file diff --git a/src/main/java/com/myplugin/Main.java b/src/main/java/com/myplugin/Main.java new file mode 100644 index 0000000..9f81e70 --- /dev/null +++ b/src/main/java/com/myplugin/Main.java @@ -0,0 +1,283 @@ +package com.myplugin; + +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scoreboard.Objective; +import org.bukkit.scoreboard.Scoreboard; + +import javax.net.ssl.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +public class Main extends JavaPlugin implements Listener, CommandExecutor, TabCompleter { + + private final Set waitingPlayers = new HashSet<>(); + private final String repoURL = "https://api.github.com/repos/ypsdf1/sdf1_plugn/releases/latest"; + private final AtomicInteger failedAttempts = new AtomicInteger(0); + private boolean circuitBroken = false; + + @Override + public void onEnable() { + // 强制初始化插件文件夹为 sdf1,防止产生 myplugn 文件夹 + if (!getDataFolder().exists()) { + getDataFolder().mkdir(); + } + saveDefaultConfig(); + + getServer().getPluginManager().registerEvents(this, this); + if (getCommand("sdf1") != null) { + getCommand("sdf1").setExecutor(this); + getCommand("sdf1").setTabCompleter(this); + } + + // 忽略 SSL 证书校验(核心修复:解决 PKIX 报错) + trustAllHttpsCertificates(); + + Bukkit.getScheduler().runTaskLater(this, () -> triggerAutoUpdate(Bukkit.getConsoleSender()), 40L); + getLogger().info("sdf1 插件已启动。SSL 证书兼容补丁已加载。"); + } + + // --- SSL 证书忽略逻辑 --- + private void trustAllHttpsCertificates() { + try { + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { return null; } + public void checkClientTrusted(X509Certificate[] certs, String authType) {} + public void checkServerTrusted(X509Certificate[] certs, String authType) {} + } + }; + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true); + } catch (Exception ignored) {} + } + + private void triggerAutoUpdate(CommandSender sender) { + String ver = getConfig().getString("version", "1.0"); + if (!getConfig().getBoolean("check-update", true) || ver.equalsIgnoreCase("999") || ver.equalsIgnoreCase("false")) { + return; + } + if (circuitBroken) return; + checkUpdate(sender, false); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + triggerAutoUpdate(Bukkit.getConsoleSender()); + } + + private String parse(String path, Map vars) { + String raw = getConfig().getString("messages." + path, "Missing: " + path); + String msg = raw.replace("&", "§"); + msg = msg.replace("{board}", getConfig().getString("scoreboard-names.main", "abc")); + msg = msg.replace("{version}", getConfig().getString("version", "1.0")); + if (vars != null) { + for (Map.Entry entry : vars.entrySet()) { + msg = msg.replace("{" + entry.getKey() + "}", entry.getValue() == null ? "NULL" : entry.getValue()); + } + } + return msg; + } + + private String normalize(String s) { + return (s == null) ? "" : s.replaceAll("[^a-zA-Z0-9]", "").toUpperCase(); + } + + private boolean isAdmin(CommandSender sender) { + if (sender instanceof ConsoleCommandSender || sender.isOp()) return true; + String raw = getConfig().getString("admin-whitelist", ""); + return Arrays.asList(raw.split(",")).stream().anyMatch(n -> n.trim().equalsIgnoreCase(sender.getName())); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!command.getName().equalsIgnoreCase("sdf1")) return false; + + if (args.length > 0) { + if (!isAdmin(sender)) { + sender.sendMessage(parse("no-permission", null)); + return true; + } + String sub = args[0].toLowerCase(); + if (sub.equals("reload")) { + reloadConfig(); + sender.sendMessage("§a[sdf1] 配置重载成功!"); + return true; + } else if (sub.equals("update")) { + circuitBroken = false; + failedAttempts.set(0); + sender.sendMessage("§b[sdf1] 正在手动连接 GitHub 仓库..."); + checkUpdate(sender, true); + return true; + } else if (sub.equals("admin")) { + if (args.length >= 3 && (sender instanceof ConsoleCommandSender || sender.isOp())) { + updateAdmin(args[1], args[2]); + sender.sendMessage("§a[sdf1] 管理员名单已更新。"); + } + return true; + } + return true; + } + + if (!(sender instanceof Player)) { + sender.sendMessage(parse("only-player", null)); + return true; + } + + Player p = (Player) sender; + waitingPlayers.add(p.getUniqueId()); + p.sendMessage(parse("input-hint", null)); + return true; + } + + private void updateAdmin(String action, String target) { + String raw = getConfig().getString("admin-whitelist", ""); + List list = new ArrayList<>(); + if (!raw.isEmpty()) list.addAll(Arrays.asList(raw.split(","))); + if (action.equalsIgnoreCase("add")) { if (!list.contains(target)) list.add(target); } + else if (action.equalsIgnoreCase("remove")) { list.remove(target); } + getConfig().set("admin-whitelist", String.join(",", list)); + saveConfig(); + } + + @EventHandler + public void onPlayerChat(AsyncPlayerChatEvent event) { + Player player = event.getPlayer(); + if (!waitingPlayers.contains(player.getUniqueId())) return; + event.setCancelled(true); + String userInput = event.getMessage().trim(); + waitingPlayers.remove(player.getUniqueId()); + + if (userInput.equals("0")) { + player.sendMessage(parse("cancel-msg", null)); + return; + } + + Scoreboard sb = Bukkit.getScoreboardManager().getMainScoreboard(); + Objective objM = sb.getObjective(getConfig().getString("scoreboard-names.main", "abc")); + if (objM == null) { player.sendMessage(parse("system-issue", null)); return; } + + String finalKey = null; + for (String entry : sb.getEntries()) { + if (entry.equals(userInput) || normalize(entry).equals(normalize(userInput))) { + finalKey = entry; + break; + } + } + + Map v = new HashMap<>(); + v.put("name", userInput); + v.put("entry", finalKey); + + if (finalKey == null) { + player.sendMessage(parse("not-found", v)); + return; + } + + int score = objM.getScore(finalKey).getScore(); + v.put("score", String.valueOf(score)); + + if (score == -1) { + player.sendMessage(parse("used-up", v)); + } else if (score == 0) { + Objective objR = sb.getObjective(getConfig().getString("scoreboard-names.record", "def")); + if (objR == null) { player.sendMessage(parse("system-issue", v)); return; } + String recName = finalKey + "_" + player.getName(); + if (objR.getScore(recName).getScore() != 0) { + player.sendMessage(parse("already-claimed", v)); + } else { + double amt = getConfig().getDouble("reward-rules.0", 10.0); + v.put("amount", String.valueOf(amt)); + pay(player, amt, v); + objR.getScore(recName).setScore(1); + } + } else { + String sk = String.valueOf(score); + if (getConfig().contains("reward-rules." + sk)) { + double amt = getConfig().getDouble("reward-rules." + sk); + v.put("amount", String.valueOf(amt)); + pay(player, amt, v); + objM.getScore(finalKey).setScore(-1); + } else { + player.sendMessage(parse("system-issue", v)); + } + } + } + + private void pay(Player p, double amt, Map v) { + p.sendMessage(parse("success-pay", v)); + Bukkit.getScheduler().runTask(this, () -> + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "eco give " + p.getName() + " " + amt)); + } + + private void checkUpdate(CommandSender sender, boolean manual) { + String currentVer = getConfig().getString("version", "1.0"); + Bukkit.getScheduler().runTaskAsynchronously(this, () -> { + try { + HttpURLConnection conn = (HttpURLConnection) new URL(repoURL).openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) sdf1-plugin"); + conn.setConnectTimeout(8000); + conn.setReadTimeout(8000); + + if (conn.getResponseCode() != 200) throw new Exception("HTTP " + conn.getResponseCode()); + + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8")); + StringBuilder response = new StringBuilder(); + String inputLine; + while ((inputLine = in.readLine()) != null) response.append(inputLine); + in.close(); + + String content = response.toString(); + if (!content.contains("\"tag_name\":\"")) throw new Exception("No tag found"); + String latest = content.split("\"tag_name\":\"")[1].split("\"")[0].replace("v", ""); + + failedAttempts.set(0); + Bukkit.getScheduler().runTask(this, () -> { + if (!latest.equals(currentVer)) { + Map v = new HashMap<>(); + v.put("latest", latest); + sender.sendMessage(parse("update-found", v)); + } else if (manual) { + sender.sendMessage(parse("update-latest", null)); + } + }); + } catch (Exception e) { + int count = failedAttempts.incrementAndGet(); + if (count >= 3 && !circuitBroken) { + circuitBroken = true; + Bukkit.getScheduler().runTask(this, () -> getLogger().warning(parse("update-circuit-break", null))); + } + if (manual) { + Bukkit.getScheduler().runTask(this, () -> sender.sendMessage("§c[sdf1] 检查更新时发生异常 (PKIX/网络): " + e.getMessage())); + } + } + }); + } + + @Override + public List onTabComplete(CommandSender s, Command c, String a, String[] args) { + if (args.length == 1 && isAdmin(s)) return Arrays.asList("reload", "admin", "update"); + if (args.length == 2 && args[0].equalsIgnoreCase("admin")) return Arrays.asList("add", "remove"); + return null; + } +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..ee6147f --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,63 @@ +# ==================================================== +# sdf1 插件配置文件 (1.21.4) +# 欢迎 DIY!请务必保持格式对齐 +# ==================================================== + +# [核心版本设置] +# 提示:如果将 version 设为 "999" 或 "false",插件将停止启动时的自动更新检查。 +version: "1.0" +# 是否开启自动检查更新 (true 为开启, false 为关闭) +check-update: true + +# [数据库配置] +# 这里的名字必须与你服务器计分板 (Scoreboard) 的名字完全一致 +scoreboard-names: + main: "abc" # 主计分板:存放口令名和对应分数 + record: "def" # 记录计分板:存放 0 分奖励的领取记录 (格式为:口令_玩家名) + +# [管理员名单] +# 只有在这里的玩家或 OP 才能执行 /sdf1 reload/admin/update 指令 +# 格式示范: "Player1,Player2,MyAdmin" +admin-whitelist: "" + +# [奖励规则] +# 格式为 "分数: 金额"。玩家在 {main} 计分板的分数达到 key,就发放对应的 money。 +# 特殊规则: +# - 分数为 0:属于永久口令,每个玩家限领一次,记录在 {record}。 +# - 分数 > 0:属于一次性口令,领完后分数变为 -1。 +reward-rules: + "0": 10.0 + "1": 10.0 + "100": 50.0 + +# [自定义消息体系] +# ---------------------------------------------------- +# 你可以自由 DIY 这里的文字,支持颜色代码 '&' +# 可用变量: +# {name} - 玩家实际输入的文字 +# {entry} - 数据库里对应的原始正确口令 (如果不匹配则显示 NULL) +# {board} - 你在上面设置的 main 计分板名字 +# {amount} - 奖励的金额 +# {score} - 该口令在计分板上的分数 +# {latest} - 远程 GitHub 仓库最新的版本号 +# {version}- 你当前配置文件里的版本号 +# ---------------------------------------------------- +messages: + no-permission: "&c[权限] 抱歉,你没有权限执行这个管理指令!" + only-player: "&c[错误] 领奖口令必须由玩家在游戏内输入。" + input-hint: "&b[验证系统] &f请在聊天栏输入口令 (当前库: &e{board}&f) [输入 0 取消]:" + cancel-msg: "&6[提示] &f你已取消本次验证。" + + # 验证失败类 (由于隐私保护,默认不打印 {entry}) + not-found: "&c[无效] &f未找到口令 &e{name} &f,请检查拼写或大小写。" + already-claimed: "&e[重复] &f这个礼包你已经领过啦,换一个试试吧!" + used-up: "&c[失效] &f该口令已被他人抢先兑换或已过期。" + system-issue: "&4[故障] &f数据异常 (Score:{score}),请截图联系管理员处理。" + + # 验证成功 + success-pay: "&a[成功] &f验证通过!恭喜获得 &6{amount} &f金币。" + + # 更新逻辑相关 + update-found: "&b[更新] &f发现新版本 &eV{latest}&f!当前版本 &7V{version}&f,请前往 GitHub 更新。" + update-latest: "&a[状态] &f插件已是最新版本,无需更新。" + update-circuit-break: "&7[安全保护] 连续 3 次连接 GitHub 失败,已熔断自动更新逻辑以节省带宽。" \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 89db724..fb7836a 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,12 +1,10 @@ -name: sdf1 -version: 1.1 -author: Sdf1_team -# 这里的路径必须指向你的 Main 类,注意大小写! -main: com.sdf1.Main -# 必须声明 API 版本,否则 1.21.4 会报错 -api-version: 1.21 -softdepend: [CY_beibao] -commands: - sdf1: - description: 主控激活口令指令 +name: sdf1 +version: 1.3 +author: Sdf1_team +main: com.sdf1.Main +api-version: 1.21 +softdepend: [CY_beibao] +commands: + sdf1: + description: 主控激活口令指令 usage: /sdf1 \ No newline at end of file