From 8b73031ef0ca024e2782a87e8a8b760cd73a505f Mon Sep 17 00:00:00 2001 From: ypsdf1 Date: Thu, 14 May 2026 21:03:03 +0800 Subject: [PATCH] =?UTF-8?q?Update:=20=E6=B7=BB=E5=8A=A0RadioManager/RadioD?= =?UTF-8?q?ownloadListener/UpdateChecker/WelcomeManager=E7=AD=89=E6=96=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 1 + .../java/Sdf1_login/ChatFilterManager.java | 1158 +++-- .../java/Sdf1_login/ChatInputManager.java | 1647 ++++---- .../java/Sdf1_login/CommissionManager.java | 227 + src/main/java/Sdf1_login/ConfigManager.java | 1 + src/main/java/Sdf1_login/DatabaseManager.java | 17 +- src/main/java/Sdf1_login/GUIManager.java | 395 +- src/main/java/Sdf1_login/InviteManager.java | 185 +- src/main/java/Sdf1_login/LoginManager.java | 604 +-- src/main/java/Sdf1_login/Main.java | 779 +++- src/main/java/Sdf1_login/PlayerStorage.java | 130 + src/main/java/Sdf1_login/QuestStorage.java | 138 + src/main/java/Sdf1_login/QuestTracker.java | 2516 +++++++++++ .../Sdf1_login/RadioDownloadListener.java | 118 + src/main/java/Sdf1_login/RadioManager.java | 874 ++++ src/main/java/Sdf1_login/TicketManager.java | 2 +- src/main/java/Sdf1_login/UpdateChecker.java | 523 +++ src/main/java/Sdf1_login/WelcomeManager.java | 163 + src/main/resources/plugin.yml | 3 +- txt/AFKManager.txt | 137 + txt/AccountRequestManager.txt | 146 + txt/ChatFilterManager.txt | 594 +++ txt/ChatInputManager.txt | 823 ++++ txt/CheckInManager.txt | 104 + txt/ConfigManager.txt | 451 ++ txt/DatabaseManager.txt | 1406 +++++++ txt/EmailManager.txt | 210 + txt/GUIManager.txt | 861 ++++ txt/GarbageManager.txt | 568 +++ txt/GiftManager.txt | 296 ++ txt/IPGroupManager.txt | 36 + txt/InventorySerializer.txt | 46 + txt/InviteManager.txt | 91 + txt/LoginManager.txt | 301 ++ txt/Main.txt | 3712 +++++++++++++++++ txt/PasswordUtils.txt | 72 + txt/PointsManager.txt | 328 ++ txt/QuestTracker.txt | 2493 +++++++++++ txt/RadioConverter.txt | 113 + txt/RadioManager.txt | 563 +++ txt/RiskControlManager.txt | 276 ++ txt/TicketManager.txt | 1650 ++++++++ txt/VerificationManager.txt | 137 + unnamed.patch | 989 +++++ 工作簿1.xlsx | Bin 0 -> 13611 bytes 新建 文本文档.txt | 1 + 46 files changed, 23884 insertions(+), 2001 deletions(-) create mode 100644 src/main/java/Sdf1_login/CommissionManager.java create mode 100644 src/main/java/Sdf1_login/PlayerStorage.java create mode 100644 src/main/java/Sdf1_login/QuestStorage.java create mode 100644 src/main/java/Sdf1_login/QuestTracker.java create mode 100644 src/main/java/Sdf1_login/RadioDownloadListener.java create mode 100644 src/main/java/Sdf1_login/RadioManager.java create mode 100644 src/main/java/Sdf1_login/UpdateChecker.java create mode 100644 src/main/java/Sdf1_login/WelcomeManager.java create mode 100644 txt/AFKManager.txt create mode 100644 txt/AccountRequestManager.txt create mode 100644 txt/ChatFilterManager.txt create mode 100644 txt/ChatInputManager.txt create mode 100644 txt/CheckInManager.txt create mode 100644 txt/ConfigManager.txt create mode 100644 txt/DatabaseManager.txt create mode 100644 txt/EmailManager.txt create mode 100644 txt/GUIManager.txt create mode 100644 txt/GarbageManager.txt create mode 100644 txt/GiftManager.txt create mode 100644 txt/IPGroupManager.txt create mode 100644 txt/InventorySerializer.txt create mode 100644 txt/InviteManager.txt create mode 100644 txt/LoginManager.txt create mode 100644 txt/Main.txt create mode 100644 txt/PasswordUtils.txt create mode 100644 txt/PointsManager.txt create mode 100644 txt/QuestTracker.txt create mode 100644 txt/RadioConverter.txt create mode 100644 txt/RadioManager.txt create mode 100644 txt/RiskControlManager.txt create mode 100644 txt/TicketManager.txt create mode 100644 txt/VerificationManager.txt create mode 100644 unnamed.patch create mode 100644 工作簿1.xlsx create mode 100644 新建 文本文档.txt diff --git a/pom.xml b/pom.xml index 2eac67e..e49b0db 100644 --- a/pom.xml +++ b/pom.xml @@ -62,4 +62,5 @@ + diff --git a/src/main/java/Sdf1_login/ChatFilterManager.java b/src/main/java/Sdf1_login/ChatFilterManager.java index 91dabc3..013b7f6 100644 --- a/src/main/java/Sdf1_login/ChatFilterManager.java +++ b/src/main/java/Sdf1_login/ChatFilterManager.java @@ -1,594 +1,564 @@ -package Sdf1_login; - -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.plugin.java.JavaPlugin; - -import java.io.*; -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class ChatFilterManager { - - private final JavaPlugin plugin; - private final List whitelistUrls = - new ArrayList<>(); - private final Set whitelistPlayers = - new TreeSet<>( - String.CASE_INSENSITIVE_ORDER); - private final Map - punishmentRules = - new LinkedHashMap<>(); - private final Map - punishmentDurations = - new LinkedHashMap<>(); - private final Map - violationCount = - new ConcurrentHashMap<>(); - private final Map - mutedPlayers = - new ConcurrentHashMap<>(); - private final Map messages = - new LinkedHashMap<>(); - private boolean notifyAdmin = true; - private boolean notifyAll = false; - private int muteDuration = 300; - - private static final Pattern URL_PATTERN = - Pattern.compile( - "(?i)(?:https?://\\S+)" - + "|(?:(?:[a-zA-Z0-9]" - + "(?:[a-zA-Z0-9\\-]*" - + "[a-zA-Z0-9])?\\.)+" - + "[a-zA-Z]{2,})" - + "(?:/\\S*)?"); - private static final Pattern DOT_VARIANTS = - Pattern.compile( - "[\uff0e\u3002\u2025\u2026\u00b7]"); - private static final Pattern COLOR_CODES = - Pattern.compile( - "\u00a7[0-9a-fk-orA-FK-OR]"); - - private static final Set FILE_EXT; - static { - Set s = new HashSet<>( - Arrays.asList( - "txt", "yml", "yaml", - "json", "xml", "java", - "class", "jar", "png", - "jpg", "jpeg", "gif", - "bmp", "svg", "ico", - "webp", "mp3", "mp4", - "avi", "mkv", "wav", - "flac", "ogg", "zip", - "rar", "7z", "tar", - "gz", "cfg", "conf", - "ini", "log", "db", - "sql", "sh", "bat", - "html", "htm", "css", - "js", "md", "pdf", - "dll", "so", "exe", - "bin", "c", "cpp", - "py", "rb", "go", - "rs", "kt", "lua", - "php", "asp", "jsp")); - FILE_EXT = Collections.unmodifiableSet(s); - } - - public ChatFilterManager(JavaPlugin plugin) { - this.plugin = plugin; - } - - // ========== 配置加载 ========== - - public void loadConfig() { - whitelistUrls.clear(); - whitelistPlayers.clear(); - punishmentRules.clear(); - punishmentDurations.clear(); - messages.clear(); - - File f = new File( - plugin.getDataFolder(), "chat.txt"); - if (!f.exists()) writeDefaultFile(f); - try (BufferedReader r = - new BufferedReader( - new InputStreamReader( - new FileInputStream(f), - StandardCharsets.UTF_8))) { - String section = ""; - String line; - while ((line = r.readLine()) != null) { - if (line.startsWith("\uFEFF")) - line = line.substring(1); - String t = line.trim(); - if (t.isEmpty() - || t.startsWith("#")) - continue; - if (t.equals("白名单") - || t.equals("白名单:")) { - section = "whitelist"; - continue; - } - if (t.equals("处罚规则") - || t.equals("处罚规则:")) { - section = "punishment"; - continue; - } - if (t.equals("白名单玩家") - || t.equals("白名单玩家:")) { - section = "players"; - continue; - } - if (t.contains(":")) { - String[] kv = t.split(":", 2); - String k = kv[0].trim(); - String v = kv[1].trim(); - if (k.equals("通知管理员")) { - notifyAdmin = parseBool(v); - continue; - } - if (k.equals("全服通报")) { - notifyAll = parseBool(v); - continue; - } - if (k.equals("禁言时长")) { - try { - muteDuration = - Integer.parseInt(v); - } catch (Exception ignored) { - } - continue; - } - } - switch (section) { - case "whitelist": - whitelistUrls.add( - t.toLowerCase()); - break; - case "punishment": - String[] parts = t.split(":"); - if (parts.length >= 2) { - try { - int cnt = - Integer.parseInt( - parts[0] - .trim()); - String type = - parts[1].trim() - .toLowerCase(); - int dur = - parts.length >= 3 - ? Integer.parseInt( - parts[2] - .trim()) - : muteDuration; - punishmentRules.put( - cnt, type); - punishmentDurations.put( - cnt, dur); - } catch (Exception ignored) { - } - } - break; - case "players": - whitelistPlayers.add(t); - break; - } - } - } catch (Exception e) { - plugin.getLogger().severe( - "[Sdf1_chat] 配置加载失败: " - + e.getMessage()); - } - - // 从消息.txt读取chat_开头的消息 - loadMessages(); - } - - private void loadMessages() { - File msgFile = new File( - plugin.getDataFolder(), - "消息.txt"); - if (!msgFile.exists()) return; - try (BufferedReader mr = - new BufferedReader( - new InputStreamReader( - new FileInputStream( - msgFile), - StandardCharsets.UTF_8))) { - String mline; - while ((mline = mr.readLine()) != null) { - mline = mline.trim(); - if (mline.isEmpty() - || mline.startsWith("#")) - continue; - // 支持 = 和 : 两种分隔符 - int eqIdx = mline.indexOf('='); - int coIdx = mline.indexOf(':'); - int splitIdx = -1; - if (eqIdx >= 0 && coIdx >= 0) { - splitIdx = Math.min(eqIdx, coIdx); - } else if (eqIdx >= 0) { - splitIdx = eqIdx; - } else if (coIdx >= 0) { - splitIdx = coIdx; - } - if (splitIdx < 0) continue; - String k = mline.substring(0, - splitIdx).trim(); - String v = mline.substring( - splitIdx + 1).trim(); - if (k.startsWith("chat_")) { - messages.put(k, v); - } - } - } catch (Exception ignored) { - } - } - - - private void writeDefaultFile(File f) { - try (PrintWriter pw = new PrintWriter( - new OutputStreamWriter( - new FileOutputStream(f), - StandardCharsets.UTF_8))) { - pw.println("# Sdf1_chat 聊天过滤配置"); - pw.println("通知管理员: true"); - pw.println("全服通报: false"); - pw.println("禁言时长: 300"); - pw.println(); - pw.println("白名单:"); - pw.println("*.minecraft.net"); - pw.println("baidu.com"); - pw.println("github.com"); - pw.println(); - pw.println("处罚规则:"); - pw.println("1:warn:0"); - pw.println("2:warn:0"); - pw.println("3:mute:300"); - pw.println("5:mute:600"); - pw.println("8:kick:0"); - pw.println("10:ban:0"); - pw.println(); - pw.println("白名单玩家:"); - } catch (IOException ignored) { - } - } - - public void saveConfig() { - File f = new File( - plugin.getDataFolder(), "chat.txt"); - try (PrintWriter pw = new PrintWriter( - new OutputStreamWriter( - new FileOutputStream(f), - StandardCharsets.UTF_8))) { - pw.println("# Sdf1_chat 聊天过滤配置"); - pw.println("通知管理员: " + notifyAdmin); - pw.println("全服通报: " + notifyAll); - pw.println("禁言时长: " + muteDuration); - pw.println(); - pw.println("白名单:"); - for (String u : whitelistUrls) - pw.println(u); - pw.println(); - pw.println("处罚规则:"); - for (Map.Entry entry - : punishmentRules.entrySet()) { - int dur = punishmentDurations - .getOrDefault(entry.getKey(), - muteDuration); - pw.println(entry.getKey() + ":" - + entry.getValue() - + ":" + dur); - } - pw.println(); - pw.println("白名单玩家:"); - for (String p : whitelistPlayers) - pw.println(p); - } catch (IOException e) { - plugin.getLogger().severe( - "[Sdf1_chat] 保存失败: " - + e.getMessage()); - } - } - - // ========== 消息 ========== - - public String msg(String key, - String... args) { - String template = messages.getOrDefault( - key, key); - for (int i = 0; i < args.length - 1; - i += 2) { - template = template.replace( - "{" + args[i] + "}", - args[i + 1]); - } - return template; - } - - // ========== URL检测 ========== - - private String normalizeMessage(String msg) { - msg = COLOR_CODES.matcher(msg) - .replaceAll(""); - msg = DOT_VARIANTS.matcher(msg) - .replaceAll("."); - return msg; - } - - public List extractUrls( - String message) { - String normalized = - normalizeMessage(message); - List urls = new ArrayList<>(); - Matcher m = URL_PATTERN - .matcher(normalized); - while (m.find()) { - String url = m.group().toLowerCase(); - url = url.replaceAll( - "[\\s.,;!?,。;!?、)》」』】" - + "\\]\\[\\((]+$", - ""); - if (!url.isEmpty()) urls.add(url); - } - return urls; - } - - public boolean isWhitelisted(String url) { - String clean = url - .replaceAll("^https?://", "") - .toLowerCase(); - int colon = clean.indexOf(':'); - int slash = clean.indexOf('/'); - if (colon > 0 && (slash < 0 - || colon < slash)) { - clean = clean.substring(0, colon); - } - slash = clean.indexOf('/'); - if (slash > 0) { - clean = clean.substring(0, slash); - } - for (String entry : whitelistUrls) { - String e = entry.toLowerCase().trim(); - if (e.startsWith("*.")) { - String suffix = e.substring(1); - if (clean.endsWith(suffix) - || clean.equals( - suffix.substring(1))) - return true; - } else { - if (clean.equals(e) - || clean.endsWith( - "." + e)) - return true; - } - } - return false; - } - - public boolean isLikelyDomain(String url) { - int lastDot = url.lastIndexOf('.'); - if (lastDot < 0 - || lastDot >= url.length() - 1) - return false; - String ext = url.substring(lastDot + 1) - .toLowerCase(); - return !FILE_EXT.contains(ext); - } - - // ========== 玩家操作 ========== - - public boolean isMuted(String name) { - Long expiry = mutedPlayers.get(name); - if (expiry == null) return false; - if (System.currentTimeMillis() - >= expiry) { - mutedPlayers.remove(name); - return false; - } - return true; - } - - public String fmtDuration(int seconds) { - if (seconds <= 0) return "0秒"; - int day = seconds / 86400; - int hr = (seconds % 86400) / 3600; - int min = (seconds % 3600) / 60; - int sec = seconds % 60; - StringBuilder sb = new StringBuilder(); - if (day > 0) sb.append(day).append("天"); - if (hr > 0) - sb.append(hr).append("小时"); - if (min > 0) - sb.append(min).append("分"); - if (sec > 0 || sb.length() == 0) - sb.append(sec).append("秒"); - return sb.toString(); - } - - public void applyPunishment(Player player, - int violation) { - String type = "warn"; - int duration = muteDuration; - int maxRule = 0; - for (Map.Entry entry - : punishmentRules.entrySet()) { - if (violation >= entry.getKey() - && entry.getKey() > maxRule) { - maxRule = entry.getKey(); - type = entry.getValue(); - duration = punishmentDurations - .getOrDefault( - entry.getKey(), - muteDuration); - } - } - if (type.equals("mute")) { - if (isMuted(player.getName())) - return; - mutedPlayers.put(player.getName(), - System.currentTimeMillis() - + (long) duration - * 1000L); - } - final String fType = type; - final int fDur = duration; - final int fV = violation; - final String fName = player.getName(); - Bukkit.getScheduler().runTask(plugin, - () -> { - Player p = Bukkit.getPlayer(fName); - if (p == null) return; - switch (fType) { - case "warn": - // 警告不处罚,只提示 - break; - case "mute": - p.sendMessage( - msg("chat_muted")); - break; - case "kick": - p.kickPlayer( - msg("chat_muted")); - break; - case "ban": - Bukkit.dispatchCommand( - Bukkit.getConsoleSender(), - "ban " + fName - + " §c多次发送违规链接"); - break; - case "banip": - InetSocketAddress addr = - p.getAddress(); - if (addr != null) { - String ip = addr - .getAddress() - .getHostAddress(); - Bukkit.dispatchCommand( - Bukkit.getConsoleSender(), - "ban-ip " + ip - + " §c发送违规链接"); - } - break; - } - }); - } - - public void unmutePlayer(String name) { - mutedPlayers.remove(name); - } - - public void resetPlayer(String name) { - mutedPlayers.remove(name); - violationCount.remove(name); - } - - public boolean isPlayerWhitelisted( - String name) { - return whitelistPlayers.contains(name); - } - - public boolean isNotifyAdmin() { - return notifyAdmin; - } - - public boolean isNotifyAll() { - return notifyAll; - } - - public int getViolationCount(String name) { - return violationCount - .getOrDefault(name, 0); - } - - public void incrementViolation( - String name) { - violationCount.put(name, - getViolationCount(name) + 1); - } - - public List getWhitelistUrls() { - return whitelistUrls; - } - - public Set getWhitelistPlayers() { - return whitelistPlayers; - } - - public Map - getPunishmentRules() { - return punishmentRules; - } - - public Map - getPunishmentDurations() { - return punishmentDurations; - } - - public Map - getMutedPlayers() { - return mutedPlayers; - } - - public void setNotifyAdmin(boolean v) { - notifyAdmin = v; - } - - public void setNotifyAll(boolean v) { - notifyAll = v; - } - - public void setMuteDuration(int v) { - muteDuration = v; - } - - public void addUrl(String url) { - whitelistUrls.add( - url.toLowerCase().trim()); - saveConfig(); - } - - public boolean removeUrl(String url) { - boolean r = whitelistUrls.remove( - url.toLowerCase().trim()); - saveConfig(); - return r; - } - - public void addWhitelistPlayer( - String name) { - whitelistPlayers.add(name); - saveConfig(); - } - - public boolean removeWhitelistPlayer( - String name) { - boolean r = - whitelistPlayers.remove(name); - saveConfig(); - return r; - } - - public void cleanExpired() { - long now = System.currentTimeMillis(); - mutedPlayers.entrySet().removeIf( - e -> e.getValue() <= now); - } - - private boolean parseBool(String v) { - String s = v.toLowerCase().trim(); - return s.equals("true") - || s.equals("on") - || s.equals("yes") - || s.equals("1") - || s.contains("开启") - || s.contains("启用") - || s.contains("是"); - } -} +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.*; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ChatFilterManager { + + private final JavaPlugin plugin; + private final List whitelistUrls = + new ArrayList<>(); + private final Set whitelistPlayers = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Map + punishmentRules = + new LinkedHashMap<>(); + private final Map + punishmentDurations = + new LinkedHashMap<>(); + private final Map + violationCount = + new ConcurrentHashMap<>(); + private final Map + mutedPlayers = + new ConcurrentHashMap<>(); + private final Map messages = + new LinkedHashMap<>(); + private boolean notifyAdmin = true; + private boolean notifyAll = false; + private int muteDuration = 300; + + private static final Pattern URL_PATTERN = + Pattern.compile( + "(?i)(?:https?://\\S+)" + + "|(?:(?:[a-zA-Z0-9]" + + "(?:[a-zA-Z0-9\\-]*" + + "[a-zA-Z0-9])?\\.)+" + + "[a-zA-Z]{2,})" + + "(?:/\\S*)?"); + private static final Pattern DOT_VARIANTS = + Pattern.compile( + "[\uff0e\u3002\u2025\u2026\u00b7]"); + private static final Pattern COLOR_CODES = + Pattern.compile( + "\u00a7[0-9a-fk-orA-FK-OR]"); + + private static final Set FILE_EXT; + static { + Set s = new HashSet<>( + Arrays.asList( + "txt", "yml", "yaml", + "json", "xml", "java", + "class", "jar", "png", + "jpg", "jpeg", "gif", + "bmp", "svg", "ico", + "webp", "mp3", "mp4", + "avi", "mkv", "wav", + "flac", "ogg", "zip", + "rar", "7z", "tar", + "gz", "cfg", "conf", + "ini", "log", "db", + "sql", "sh", "bat", + "html", "htm", "css", + "js", "md", "pdf", + "dll", "so", "exe", + "bin", "c", "cpp", + "py", "rb", "go", + "rs", "kt", "lua", + "php", "asp", "jsp")); + FILE_EXT = Collections.unmodifiableSet(s); + } + + public ChatFilterManager(JavaPlugin plugin) { + this.plugin = plugin; + } + + // ========== 配置加载 ========== + + public void loadConfig() { + whitelistUrls.clear(); + whitelistPlayers.clear(); + punishmentRules.clear(); + punishmentDurations.clear(); + messages.clear(); + + File f = new File( + plugin.getDataFolder(), "chat.txt"); + if (!f.exists()) writeDefaultFile(f); + try (BufferedReader r = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8))) { + String section = ""; + String line; + while ((line = r.readLine()) != null) { + if (line.startsWith("\uFEFF")) + line = line.substring(1); + String t = line.trim(); + if (t.isEmpty() + || t.startsWith("#")) + continue; + if (t.equals("白名单") + || t.equals("白名单:")) { + section = "whitelist"; + continue; + } + if (t.equals("处罚规则") + || t.equals("处罚规则:")) { + section = "punishment"; + continue; + } + if (t.equals("白名单玩家") + || t.equals("白名单玩家:")) { + section = "players"; + continue; + } + if (t.contains(":")) { + String[] kv = t.split(":", 2); + String k = kv[0].trim(); + String v = kv[1].trim(); + if (k.equals("通知管理员")) { + notifyAdmin = parseBool(v); + continue; + } + if (k.equals("全服通报")) { + notifyAll = parseBool(v); + continue; + } + if (k.equals("禁言时长")) { + try { + muteDuration = + Integer.parseInt(v); + } catch (Exception ignored) { + } + continue; + } + } + switch (section) { + case "whitelist": + whitelistUrls.add( + t.toLowerCase()); + break; + case "punishment": + String[] parts = t.split(":"); + if (parts.length >= 2) { + try { + int cnt = + Integer.parseInt( + parts[0] + .trim()); + String type = + parts[1].trim() + .toLowerCase(); + int dur = + parts.length >= 3 + ? Integer.parseInt( + parts[2] + .trim()) + : muteDuration; + punishmentRules.put( + cnt, type); + punishmentDurations.put( + cnt, dur); + } catch (Exception ignored) { + } + } + break; + case "players": + whitelistPlayers.add(t); + break; + } + } + } catch (Exception e) { + plugin.getLogger().severe( + "[Sdf1_chat] 配置加载失败: " + + e.getMessage()); + } + + // 从消息.txt读取chat_开头的消息 + loadMessages(); + } + + private void loadMessages() { + File msgFile = new File( + plugin.getDataFolder(), + "消息.txt"); + if (!msgFile.exists()) return; + try (BufferedReader mr = + new BufferedReader( + new InputStreamReader( + new FileInputStream( + msgFile), + StandardCharsets.UTF_8))) { + String mline; + while ((mline = mr.readLine()) != null) { + mline = mline.trim(); + if (mline.isEmpty() + || mline.startsWith("#")) + continue; + // 支持 = 和 : 两种分隔符 + int eqIdx = mline.indexOf('='); + int coIdx = mline.indexOf(':'); + int splitIdx = -1; + if (eqIdx >= 0 && coIdx >= 0) { + splitIdx = Math.min(eqIdx, coIdx); + } else if (eqIdx >= 0) { + splitIdx = eqIdx; + } else if (coIdx >= 0) { + splitIdx = coIdx; + } + if (splitIdx < 0) continue; + String k = mline.substring(0, + splitIdx).trim(); + String v = mline.substring( + splitIdx + 1).trim(); + if (k.startsWith("chat_")) { + messages.put(k, v); + } + } + } catch (Exception ignored) { + } + } + + + private void writeDefaultFile(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# Sdf1_chat 聊天过滤配置"); + pw.println("通知管理员: true"); + pw.println("全服通报: false"); + pw.println("禁言时长: 300"); + pw.println(); + pw.println("白名单:"); + pw.println("*.minecraft.net"); + pw.println("baidu.com"); + pw.println("github.com"); + pw.println(); + pw.println("处罚规则:"); + pw.println("1:warn:0"); + pw.println("2:warn:0"); + pw.println("3:mute:300"); + pw.println("5:mute:600"); + pw.println("8:kick:0"); + pw.println("10:ban:0"); + pw.println(); + pw.println("白名单玩家:"); + } catch (IOException ignored) { + } + } + + public void saveConfig() { + File f = new File( + plugin.getDataFolder(), "chat.txt"); + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# Sdf1_chat 聊天过滤配置"); + pw.println("通知管理员: " + notifyAdmin); + pw.println("全服通报: " + notifyAll); + pw.println("禁言时长: " + muteDuration); + pw.println(); + pw.println("白名单:"); + for (String u : whitelistUrls) + pw.println(u); + pw.println(); + pw.println("处罚规则:"); + for (Map.Entry entry + : punishmentRules.entrySet()) { + int dur = punishmentDurations + .getOrDefault(entry.getKey(), + muteDuration); + pw.println(entry.getKey() + ":" + + entry.getValue() + + ":" + dur); + } + pw.println(); + pw.println("白名单玩家:"); + for (String p : whitelistPlayers) + pw.println(p); + } catch (IOException e) { + plugin.getLogger().severe( + "[Sdf1_chat] 保存失败: " + + e.getMessage()); + } + } + + // ========== 消息 ========== + + public String msg(String key, + String... args) { + String template = messages.getOrDefault( + key, key); + for (int i = 0; i < args.length - 1; + i += 2) { + template = template.replace( + "{" + args[i] + "}", + args[i + 1]); + } + return template; + } + + // ========== URL检测 ========== + + private String normalizeMessage(String msg) { + msg = COLOR_CODES.matcher(msg) + .replaceAll(""); + msg = DOT_VARIANTS.matcher(msg) + .replaceAll("."); + return msg; + } + + public List extractUrls( + String message) { + String normalized = + normalizeMessage(message); + List urls = new ArrayList<>(); + Matcher m = URL_PATTERN + .matcher(normalized); + while (m.find()) { + String url = m.group().toLowerCase(); + url = url.replaceAll( + "[\\s.,;!?,。;!?、)》」』】" + + "\\]\\[\\((]+$", + ""); + if (!url.isEmpty()) + urls.add(url); + } + return urls; + } + + + public boolean isLikelyDomain(String url) { + int lastDot = url.lastIndexOf('.'); + if (lastDot < 0 + || lastDot >= url.length() - 1) + return false; + String ext = url.substring(lastDot + 1) + .toLowerCase(); + return !FILE_EXT.contains(ext); + } + + // ========== 玩家操作 ========== + + public boolean isMuted(String name) { + Long expiry = mutedPlayers.get(name); + if (expiry == null) return false; + if (System.currentTimeMillis() + >= expiry) { + mutedPlayers.remove(name); + return false; + } + return true; + } + + public String fmtDuration(int seconds) { + if (seconds <= 0) return "0秒"; + int day = seconds / 86400; + int hr = (seconds % 86400) / 3600; + int min = (seconds % 3600) / 60; + int sec = seconds % 60; + StringBuilder sb = new StringBuilder(); + if (day > 0) sb.append(day).append("天"); + if (hr > 0) + sb.append(hr).append("小时"); + if (min > 0) + sb.append(min).append("分"); + if (sec > 0 || sb.length() == 0) + sb.append(sec).append("秒"); + return sb.toString(); + } + + public void applyPunishment(Player player, + int violation) { + String type = "warn"; + int duration = muteDuration; + int maxRule = 0; + for (Map.Entry entry + : punishmentRules.entrySet()) { + if (violation >= entry.getKey() + && entry.getKey() > maxRule) { + maxRule = entry.getKey(); + type = entry.getValue(); + duration = punishmentDurations + .getOrDefault( + entry.getKey(), + muteDuration); + } + } + if (type.equals("mute")) { + if (isMuted(player.getName())) + return; + mutedPlayers.put(player.getName(), + System.currentTimeMillis() + + (long) duration + * 1000L); + } + final String fType = type; + final int fDur = duration; + final int fV = violation; + final String fName = player.getName(); + Bukkit.getScheduler().runTask(plugin, + () -> { + Player p = Bukkit.getPlayer(fName); + if (p == null) return; + switch (fType) { + case "warn": + // 警告不处罚,只提示 + break; + case "mute": + p.sendMessage( + msg("chat_muted")); + break; + case "kick": + p.kickPlayer( + msg("chat_muted")); + break; + case "ban": + Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), + "ban " + fName + + " §c多次发送违规链接"); + break; + case "banip": + InetSocketAddress addr = + p.getAddress(); + if (addr != null) { + String ip = addr + .getAddress() + .getHostAddress(); + Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), + "ban-ip " + ip + + " §c发送违规链接"); + } + break; + } + }); + } + + public void unmutePlayer(String name) { + mutedPlayers.remove(name); + } + + public void resetPlayer(String name) { + mutedPlayers.remove(name); + violationCount.remove(name); + } + + public boolean isPlayerWhitelisted( + String name) { + return whitelistPlayers.contains(name); + } + + public boolean isNotifyAdmin() { + return notifyAdmin; + } + + public boolean isNotifyAll() { + return notifyAll; + } + + public int getViolationCount(String name) { + return violationCount + .getOrDefault(name, 0); + } + + public void incrementViolation( + String name) { + violationCount.put(name, + getViolationCount(name) + 1); + } + + public List getWhitelistUrls() { + return whitelistUrls; + } + + public Set getWhitelistPlayers() { + return whitelistPlayers; + } + + public Map + getPunishmentRules() { + return punishmentRules; + } + + public Map + getPunishmentDurations() { + return punishmentDurations; + } + + public Map + getMutedPlayers() { + return mutedPlayers; + } + + public void setNotifyAdmin(boolean v) { + notifyAdmin = v; + } + + public void setNotifyAll(boolean v) { + notifyAll = v; + } + + public void setMuteDuration(int v) { + muteDuration = v; + } + + public void addUrl(String url) { + whitelistUrls.add( + url.toLowerCase().trim()); + saveConfig(); + } + + public boolean removeUrl(String url) { + boolean r = whitelistUrls.remove( + url.toLowerCase().trim()); + saveConfig(); + return r; + } + + public void addWhitelistPlayer( + String name) { + whitelistPlayers.add(name); + saveConfig(); + } + + public boolean removeWhitelistPlayer( + String name) { + boolean r = + whitelistPlayers.remove(name); + saveConfig(); + return r; + } + + public void cleanExpired() { + long now = System.currentTimeMillis(); + mutedPlayers.entrySet().removeIf( + e -> e.getValue() <= now); + } + + private boolean parseBool(String v) { + String s = v.toLowerCase().trim(); + return s.equals("true") + || s.equals("on") + || s.equals("yes") + || s.equals("1") + || s.contains("开启") + || s.contains("启用") + || s.contains("是"); + } +} diff --git a/src/main/java/Sdf1_login/ChatInputManager.java b/src/main/java/Sdf1_login/ChatInputManager.java index 233546a..e564b28 100644 --- a/src/main/java/Sdf1_login/ChatInputManager.java +++ b/src/main/java/Sdf1_login/ChatInputManager.java @@ -1,824 +1,823 @@ -package Sdf1_login; - -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.util.HashMap; -import java.util.Map; - -public class ChatInputManager { - - public enum InputType { - NONE, - CHANGE_PWD_STEP1, - CHANGE_PWD_STEP2, - SET_EMAIL, - SET_EMAIL_VERIFY, - ADMIN_AUTH, - ADMIN_SET_POINTS, - ADMIN_SET_PWD, - ADMIN_DELETE_CONFIRM, - ADMIN_SET_AFK_TIME, - SMTP_CONFIG, - SMTP_VERIFY, - BACK_CHECKIN, - INVITE_INPUT_CODE, - TICKET_TITLE, - TICKET_DESC, - TICKET_SCORE, - TICKET_REJECT, - TICKET_ASSIGN, - TICKET_INVITE_PROVIDER, - TICKET_REMOVE_PROVIDER, - TICKET_COMPLETE, - TICKET_REPORT_ISSUE, - TICKET_REPLY, - TICKET_ADMIN_TYPE, - TICKET_ADMIN_TITLE, - TICKET_ADMIN_DESC, - TICKET_ADMIN_PROVIDER - } - - public static class PlayerState { - public InputType type = InputType.NONE; - public String targetPlayer = ""; - public String ticketType = ""; - public String ticketTitle = ""; - public String ticketDesc = ""; - public int ticketId = 0; - public int smtpStep = 0; - public int scoreValue = 0; - } - - private final Map states = - new HashMap<>(); - private Main plugin; - - public PlayerState getState(Player p) { - return states.computeIfAbsent( - p.getName(), - k -> new PlayerState()); - } - - public void reset(Player p) { - states.remove(p.getName()); - } - - public void handleInput(Player p, String msg, - Main mainPlugin) { - if (this.plugin == null) - this.plugin = mainPlugin; - PlayerState state = getState(p); - ConfigManager config = - mainPlugin.getConfig2(); - DatabaseManager db = mainPlugin.getDb(); - - switch (state.type) { - - case CHANGE_PWD_STEP1: { - String name = p.getName(); - String salt = (String) db.getField( - name, "password_salt"); - String hash = - PasswordUtils.hash(msg, salt); - boolean mainOk = - db.checkPassword(name, hash); - if (mainOk) { - state.type = - InputType.CHANGE_PWD_STEP2; - state.ticketTitle = "main"; - p.sendMessage( - "§e请输入新密码:"); - return; - } - boolean tempOk = - db.checkPasswordOrTemp( - name, hash); - if (tempOk) { - state.type = - InputType.CHANGE_PWD_STEP2; - state.ticketTitle = "temp"; - p.sendMessage( - "§e请输入新密码:"); - return; - } - p.sendMessage(config.msg( - "password_wrong")); - return; - } - - case CHANGE_PWD_STEP2: { - String name = p.getName(); - if (!PasswordUtils.validate(msg)) { - p.sendMessage(config.msg( - "password_format_error")); - reset(p); - return; - } - String oldHash = - (String) db.getField(name, - "password_hash"); - String oldSalt = - (String) db.getField(name, - "password_salt"); - String newSalt = - PasswordUtils.generateSalt(); - String newHash = - PasswordUtils.hash( - msg, newSalt); - if (newHash.equals(oldHash)) { - p.sendMessage(config.msg( - "password_same")); - reset(p); - return; - } - mainPlugin.recordPasswordChange( - name, oldHash, oldSalt); - db.setField(name, - "password_hash", newHash); - db.setField(name, - "password_salt", newSalt); - mainPlugin - .getNeedsPasswordChange() - .remove(name); - p.sendMessage(config.msg( - "password_changed")); - reset(p); - return; - } - - case SET_EMAIL: { - if (!msg.contains("@") - || !msg.contains(".")) { - p.sendMessage(config.msg( - "email_format_error")); - reset(p); - return; - } - String oldEmail = (String) - db.getField(p.getName(), - "email"); - String emailAddr = msg.trim(); - // 已绑定邮箱:验证登录密码 - if (oldEmail != null - && !oldEmail.isEmpty()) { - state.targetPlayer = emailAddr; - state.type = - InputType.SET_EMAIL_VERIFY; - state.ticketTitle = - "need_pwd"; - p.sendMessage( - "§e您已绑定邮箱: " - + oldEmail); - p.sendMessage( - "§e请验证登录密码以确认修改:"); - return; - } - // 未绑定邮箱:直接发验证码 - String code = String.valueOf( - (int) (Math.random() - * 900000 - + 100000)); - state.targetPlayer = emailAddr; - state.ticketTitle = code; - state.type = - InputType.SET_EMAIL_VERIFY; - final String to = emailAddr; - final String c = code; - final Player fp = p; - Bukkit.getScheduler() - .runTaskAsynchronously( - mainPlugin, () -> { - boolean sent = mainPlugin - .getEmail() - .sendVerifyCode( - to, c); - Bukkit.getScheduler() - .runTask(mainPlugin, - () -> { - if (sent) { - fp.sendMessage( - "§a验证码已发送到 " - + to); - fp.sendMessage( - "§e输入6位验证码(0取消):"); - } else { - fp.sendMessage( - "§c发送失败"); - reset(fp); - } - }); - }); - return; - } - - - case SET_EMAIL_VERIFY: { - if ("0".equals(msg.trim()) - || "取消".equals( - msg.trim())) { - p.sendMessage( - "§c邮箱设置已取消"); - reset(p); - return; - } - String expected = - state.ticketTitle; - if (msg.trim().equals(expected)) { - db.setField(p.getName(), - "email", - state.targetPlayer); - p.sendMessage( - "§a邮箱已设置: " - + state.targetPlayer); - reset(p); - } else { - p.sendMessage( - "§c验证码错误(0取消):"); - } - return; - } - - case ADMIN_AUTH: { - if (msg.equals( - config.adminPassword)) { - Bukkit.getScheduler().runTask( - mainPlugin, () -> - mainPlugin - .getGui() - .openAdmin(p)); - } else { - p.sendMessage( - "§c管理密码错误"); - } - reset(p); - return; - } - - case ADMIN_SET_POINTS: { - try { - int pts = Integer.parseInt( - msg.trim()); - db.setField( - state.targetPlayer, - "points", pts); - p.sendMessage("§a已将 " - + state.targetPlayer - + " 积分设为 " + pts); - } catch ( - NumberFormatException e) { - p.sendMessage( - "§c请输入有效数字"); - } - String tgt = - state.targetPlayer; - Bukkit.getScheduler().runTask( - mainPlugin, () -> - mainPlugin - .getGui() - .openUserDetail( - p, tgt)); - reset(p); - return; - } - - case ADMIN_SET_PWD: { - if (!PasswordUtils.validate(msg)) { - p.sendMessage(config.msg( - "password_format_error")); - reset(p); - return; - } - String tgt = state.targetPlayer; - String salt = - PasswordUtils - .generateSalt(); - String hash = - PasswordUtils.hash( - msg, salt); - db.setField(tgt, - "temp_password", hash); - db.setField(tgt, "temp_pw_expire", - System.currentTimeMillis() - + 300000L); - db.setField(tgt, - "temp_pw_used", 0); - p.sendMessage("§a已为 " + tgt - + " 设置临时密码"); - Player tp = - Bukkit.getPlayer(tgt); - if (tp != null && tp.isOnline()) - tp.kickPlayer( - "§c管理员已设置临时密码," - + "请使用临时密码登录"); - // 邮件通知 - String emailAddr = (String) - db.getField(tgt, "email"); - if (emailAddr != null - && !emailAddr.isEmpty()) { - String time = - new java.text.SimpleDateFormat( - "yyyy-MM-dd HH:mm:ss") - .format(new java.util.Date()); - String body = "玩家 " + tgt - + ":\n您的密码已于 " - + time - + " 被管理员重置为临时密码。\n" - + "如果是本人操作请忽略。\n" - + "如非本人操作请立即联系管理员。"; - final String to = emailAddr; - final String fBody = body; - final String fName = tgt; - Bukkit.getScheduler() - .runTaskAsynchronously( - mainPlugin, () -> - mainPlugin.getEmail() - .sendTempPassword( - to, - fName, - fBody)); - } - reset(p); - return; - } - - case ADMIN_DELETE_CONFIRM: { - if (msg.equals( - state.targetPlayer)) { - db.deleteUser( - state.targetPlayer); - p.sendMessage("§a已删除 " - + state.targetPlayer); - Player tgt = - Bukkit.getPlayer( - state.targetPlayer); - if (tgt != null - && tgt.isOnline()) - tgt.kickPlayer( - "§c账号已被删除"); - } else { - p.sendMessage( - "§c输入不匹配,已取消"); - } - reset(p); - return; - } - - case ADMIN_SET_AFK_TIME: { - try { - int minutes = - Integer.parseInt( - msg.trim()); - config.afkTimeout = - minutes * 60; - config.saveSettings(); - p.sendMessage( - "§a挂机时长已设为 " - + minutes - + " 分钟"); - } catch ( - NumberFormatException e) { - p.sendMessage( - "§c请输入有效数字"); - } - reset(p); - return; - } - - case SMTP_CONFIG: { - String m = msg.trim(); - if ("000".equals(m) - || "取消取消取消" - .equals(m)) { - p.sendMessage( - "§cSMTP配置已取消"); - reset(p); - return; - } - if ("取消".equals(m)) { - p.sendMessage( - "§cSMTP配置已取消"); - reset(p); - return; - } - int step = state.smtpStep; - if (step <= 1) { - if (!"0".equals(m)) - config.setSmtp( - "smtp地址", m); - state.smtpStep = 2; - p.sendMessage("§e当前端口: " - + config.getSmtp( - "smtp端口")); - p.sendMessage( - "§e输入端口(0跳过):"); - } else if (step == 2) { - if (!"0".equals(m)) - config.setSmtp( - "smtp端口", m); - state.smtpStep = 3; - p.sendMessage( - "§e输入发件邮箱:"); - } else if (step == 3) { - if (!"0".equals(m)) - config.setSmtp( - "smtp账号", m); - state.smtpStep = 4; - p.sendMessage( - "§e输入SMTP密码(0跳过):"); - } else if (step == 4) { - if (!"0".equals(m)) - config.setSmtp( - "smtp密码", m); - state.smtpStep = 5; - p.sendMessage( - "§e输入接收邮箱(留空发到发件邮箱):"); - } else if (step == 5) { - String recv = m; - if (recv.isEmpty() - || "0".equals(recv)) - recv = config.getSmtp( - "smtp账号"); - config.setSmtp( - "验证码接收邮箱", recv); - if (!config - .canSendSmtpVerify()) { - p.sendMessage( - "§c发送间隔不足1分钟"); - reset(p); - return; - } - String code = config - .generateSmtpVerifyCode(); - String addr = config.getSmtp( - "smtp地址"); - String acct = config.getSmtp( - "smtp账号"); - if (acct.isEmpty() - || addr.isEmpty()) { - p.sendMessage( - "§c地址或账号为空"); - reset(p); - return; - } - config.recordSmtpSendTime(); - final String fRecv = recv; - final String fCode = code; - final Player fp = p; - Bukkit.getScheduler() - .runTaskAsynchronously( - mainPlugin, () -> { - Bukkit.getScheduler() - .runTask( - mainPlugin, - () -> fp.sendMessage( - "§e正在发送验证码...")); - boolean sent = - mainPlugin.getEmail() - .sendVerifyCode( - fRecv, - fCode); - final boolean fSent = sent; - Bukkit.getScheduler() - .runTask( - mainPlugin, - () -> { - if (fSent) { - state.type = - InputType - .SMTP_VERIFY; - state.smtpStep = 0; - fp.sendMessage( - "§a验证码已发送到 " - + fRecv); - fp.sendMessage( - "§e输入6位验证码(0取消):"); - } else { - fp.sendMessage( - "§c发送失败,查看控制台"); - reset(fp); - } - }); - }); - } - return; - } - - case SMTP_VERIFY: { - if ("0".equals(msg.trim()) - || "取消".equals( - msg.trim())) { - p.sendMessage( - "§cSMTP配置已取消"); - reset(p); - return; - } - if (config.checkSmtpVerifyCode( - msg.trim())) { - config.saveSmtp(); - config.clearSmtpVerifyCode(); - String addr = config.getSmtp( - "smtp地址"); - String port = config.getSmtp( - "smtp端口"); - String acct = config.getSmtp( - "smtp账号"); - String recv = config.getSmtp( - "验证码接收邮箱"); - boolean ssl = config.smtpSsl; - p.sendMessage("§a§l=== SMTP配置完成 ==="); - p.sendMessage("§7地址: §f" + addr); - p.sendMessage("§7端口: §f" + port); - p.sendMessage("§7账号: §f" + acct); - p.sendMessage("§7接收: §f" + recv); - p.sendMessage("§7加密: §f" + ssl); - p.sendMessage("§a§l===================="); - reset(p); - } else { - p.sendMessage( - "§c验证码错误(0取消):"); - } - return; - } - - case BACK_CHECKIN: { - int cost = 20; - if (!mainPlugin.getDb() - .deductPoints( - p.getName(), cost)) { - p.sendMessage( - "§c积分不足,补签需 " - + cost + " 积分"); - reset(p); - return; - } - p.sendMessage( - "§a补签成功!扣除 " - + cost + " 积分。" - + "日期: " + msg); - reset(p); - return; - } - - case INVITE_INPUT_CODE: { - String code = msg.trim(); - String myName = p.getName(); - String myCode = - (String) db.getField( - myName, - "invite_code"); - if (code.isEmpty()) { - p.sendMessage( - "§c邀请码不能为空"); - reset(p); - return; - } - if (code.equals(myCode)) { - p.sendMessage( - "§c不能用自己邀请码"); - reset(p); - return; - } - boolean found = false; - for (Map u : - db.getAllUsers()) { - String uc = (String) - u.getOrDefault( - "invite_code", ""); - if (code.equals(uc)) { - String inv = (String) - u.get("player_name"); - db.setField(myName, - "invited_by", inv); - db.addPoints(inv, 50); - p.sendMessage( - "§a邀请成功!邀请人: " - + inv); - Player ip = - Bukkit.getPlayer(inv); - if (ip != null - && ip.isOnline()) - ip.sendMessage("§a" - + myName - + " 使用了您的邀请码"); - found = true; - break; - } - } - if (!found) - p.sendMessage( - "§c邀请码不存在"); - reset(p); - return; - } - - case TICKET_TITLE: { - state.ticketTitle = msg; - state.type = - InputType.TICKET_DESC; - p.sendMessage( - "§e请输入工单详细描述:"); - return; - } - - case TICKET_DESC: { - String tt = state.ticketType; - String tl = state.ticketTitle; - reset(p); - mainPlugin.getTicket() - .submitTicket( - p, tt, tl, msg); - return; - } - - case TICKET_SCORE: { - int tid = state.ticketId; - try { - int sc = Integer.parseInt( - msg.trim()); - reset(p); - mainPlugin.getTicket() - .confirmAndScore( - p, tid, sc); - } catch ( - NumberFormatException e) { - reset(p); - p.sendMessage( - "§c请输入数字1-5"); - } - return; - } - - case TICKET_REJECT: { - int tid = state.ticketId; - reset(p); - mainPlugin.getTicket() - .rejectTicket( - p, tid, msg); - return; - } - - case TICKET_ASSIGN: { - int tid = state.ticketId; - String[] parts = msg.trim() - .split("\\s+"); - String prov = parts[0]; - int price = 0; - if (parts.length >= 2) { - try { - price = Integer.parseInt( - parts[1]); - } catch ( - NumberFormatException e) { - p.sendMessage( - "§c价格格式错误"); - } - } - reset(p); - mainPlugin.getTicket() - .assignToProvider( - p, tid, prov, - price); - final int fTid = tid; - Bukkit.getScheduler().runTask( - mainPlugin, () -> - mainPlugin - .getTicket() - .openDetail( - p, fTid)); - return; - } - - case TICKET_INVITE_PROVIDER: { - String tgt = msg.trim(); - reset(p); - if (!db.userExists(tgt)) { - p.sendMessage( - "§c玩家不存在"); - return; - } - db.addServiceProvider( - tgt, "waiter"); - p.sendMessage("§a已邀请 " + tgt - + " 成为服务商"); - return; - } - - case TICKET_REMOVE_PROVIDER: { - String tgt = msg.trim(); - reset(p); - if (!db.isServiceProvider(tgt)) { - p.sendMessage("§c" + tgt - + " 不是服务商"); - return; - } - db.removeServiceProvider(tgt); - p.sendMessage("§a已移除 " + tgt); - return; - } - - case TICKET_COMPLETE: { - try { - int tid = Integer.parseInt( - msg.trim()); - mainPlugin.getTicket() - .completeAsProvider( - p, tid); - } catch ( - NumberFormatException e) { - p.sendMessage( - "§c请输入有效工单号"); - } - reset(p); - return; - } - - case TICKET_REPORT_ISSUE: { - String report = msg.trim(); - reset(p); - p.sendMessage("§e异常已上报"); - String tag = config.adminTag; - for (Player op : - Bukkit.getOnlinePlayers()) { - if (op.getScoreboardTags() - .contains(tag)) { - op.sendMessage( - "§c§l[工单异常] §f" - + p.getName() - + ": " + report); - } - } - return; - } - - case TICKET_REPLY: { - int replyTid = state.ticketId; - reset(p); - mainPlugin.getTicket() - .replyTicket( - p, replyTid, msg); - return; - } - - case TICKET_ADMIN_TYPE: { - state.ticketType = msg.trim(); - state.type = - InputType - .TICKET_ADMIN_TITLE; - p.sendMessage( - "§e请输入工单标题:"); - return; - } - - case TICKET_ADMIN_TITLE: { - state.ticketTitle = msg; - state.type = - InputType - .TICKET_ADMIN_DESC; - p.sendMessage( - "§e请输入工单描述:"); - return; - } - - case TICKET_ADMIN_DESC: { - state.targetPlayer = msg; - state.type = - InputType - .TICKET_ADMIN_PROVIDER; - p.sendMessage( - "§e输入服务商名和价格:"); - p.sendMessage( - "§7例: playerA 20"); - return; - } - - case TICKET_ADMIN_PROVIDER: { - String[] parts2 = msg.trim() - .split("\\s+"); - String prov2 = parts2[0]; - int price2 = 0; - if (parts2.length >= 2) { - try { - price2 = Integer.parseInt( - parts2[1]); - } catch ( - NumberFormatException e) { - } - } - String t2 = state.ticketType; - String ti2 = state.ticketTitle; - String d2 = state.targetPlayer; - reset(p); - mainPlugin.getTicket() - .adminCreateAndAssign( - p, t2, ti2, - d2, prov2, price2); - return; - } - - default: - reset(p); - return; - } - } -} +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; + +public class ChatInputManager { + + public enum InputType { + NONE, + CHANGE_PWD_STEP1, + CHANGE_PWD_STEP2, + SET_EMAIL, + SET_EMAIL_VERIFY, + ADMIN_AUTH, + ADMIN_SET_POINTS, + ADMIN_SET_PWD, + ADMIN_DELETE_CONFIRM, + ADMIN_SET_AFK_TIME, + SMTP_CONFIG, + SMTP_VERIFY, + BACK_CHECKIN, + INVITE_INPUT_CODE, + TICKET_TITLE, + TICKET_DESC, + TICKET_SCORE, + TICKET_REJECT, + TICKET_ASSIGN, + TICKET_INVITE_PROVIDER, + TICKET_REMOVE_PROVIDER, + TICKET_COMPLETE, + TICKET_REPORT_ISSUE, + TICKET_REPLY, + TICKET_ADMIN_TYPE, + TICKET_ADMIN_TITLE, + TICKET_ADMIN_DESC, + TICKET_ADMIN_PROVIDER + } + + public static class PlayerState { + public InputType type = InputType.NONE; + public String targetPlayer = ""; + public String ticketType = ""; + public String ticketTitle = ""; + public String ticketDesc = ""; + public int ticketId = 0; + public int smtpStep = 0; + public int scoreValue = 0; + } + + private final Map states = + new HashMap<>(); + private Main plugin; + + public PlayerState getState(Player p) { + return states.computeIfAbsent( + p.getName(), + k -> new PlayerState()); + } + + public void reset(Player p) { + states.remove(p.getName()); + } + + public void handleInput(Player p, String msg, + Main mainPlugin) { + if (this.plugin == null) + this.plugin = mainPlugin; + PlayerState state = getState(p); + ConfigManager config = + mainPlugin.getConfig2(); + DatabaseManager db = mainPlugin.getDb(); + + switch (state.type) { + + case CHANGE_PWD_STEP1: { + String name = p.getName(); + String salt = (String) db.getField( + name, "password_salt"); + String hash = + PasswordUtils.hash(msg, salt); + boolean mainOk = + db.checkPassword(name, hash); + if (mainOk) { + state.type = + InputType.CHANGE_PWD_STEP2; + state.ticketTitle = "main"; + p.sendMessage( + "§e请输入新密码:"); + return; + } + boolean tempOk = + db.checkPasswordOrTemp( + name, hash); + if (tempOk) { + state.type = + InputType.CHANGE_PWD_STEP2; + state.ticketTitle = "temp"; + p.sendMessage( + "§e请输入新密码:"); + return; + } + p.sendMessage(config.msg( + "password_wrong")); + return; + } + + case CHANGE_PWD_STEP2: { + String name = p.getName(); + if (!PasswordUtils.validate(msg)) { + p.sendMessage(config.msg( + "password_format_error")); + reset(p); + return; + } + String oldHash = + (String) db.getField(name, + "password_hash"); + String oldSalt = + (String) db.getField(name, + "password_salt"); + String newSalt = + PasswordUtils.generateSalt(); + String newHash = + PasswordUtils.hash( + msg, newSalt); + if (newHash.equals(oldHash)) { + p.sendMessage(config.msg( + "password_same")); + reset(p); + return; + } + mainPlugin.recordPasswordChange( + name, oldHash, oldSalt); + db.setField(name, + "password_hash", newHash); + db.setField(name, + "password_salt", newSalt); + mainPlugin + .getNeedsPasswordChange() + .remove(name); + p.sendMessage(config.msg( + "password_changed")); + reset(p); + return; + } + + case SET_EMAIL: { + if (!msg.contains("@") + || !msg.contains(".")) { + p.sendMessage(config.msg( + "email_format_error")); + reset(p); + return; + } + String oldEmail = (String) + db.getField(p.getName(), + "email"); + String emailAddr = msg.trim(); + // 已绑定邮箱:验证登录密码 + if (oldEmail != null + && !oldEmail.isEmpty()) { + state.targetPlayer = emailAddr; + state.type = + InputType.SET_EMAIL_VERIFY; + state.ticketTitle = + "need_pwd"; + p.sendMessage( + "§e您已绑定邮箱: " + + oldEmail); + p.sendMessage( + "§e请验证登录密码以确认修改:"); + return; + } + // 未绑定邮箱:直接发验证码 + String code = String.valueOf( + (int) (Math.random() + * 900000 + + 100000)); + state.targetPlayer = emailAddr; + state.ticketTitle = code; + state.type = + InputType.SET_EMAIL_VERIFY; + final String to = emailAddr; + final String c = code; + final Player fp = p; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> { + boolean sent = mainPlugin + .getEmail() + .sendVerifyCode( + to, c); + Bukkit.getScheduler() + .runTask(mainPlugin, + () -> { + if (sent) { + fp.sendMessage( + "§a验证码已发送到 " + + to); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败"); + reset(fp); + } + }); + }); + return; + } + + + case SET_EMAIL_VERIFY: { + if ("0".equals(msg.trim()) + || "取消".equals( + msg.trim())) { + p.sendMessage( + "§c邮箱设置已取消"); + reset(p); + return; + } + String expected = + state.ticketTitle; + if (msg.trim().equals(expected)) { + db.setField(p.getName(), + "email", + state.targetPlayer); + p.sendMessage( + "§a邮箱已设置: " + + state.targetPlayer); + reset(p); + } else { + p.sendMessage( + "§c验证码错误(0取消):"); + } + return; + } + + case ADMIN_AUTH: { + if (msg.equals( + config.adminPassword)) { + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getGui() + .openAdmin(p)); + } else { + p.sendMessage( + "§c管理密码错误"); + } + reset(p); + return; + } + + case ADMIN_SET_POINTS: { + try { + int pts = Integer.parseInt( + msg.trim()); + db.setField( + state.targetPlayer, + "points", pts); + p.sendMessage("§a已将 " + + state.targetPlayer + + " 积分设为 " + pts); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效数字"); + } + String tgt = + state.targetPlayer; + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getGui() + .openUserDetail( + p, tgt)); + reset(p); + return; + } + + case ADMIN_SET_PWD: { + if (!PasswordUtils.validate(msg)) { + p.sendMessage(config.msg( + "password_format_error")); + reset(p); + return; + } + String tgt = state.targetPlayer; + String salt = + PasswordUtils + .generateSalt(); + String hash = + PasswordUtils.hash( + msg, salt); + db.setField(tgt, + "temp_password", hash); + db.setField(tgt, "temp_pw_expire", + System.currentTimeMillis() + + 300000L); + db.setField(tgt, + "temp_pw_used", 0); + p.sendMessage("§a已为 " + tgt + + " 设置临时密码"); + Player tp = + Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + tp.kickPlayer( + "§c管理员已设置临时密码," + + "请使用临时密码登录"); + // 邮件通知 + String emailAddr = (String) + db.getField(tgt, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String time = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()); + String body = "玩家 " + tgt + + ":\n您的密码已于 " + + time + + " 被管理员重置为临时密码。\n" + + "如果是本人操作请忽略。\n" + + "如非本人操作请立即联系管理员。"; + final String to = emailAddr; + final String fBody = body; + final String fName = tgt; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> + mainPlugin.getEmail() + .sendTempPassword( + to, + fName, + fBody)); + } + reset(p); + return; + } + + case ADMIN_DELETE_CONFIRM: { + if (msg.equals( + state.targetPlayer)) { + db.deleteUser( + state.targetPlayer); + p.sendMessage("§a已删除 " + + state.targetPlayer); + Player tgt = + Bukkit.getPlayer( + state.targetPlayer); + if (tgt != null + && tgt.isOnline()) + tgt.kickPlayer( + "§c账号已被删除"); + } else { + p.sendMessage( + "§c输入不匹配,已取消"); + } + reset(p); + return; + } + + case ADMIN_SET_AFK_TIME: { + try { + int minutes = + Integer.parseInt( + msg.trim()); + config.afkTimeout = + minutes * 60; + config.saveSettings(); + p.sendMessage( + "§a挂机时长已设为 " + + minutes + + " 分钟"); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效数字"); + } + reset(p); + return; + } + + case SMTP_CONFIG: { + String m = msg.trim(); + if ("000".equals(m) + || "取消取消取消" + .equals(m)) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + if ("取消".equals(m)) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + int step = state.smtpStep; + if (step <= 1) { + if (!"0".equals(m)) + config.setSmtp( + "smtp地址", m); + state.smtpStep = 2; + p.sendMessage("§e当前端口: " + + config.getSmtp( + "smtp端口")); + p.sendMessage( + "§e输入端口(0跳过):"); + } else if (step == 2) { + if (!"0".equals(m)) + config.setSmtp( + "smtp端口", m); + state.smtpStep = 3; + p.sendMessage( + "§e输入发件邮箱:"); + } else if (step == 3) { + if (!"0".equals(m)) + config.setSmtp( + "smtp账号", m); + state.smtpStep = 4; + p.sendMessage( + "§e输入SMTP密码(0跳过):"); + } else if (step == 4) { + if (!"0".equals(m)) + config.setSmtp( + "smtp密码", m); + state.smtpStep = 5; + p.sendMessage( + "§e输入接收邮箱(留空发到发件邮箱):"); + } else if (step == 5) { + String recv = m; + if (recv.isEmpty() + || "0".equals(recv)) + recv = config.getSmtp( + "smtp账号"); + config.setSmtp( + "验证码接收邮箱", recv); + if (!config + .canSendSmtpVerify()) { + p.sendMessage( + "§c发送间隔不足1分钟"); + reset(p); + return; + } + String code = config + .generateSmtpVerifyCode(); + String addr = config.getSmtp( + "smtp地址"); + String acct = config.getSmtp( + "smtp账号"); + if (acct.isEmpty() + || addr.isEmpty()) { + p.sendMessage( + "§c地址或账号为空"); + reset(p); + return; + } + config.recordSmtpSendTime(); + final String fRecv = recv; + final String fCode = code; + final Player fp = p; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> { + Bukkit.getScheduler() + .runTask( + mainPlugin, + () -> fp.sendMessage( + "§e正在发送验证码...")); + boolean sent = + mainPlugin.getEmail() + .sendVerifyCode( + fRecv, + fCode); + final boolean fSent = sent; + Bukkit.getScheduler() + .runTask( + mainPlugin, + () -> { + if (fSent) { + state.type = + InputType + .SMTP_VERIFY; + state.smtpStep = 0; + fp.sendMessage( + "§a验证码已发送到 " + + fRecv); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败,查看控制台"); + reset(fp); + } + }); + }); + } + return; + } + + case SMTP_VERIFY: { + if ("0".equals(msg.trim()) + || "取消".equals( + msg.trim())) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + if (config.checkSmtpVerifyCode( + msg.trim())) { + config.saveSmtp(); + config.clearSmtpVerifyCode(); + String addr = config.getSmtp( + "smtp地址"); + String port = config.getSmtp( + "smtp端口"); + String acct = config.getSmtp( + "smtp账号"); + String recv = config.getSmtp( + "验证码接收邮箱"); + boolean ssl = config.smtpSsl; + p.sendMessage("§a§l=== SMTP配置完成 ==="); + p.sendMessage("§7地址: §f" + addr); + p.sendMessage("§7端口: §f" + port); + p.sendMessage("§7账号: §f" + acct); + p.sendMessage("§7接收: §f" + recv); + p.sendMessage("§7加密: §f" + ssl); + p.sendMessage("§a§l===================="); + reset(p); + } else { + p.sendMessage( + "§c验证码错误(0取消):"); + } + return; + } + + case BACK_CHECKIN: { + int cost = 20; + if (!mainPlugin.getDb() + .deductPoints( + p.getName(), cost)) { + p.sendMessage( + "§c积分不足,补签需 " + + cost + " 积分"); + reset(p); + return; + } + p.sendMessage( + "§a补签成功!扣除 " + + cost + " 积分。" + + "日期: " + msg); + reset(p); + return; + } + + case INVITE_INPUT_CODE: { + String code = msg.trim(); + String myName = p.getName(); + String myCode = + (String) db.getField( + myName, + "invite_code"); + if (code.isEmpty()) { + p.sendMessage( + "§c邀请码不能为空"); + reset(p); + return; + } + if (code.equals(myCode)) { + p.sendMessage( + "§c不能用自己邀请码"); + reset(p); + return; + } + boolean found = false; + for (Map u : + db.getAllUsers()) { + String uc = (String) + u.getOrDefault( + "invite_code", ""); + if (code.equals(uc)) { + String inv = (String) + u.get("player_name"); + db.setField(myName, + "invited_by", inv); + db.addPoints(inv, 50); + p.sendMessage( + "§a邀请成功!邀请人: " + + inv); + Player ip = + Bukkit.getPlayer(inv); + if (ip != null + && ip.isOnline()) + ip.sendMessage("§a" + + myName + + " 使用了您的邀请码"); + found = true; + break; + } + } + if (!found) + p.sendMessage( + "§c邀请码不存在"); + reset(p); + return; + } + + case TICKET_TITLE: { + state.ticketTitle = msg; + state.type = + InputType.TICKET_DESC; + p.sendMessage( + "§e请输入工单详细描述:"); + return; + } + + case TICKET_DESC: { + String tt = state.ticketType; + String tl = state.ticketTitle; + reset(p); + mainPlugin.getTicket() + .submitTicket( + p, tt, tl, msg); + return; + } + + case TICKET_SCORE: { + int tid = state.ticketId; + try { + int sc = Integer.parseInt( + msg.trim()); + reset(p); + mainPlugin.getTicket() + .confirmAndScore( + p, tid, sc); + } catch ( + NumberFormatException e) { + reset(p); + p.sendMessage( + "§c请输入数字1-5"); + } + return; + } + + case TICKET_REJECT: { + int tid = state.ticketId; + reset(p); + mainPlugin.getTicket() + .rejectTicket( + p, tid, msg); + return; + } + + case TICKET_ASSIGN: { + int tid = state.ticketId; + String[] parts = msg.trim() + .split("\\s+"); + String prov = parts[0]; + int price = 0; + if (parts.length >= 2) { + try { + price = Integer.parseInt( + parts[1]); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c价格格式错误"); + } + } + reset(p); + mainPlugin.getTicket() + .assignToProvider( + p, tid, prov, + price); + final int fTid = tid; + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getTicket() + .openDetail( + p, fTid)); + return; + } + + case TICKET_INVITE_PROVIDER: { + String tgt = msg.trim(); + reset(p); + if (!db.userExists(tgt)) { + p.sendMessage( + "§c玩家不存在"); + return; + } + db.addServiceProvider( + tgt, "waiter"); + p.sendMessage("§a已邀请 " + tgt + + " 成为服务商"); + return; + } + + case TICKET_REMOVE_PROVIDER: { + String tgt = msg.trim(); + reset(p); + if (!db.isServiceProvider(tgt)) { + p.sendMessage("§c" + tgt + + " 不是服务商"); + return; + } + db.removeServiceProvider(tgt); + p.sendMessage("§a已移除 " + tgt); + return; + } + + case TICKET_COMPLETE: { + try { + int tid = Integer.parseInt( + msg.trim()); + mainPlugin.getTicket() + .completeAsProvider( + p, tid); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效工单号"); + } + reset(p); + return; + } + + case TICKET_REPORT_ISSUE: { + String report = msg.trim(); + reset(p); + p.sendMessage("§e异常已上报"); + String tag = config.adminTag; + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.getScoreboardTags() + .contains(tag)) { + op.sendMessage( + "§c§l[工单异常] §f" + + p.getName() + + ": " + report); + } + } + return; + } + + case TICKET_REPLY: { + int replyTid = state.ticketId; + reset(p); + mainPlugin.getTicket() + .replyTicket( + p, replyTid, msg); + return; + } + + case TICKET_ADMIN_TYPE: { + state.ticketType = msg.trim(); + state.type = + InputType + .TICKET_ADMIN_TITLE; + p.sendMessage( + "§e请输入工单标题:"); + return; + } + + case TICKET_ADMIN_TITLE: { + state.ticketTitle = msg; + state.type = + InputType + .TICKET_ADMIN_DESC; + p.sendMessage( + "§e请输入工单描述:"); + return; + } + + case TICKET_ADMIN_DESC: { + state.targetPlayer = msg; + state.type = + InputType + .TICKET_ADMIN_PROVIDER; + p.sendMessage( + "§e输入服务商名和价格:"); + p.sendMessage( + "§7例: playerA 20"); + return; + } + + case TICKET_ADMIN_PROVIDER: { + String[] parts2 = msg.trim() + .split("\\s+"); + String prov2 = parts2[0]; + int price2 = 0; + if (parts2.length >= 2) { + try { + price2 = Integer.parseInt( + parts2[1]); + } catch ( + NumberFormatException e) { + } + } + String t2 = state.ticketType; + String ti2 = state.ticketTitle; + String d2 = state.targetPlayer; + reset(p); + mainPlugin.getTicket() + .adminCreateAndAssign( + p, t2, ti2, + d2, prov2, price2); + return; + } + + default: + reset(p); + } + } +} diff --git a/src/main/java/Sdf1_login/CommissionManager.java b/src/main/java/Sdf1_login/CommissionManager.java new file mode 100644 index 0000000..cb24687 --- /dev/null +++ b/src/main/java/Sdf1_login/CommissionManager.java @@ -0,0 +1,227 @@ +package Sdf1_login; + +import org.bukkit.entity.Player; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.logging.Logger; + +public class CommissionManager { + + private final Main main; + private final Logger logger; + private final List rules = + new ArrayList<>(); + + public CommissionManager(Main main) { + this.main = main; + this.logger = main.getLogger(); + loadRules(); + } + + public void loadRules() { + rules.clear(); + File f = new File( + main.getDataFolder(), + "提成规则.txt"); + if (!f.exists()) { + writeDefault(f); + } + try (BufferedReader br = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets + .UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() + || line.startsWith("#")) + continue; + // 格式: 邀请人数:提成百分比 + // 如 "5:15" = 邀请5人提成15% + String[] parts = + line.split(":"); + if (parts.length >= 2) { + try { + int count = Integer.parseInt( + parts[0].trim()); + double rate = Double + .parseDouble( + parts[1] + .trim()) / 100.0; + rules.add(new double[]{ + count, rate}); + } catch (Exception ignored) { + } + } + } + // 按人数排序(从大到小) + rules.sort((a, b) -> + Double.compare(b[0], a[0])); + logger.info( + "[Commission] 加载 " + + rules.size() + " 条提成规则"); + } catch (Exception e) { + logger.warning( + "[Commission] 加载失败: " + + e.getMessage()); + } + } + + private void writeDefault(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# 邀请提成规则"); + pw.println("# 格式: 邀请人数:提成百分比"); + pw.println("# 邀请10人提成20%"); + pw.println("10:20"); + pw.println("# 邀请5人提成15%"); + pw.println("5:15"); + pw.println("# 邀请1人提成10%"); + pw.println("1:10"); + } catch (IOException ignored) { + } + } + + /** + * 获取邀请人的提成比例 + * 根据已邀请人数匹配最高规则 + */ + public double getCommissionRate( + String inviter) { + int inviteCount = getInviteCount( + inviter); + for (double[] rule : rules) { + if (inviteCount >= (int) rule[0]) { + return rule[1]; + } + } + return 0; + } + + /** + * 获取邀请人数 + */ + public int getInviteCount( + String inviter) { + try { + Object val = + main.getDb().getField( + inviter, + "monthly_invite_count"); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + Object total = + main.getDb().getField( + inviter, + "total_invite_count"); + if (total instanceof Number) { + return ((Number) total).intValue(); + } + } catch (Exception ignored) { + } + return 0; + } + + /** + * 发放提成 + * 被邀请人获得奖励时,邀请人获得提成 + */ + public void payCommission( + String invitee, + double originalAmount) { + // 查找邀请人 + String inviter = getInviter(invitee); + if (inviter == null + || inviter.isEmpty()) return; + + double rate = getCommissionRate( + inviter); + if (rate <= 0) return; + + double commission = + originalAmount * rate; + if (commission <= 0) return; + + // 通过 Vault 发放 + var rsp = main.getServer() + .getServicesManager() + .getRegistration( + net.milkbowl.vault + .economy.Economy.class); + if (rsp == null) return; + net.milkbowl.vault.economy.Economy econ = + rsp.getProvider(); + if (econ == null) return; + + econ.depositPlayer(inviter, commission); + + // 通知邀请人 + Player inviterP = + main.getServer() + .getPlayer(inviter); + if (inviterP != null + && inviterP.isOnline()) { + inviterP.sendMessage( + "§a§l[邀请提成] §f" + + invitee + " 完成任务," + + "你获得 §e" + + String.format("%.2f", + commission) + + " §f游戏币提成 (" + + (int)(rate * 100) + "%)"); + } + + logger.info( + "[Commission] 提成: " + + inviter + " ← " + + invitee + " | " + + String.format("%.2f", + originalAmount) + + " × " + + (int)(rate * 100) + "% = " + + String.format("%.2f", + commission)); + } + + /** + * 获取邀请人 + */ + private String getInviter(String player) { + try { + Object val = + main.getDb().getField( + player, + "invited_by"); + if (val != null + && !val.toString() + .isEmpty()) { + return val.toString(); + } + } catch (Exception ignored) { + } + return null; + } + + /** + * 判断金额是否包含经济内容 + */ + public boolean hasEconomyContent( + String text) { + String t = text.toLowerCase(); + return t.contains("游戏币") + || t.contains("金币") + || t.contains("积分") + || t.contains("块钱") + || t.contains("元") + || t.contains("$") + || t.contains("经济") + || t.contains("余额"); + } +} diff --git a/src/main/java/Sdf1_login/ConfigManager.java b/src/main/java/Sdf1_login/ConfigManager.java index a22ac57..f91a00d 100644 --- a/src/main/java/Sdf1_login/ConfigManager.java +++ b/src/main/java/Sdf1_login/ConfigManager.java @@ -25,6 +25,7 @@ public class ConfigManager { public long smtpVerifyExpire = 0; public long smtpLastSendTime = 0; public String smtpConfiguringPlayer = ""; + public String updatePlatform = "gh"; public ConfigManager(File dataFolder) { diff --git a/src/main/java/Sdf1_login/DatabaseManager.java b/src/main/java/Sdf1_login/DatabaseManager.java index 95102f5..d3816a3 100644 --- a/src/main/java/Sdf1_login/DatabaseManager.java +++ b/src/main/java/Sdf1_login/DatabaseManager.java @@ -213,6 +213,18 @@ public class DatabaseManager { + ")"); safeAdd(st, "menu_snowball", "INTEGER DEFAULT 1"); + // ===== 独立任务进度表 ===== + st.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + + "quest_progress (" + + "player_name TEXT NOT NULL, " + + "condition_key TEXT NOT NULL, " + + "progress_value INTEGER DEFAULT 0, " + + "completed INTEGER DEFAULT 0, " + + "PRIMARY KEY (player_name, " + + "condition_key)" + + ")"); + st.close(); logger.info("[Sdf1_login] 数据库初始化完成"); } catch (Exception e) { @@ -861,9 +873,8 @@ public class DatabaseManager { return list; } - - /** - * 获取指定ID的备份数据 + /* + 获取指定ID的备份数据 */ public String[] getInventoryBackupById( int backupId) { diff --git a/src/main/java/Sdf1_login/GUIManager.java b/src/main/java/Sdf1_login/GUIManager.java index 02230d6..6c60ac6 100644 --- a/src/main/java/Sdf1_login/GUIManager.java +++ b/src/main/java/Sdf1_login/GUIManager.java @@ -5,7 +5,6 @@ import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.*; import org.bukkit.inventory.meta.ItemMeta; -import java.io.File; import java.util.*; @@ -42,7 +41,7 @@ public class GUIManager { g.setItem(14, mkItem(Material.EMERALD, "§d§l积分商城")); g.setItem(16, mkItem(Material.CHEST, - "§e§l新人礼包")); + "§e§l任务中心")); g.setItem(18, mkItem(Material.BOOK, "§6工单系统", "§7提交bug、求助、举报")); @@ -371,6 +370,7 @@ public class GUIManager { "§7双击管理")); slot++; } + g.setItem(53, mkItem(Material.ARROW, "§7返回")); p.openInventory(g); } @@ -379,90 +379,279 @@ public class GUIManager { Inventory g = Bukkit.createInventory( null, 27, T_TASK_CENTER); fillBg(g); + + QuestTracker qt = + plugin.getQuestTracker(); + String player = p.getName(); + + // 新人任务进度 + List newQuests = + qt.getQuests("新人任务"); + int newTotal = newQuests.size(); + int newDone = + qt.getCompletedStageCount( + player, newQuests); + QuestTracker.QuestFile newCurrent = + qt.getCurrentStage( + player, newQuests); + + String newInfo = newDone + "/" + newTotal; + List newLore = new ArrayList<>(); + newLore.add("§7进度: §e" + newInfo); + if (newCurrent != null) { + newLore.add("§7当前: §f" + + newCurrent.displayName); + if (newCurrent.hasConditions()) { + List unmet = + qt.getUnmetConditions( + player, + newCurrent.conditions); + if (!unmet.isEmpty()) { + for (String c : unmet) { + newLore.add("§7 " + + qt.getProgress( + player, c)); + } + } else { + newLore.add("§a✓ 条件已满足"); + } + } + } else { + newLore.add("§a§l全部完成!"); + } + g.setItem(10, mkItem(Material.BOOK, - "§e§l新人任务", - "§7完成新手阶段获取奖励")); + "§e§l新人任务 §7[" + + newInfo + "]", + newLore.toArray(new String[0]))); + + // 主线任务 + List mainQuests = + qt.getQuests("主线任务"); g.setItem(12, mkItem(Material.COMPASS, - "§b§l主线任务", + "§b§l主线任务 §7[" + + qt.getCompletedStageCount( + player, mainQuests) + + "/" + mainQuests.size() + + "]", "§7查看主线任务列表")); + + // 支线任务 + List sideQuests = + qt.getQuests("支线任务"); g.setItem(14, mkItem(Material.PAPER, - "§a§l支线任务", + "§a§l支线任务 §7[" + + qt.getCompletedStageCount( + player, sideQuests) + + "/" + sideQuests.size() + + "]", "§7查看支线任务列表")); + g.setItem(16, mkItem(Material.EMERALD, "§d§l每日签到", "§7签到获取积分")); - g.setItem(22, mkItem(Material.ARROW, "§7返回")); - p.openInventory(g); - } - public void openGiftStages(Player p) { - Inventory g = Bukkit.createInventory( - null, 27, T_GIFT_STAGES); - fillBg(g); - int max = plugin.getGift().getMaxStage(); - for (int i = 0; i < max && i < 9; i++) { - int stage = i + 1; - boolean claimed = plugin.getDb() - .getField(p.getName(), - "gift_claimed") != null - && ((String) plugin.getDb() - .getField(p.getName(), - "gift_claimed")) - .contains("[" + stage + "]"); - boolean can = plugin.getGift() - .canClaim(p, stage); - Material mat = claimed ? Material.LIME_WOOL - : (can ? Material.YELLOW_WOOL - : Material.RED_WOOL); - String status = claimed ? "§a已领取" - : (can ? "§e可领取" : "§c未达标"); - g.setItem(10 + i, mkItem(mat, - "§e第" + stage + "阶段", - "§7" + status)); - } - g.setItem(26, mkItem(Material.ARROW, "§7返回")); + + // 打卡上报按钮(slot 21) + g.setItem(21, mkItem( + Material.EMERALD, + "§a§l打卡上报", + "§7验证当前阶段任务完成情况", + "§7满足条件将自动标记完成")); + + // 返回按钮(slot 26) + g.setItem(26, mkItem( + Material.ARROW, "§7返回")); p.openInventory(g); } - public void openTaskList(Player p, String folderName) { - File folder = new File( - plugin.getDataFolder(), folderName); - List> tasks = - plugin.getGift() - .loadTasksFromFolder(folder); - int size = Math.max(54, - ((tasks.size() / 9) + 1) * 9); - if (size > 54) size = 54; - Inventory g = Bukkit.createInventory(null, size, - "§d§l" + folderName); + + public void openGiftStages(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_GIFT_STAGES); fillBg(g); - for (int i = 0; i < tasks.size() - && i < 45; i++) { - Map task = tasks.get(i); - String name = (String) task.get("name"); - List conds = - (List) task.get("conditions"); - List rewards = - (List) task.get("rewards"); + + QuestTracker qt = + plugin.getQuestTracker(); + List quests = + qt.getQuests("新人任务"); + int total = quests.size(); + int completed = + qt.getCompletedStageCount( + p.getName(), quests); + QuestTracker.QuestFile current = + qt.getCurrentStage( + p.getName(), quests); + + g.setItem(4, mkItem(Material.BOOK, + "§e§l新人任务进度", + "§7已完成: §e" + completed + + "/" + total, + current != null + ? "§7当前: §f" + + current.displayName + : "§a§l全部完成!")); + + for (int i = 0; + i < quests.size() && i < 44; i++) { + QuestTracker.QuestFile qf = + quests.get(i); + boolean done = + qt.isStageCompleted( + p.getName(), qf); + boolean isCurrent = + (current != null + && current == qf); + + Material mat; List lore = new ArrayList<>(); - lore.add("§7───── 条件 ─────"); - if (conds != null) - for (String c : conds) - lore.add("§e" + c); - lore.add(""); - lore.add("§7───── 奖励 ─────"); - if (rewards != null) - for (String r2 : rewards) - lore.add("§a" + r2); - g.setItem(i, mkItem(Material.BOOK, - "§e" + name, + + if (done) { + mat = Material.LIME_WOOL; + } else if (isCurrent) { + mat = Material.YELLOW_WOOL; + } else { + mat = Material.RED_WOOL; + } + + if (!qf.details.isEmpty()) { + lore.add("§7---- 任务详情 ----"); + for (String d : qf.details) { + lore.add("§f" + d); + } + } + + if (qf.hasConditions() + && (done || isCurrent)) { + lore.add(""); + for (String c : qf.conditions) { + if (qt.isOptional(c)) + continue; + if (qt.isConditionMet( + p.getName(), c)) { + lore.add("§7 §a✓ " + + qt.getProgress( + p.getName(), c)); + } else { + lore.add("§7 §c✗ " + + qt.getProgress( + p.getName(), c)); + } + } + } + + if (!qf.rewards.isEmpty()) { + lore.add(""); + lore.add("§7---- 奖励 ----"); + for (String r : qf.rewards) { + lore.add("§a" + r); + } + } + + if (done + && !qf.rewards.isEmpty() + && !qt.hasClaimed( + p.getName(), qf)) { + lore.add(""); + lore.add("§e§l点击领取奖励"); + } + if (done && qt.hasClaimed( + p.getName(), qf)) { + lore.add(""); + lore.add("§7已领取奖励"); + } + + g.setItem(10 + i, mkItem(mat, + "§e" + qf.displayName, lore.toArray(new String[0]))); } + // 返回按钮 + g.setItem(53, mkItem( + Material.ARROW, "§7返回")); + p.openInventory(g); + } + + + + + public void openTaskList(Player p, + String folderName) { + QuestTracker qt = + plugin.getQuestTracker(); + List tasks = + qt.getQuests(folderName); + + int size = Math.min(54, + Math.max(54, + ((tasks.size() / 9) + 1) * 9)); + Inventory g = Bukkit.createInventory( + null, size, + "§d§l" + folderName); + fillBg(g); + + for (int i = 0; + i < tasks.size() + && i < 45; i++) { + QuestTracker.QuestFile qf = + tasks.get(i); + boolean done = + qt.isStageCompleted( + p.getName(), qf); + Material mat = done + ? Material.LIME_WOOL + : Material.BOOK; + + List lore = new ArrayList<>(); + + // 解锁条件 + if (qf.hasConditions()) { + lore.add("§7──── 解锁条件 ────"); + for (String c : qf.conditions) { + if (qt.isOptional(c)) + continue; + if (qt.isConditionMet( + p.getName(), c)) { + lore.add("§a ✓ " + + qt.getProgress( + p.getName(), c)); + } else { + lore.add("§c ✗ " + + qt.getProgress( + p.getName(), c)); + } + } + } + + // 任务详情 + if (!qf.details.isEmpty()) { + lore.add(""); + lore.add("§7──── 任务详情 ────"); + for (String d : qf.details) { + lore.add("§f" + d); + } + } + + // 任务奖励 + if (!qf.rewards.isEmpty()) { + lore.add(""); + lore.add("§7──── 任务奖励 ────"); + for (String r : qf.rewards) { + lore.add("§a" + r); + } + } + + g.setItem(i, mkItem(mat, + (done ? "§a✓ " : "§e") + + qf.displayName, + lore.toArray(new String[0]))); + } + if (tasks.isEmpty()) { - g.setItem(22, mkItem(Material.BARRIER, + g.setItem(22, mkItem( + Material.BARRIER, "§7暂无任务")); } - g.setItem(size - 1, - mkItem(Material.ARROW, "§7返回")); + g.setItem(size - 1, mkItem( + Material.ARROW, "§7返回")); p.openInventory(g); } @@ -494,11 +683,81 @@ public class GUIManager { Material.SNOWBALL, "§e§l雪球菜单", "§7点击为该玩家发放菜单雪球")); + g.setItem(16, mkItem( + Material.ENDER_EYE, + "§b§l打卡记录", + "§7查看该玩家的群系打卡记录")); g.setItem(22, mkItem(Material.ARROW, "§7返回")); p.openInventory(g); } + public void openCheckInRecords( + Player p, String target) { + Inventory g = Bukkit.createInventory( + null, 54, + "§b§l打卡记录: " + target); + fillBg(g); + + QuestTracker qt = + plugin.getQuestTracker(); + List records = + qt.getPlayerRecords(target); + + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss"); + + int slot = 0; + for (int i = records.size() - 1; + i >= 0 && slot < 45; i--) { + String r = records.get(i); + String[] parts = r.split("\\|"); + if (parts.length < 7) continue; + + String world = parts[1]; + String biome = parts[2]; + String category = parts[3]; + String x = parts[4]; + String y = parts[5]; + String z = parts[6]; + String timeStr = ""; + if (parts.length >= 8) { + try { + long ts = Long.parseLong( + parts[7]); + timeStr = sdf.format( + new java.util.Date( + ts)); + } catch (Exception e) { + timeStr = parts[7]; + } + } + + g.setItem(slot, mkItem( + Material.PAPER, + "§e" + category + " - " + + biome, + "§7时间: " + timeStr, + "§7世界: " + world, + "§7坐标: " + x + + ", " + y + + ", " + z)); + slot++; + } + + if (slot == 0) { + g.setItem(22, mkItem( + Material.BARRIER, + "§7暂无打卡记录")); + } + + g.setItem(53, mkItem( + Material.ARROW, "§7返回")); + p.openInventory(g); + } + + /** * 打开账号请求面板 * 玩家通过 /oa 命令触发 diff --git a/src/main/java/Sdf1_login/InviteManager.java b/src/main/java/Sdf1_login/InviteManager.java index 23f80d2..67c52a0 100644 --- a/src/main/java/Sdf1_login/InviteManager.java +++ b/src/main/java/Sdf1_login/InviteManager.java @@ -1,96 +1,89 @@ -package Sdf1_login; - -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import java.text.SimpleDateFormat; -import java.util.*; - -public class InviteManager { - - private final Main plugin; - - public InviteManager(Main plugin) { - this.plugin = plugin; - } - - public String generateCode(Player p) { - String code = PasswordUtils.generateInviteCode(); - plugin.getDb().setField( - p.getName(), "invite_code", code); - return code; - } - - public void onInviteeCheckIn(String inviteeName) { - String invitedBy = (String) plugin.getDb() - .getField(inviteeName, "invited_by"); - if (invitedBy == null || invitedBy.isEmpty()) - return; - - String month = new SimpleDateFormat("yyyy-MM") - .format(new Date()); - String lastMonth = (String) plugin.getDb() - .getField(invitedBy, "invite_month"); - - if (!month.equals(lastMonth)) { - plugin.getDb().setField( - invitedBy, "invite_month", month); - plugin.getDb().setField( - invitedBy, "monthly_invite_count", 0); - } - - int count = ((Number) plugin.getDb() - .getField(invitedBy, - "monthly_invite_count")) - .intValue(); - plugin.getDb().setField( - invitedBy, "monthly_invite_count", - count + 1); - - double rate = 0.10; - if (count + 1 > 15) { - rate = 0.60; - } else if (count + 1 > 10) { - rate = 0.35; - } else if (count + 1 > 5) { - rate = 0.15; - } - - int bonus = (int) (1 * rate); - if (bonus > 0) { - plugin.getDb().addPoints(invitedBy, bonus); - Player inviterPlayer = Bukkit - .getPlayerExact(invitedBy); - if (inviterPlayer != null - && inviterPlayer.isOnline()) { - inviterPlayer.sendMessage( - plugin.getConfig2() - .msg("invite_referral_bonus") - .replace("{points}", - String.valueOf(bonus))); - } - } - } - - public List> getAllInviteData() { - List> result = - new ArrayList<>(); - for (Map user - : plugin.getDb().getAllUsers()) { - String code = (String) user.getOrDefault( - "invite_code", ""); - if (!code.isEmpty()) { - Map row = - new LinkedHashMap<>(); - row.put("player", - user.get("player_name")); - row.put("code", code); - row.put("monthly", - user.getOrDefault( - "monthly_invite_count", 0)); - result.add(row); - } - } - return result; - } -} +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.text.SimpleDateFormat; +import java.util.*; + +public class InviteManager { + + private final Main plugin; + + public InviteManager(Main plugin) { + this.plugin = plugin; + } + + public String generateCode(Player p) { + String code = PasswordUtils.generateInviteCode(); + plugin.getDb().setField( + p.getName(), "invite_code", code); + return code; + } + + public void onInviteeCheckIn(String inviteeName) { + String invitedBy = (String) plugin.getDb() + .getField(inviteeName, "invited_by"); + if (invitedBy == null || invitedBy.isEmpty()) + return; + + String month = new SimpleDateFormat("yyyy-MM") + .format(new Date()); + String lastMonth = (String) plugin.getDb() + .getField(invitedBy, "invite_month"); + + if (!month.equals(lastMonth)) { + plugin.getDb().setField( + invitedBy, "invite_month", month); + plugin.getDb().setField( + invitedBy, "monthly_invite_count", 0); + } + + int count = ((Number) plugin.getDb() + .getField(invitedBy, + "monthly_invite_count")) + .intValue(); + plugin.getDb().setField( + invitedBy, "monthly_invite_count", + count + 1); + + double rate = plugin.getCommission() + .getCommissionRate(invitedBy); + int bonus = (int) (1 * rate); + if (bonus > 0) { + plugin.getDb().addPoints(invitedBy, bonus); + Player inviterPlayer = Bukkit + .getPlayerExact(invitedBy); + if (inviterPlayer != null + && inviterPlayer.isOnline()) { + inviterPlayer.sendMessage( + plugin.getConfig2() + .msg("invite_referral_bonus") + .replace("{points}", + String.valueOf(bonus))); + } + } + } + + public List> getAllInviteData() { + List> result = + new ArrayList<>(); + for (Map user + : plugin.getDb().getAllUsers()) { + String code = (String) user.getOrDefault( + "invite_code", ""); + if (!code.isEmpty()) { + Map row = + new LinkedHashMap<>(); + row.put("player", + user.get("player_name")); + row.put("code", code); + row.put("monthly", + user.getOrDefault( + "monthly_invite_count", 0)); + result.add(row); + } + } + return result; + } +} diff --git a/src/main/java/Sdf1_login/LoginManager.java b/src/main/java/Sdf1_login/LoginManager.java index 4946c11..a32263a 100644 --- a/src/main/java/Sdf1_login/LoginManager.java +++ b/src/main/java/Sdf1_login/LoginManager.java @@ -1,302 +1,302 @@ -package Sdf1_login; - -import org.bukkit.Bukkit; -import org.bukkit.Sound; -import org.bukkit.entity.Player; - -public class LoginManager { - - private final Main plugin; - - public LoginManager(Main plugin) { - this.plugin = plugin; - } - - public boolean handleRegister(Player p, - String[] args) { - String name = p.getName(); - - // 已注册玩家不能再注册 - if (plugin.getDb().userExists(name)) { - p.sendMessage(plugin.getConfig2() - .msg("already_registered")); - return true; - } - - // IP注册限制检查 - String ip = plugin.getPlayerIP(p); - if (ip != null && plugin.getConfig2() - .maxAccountsPerIP > 0) { - if (!plugin.getIPGroup() - .canRegister(ip)) { - int count = plugin.getIPGroup() - .getAccountCount(ip); - int max = plugin.getConfig2() - .maxAccountsPerIP; - p.sendMessage("§c该IP注册已达上限(" - + count + "/" + max + ")"); - return true; - } - } - - if (args.length < 1) { - p.sendMessage("§c用法: /reg <密码>"); - p.sendMessage("§7密码要求: 6位以上," - + "包含大小写字母和数字"); - return true; - } - - String pwd = args[0]; - if (pwd.length() < 6) { - p.sendMessage("§c密码长度不足6位"); - return true; - } - if (!pwd.matches(".*[a-z].*") - || !pwd.matches(".*[A-Z].*") - || !pwd.matches(".*[0-9].*")) { - p.sendMessage("§c密码必须包含大写字母、" - + "小写字母和数字"); - return true; - } - - String salt = - PasswordUtils.generateSalt(); - String hash = - PasswordUtils.hash(pwd, salt); - plugin.getDb().createUser( - name, hash, salt); - plugin.getLoggedIn().add(name); - plugin.getDb().setLoggedIn(name, true); - plugin.getDb().setField(name, - "last_login_time", - System.currentTimeMillis()); - plugin.getDb().setField(name, - "last_online_check", - System.currentTimeMillis()); - plugin.getDb().recordIP(name, ip); - plugin.getDb().setField(name, - "register_time", - System.currentTimeMillis()); - plugin.restoreInventory(p); - plugin.recordIPLogin(p); - plugin.giveMenuSnowball(p); - - p.sendMessage(plugin.getConfig2() - .msg("reg_success")); - playRegisterSound(p); - - try { - org.bukkit.plugin.Plugin cy = - Bukkit.getPluginManager() - .getPlugin("CY_beibao"); - if (cy != null && cy.isEnabled()) { - cy.getClass().getMethod( - "onSdf1Activation", - String.class, - int.class, int.class) - .invoke(cy, name, 9, 0); - } - } catch (Exception ignored) { - } - return true; - } - - private void playRegisterSound(Player p) { - p.playSound(p.getLocation(), - Sound.BLOCK_NOTE_BLOCK_CHIME, - 1.0f, 0.79f); - p.playSound(p.getLocation(), - Sound.BLOCK_NOTE_BLOCK_BASS, - 1.0f, 0.79f); - p.playSound(p.getLocation(), - Sound.BLOCK_NOTE_BLOCK_PLING, - 1.0f, 0.79f); - } - - - public boolean handleLogin(Player p, - String[] args) { - String name = p.getName(); - try { - if (plugin.getLoggedIn().contains(name)) { - p.sendMessage(plugin.getConfig2() - .msg("already_logged_in")); - return true; - } - if (!plugin.getDb().userExists(name)) { - p.sendMessage(plugin.getConfig2() - .msg("not_registered")); - return true; - } - if (args.length < 1) { - p.sendMessage("§c用法: /login <密码>"); - return true; - } - String pwd = args[0]; - String salt = (String) plugin.getDb() - .getField(name, "password_salt"); - if (salt == null) { - p.sendMessage( - "§c数据异常,请联系管理员"); - plugin.getLogger().severe( - "[Sdf1_login] 玩家 " + name - + " 盐值为null"); - return true; - } - String hash = - PasswordUtils.hash(pwd, salt); - - boolean matchMain = - plugin.getDb().checkPassword( - name, hash); - if (matchMain) { - plugin.getLoggedIn().add(name); - plugin.getDb().setLoggedIn( - name, true); - plugin.getDb().setField(name, - "last_login_time", - System.currentTimeMillis()); - plugin.getDb().setField(name, - "last_online_check", - System.currentTimeMillis()); - // 只在有有效备份时恢复 - plugin.restoreInventory(p); - plugin.recordIPLogin(p); - plugin.giveMenuSnowball(p); - p.sendMessage(plugin.getConfig2() - .msg("login_success")); - activateCY(p, name); - return true; - } - - boolean matchTemp = - plugin.getDb() - .checkPasswordOrTemp( - name, hash); - if (matchTemp) { - plugin.getLoggedIn().add(name); - plugin.getDb().setLoggedIn( - name, true); - plugin.getDb().setField(name, - "last_login_time", - System.currentTimeMillis()); - plugin.getDb().setField(name, - "last_online_check", - System.currentTimeMillis()); - plugin.restoreInventory(p); - plugin.recordIPLogin(p); - plugin.giveMenuSnowball(p); - plugin.getNeedsPasswordChange() - .add(name); - p.sendMessage(plugin.getConfig2() - .msg("login_success")); - p.sendMessage( - "§c§l[警告] §f您使用的是临时密码,请尽快修改密码!"); - p.sendMessage( - "§7用法: /sdf1_login pw"); - activateCY(p, name); - return true; - } - - - p.sendMessage(plugin.getConfig2() - .msg("login_failed")); - } catch (Exception e) { - plugin.getLogger().severe( - "[Sdf1_login] handleLogin异常: " - + e.getMessage()); - e.printStackTrace(); - p.sendMessage( - "§c登录过程出错,请联系管理员"); - } - return true; - } - - - public void handleReset(Player p) { - String name = p.getName(); - String emailAddr = (String) - plugin.getDb().getField( - name, "email"); - if (emailAddr == null - || emailAddr.isEmpty()) { - p.sendMessage( - "§c未设置邮箱,无法重置密码"); - return; - } - String tempPwd = PasswordUtils - .generateTempPassword(); - String salt = (String) - plugin.getDb().getField( - name, "password_salt"); - String hash = PasswordUtils.hash( - tempPwd, salt); - plugin.getDb().setField(name, - "temp_password", hash); - plugin.getDb().setField(name, - "temp_pw_expire", - System.currentTimeMillis() - + 300000L); - - plugin.getDb().setField(name, - "temp_pw_used", 0); - p.sendMessage("§e正在发送临时密码..."); - final String to = emailAddr; - final String fName = name; - final String pwd = tempPwd; - final Main self = plugin; - Bukkit.getScheduler() - .runTaskAsynchronously(self, - () -> { - final boolean[] sent = {false}; - Thread t = new Thread(() -> { - sent[0] = self.getEmail() - .sendTempPassword( - to, fName, pwd); - }, "Sdf1_login-reset"); - t.setDaemon(true); - t.start(); - try { - t.join(15000); - } catch ( - InterruptedException ignored) { - } - if (t.isAlive()) { - t.interrupt(); - Bukkit.getScheduler().runTask( - self, () -> - p.sendMessage( - "§c邮件发送超时")); - return; - } - Bukkit.getScheduler().runTask( - self, () -> { - if (sent[0]) { - p.sendMessage( - "§a临时密码已发送到 " - + to); - } else { - p.sendMessage( - "§c邮件发送失败"); - } - }); - }); - } - - private void activateCY(Player p, - String name) { - try { - org.bukkit.plugin.Plugin cy = - Bukkit.getPluginManager() - .getPlugin("CY_beibao"); - if (cy != null && cy.isEnabled()) { - cy.getClass().getMethod( - "onSdf1Activation", - String.class, - int.class, int.class) - .invoke(cy, name, 0, 0); - } - } catch (Exception ignored) { - } - } -} +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.entity.Player; + +public class LoginManager { + + private final Main plugin; + + public LoginManager(Main plugin) { + this.plugin = plugin; + } + + public boolean handleRegister(Player p, + String[] args) { + String name = p.getName(); + + // 已注册玩家不能再注册 + if (plugin.getDb().userExists(name)) { + p.sendMessage(plugin.getConfig2() + .msg("already_registered")); + return true; + } + + // IP注册限制检查 + String ip = plugin.getPlayerIP(p); + if (ip != null && plugin.getConfig2() + .maxAccountsPerIP > 0) { + if (!plugin.getIPGroup() + .canRegister(ip)) { + int count = plugin.getIPGroup() + .getAccountCount(ip); + int max = plugin.getConfig2() + .maxAccountsPerIP; + p.sendMessage("§c该IP注册已达上限(" + + count + "/" + max + ")"); + return true; + } + } + + if (args.length < 1) { + p.sendMessage("§c用法: /reg <密码>"); + p.sendMessage("§7密码要求: 6位以上," + + "包含大小写字母和数字"); + return true; + } + + String pwd = args[0]; + if (pwd.length() < 6) { + p.sendMessage("§c密码长度不足6位"); + return true; + } + if (!pwd.matches(".*[a-z].*") + || !pwd.matches(".*[A-Z].*") + || !pwd.matches(".*[0-9].*")) { + p.sendMessage("§c密码必须包含大写字母、" + + "小写字母和数字"); + return true; + } + + String salt = + PasswordUtils.generateSalt(); + String hash = + PasswordUtils.hash(pwd, salt); + plugin.getDb().createUser( + name, hash, salt); + plugin.getLoggedIn().add(name); + plugin.getDb().setLoggedIn(name, true); + plugin.getDb().setField(name, + "last_login_time", + System.currentTimeMillis()); + plugin.getDb().setField(name, + "last_online_check", + System.currentTimeMillis()); + plugin.getDb().recordIP(name, ip); + plugin.getDb().setField(name, + "register_time", + System.currentTimeMillis()); + plugin.restoreInventory(p); + plugin.recordIPLogin(p); + plugin.giveMenuSnowball(p); + + p.sendMessage(plugin.getConfig2() + .msg("reg_success")); + playRegisterSound(p); + + try { + org.bukkit.plugin.Plugin cy = + Bukkit.getPluginManager() + .getPlugin("CY_beibao"); + if (cy != null && cy.isEnabled()) { + cy.getClass().getMethod( + "onSdf1Activation", + String.class, + int.class, int.class) + .invoke(cy, name, 9, 0); + } + } catch (Exception ignored) { + } + return true; + } + + private void playRegisterSound(Player p) { + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_CHIME, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_BASS, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_PLING, + 1.0f, 0.79f); + } + + + public boolean handleLogin(Player p, + String[] args) { + String name = p.getName(); + try { + if (plugin.getLoggedIn().contains(name)) { + p.sendMessage(plugin.getConfig2() + .msg("already_logged_in")); + return true; + } + if (!plugin.getDb().userExists(name)) { + p.sendMessage(plugin.getConfig2() + .msg("not_registered")); + return true; + } + if (args.length < 1) { + p.sendMessage("§c用法: /login <密码>"); + return true; + } + String pwd = args[0]; + String salt = (String) plugin.getDb() + .getField(name, "password_salt"); + if (salt == null) { + p.sendMessage( + "§c数据异常,请联系管理员"); + plugin.getLogger().severe( + "[Sdf1_login] 玩家 " + name + + " 盐值为null"); + return true; + } + String hash = + PasswordUtils.hash(pwd, salt); + + boolean matchMain = + plugin.getDb().checkPassword( + name, hash); + if (matchMain) { + plugin.getLoggedIn().add(name); + plugin.getDb().setLoggedIn( + name, true); + plugin.getDb().setField(name, + "last_login_time", + System.currentTimeMillis()); + plugin.getDb().setField(name, + "last_online_check", + System.currentTimeMillis()); + // 只在有有效备份时恢复 + plugin.restoreInventory(p); + plugin.recordIPLogin(p); + plugin.giveMenuSnowball(p); + p.sendMessage(plugin.getConfig2() + .msg("login_success")); + activateCY(p, name); + return true; + } + + boolean matchTemp = + plugin.getDb() + .checkPasswordOrTemp( + name, hash); + if (matchTemp) { + plugin.getLoggedIn().add(name); + plugin.getDb().setLoggedIn( + name, true); + plugin.getDb().setField(name, + "last_login_time", + System.currentTimeMillis()); + plugin.getDb().setField(name, + "last_online_check", + System.currentTimeMillis()); + plugin.restoreInventory(p); + plugin.recordIPLogin(p); + plugin.giveMenuSnowball(p); + plugin.getNeedsPasswordChange() + .add(name); + p.sendMessage(plugin.getConfig2() + .msg("login_success")); + p.sendMessage( + "§c§l[警告] §f您使用的是临时密码,请尽快修改密码!"); + p.sendMessage( + "§7用法: /sdf1_login pw"); + activateCY(p, name); + return true; + } + + + p.sendMessage(plugin.getConfig2() + .msg("login_failed")); + } catch (Exception e) { + plugin.getLogger().severe( + "[Sdf1_login] handleLogin异常: " + + e.getMessage()); + e.printStackTrace(); + p.sendMessage( + "§c登录过程出错,请联系管理员"); + } + return true; + } + + + public void handleReset(Player p) { + String name = p.getName(); + String emailAddr = (String) + plugin.getDb().getField( + name, "email"); + if (emailAddr == null + || emailAddr.isEmpty()) { + p.sendMessage( + "§c未设置邮箱,无法重置密码"); + return; + } + String tempPwd = PasswordUtils + .generateTempPassword(); + String salt = (String) + plugin.getDb().getField( + name, "password_salt"); + String hash = PasswordUtils.hash( + tempPwd, salt); + plugin.getDb().setField(name, + "temp_password", hash); + plugin.getDb().setField(name, + "temp_pw_expire", + System.currentTimeMillis() + + 300000L); + + plugin.getDb().setField(name, + "temp_pw_used", 0); + p.sendMessage("§e正在发送临时密码..."); + final String to = emailAddr; + final String fName = name; + final String pwd = tempPwd; + final Main self = plugin; + Bukkit.getScheduler() + .runTaskAsynchronously(self, + () -> { + final boolean[] sent = {false}; + Thread t = new Thread(() -> { + sent[0] = self.getEmail() + .sendTempPassword( + to, fName, pwd); + }, "Sdf1_login-reset"); + t.setDaemon(true); + t.start(); + try { + t.join(15000); + } catch ( + InterruptedException ignored) { + } + if (t.isAlive()) { + t.interrupt(); + Bukkit.getScheduler().runTask( + self, () -> + p.sendMessage( + "§c邮件发送超时")); + return; + } + Bukkit.getScheduler().runTask( + self, () -> { + if (sent[0]) { + p.sendMessage( + "§a临时密码已发送到 " + + to); + } else { + p.sendMessage( + "§c邮件发送失败"); + } + }); + }); + } + + private void activateCY(Player p, + String name) { + try { + org.bukkit.plugin.Plugin cy = + Bukkit.getPluginManager() + .getPlugin("CY_beibao"); + if (cy != null && cy.isEnabled()) { + cy.getClass().getMethod( + "onSdf1Activation", + String.class, + int.class, int.class) + .invoke(cy, name, 0, 0); + } + } catch (Exception ignored) { + } + } +} diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java index bb4e5b5..e206751 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -9,14 +9,28 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Villager; +import Sdf1_login.ChatInputManager.InputType; + + + + import java.util.TreeSet; import java.util.UUID; import java.util.LinkedHashMap; import java.util.concurrent.ConcurrentHashMap; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.inventory.CraftItemEvent; +import org.bukkit.event.player.PlayerFishEvent; + import net.milkbowl.vault.economy.Economy; import org.bukkit.Bukkit; -import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.entity.Player; @@ -32,7 +46,6 @@ import org.bukkit.event.inventory.InventoryDragEvent; import org.bukkit.event.player.AsyncPlayerChatEvent; import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerPickupItemEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.inventory.Inventory; @@ -75,6 +88,9 @@ public class Main extends JavaPlugin private final Set needsPasswordChange = new TreeSet<>( String.CASE_INSENSITIVE_ORDER); + private final Set frozenFlightPlayers = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); private final Map joinTime = new ConcurrentHashMap<>(); private final Map joinLoc = @@ -85,6 +101,15 @@ public class Main extends JavaPlugin private final Map lastActivity = new ConcurrentHashMap<>(); + // 登录后首次回收飞行(仅一次) + private final Set removeFlightOnce = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + public RadioManager radio; + public RadioManager getRadio() { + return radio; + } + private static class PwdRollback { final String hash; @@ -109,7 +134,9 @@ public class Main extends JavaPlugin } // ===== Getters ===== - public DatabaseManager getDb() { return db; } + public DatabaseManager getDb() { + return db; + } public ConfigManager getConfig2() { return config; } public EmailManager getEmail() { return email; } public Economy getEconomy() { return economy; } @@ -129,10 +156,23 @@ public class Main extends JavaPlugin public GarbageManager getGarbage() { return garbage; } public Set getLoggedIn() { return loggedIn; } public Set getNeedsPasswordChange() { return needsPasswordChange; } + private WelcomeManager welcome; + private UpdateChecker updateChecker; + private QuestTracker questTracker; + private CommissionManager commission; + public CommissionManager getCommission() { + return commission; + } + + // ===== Enable/Disable ===== @Override public void onEnable() { + radio = new RadioManager(this); + radio.init(); + getServer().getPluginManager().registerEvents( + new RadioDownloadListener(this), this); getDataFolder().mkdirs(); config = new ConfigManager(getDataFolder()); config.loadMessages(); @@ -167,6 +207,11 @@ public class Main extends JavaPlugin garbage.startAutoCleanup(); chatFilter = new ChatFilterManager(this); chatFilter.loadConfig(); + welcome = new WelcomeManager(this); + updateChecker = new UpdateChecker(this); + questTracker = new QuestTracker(this); + commission = new CommissionManager(this); + setupEconomy(); @@ -198,8 +243,8 @@ public class Main extends JavaPlugin getCommand("oa").setExecutor(this); if (getCommand("menu") != null) getCommand("menu").setExecutor(this); - - + if (getCommand("radio") != null) + getCommand("radio").setExecutor(this); getServer().getPluginManager() .registerEvents(this, this); @@ -208,17 +253,33 @@ public class Main extends JavaPlugin startTimeoutCheck(); startPasswordReminder(); startTicketAutoProcess(); + startPlayTimeTracker(); + // 启动后异步检查更新 + updateChecker.checkOnEnable(); getLogger().info( "Sdf1_login v1.0 | 就绪"); + for (Player online + : Bukkit.getOnlinePlayers()) { + radio.sendResourcePack(online); + } } - @Override + @Override public void onDisable() { - if (db != null) db.close(); - if (garbage != null) garbage.close(); + if (questTracker != null) { + questTracker.shutdown(); + } + if (radio != null) { + radio.stopAllLoginRadio(); + radio.stopMainRadio(); + } + + if (questTracker != null) { + } } +// 没替换,只是粗略看了下,有几个问题 private void setupEconomy() { if (getServer().getPluginManager() .getPlugin("Vault") == null) return; @@ -229,6 +290,15 @@ public class Main extends JavaPlugin if (rsp != null) economy = rsp.getProvider(); } + public WelcomeManager getWelcome() { + return welcome; + } + public UpdateChecker getUpdateChecker() { + return updateChecker; + } + public QuestTracker getQuestTracker() { + return questTracker; + } // ===== Timers ===== private void startTimeoutCheck() { @@ -431,9 +501,37 @@ public class Main extends JavaPlugin + name); e.printStackTrace(); } + } + @EventHandler + public void onBlockPlace_Quest( + BlockPlaceEvent e) { + if (questTracker == null) return; + Player p = e.getPlayer(); + if (isFrozen(p)) return; + questTracker.onBlockPlaced( + p.getName(), + e.getBlock().getType().name()); + } + + @EventHandler + public void onMoveBiome( + PlayerMoveEvent e) { + if (questTracker == null) return; + Player p = e.getPlayer(); + if (isFrozen(p)) return; + Location to = e.getTo(); + if (to == null) return; + String fromKey = e.getFrom().getBlock() + .getBiome().getKey().getKey(); + String toKey = to.getBlock() + .getBiome().getKey().getKey(); + if (fromKey.equals(toKey)) return; + questTracker.onPlayerMove( + p.getName(), toKey); + } public void backupInventory(Player p) { @@ -563,6 +661,8 @@ public class Main extends JavaPlugin req.id))); return; } + radio.stopLoginRadio(p); + } String salt = PasswordUtils.generateSalt(); @@ -579,6 +679,7 @@ public class Main extends JavaPlugin } loggedIn.add(name); + removeFlightOnce.add(name); db.setLoggedIn(name, true); db.setField(name, "last_login_time", @@ -590,6 +691,15 @@ public class Main extends JavaPlugin joinTime.remove(p.getUniqueId()); recordIPLogin(p); + // 标记注册登录条件完成 + if (questTracker != null) { + questTracker.markConditionComplete( + name, "完成注册登录"); + } + + // 欢迎信息 + this.getWelcome().onLogin(p); + // 只在有有效备份时恢复 restoreInventory(p); @@ -597,8 +707,83 @@ public class Main extends JavaPlugin p.sendMessage("§a[Sdf1_login] §f您已自动登录!"); activateBeibao(p); pushPendingAlerts(p); + } + // ===== 任务追踪事件 ===== + @EventHandler + public void onBlockMine(BlockBreakEvent event) { + if (questTracker == null) return; // ← 新增 + Player p = event.getPlayer(); + if (isFrozen(p)) return; + questTracker.onBlockMined( + p.getName(), + event.getBlock().getType().name()); + } + + @EventHandler + public void onMobDeath(EntityDeathEvent e) { + if (questTracker == null) return; // ← 新增 + if (e.getEntity().getKiller() == null) + return; + Player p = e.getEntity().getKiller(); + questTracker.onMobKill( + p.getName(), + e.getEntity().getType().name()); + } + + @EventHandler + public void onPlayerDeath( + PlayerDeathEvent e) { + if (questTracker == null) return; // ← 新增 + Player killer = + e.getEntity().getKiller(); + if (killer == null) return; + questTracker.onPlayerKill( + killer.getName()); + } + + @EventHandler + public void onCraft( + CraftItemEvent e) { + if (questTracker == null) return; // ← 新增 + if (!(e.getWhoClicked() + instanceof Player)) return; + Player p = (Player) e.getWhoClicked(); + if (isFrozen(p)) return; + questTracker.onItemCrafted( + p.getName()); + } + + @EventHandler + public void onFish( + PlayerFishEvent e) { + if (questTracker == null) return; // ← 新增 + if (e.getState() + != PlayerFishEvent + .State.CAUGHT_FISH) return; + if (!(e.getCaught() + instanceof + org.bukkit.entity.Item)) return; + Player p = e.getPlayer(); + questTracker.onFishCaught( + p.getName()); + } + + // 在线时长追踪(每分钟) + public void startPlayTimeTracker() { + Bukkit.getScheduler() + .runTaskTimer(this, + () -> { + if (questTracker == null) return; + for (Player p : + Bukkit.getOnlinePlayers()) { + if (isFrozen(p)) continue; + questTracker.onPlayTime( + p.getName(), + 60000L); + } + }, 1200L, 1200L); } private void activateBeibao(Player p) { @@ -683,35 +868,6 @@ public class Main extends JavaPlugin return true; } - public void openTaskPanel(Player p) { - Inventory g = Bukkit.createInventory( - null, 27, "§6§l任务面板"); - ItemStack gl = new ItemStack( - Material.GRAY_STAINED_GLASS_PANE); - ItemMeta glm = gl.getItemMeta(); - if (glm != null) { - glm.setDisplayName(" "); - gl.setItemMeta(glm); - } - for (int i = 0; i < 27; i++) - g.setItem(i, gl); - Map user = - db.getUser(p.getName()); - int stage = ((Number) user.getOrDefault( - "gift_stage", 0)).intValue(); - String claimed = (String) user.getOrDefault( - "gift_claimed", ""); - g.setItem(11, mkItem(Material.BOOK, - "§e新人礼包进度", - "§7当前阶段: §e" + stage, - "§7已领取: §a" + (claimed.isEmpty() - ? "无" : claimed))); - g.setItem(15, mkItem(Material.CLOCK, - "§b签到", "§7今日签到获取积分")); - g.setItem(22, mkItem(Material.ARROW, - "§7返回")); - p.openInventory(g); - } public void openAccountRequestPanel(Player p) { Inventory g = Bukkit.createInventory( @@ -1071,7 +1227,20 @@ public class Main extends JavaPlugin } } - // ===== Events ===== + @EventHandler + public void onTeleportBiome( + PlayerTeleportEvent e) { + if (questTracker == null) return; + Player p = e.getPlayer(); + if (isFrozen(p)) return; + Location to = e.getTo(); + if (to == null) return; + String toKey = to.getBlock() + .getBiome().getKey().getKey(); + questTracker.onPlayerMove( + p.getName(), toKey); + } + @EventHandler public void onJoin(PlayerJoinEvent e) { Player p = e.getPlayer(); @@ -1081,7 +1250,6 @@ public class Main extends JavaPlugin chatInput.reset(p); if (needsPasswordChange.contains(name)) needsPasswordChange.remove(name); - lastActivity.put(p.getUniqueId(), System.currentTimeMillis()); if (needsPasswordChange.contains(name)) @@ -1103,6 +1271,10 @@ public class Main extends JavaPlugin } if (isFrozen(p)) { p.setAllowFlight(true); + frozenFlightPlayers.add(p.getName()); + } + if (questTracker != null) { + questTracker.debugLogin(p.getName()); } if (!"manual".equals(config.approvalMode) @@ -1159,23 +1331,43 @@ public class Main extends JavaPlugin }); } }); - } + // ===== 在 onJoin 方法的最后一行(关闭括号之前)加入 ===== + // 延迟2秒发送广播资源包,确保客户端完全就绪 + final Player radioTarget = p; + Bukkit.getScheduler().runTaskLater(this, () -> { + if (radioTarget.isOnline()) { + radio.sendResourcePack(radioTarget); + } + }, 40L); // 40 tick = 2 秒 + // 发送广播资源包 + final Player rpTarget = p; + Bukkit.getScheduler().runTaskLater(this, + () -> { + if (rpTarget != null + && rpTarget.isOnline()) { + radio.sendResourcePack( + rpTarget); + } + }, 60L); + + } // ← onJoin 的结束括号 @EventHandler public void onQuit(PlayerQuitEvent e) { Player p = e.getPlayer(); + // 记录冻结状态的飞行 + if (!db.userExists(p.getName()) + || !loggedIn.contains(p.getName())) { + p.setAllowFlight(true); + frozenFlightPlayers.add(p.getName()); + } + String name = p.getName(); chatInput.reset(p); boolean wasLoggedIn = loggedIn.contains(name); - // 判断是否需要保存: - // 1. 已登录 → 保存当前背包 - // 2. 未登录但有真实物品 → 保存 - // (插件安装前的老玩家) - // 3. 未登录且背包被插件清空 → 不保存 - // (避免覆盖有效备份) boolean hasRealItems = false; for (ItemStack it : p.getInventory() .getContents()) { @@ -1209,8 +1401,7 @@ public class Main extends JavaPlugin double exp = p.getExp(); debugPrintBase64( - "退出保存", name, - contents); + "退出保存", name, contents); db.saveInventoryBackup(name, contents, armor, extra, level, exp); @@ -1223,8 +1414,7 @@ public class Main extends JavaPlugin } else { getLogger().info( "[BACKUP_DEBUG] 退出跳过 " - + name - + ": 背包为空" + + name + ": 背包为空" + "(插件已清空)"); } @@ -1234,10 +1424,12 @@ public class Main extends JavaPlugin recordIPLogin(p); } + frozenFlightPlayers.remove(name); // ← 新增 lastActivity.remove(p.getUniqueId()); joinTime.remove(p.getUniqueId()); joinLoc.remove(p.getUniqueId()); needsPasswordChange.remove(name); + } private static final String MENU_SNOWBALL_TAG = @@ -1343,12 +1535,22 @@ public class Main extends JavaPlugin return false; } - @EventHandler public void onMove(PlayerMoveEvent e) { Player p = e.getPlayer(); - if (!isFrozen(p)) return; + // 已登录:一次性关闭飞行 + if (!isFrozen(p)) { + if (frozenFlightPlayers.remove( + p.getName())) { + p.setAllowFlight(false); + p.setFlying(false); + } + return; + } + + + // 未登录:检查是否能移动 Location from = e.getFrom(); Location to = e.getTo(); if (to == null) return; @@ -1360,37 +1562,36 @@ public class Main extends JavaPlugin return; } - // 有位移 → 取消 + // 有位移 → 取消移动 e.setCancelled(true); - // 防止"服务器未启用飞行"踢出 - p.setAllowFlight(true); + // 确保允许飞行(防止被踢出) + if (!p.getAllowFlight()) { + p.setAllowFlight(true); + } + + // 确保已记录(首次加入时未触发移动的情况) + if (!frozenFlightPlayers.contains( + p.getName())) { + frozenFlightPlayers.add(p.getName()); + } } @EventHandler public void onInteract(PlayerInteractEvent e) { Player p = e.getPlayer(); - if (isFrozen(p)) { - e.setCancelled(true); + ItemStack item = e.getItem(); + if (item == null + || item.getType() + != Material.SNOWBALL) return; - } - // 右键菜单雪球 - if (e.getAction().name() - .contains("RIGHT") - || e.getAction().name() - .contains("LEFT")) { - ItemStack hand = - p.getInventory() - .getItemInMainHand(); - if (isMenuSnowball(hand)) { - e.setCancelled(true); - gui.openMain(p); - return; - } - } + if (!isMenuSnowball(item)) return; + if (isFrozen(p)) return; + if (!loggedIn.contains(p.getName())) return; + e.setCancelled(true); + gui.openMain(p); } - @EventHandler public void onBlockBreak(BlockBreakEvent e) { Player p = e.getPlayer(); @@ -1471,10 +1672,35 @@ public class Main extends JavaPlugin e.setCancelled(true); return; } +// 在 onChat 方法中,isFrozen 检查之后 +// (在 chatFilter 检查之前) + if (!isFrozen(p) + && questTracker != null) { + final String pn = p.getName(); + Bukkit.getScheduler().runTask(this, + () -> questTracker + .onPlayerChat(pn)); + } if (chatFilter == null) return; + // 安全兜底:任何输入状态下跳过过滤 + if (chatInput.getState(p).type + != ChatInputManager.InputType.NONE) { + return; + } + if (chatFilter.isPlayerWhitelisted( p.getName())) return; + // 联控:绑定邮箱/配置SMTP时跳过URL过滤 + InputType inType = + chatInput.getState(p).type; + if (inType == InputType.SET_EMAIL + || inType == InputType.SET_EMAIL_VERIFY + || inType == InputType.SMTP_CONFIG + || inType == InputType.SMTP_VERIFY) { + return; + } + if (chatFilter.isMuted(p.getName())) { e.setCancelled(true); p.sendMessage( @@ -1487,7 +1713,7 @@ public class Main extends JavaPlugin if (urls.isEmpty()) return; for (String u : urls) { - if (chatFilter.isWhitelisted(u)) + if (chatFilter.isPlayerWhitelisted(u)) continue; if (!chatFilter.isLikelyDomain(u)) continue; @@ -1556,7 +1782,7 @@ public class Main extends JavaPlugin if (e.isShiftClick() && e.getClickedInventory() != null && e.getClickedInventory() - == p.getInventory() + != e.getView().getTopInventory() && e.getCurrentItem() != null && e.getCurrentItem().getType() != Material.AIR @@ -1565,21 +1791,78 @@ public class Main extends JavaPlugin && title.equals( "§6§l垃圾回收站")) { e.setCancelled(true); + ItemStack clicked = e.getCurrentItem().clone(); + + getLogger().info( + "[垃圾站] Shift存入: " + + clicked.getType() + + " x" + clicked.getAmount()); + + // 先存入数据库 garbage.saveItem(clicked); - e.getInventory().setItem( - e.getSlot(), null); + + // 关闭背包强制同步服务器与客户端状态 + // (解决Paper的shift-click取消后仍移动物品的问题) + p.closeInventory(); + + // 1 tick后:移除物品 + 重新打开垃圾站 Bukkit.getScheduler() .runTaskLater(this, () -> { - garbage.openRecyclePage(p, - garbage.getRecyclePage( - p.getName())); - }, 1L); + // 此时物品状态已同步 + // 搜索并移除对应物品 + boolean found = false; + for (int i = 0; + i < p.getInventory() + .getSize(); + i++) { + ItemStack is = + p.getInventory() + .getItem(i); + if (is != null + && is.getType() + == clicked + .getType() + && is.getAmount() + == clicked + .getAmount()) { + p.getInventory() + .setItem( + i, + null); + found = true; + getLogger() + .info( + "[垃圾站] " + + "移除成功: slot=" + + i); + break; + } + } + if (!found) { + getLogger() + .info( + "[垃圾站] " + + "物品已被移走"); + } + + p.updateInventory(); + + // 重新打开垃圾站 + garbage + .openRecyclePage( + p, + garbage + .getRecyclePage( + p + .getName())); + }, 2L); return; } + if (needsPasswordChange .contains(p.getName()) && !title.equals( @@ -1588,6 +1871,30 @@ public class Main extends JavaPlugin p.sendMessage("§c请先修改密码!"); return; } + // 任务中心 + if (title.equals( + GUIManager.T_TASK_CENTER)) { + e.setCancelled(true); + if (slot == 26) gui.openMain(p); + else if (slot == 10) + gui.openGiftStages(p); + else if (slot == 12) + gui.openTaskList(p, "主线任务"); + else if (slot == 14) + gui.openTaskList(p, "支线任务"); + else if (slot == 16) { + if (dailySign(p)) { + gui.openTaskCenter(p); + playSuccessSound(p); + } else + p.sendMessage(config.msg( + "checkin_already")); + } + else if (slot == 21) { + handleCheckInReport(p); + } + return; + } // 工单GUI if (title.equals( @@ -1830,24 +2137,51 @@ public class Main extends JavaPlugin return; } - // 新人礼包 + // 新人任务 if (title.equals( GUIManager.T_GIFT_STAGES)) { e.setCancelled(true); - if (slot == 26) + if (slot == 53) { gui.openTaskCenter(p); - else if (slot >= 10 && slot <= 18) { - int stage = slot - 9; - if (gift.canClaim(p, stage)) { - gift.claimReward(p, stage); - gui.openGiftStages(p); - } else - p.sendMessage(config.msg( - "gift_not_ready")); + } else if (slot == 52) { + // 打卡上报 + handleCheckInReport(p); + } else if (slot == 4) { + // 进度标题 + } else if (slot >= 10 && slot < 53) { + int idx = slot - 10; + QuestTracker qt = questTracker; + List quests = + qt.getQuests("新人任务"); + if (idx < quests.size()) { + QuestTracker.QuestFile qf = + quests.get(idx); + boolean done = + qt.isStageCompleted( + p.getName(), qf); + if (done + && !qt.hasClaimed( + p.getName(), qf)) { + qt.markClaimed( + p.getName(), qf); + qt.claimRewards(p, qf); + gui.openGiftStages(p); + } else if (done + && qt.hasClaimed( + p.getName(), qf)) { + p.sendMessage( + "§c该阶段奖励已领取"); + } else { + p.sendMessage( + "§c该阶段尚未完成"); + } + } } return; } + + // 邀请 if (title.equals(GUIManager.T_INVITE)) { e.setCancelled(true); @@ -1916,6 +2250,8 @@ public class Main extends JavaPlugin p.closeInventory(); p.sendMessage("§e输入" + tgt + "的临时密码:"); + } else if (slot == 13) { + gui.openCheckInRecords(p, tgt); } else if (slot == 14) { Player tp = Bukkit.getPlayer(tgt); if (tp != null && tp.isOnline()) { @@ -1925,9 +2261,9 @@ public class Main extends JavaPlugin } else { p.sendMessage("§c玩家不在线"); } + } else if (slot == 16) { - - } else if (slot == 16) { + } else if (slot == 16) { chatInput.getState(p).type = ChatInputManager.InputType .ADMIN_DELETE_CONFIRM; @@ -1976,21 +2312,6 @@ public class Main extends JavaPlugin return; } - // 任务面板 - if (title.equals("§6§l任务面板")) { - e.setCancelled(true); - if (slot == 22) gui.openMain(p); - else if (slot == 15) { - if (dailySign(p)) { - openTaskPanel(p); - playSuccessSound(p); - } else - p.sendMessage(config.msg( - "checkin_already")); - } - return; - } - // 账号请求面板 if (title.equals("§e§l账号请求")) { e.setCancelled(true); @@ -2018,6 +2339,171 @@ public class Main extends JavaPlugin return; } } + /** + * 打卡上报:验证当前阶段条件 + */ + + private void handleCheckInReport(Player p) { + QuestTracker qt = questTracker; + if (qt == null) { + p.sendMessage("§c任务系统未加载"); + return; + } + + p.sendMessage("§e§l[打卡] §f正在检测环境..."); + + // === 检测当前群系 === + Location loc = p.getLocation(); + String biomeKey = loc.getBlock() + .getBiome().getKey().getKey(); + String worldName = loc.getWorld().getName(); + + List biomeMatched = + qt.checkInBiome( + p.getName(), + biomeKey, worldName, + loc.getX(), loc.getY(), + loc.getZ()); + + qt.recordCheckIn( + p.getName(), biomeKey, + qt.mapBiomeCategory( + biomeKey, worldName), + worldName, + loc.getX(), loc.getY(), + loc.getZ()); + + p.sendMessage("§7 群系: §e" + + biomeKey); + p.sendMessage("§7 分类: §e" + + qt.mapBiomeCategory( + biomeKey, worldName)); + p.sendMessage("§7 世界: §e" + + worldName); + p.sendMessage("§7 坐标: §e" + + String.format("%.0f, %.0f, %.0f", + loc.getX(), loc.getY(), + loc.getZ())); + + if (!biomeMatched.isEmpty()) { + for (String m : biomeMatched) { + p.sendMessage("§7 §a✓ " + m); + } + } + + // === 检测附近村庄(50格内有村民) === + boolean nearVillage = false; + for (Entity entity + : p.getNearbyEntities(50, 50, 50)) { + if (entity instanceof Villager) { + nearVillage = true; + break; + } + } + if (nearVillage) { + boolean counted = + qt.onReportVillage( + p.getName(), + worldName, + loc.getX(), + loc.getY(), + loc.getZ()); + if (counted) { + p.sendMessage( + "§7 §a村庄上报计数成功!"); + } else { + p.sendMessage( + "§7 §e村庄32格内已上报过"); + } + } + + + // === 验证所有未完成的任务 === + p.sendMessage(""); + p.sendMessage("§e§l[打卡] §f正在验证" + + "未完成的任务..."); + + List categories = + Arrays.asList( + "新人任务", + "主线任务", + "支线任务"); + + int totalMet = 0; + int totalChecked = 0; + int finishedCount = 0; + + for (String cat : categories) { + List quests = + qt.getQuests(cat); + for (QuestTracker.QuestFile qf + : quests) { + if (qt.isStageCompleted( + p.getName(), qf)) { + finishedCount++; + continue; + } + if (!qf.hasConditions()) + continue; + + p.sendMessage("§e§l[" + + qf.displayName + "]"); + + for (String cond + : qf.conditions) { + if (qt.isOptional(cond)) + continue; + totalChecked++; + + boolean met = + qt.isConditionMet( + p.getName(), + cond); + + if (met) { + totalMet++; + p.sendMessage("§7 §a✓ " + + qt.getProgress( + p.getName(), + cond)); + } else { + p.sendMessage("§7 §c✗ " + + qt.getProgress( + p.getName(), + cond)); + } + } + } + } + + p.sendMessage(""); + if (finishedCount > 0) { + p.sendMessage("§7已跳过 " + + finishedCount + + " 个已完成的阶段"); + } + if (totalChecked == 0) { + p.sendMessage( + "§a§l[打卡] §f没有" + + "未完成的条件"); + } else if (totalMet >= totalChecked) { + p.sendMessage( + "§a§l[打卡] §f全部通过!(" + + totalMet + "/" + + totalChecked + ")"); + playSuccessSound(p); + } else { + p.sendMessage( + "§c§l[打卡] §f未通过 (" + + totalMet + "/" + + totalChecked + + ") §c请继续完成"); + } + + gui.openTaskCenter(p); + } + + @EventHandler public void onInvDrag(InventoryDragEvent e) { @@ -2025,6 +2511,7 @@ public class Main extends JavaPlugin return; Player p = (Player) e.getWhoClicked(); if (isFrozen(p)) { + frozenFlightPlayers.add(p.getName()); e.setCancelled(true); return; } @@ -2114,6 +2601,16 @@ public class Main extends JavaPlugin return true; } + if (cmd.getName().equalsIgnoreCase("radio")) { + getLogger().info( + "[Radio] 收到命令: " + + Arrays.toString(args)); + + // 统一交给 RadioManager 处理 + radio.handleCommand(sender, args); + return true; + } + // /垃圾清理 if (cmdName.equals("垃圾清理")) { @@ -2158,7 +2655,7 @@ public class Main extends JavaPlugin } return true; } - +// /reg if (cmdName.equals("reg") || cmdName.equals("注册")) { if (!(sender instanceof Player)) { @@ -2181,11 +2678,24 @@ public class Main extends JavaPlugin e.printStackTrace(); p.sendMessage("§c注册出错,请联系管理员"); } + // 注册/登录成功后统一处理 + if (loggedIn.contains(p.getName())) { + frozenFlightPlayers.remove(p.getName()); + p.setAllowFlight(false); + p.setFlying(false); + removeFlightOnce.add(p.getName()); + if (questTracker != null) { + questTracker.markConditionComplete( + p.getName(), + "完成注册登录"); + } + this.getWelcome().onLogin(p); + restoreInventory(p); + giveMenuSnowball(p); + } return true; } - - if (cmdName.equals("login") || cmdName.equals("l") || cmdName.equals("登录")) { @@ -2196,6 +2706,10 @@ public class Main extends JavaPlugin Player p = (Player) sender; chatInput.reset(p); if (loggedIn.contains(p.getName())) { + frozenFlightPlayers.remove(p.getName()); + p.setAllowFlight(false); + p.setFlying(false); + removeFlightOnce.add(p.getName()); p.sendMessage("§c您已登录"); return true; } @@ -2208,6 +2722,17 @@ public class Main extends JavaPlugin e.printStackTrace(); p.sendMessage("§c登录出错,请联系管理员"); } + // 登录成功后统一处理 + if (loggedIn.contains(p.getName())) { + if (questTracker != null) { + questTracker.markConditionComplete( + p.getName(), + "完成注册登录"); + } + this.getWelcome().onLogin(p); + restoreInventory(p); + giveMenuSnowball(p); + } return true; } @@ -2371,6 +2896,19 @@ public class Main extends JavaPlugin } String sub = args[0].toLowerCase(); + // update - 检查更新(控制台+玩家均可) + if (sub.equals("update")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + sender.sendMessage("§e[更新] 正在检测..."); + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> updateChecker + .checkUpdate(sender)); + return true; + } // stop if (sub.equals("stop")) { @@ -2570,6 +3108,20 @@ public class Main extends JavaPlugin "§e§l================================"); } } + // radio - 播放/停止广播 + if (sub.equals("radio")) { + // /sdf1_login radio wrktrx3.ogg + // args = ["radio", "wrktrx3.ogg"] + // 剥掉 "radio",把剩余参数传给 handleCommand + String[] radioArgs = + new String[args.length - 1]; + System.arraycopy( + args, 1, + radioArgs, 0, + radioArgs.length); + radio.handleCommand(sender, radioArgs); + return true; + } // pw / password if (sub.equals("pw") @@ -2632,6 +3184,9 @@ public class Main extends JavaPlugin db.clearTempPassword( p2.getName()); } + frozenFlightPlayers.remove(p2.getName()); + p2.setAllowFlight(false); + p2.setFlying(false); needsPasswordChange.remove( p2.getName()); p2.sendMessage(config.msg( @@ -3164,7 +3719,7 @@ public class Main extends JavaPlugin "get", "del", "set", "take", "add", "kick", "ticket", "oa", "stop", - "shopadd", "shopdel","back")); + "shopadd", "shopdel","back","update")); } else { list.addAll(Arrays.asList( "pw", "email", "sign", diff --git a/src/main/java/Sdf1_login/PlayerStorage.java b/src/main/java/Sdf1_login/PlayerStorage.java new file mode 100644 index 0000000..f93caa3 --- /dev/null +++ b/src/main/java/Sdf1_login/PlayerStorage.java @@ -0,0 +1,130 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.*; +import java.util.logging.Logger; + +public class PlayerStorage { + private Connection conn; + private final Logger logger; + + public PlayerStorage(File dataFolder, + Logger logger) { + this.logger = logger; + File dbFile = new File( + dataFolder, + "quest_data.db"); + try { + Class.forName( + "org.sqlite.JDBC"); + conn = DriverManager + .getConnection( + "jdbc:sqlite:" + + dbFile + .getAbsolutePath()); + conn.setAutoCommit(true); + Statement s = + conn.createStatement(); + s.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + + "quest_kv (" + + "player TEXT NOT NULL," + + "key TEXT NOT NULL," + + "value TEXT," + + "PRIMARY KEY" + + "(player, key))"); + s.close(); + logger.info( + "[QuestStorage] 初始化: " + + dbFile.getAbsolutePath()); + } catch (Exception e) { + logger.severe( + "[QuestStorage] 失败: " + + e.getMessage()); + } + } + + public Object getCondition( + String player, String key) { + if (conn == null) return null; + try { + PreparedStatement ps = + conn.prepareStatement( + "SELECT value " + + "FROM quest_kv " + + "WHERE player=? " + + "AND key=?"); + ps.setString(1, + player.toLowerCase()); + ps.setString(2, key); + ResultSet rs = + ps.executeQuery(); + String val = rs.next() + ? rs.getString("value") + : null; + rs.close(); + ps.close(); + return parseValue(val); + } catch (SQLException e) { + return null; + } + } + + public void setCondition( + String player, String key) { + setConditionRaw( + player, key, "true"); + } + public void debugPlayer(String player) { + + } + public void setConditionRaw( + String player, String key, + String value) { + if (conn == null) return; + try { + PreparedStatement ps = + conn.prepareStatement( + "INSERT OR REPLACE " + + "INTO quest_kv" + + "(player,key,value)" + + " VALUES(?,?,?)"); + ps.setString(1, + player.toLowerCase()); + ps.setString(2, key); + ps.setString(3, value); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + logger.warning( + "[QuestStorage] 写入失败: " + + key + "=" + value + + " | " + e.getMessage()); + } + } + + public void close() { + try { + if (conn != null + && !conn.isClosed()) + conn.close(); + } catch (Exception e) {} + } + + private Object parseValue(String v) { + if (v == null) return null; + try { + return Long.parseLong(v); + } catch (Exception e1) { + try { + return Double.parseDouble(v); + } catch (Exception e2) { + if ("true".equalsIgnoreCase(v)) + return true; + if ("false".equalsIgnoreCase(v)) + return false; + return v; + } + } + } +} diff --git a/src/main/java/Sdf1_login/QuestStorage.java b/src/main/java/Sdf1_login/QuestStorage.java new file mode 100644 index 0000000..45dbabf --- /dev/null +++ b/src/main/java/Sdf1_login/QuestStorage.java @@ -0,0 +1,138 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class QuestStorage { + + private Connection conn; + + public QuestStorage(File dataFolder) { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File( + dataFolder, + "quest_data.db"); + conn = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getPath()); + initTable(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void initTable() + throws SQLException { + Statement stmt = + conn.createStatement(); + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS " + + "quest_conditions (" + + "player TEXT NOT NULL, " + + "condition_key TEXT NOT NULL, " + + "value TEXT DEFAULT '1', " + + "timestamp INTEGER, " + + "PRIMARY KEY " + + "(player, condition_key))"); + stmt.close(); + } + + /** + * 标记条件已完成(value = "1") + */ + public void setCondition( + String player, + String condition) { + setConditionRaw( + player, condition, "1"); + } + + /** + * 写入任意值(计数器等) + */ + public void setConditionRaw( + String player, + String condition, + String value) { + try { + PreparedStatement ps = + conn.prepareStatement( + "INSERT OR REPLACE " + + "INTO " + + "quest_conditions " + + "(player, " + + "condition_key, " + + "value, timestamp) " + + "VALUES " + + "(?, ?, ?, ?)"); + ps.setString(1, player); + ps.setString(2, condition); + ps.setString(3, value); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * 查询条件值 + */ + public Object getCondition( + String player, + String condition) { + try { + PreparedStatement ps = + conn.prepareStatement( + "SELECT value FROM " + + "quest_conditions " + + "WHERE player = ? " + + "AND " + + "condition_key = ?"); + ps.setString(1, player); + ps.setString(2, condition); + ResultSet rs = + ps.executeQuery(); + if (rs.next()) { + String val = + rs.getString("value"); + rs.close(); + ps.close(); + // 尝试解析为数字 + try { + if (val.contains(".")) { + return Double.parseDouble( + val); + } + return Long.parseLong(val); + } catch (NumberFormatException e) { + return val; + } + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void close() { + try { + if (conn != null + && !conn.isClosed()) { + conn.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/Sdf1_login/QuestTracker.java b/src/main/java/Sdf1_login/QuestTracker.java new file mode 100644 index 0000000..27132c5 --- /dev/null +++ b/src/main/java/Sdf1_login/QuestTracker.java @@ -0,0 +1,2516 @@ +package Sdf1_login; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + + +public class QuestTracker { + + private final Plugin plugin; + private final PlayerStorage storage; + private final Map + playerBiome = new HashMap<>(); + private final Map> + questCache = new HashMap<>(); + private final List + checkInRecords = new ArrayList<>(); + private final Map + lastCheckInPos = new HashMap<>(); + + public void recordCheckIn( + String player, + String biomeKey, + String category, + String worldName, + double x, double y, double z) { + String record = player + + "|" + worldName + + "|" + biomeKey + + "|" + category + + "|" + String.format( + "%.1f", x) + + "|" + String.format( + "%.1f", y) + + "|" + String.format( + "%.1f", z) + + "|" + System.currentTimeMillis(); + checkInRecords.add(record); + plugin.getLogger().info( + "[QuestTracker] 打卡记录: " + + record); + } + + public List getCheckInRecords() { + return new ArrayList<>(checkInRecords); + } + + public List getPlayerRecords( + String player) { + List list = new ArrayList<>(); + for (String r : checkInRecords) { + if (r.startsWith( + player.toLowerCase() + + "|")) { + list.add(r); + } + } + return list; + } + + + public QuestTracker(Plugin plugin) { + this.plugin = plugin; + this.storage = new PlayerStorage( + plugin.getDataFolder(), + plugin.getLogger()); + loadAllQuests(); + } + + public void debugPlayer(String player) { + plugin.getLogger().info( + "[QuestTracker] === " + + player + " 任务数据 ==="); + } + public void shutdown() { + if (storage != null) { + storage.close(); + } + } + + /** + * 玩家移动时记录当前群系 + */ + public void onPlayerMove( + String player, + String biomeName) { + playerBiome.put(player, biomeName); + } + /** + * 标记玩家已打卡该群系 + */ + // ========== 群系分类映射表 ========== + private static final Map + BIOME_CATEGORY = new HashMap<>(); + static { + // 海洋群系 + BIOME_CATEGORY.put("minecraft:ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:frozen_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_frozen_ocean", "海洋"); + // 平原群系 + BIOME_CATEGORY.put("minecraft:plains", "平原"); + BIOME_CATEGORY.put("minecraft:sunflower_plains", "平原"); + BIOME_CATEGORY.put("minecraft:meadow", "平原"); + // 森林群系 + BIOME_CATEGORY.put("minecraft:forest", "森林"); + BIOME_CATEGORY.put("minecraft:flower_forest", "森林"); + BIOME_CATEGORY.put("minecraft:birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:dark_forest", "森林"); + BIOME_CATEGORY.put("minecraft:taiga", "森林"); + BIOME_CATEGORY.put("minecraft:snowy_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_pine_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_spruce_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:cherry_grove", "森林"); + BIOME_CATEGORY.put("minecraft:jungle", "森林"); + BIOME_CATEGORY.put("minecraft:sparse_jungle", "森林"); + BIOME_CATEGORY.put("minecraft:bamboo_jungle", "森林"); + // 沙漠群系 + BIOME_CATEGORY.put("minecraft:desert", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna_plateau", "沙漠"); + BIOME_CATEGORY.put("minecraft:beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:snowy_beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:windswept_savanna", "沙漠"); + // 恶地群系 + BIOME_CATEGORY.put("minecraft:badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:wooded_badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:eroded_badlands", "恶地"); + // 雪原群系 + BIOME_CATEGORY.put("minecraft:snowy_plains", "雪原"); + BIOME_CATEGORY.put("minecraft:grove", "雪原"); + BIOME_CATEGORY.put("minecraft:ice_spikes", "雪原"); + BIOME_CATEGORY.put("minecraft:jagged_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_river", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:stony_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:snowy_slopes", "雪原"); + // 沙砾群系 + BIOME_CATEGORY.put("minecraft:windswept_hills", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_forest", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_gravelly_hills", "沙砾"); + // 沼泽群系 + BIOME_CATEGORY.put("minecraft:swamp", "沼泽"); + BIOME_CATEGORY.put("minecraft:mangrove_swamp", "沼泽"); + // 河流 + BIOME_CATEGORY.put("minecraft:river", "河流"); + // 蘑菇岛 + BIOME_CATEGORY.put("minecraft:mushroom_fields", "蘑菇岛"); + // 地下群系 + BIOME_CATEGORY.put("minecraft:dripstone_caves", "地下"); + BIOME_CATEGORY.put("minecraft:lush_caves", "地下"); + BIOME_CATEGORY.put("minecraft:deep_dark", "地下"); + // 下界群系 + BIOME_CATEGORY.put("minecraft:nether_wastes", "下界"); + BIOME_CATEGORY.put("minecraft:crimson_forest", "下界"); + BIOME_CATEGORY.put("minecraft:warped_forest", "下界"); + BIOME_CATEGORY.put("minecraft:soul_sand_valley", "下界"); + BIOME_CATEGORY.put("minecraft:basalt_deltas", "下界"); + // 末地群系 + BIOME_CATEGORY.put("minecraft:the_end", "末地"); + BIOME_CATEGORY.put("minecraft:the_void", "末地"); + BIOME_CATEGORY.put("minecraft:end_highlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_midlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_barrens", "末地"); + } + + // 所有群系大类列表 + private static final String[] ALL_CATEGORIES = + {"海洋", "平原", "森林", "沙漠", + "恶地", "雪原", "沙砾", "沼泽", + "河流", "蘑菇岛", "地下", + "下界", "末地"}; + + // 所有群系分类的条件关键词映射 + private static final Map + CATEGORY_KEYWORDS = new HashMap<>(); + static { + CATEGORY_KEYWORDS.put("海洋", "海洋"); + CATEGORY_KEYWORDS.put("平原", "平原"); + CATEGORY_KEYWORDS.put("森林", "森林"); + CATEGORY_KEYWORDS.put("沙漠", "沙漠"); + CATEGORY_KEYWORDS.put("恶地", "恶地"); + CATEGORY_KEYWORDS.put("雪原", "雪原"); + CATEGORY_KEYWORDS.put("沙砾", "沙砾"); + CATEGORY_KEYWORDS.put("沼泽", "沼泽"); + CATEGORY_KEYWORDS.put("河流", "河流"); + CATEGORY_KEYWORDS.put("蘑菇岛", "蘑菇岛"); + CATEGORY_KEYWORDS.put("地下", "地下"); + CATEGORY_KEYWORDS.put("下界", "下界"); + CATEGORY_KEYWORDS.put("末地", "末地"); + // 条件文本中的别名映射 + CATEGORY_KEYWORDS.put("地狱", "下界"); + CATEGORY_KEYWORDS.put("下届", "下界"); + CATEGORY_KEYWORDS.put("主岛", "末地"); + } + + /** + * 用世界维度名分类(最可靠) + * 世界名含 the_nether → 下界 + * 世界名含 the_end → 末地 + * 其余 → 主世界 + */ + public String mapBiomeCategory( + String rawKey, String worldName) { + // 下界维度优先用世界名 + if (worldName != null) { + String w = worldName.toLowerCase(); + if (w.contains("nether")) + return "下界"; + if (w.contains("end")) + return "末地"; + } + // 用群系映射表 + if (rawKey != null) { + String cat = + BIOME_CATEGORY.get( + rawKey.toLowerCase()); + if (cat != null) return cat; + } + return "主世界"; + } + + +// ========== 解析为区块 ========== + + private List
parseIntoSections( + List lines) { + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + return parseHashSections(lines); + } else if (hasDash) { + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + continue; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseHashSections(converted); + } + return new ArrayList<>(); + } + + private List
parseHashSections( + List lines) { + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add(new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } else if (t.isEmpty()) { + // skip + } else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty() + && curTitle != null) { + curLines.add(cond); + } + } + } + if (curTitle != null) { + sections.add(new Section( + curTitle, curLines)); + } + return sections; + } + + private List
buildFlatSections( + List lines, + String category) { + List conditions = + new ArrayList<>(); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + conditions.add(cond); + } + } + if (conditions.isEmpty()) + return new ArrayList<>(); + List
list = + new ArrayList<>(); + list.add(new Section( + "第一阶段", conditions)); + return list; + } + + // ========== 文件加载入口 ========== + + private void loadAllQuests() { + loadCategory("新人任务"); + loadCategory("主线任务"); + loadCategory("支线任务"); + } + + private void loadCategory(String name) { + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 扫描中..."); + File folder = new File( + plugin.getDataFolder(), + name); + String path = "plugins/Sdf1_login/" + + name + "/"; + + if (!folder.exists()) { + folder.mkdirs(); + plugin.getLogger().warning( + "[QuestTracker] 已创建目录: " + + path); + questCache.put(name, + new ArrayList<>()); + return; + } + + File[] mdFiles = folder.listFiles( + (d, n) -> n.endsWith(".md")); + + if (mdFiles == null + || mdFiles.length == 0) { + plugin.getLogger().warning( + "[QuestTracker] " + path + + " 下无 .md 文件"); + questCache.put(name, + new ArrayList<>()); + return; + } + Arrays.sort(mdFiles); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 发现 " + mdFiles.length + + " 个文件,开始扫描..."); + + // 合并所有文件的阶段 + TreeMap merged = + new TreeMap<>(); + int fileCount = 0; + + for (int fi = 0; + fi < mdFiles.length; fi++) { + File file = mdFiles[fi]; + List lines = + readFileLines(file); + + if (lines.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 空文件,跳过"); + continue; + } + + List
sections = + parseIntoSections(lines); + + if (sections.isEmpty()) { + sections = buildFlatSections( + lines, name); + } + + if (sections.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 无法识别,跳过"); + continue; + } + + // 每个文件独立处理阶段号 + int defaultNum = fi + 1; + buildSingleFileStages( + sections, name, + merged, defaultNum); + + fileCount++; + } + + if (merged.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + name + + ": 无有效任务数据"); + questCache.put(name, + new ArrayList<>()); + return; + } + + // 日志 + for (Map.Entry en + : merged.entrySet()) { + int num = en.getKey() >= 1000 + ? en.getKey() - 999 + : en.getKey(); + plugin.getLogger().info( + "[QuestTracker] " + name + + " 阶段" + num + + " \"" + + en.getValue().displayName + + "\" → 条件" + + en.getValue() + .conditions.size() + + "项, 详情" + + en.getValue() + .details.size() + + "项, 奖励" + + en.getValue() + .rewards.size() + + "项"); + } + + List list = + new ArrayList<>( + merged.values()); + questCache.put(name, list); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 解析完成 → " + + list.size() + " 个阶段" + + " (来自 " + fileCount + + " 个文件)"); + } + + private Integer extractStageNumber( + String title) { + if (title == null) return null; + String t = title.toLowerCase().trim(); + + // 含阶段/stage/关/chapter 关键词 + boolean isStage = + t.contains("阶段") + || t.contains("stage") + || t.contains("关卡") + || t.contains("章节") + || t.contains("chapter"); + + if (isStage) { + String digits = title + .replaceAll("[^0-9]", ""); + if (!digits.isEmpty()) { + try { + return Integer.parseInt(digits); + } catch (NumberFormatException e) { + // fall through + } + } + return parseChineseNumber(title); + } + + // 无关键词但以数字开头(如 "1. 挖掘") + String trimmed = t + .replaceAll("^[.、\\s]+", "") + .trim(); + if (trimmed.length() > 0 + && Character.isDigit( + trimmed.charAt(0))) { + String num = trimmed + .replaceAll("[^0-9]", ""); + if (!num.isEmpty()) { + try { + return Integer.parseInt(num); + } catch (NumberFormatException e) { + // fall through + } + } + } + + return null; + } +// ========== 领取记录 ========== + + /** + * 标记已领取某阶段奖励 + */ + public void markClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return; + // 用阶段编号代替显示名,保证稳定 + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + storage.setCondition(player, key); + } + + public boolean hasClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return false; + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + return toBool( + storage.getCondition( + player, key)); + } + + private int parseChineseNumber( + String title) { + String[] cn = {"一", "二", "三", + "四", "五", "六", "七", + "八", "九", "十"}; + if (title.contains("十")) { + for (int j = 0; j < 9; j++) { + if (title.contains(cn[j])) { + return 10 + j + 1; + } + } + return 10; + } + for (int i = 0; i < 9; i++) { + if (title.contains(cn[i])) { + return i + 1; + } + } + return 0; + } + + // ========== md 解析核心 ========== + + /** + * 读取文件所有行 + */ + private List readFileLines( + File file) { + List lines = + new ArrayList<>(); + try { + BufferedReader br = + new BufferedReader( + new FileReader(file)); + String line; + while ((line = br.readLine()) + != null) { + lines.add(line); + } + br.close(); + } catch (Exception e) { + plugin.getLogger().warning( + "[QuestTracker] 读取 " + + file.getName() + + " 失败: " + + e.getMessage()); + } + return lines; + } + + /** + * 自动检测格式并解析单个文件 + */ + private List parseMdFile( + File file, String category) { + List lines = + readFileLines(file); + if (lines.isEmpty()) + return new ArrayList<>(); + + // 检测分隔符 + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) + hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + // # 分隔格式 + return parseSections( + lines, category); + } else if (hasDash) { + // ---- 分隔格式:转换为 # 后复用 + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + title = "separator"; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseSections( + converted, category); + } else { + // 无分隔符:整文件一个阶段 + return parseFlat(lines, category); + } + } + + /** + * # 分隔格式 → 区块 → 阶段 + */ + private List parseSections( + List lines, + String category) { + // 拆分为区块 + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + + // # 开头 → 新区块 + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add( + new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } + // 空行/分隔线 → 跳过 + else if (t.isEmpty() + || t.startsWith("----") + || t.startsWith("====") + || t.startsWith("***")) { + // skip + } + // 普通行 → 区块内容 + else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + curLines.add(cond); + } + } + } + // 保存最后一个区块 + if (curTitle != null) { + sections.add( + new Section( + curTitle, curLines)); + } + + // 直接转为 QuestFile 列表 + List result = + new ArrayList<>(); + int num = 1; + for (Section sec : sections) { + QuestFile qf = new QuestFile(); + qf.displayName = + extractDisplayName( + sec.title); + qf.fileName = category; + qf.conditions = + new ArrayList<>(sec.lines); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + result.add(qf); + num++; + } + return result; + } + + /** + * 无分隔符:整文件 = 一个阶段 + */ + private List parseFlat( + List lines, + String category) { + List list = + new ArrayList<>(); + QuestFile qf = makeDefaultStage( + category); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + qf.conditions.add(cond); + } + } + if (!qf.conditions.isEmpty()) { + list.add(qf); + } + return list; + } +// ========== Vault 经济奖励发放 ========== + + /** + * 发放阶段奖励 + * 含经济内容的走 Vault API + */ + public void claimRewards( + Player player, + QuestFile qf) { + if (qf == null + || qf.rewards == null + || qf.rewards.isEmpty()) { + player.sendMessage( + "§c无可领取的奖励"); + return; + } + + net.milkbowl.vault.economy.Economy econ = + ((Main) plugin).getEconomy(); + + int sentCount = 0; + + for (String reward : qf.rewards) { + String rt = reward.trim(); + if (rt.isEmpty()) continue; + + // 检查可选奖励标记 + if (hasOptionalMarker(rt)) { + if (!areAllConditionsMet(player.getName(), qf)) + player.sendMessage( + "§7[可选奖励] " + + "需完成所有可选条件: " + + rt); + continue; + } + + + // 按 + 分割子项 + String[] parts = + rt.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 尝试发经济 + double amt = + extractEconomyAmount(part); + if (amt > 0 && econ != null) { + double before = + econ.getBalance(player); + econ.depositPlayer( + player, amt); + player.sendMessage( + "§a§l[任务奖励] §f" + + part + + " §7(余额: §e" + + String.format( + "%.2f", before) + + " §7→ §a" + + String.format( + "%.2f", + econ.getBalance( + player)) + + "§7)"); + // 通知提成系统 + if (plugin instanceof Main) { + ((Main) plugin) + .getCommission() + .payCommission( + player + .getName(), + amt); + } + + sentCount++; + continue; + } + + // 尝试发物品 + java.util.List items = + parseRewardItems(part); + if (!items.isEmpty()) { + for (ItemStack is : items) { + player.getInventory() + .addItem(is); + } + player.sendMessage( + "§a§l[任务奖励] §f" + + part); + sentCount++; + continue; + } + + // 非经济非物品 → 仅消息 + player.sendMessage( + "§a§l[任务奖励] §f" + + part); + sentCount++; + } + } + + plugin.getLogger().info( + "[QuestTracker] " + + player.getName() + + " 领取了 \"" + + qf.displayName + + "\" 共 " + sentCount + + " 项奖励"); + } + /** + * 检查奖励文本是否含可选标记 + */ + private boolean hasOptionalMarker( + String text) { + return text.contains("(可选)") + || text.contains("(进阶)") + || text.contains("【可选】") + || text.contains("【进阶】"); + } + + /** + * 检查阶段所有条件(含可选)是否全部满足 + * 用于决定可选奖励是否发放 + */ + private boolean areAllConditionsMet( + String player, QuestFile qf) { + if (qf.conditions == null + || qf.conditions.isEmpty()) + return true; + for (String cond : qf.conditions) { + if (!isConditionMet(player, cond)) + return false; + } + return true; + } + + /** + * 从奖励文本中提取经济金额 + * 支持: "500游戏币" "500" "金币500" + * "$500" "1000-5000"(随机) "1000~5000" + */ + private double extractEconomyAmount( + String text) { + String t = text.replaceAll(" ", ""); + + // 必须包含经济关键词才算经济奖励 + boolean hasEcon = + t.contains("游戏币") + || t.contains("金币") + || t.contains("积分") + || t.contains("块钱") + || t.contains("元") + || t.contains("$") + || t.contains("经济") + || t.contains("余额") + || t.contains("发钱"); + + if (!hasEcon) return 0; + + // 范围: "1000-5000" 或 "1000~5000" + Matcher rm = Pattern + .compile("(\\d+)[-~](\\d+)") + .matcher(t); + if (rm.find()) { + double min = Double.parseDouble( + rm.group(1)); + double max = Double.parseDouble( + rm.group(2)); + return min + Math.random() + * (max - min); + } + + // 纯数字(含 $ 前缀) + Matcher nm = Pattern + .compile("\\$?(\\d+(\\.\\d+)?)") + .matcher(t); + if (nm.find()) { + return Double.parseDouble( + nm.group(1)); + } + + return 0; + } + /** + * 解析奖励物品文本 + * "皮革套装*1" → 4件皮革装备 + * "木剑*1" → 1把木剑 + * "钻石*64" → 64颗钻石 + */ + private List parseRewardItems( + String text) { + List result = + new ArrayList<>(); + + // 去掉条件前缀 + // 如 "完成注册登录:皮革套装*1" + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + + // 按 + 分割多个物品 + String[] parts = + text.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 跳过经济文本 + if (extractEconomyAmount(part) > 0) + continue; + + int amount = 1; + String itemName = part; + + // 解析 *N 数量 + if (part.contains("*")) { + String[] sp = + part.split("\\*"); + itemName = sp[0].trim(); + try { + amount = Integer.parseInt( + sp[1].trim()); + } catch (Exception e) { + amount = 1; + } + } + + List mats = + matchMaterials(itemName); + for (Material m : mats) { + int give = m.name() + .contains("HELMET") + || m.name() + .contains("CHESTPLATE") + || m.name() + .contains("LEGGINGS") + || m.name() + .contains("BOOTS") + ? 1 : amount; + result.add( + new ItemStack(m, give)); + } + } + return result; + } + + /** + * 中文物品名 → Material 列表 + * 套装返回多个部件 + */ + private List matchMaterials( + String name) { + List list = + new ArrayList<>(); + String n = name.toLowerCase().trim(); + + // 套装 + if (n.contains("皮革套装") + || n.contains("皮套装")) { + list.add(Material.LEATHER_HELMET); + list.add(Material.LEATHER_CHESTPLATE); + list.add(Material.LEATHER_LEGGINGS); + list.add(Material.LEATHER_BOOTS); + return list; + } + if (n.contains("铁套装") + || n.contains("铁甲")) { + list.add(Material.IRON_HELMET); + list.add(Material.IRON_CHESTPLATE); + list.add(Material.IRON_LEGGINGS); + list.add(Material.IRON_BOOTS); + return list; + } + if (n.contains("钻石套装") + || n.contains("钻甲")) { + list.add(Material.DIAMOND_HELMET); + list.add(Material.DIAMOND_CHESTPLATE); + list.add(Material.DIAMOND_LEGGINGS); + list.add(Material.DIAMOND_BOOTS); + return list; + } + // 木制工具套装 + if (n.contains("木制工具") + || n.contains("木工具")) { + list.add(Material.WOODEN_SWORD); + list.add(Material.WOODEN_PICKAXE); + list.add(Material.WOODEN_AXE); + list.add(Material.WOODEN_SHOVEL); + return list; + } + if (n.contains("石制工具") + || n.contains("石工具")) { + list.add(Material.STONE_SWORD); + list.add(Material.STONE_PICKAXE); + list.add(Material.STONE_AXE); + list.add(Material.STONE_SHOVEL); + return list; + } + if (n.contains("铁工具")) { + list.add(Material.IRON_SWORD); + list.add(Material.IRON_PICKAXE); + list.add(Material.IRON_AXE); + list.add(Material.IRON_SHOVEL); + return list; + } + + // 单个物品 + if (n.contains("木剑")) + list.add(Material.WOODEN_SWORD); + else if (n.contains("木镐")) + list.add(Material.WOODEN_PICKAXE); + else if (n.contains("铁剑")) + list.add(Material.IRON_SWORD); + else if (n.contains("铁镐")) + list.add(Material.IRON_PICKAXE); + else if (n.contains("钻石剑")) + list.add(Material.DIAMOND_SWORD); + else if (n.contains("钻石镐")) + list.add(Material.DIAMOND_PICKAXE); + else if (n.contains("钻石")) + list.add(Material.DIAMOND); + else if (n.contains("铁锭")) + list.add(Material.IRON_INGOT); + else if (n.contains("金锭") + || n.contains("金块")) + list.add(Material.GOLD_INGOT); + else if (n.contains("煤炭")) + list.add(Material.COAL); + else if (n.contains("木棍")) + list.add(Material.STICK); + else if (n.contains("弓")) + list.add(Material.BOW); + else if (n.contains("箭")) + list.add(Material.ARROW); + else if (n.contains("床")) + list.add(Material.RED_BED); + else { + // 尝试直接匹配 Material 名 + try { + list.add(Material.valueOf( + n.toUpperCase() + .replaceAll("[^A-Z_]", + ""))); + } catch (Exception e) { + // 未知物品 + } + } + return list; + } + + private QuestFile getOrCreateStage( + TreeMap map, + int key, + String category) { + if (!map.containsKey(key)) { + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + key; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + } + return map.get(key); + } + + /** + * 单个文件的区块 → 阶段 + * defaultStageNum: 文件无阶段标题时的默认编号 + */ + private void buildSingleFileStages( + List
sections, + String category, + TreeMap stageMap, + int defaultStageNum) { + Integer curKey = null; + + for (Section sec : sections) { + Integer stageNum = + extractStageNumber(sec.title); + + if (stageNum != null) { + curKey = stageNum; + } else if (curKey == null) { + curKey = defaultStageNum; + } + + QuestFile qf = getOrCreateStage( + stageMap, curKey, category); + + String displayName = + extractDisplayName(sec.title); + + // 设置显示名 + if (!displayName.isEmpty() + && qf.displayName + .startsWith("阶段 ")) { + qf.displayName = displayName; + } + + // 路由 + if (stageNum != null + && sec.lines.isEmpty()) { + if (!displayName.isEmpty()) { + qf.displayName = displayName; + } + } + else if (isRewardTitle(sec.title)) { + // 奖励 → 只进 rewards + qf.rewards.addAll(sec.lines); + } + else if (isDetailsTitle(sec.title)) { + // 详情 → 进 details + conditions + qf.details.addAll(sec.lines); + for (String line : sec.lines) { + if (!qf.conditions + .contains(line)) { + qf.conditions.add(line); + } + } + } + else if (isConditionsTitle( + sec.title)) { + // 条件 → 只进 conditions + qf.conditions.addAll(sec.lines); + } + else { + // 其他 → 条件 + qf.conditions.addAll(sec.lines); + } + } + } + + /** + * "主线任务-斗志昂扬" → "斗志昂扬" + * "支线-钓鱼" → "钓鱼" + * "阶段 1" → "阶段 1" + * "第一阶段" → "第一阶段" + */ + private String extractDisplayName( + String title) { + if (title == null) return ""; + // 按 - 或 — 或 – 分割 + String[] parts = title.split( + "\\s*[-—–]\\s*", 2); + if (parts.length >= 2) { + String right = parts[1].trim(); + if (!right.isEmpty()) { + return right; + } + } + return title; + } + private DatabaseManager getDb() { + return ((Main) plugin).getDb(); + } + + private boolean isDetailsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("任务详情") + || t.contains("详情") + || t.contains("任务说明") + || t.contains("任务目标") + || t.contains("description") + || t.contains("任务要求"); + } + + private boolean isConditionsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("解锁条件") + || t.contains("条件") + || t.contains("前置条件") + || t.contains("前置要求") + || t.contains("解锁") + || t.contains("condition"); + } + + + private void ensureStage( + TreeMap map, + String category, + QuestFile current, + int counter) { + if (current != null) return; + // 用大数字确保排在有编号阶段后面 + int key = 1000 + counter; + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + (counter + 1); + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + // 更新调用方的引用(通过返回值) + // 这里用 map 操作,current 无法直接更新 + // 所以改用返回值方式 + } + /** + * 自动将条件中的奖励内容移到 rewards + */ + private void separateRewards(QuestFile qf) { + List remaining = + new ArrayList<>(); + for (String line : qf.conditions) { + if (isRewardContent(line)) { + if (!qf.rewards.contains(line)) { + qf.rewards.add(line); + } + } else { + remaining.add(line); + } + } + qf.conditions = remaining; + } + private boolean checkStageDependency( + String player, + String condition) { + Matcher m = Pattern.compile( + "第([一二三四五六七八九十\\d]+)" + + "阶段") + .matcher(condition); + + while (m.find()) { + String numStr = m.group(1); + int stageNum = + parseChineseNumber(numStr); + if (stageNum == 0) { + try { + stageNum = + Integer.parseInt(numStr); + } catch (Exception e) { + return false; + } + } + + boolean stageDone = false; + for (List quests + : questCache.values()) { + for (QuestFile qf : quests) { + Integer qfNum = + extractStageNumber( + qf.displayName); + if (qfNum != null + && qfNum == stageNum) { + if (isStageComplete(player, + qf.conditions)) { + stageDone = true; + } + break; + } + } + if (stageDone) break; + } + + if (!stageDone) return false; + } + return true; + } + + /** + * 判断一行内容是否为奖励 + */ + private boolean isRewardContent( + String line) { + String l = line.toLowerCase(); + // 含货币关键词 + if (l.contains("游戏币") + || l.contains("金币") + || l.contains("积分") + || l.contains("钻石") + || l.contains("发钱") + || l.contains("发放") + || l.contains("经验") + || l.contains("等级") + || l.contains("经济") + || l.contains("余额") + || l.contains("块钱")) + return true; + // 纯数字+单位(如 "500游戏币") + if (l.matches( + ".*\\d+\\s*(游戏币|金币|元|积分|" + + "钻石|块钱|经验|级|经济|余额).*")) + return true; + // $前缀(如 "$500") + if (l.matches(".*\\$\\d+.*")) + return true; + // 范围格式(如 "1000-5000" 或 "1000~5000") + if (l.matches(".*\\d+[-~]\\d+.*") + && !l.contains("阶段") + && !l.contains("stage")) + return true; + return false; + } + + // ========== 标题分类 ========== + + private boolean isStageTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("阶段") + || t.contains("stage") + || t.contains("第一") + || t.contains("第二") + || t.contains("第三") + || t.contains("第四") + || t.contains("第五"); + } + + private boolean isRewardTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("奖励") + || t.contains("奖品") + || t.contains("报酬") + || t.contains("reward") + || t.contains("奖赏") + || t.contains("礼包内容") + || t.contains("礼包") + || t.contains("物品奖励"); + } + + + private QuestFile makeDefaultStage( + String category) { + QuestFile qf = new QuestFile(); + qf.displayName = "第一阶段"; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + return qf; + } + + private static class Section { + final String title; + final List lines; + Section(String title, + List lines) { + this.title = title; + this.lines = lines; + } + } + + // ========== 公开查询 ========== + + public List getQuests( + String category) { + List list = + questCache.get(category); + return list != null ? list + : new ArrayList<>(); + } + + public int getCompletedStageCount( + String player, + List quests) { + int count = 0; + if (quests == null) return 0; + for (QuestFile qf : quests) { + if (isStageComplete(player, + qf.conditions)) { + count++; + } + } + return count; + } + + public QuestFile getCurrentStage( + String player, + List quests) { + if (quests == null) return null; + for (QuestFile qf : quests) { + if (!isStageComplete( + player, qf.conditions)) { + return qf; + } + } + return null; + } + + public int getStageNumber( + List quests, + QuestFile qf) { + if (quests == null || qf == null) + return 0; + return quests.indexOf(qf) + 1; + } + + public int getStageNumber( + QuestFile qf) { + if (qf == null) return 0; + List quests = + getQuests(qf.fileName); + return getStageNumber(quests, qf); + } + + public boolean isStageCompleted( + String player, + QuestFile qf) { + if (qf == null) return false; + return isStageComplete( + player, qf.conditions); + } + + public List getUnmetConditions( + String player, + List conditions) { + List unmet = + new ArrayList<>(); + if (conditions == null) return unmet; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) { + unmet.add( + getProgress( + player, cond)); + } + } + return unmet; + } + + public String getProgress( + String player, + String rawCond) { + if (player == null + || rawCond == null) + return "§7未知"; + + String key = stripOptional(rawCond); + // 打卡群系进度 + if (key.contains("打卡") + && (key.contains("群系") + || key.contains("风景点"))) { + + String biome = + extractBiome(key); + + if ("ALL".equals(biome)) { + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + + if (!biome.isEmpty()) { + int catCount = getCounter( + player, + "biomecat_" + biome); + return "§7打卡 " + biome + + "群系: §e" + + catCount + "/1"; + } + + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + + + + // 阶段依赖 + if (key.contains("完成") + && key.contains("阶段")) { + boolean done = checkStageDependency( + player, key); + return done + ? "§a✓ " + key + : "§c✗ " + key; + } + + + // 注册登录(检查主数据库是否已注册) + // 注册登录(检查主数据库是否已注册) + if ("完成注册登录".equals(key)) { + boolean done = getDb() != null + && getDb().userExists( + player); + return done + ? "§a✓ 完成注册登录" + : "§c✗ 完成注册登录"; + } + + // 绑定邮箱 + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + boolean done = val != null + && !val.toString().isEmpty(); + return done + ? "§a✓ 绑定Email" + : "§c✗ 绑定Email"; + } + + // 在线时间 + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + needed + "小时"; + } + + // 打招呼/发消息/聊天 + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + int cur = getCounter( + player, "CHAT"); + return "§7聊天: §e" + cur + "/1"; + } + + // 破坏/放置方块 + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return "§7方块操作: §e" + + total + "/" + needed; + } + + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int totalDays = getIntField( + player, + "total_checkin_days"); + if (totalDays == 0) { + totalDays = getIntField( + player, + "checkin_streak"); + } + return "§7连续登录: §e" + + totalDays + "/" + needed + "天"; + } + + + // 挖矿 + if (key.startsWith("MINE:")) { + String[] parts = key.split(":"); + if (parts.length >= 3) { + int needed = + parseInt(parts[2]); + int cur = getCounter( + player, + "MINE:" + parts[1]); + return "§7挖 " + parts[1] + + ": §e" + cur + + "/" + needed; + } + } + + // 击杀生物 + if (key.startsWith("KILL:")) { + String[] parts = key.split(":"); + if (parts.length >= 3) { + int needed = + parseInt(parts[2]); + int cur = getCounter( + player, + "KILL:" + parts[1]); + return "§7杀 " + parts[1] + + ": §e" + cur + + "/" + needed; + } + } + + // 击杀玩家 + if (key.startsWith("KILL_PLAYER")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "KILL_PLAYER"); + return "§7击杀玩家: §e" + + cur + "/" + needed; + } + + // 合成 + if (key.startsWith("CRAFT")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "CRAFT"); + return "§7合成: §e" + + cur + "/" + needed; + } + + // 钓鱼 + if (key.startsWith("FISH")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "FISH"); + return "§7钓鱼: §e" + + cur + "/" + needed; + } + + // 在线时长(毫秒) + if (key.startsWith("PLAYTIME")) { + String[] parts = key.split(":"); + long needed = parts.length >= 2 + ? Long.parseLong(parts[1]) + : 0; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + formatMs(needed); + } + // 上报村庄 + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7上报村庄: §e" + + cur + "/" + needed; + } + + // 探索村庄 + if (key.contains("探索") + && key.contains("村庄")) { + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7探索村庄: §e" + + cur + "/1"; + } + // ===== 修改后 ===== + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + + // 优先使用 total_checkin_days(累计天数) + int totalDays = getIntField( + player, + "total_checkin_days"); + if (totalDays == 0) { + totalDays = getIntField( + player, + "checkin_streak"); + } + if (totalDays == 0) { + totalDays = getIntField( + player, "streak"); + } + + plugin.getLogger().info( + "[QuestTracker] 登录检测: " + + player + + " 需要=" + needed + + " 实际(累计天数)=" + totalDays); + return "§7近7天登录: §e" + + totalDays + "/" + + needed + "天"; + + + } + + + // 完成新手任务全部 + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7新手任务: §e" + + done + "/" + + quests.size(); + } + + // 完成主线任务全部 + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7主线任务: §e" + + done + "/" + + quests.size(); + } + + return "§7" + key; + } + private int getIntField( + String player, String field) { + try { + Object val = getDb().getField( + player, field); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + if (val != null) { + return Integer.parseInt( + val.toString().trim()); + } + } catch (Exception e) { + // 忽略 + } + return 0; + } + + public List checkInBiome( + String player, + String rawBiomeKey, + String worldName, + double x, double y, double z) { + List matched = + new ArrayList<>(); + + // 32格去重 + String posKey = + player + "_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + String lastPos = + lastCheckInPos.get(posKey); + if (curPos.equals(lastPos)) { + plugin.getLogger().info( + "[QuestTracker] 打卡忽略: " + + player + + " 32格内重复"); + return matched; + } + lastCheckInPos.put(posKey, curPos); + + String category = + mapBiomeCategory( + rawBiomeKey, worldName); + + // 记录总打卡 + int allCur = getCounter( + player, "biome_ALL"); + setCounter(player, "biome_ALL", + allCur + 1); + + // 记录分类打卡 + if (!category.isEmpty() + && !"主世界" + .equals(category)) { + String catKey = + "biomecat_" + category; + int catCur = getCounter( + player, catKey); + setCounter(player, catKey, + catCur + 1); + } + + // 记录具体群系 + String bioKey = + "biome_" + rawBiomeKey; + int bioCur = getCounter( + player, bioKey); + setCounter(player, bioKey, + bioCur + 1); + + // 检查所有任务条件是否匹配 + List categories = + Arrays.asList( + "新人任务", + "主线任务", + "支线任务"); + + for (String cat : categories) { + List quests = + getQuests(cat); + for (QuestFile qf : quests) { + if (isStageCompleted( + player, qf)) + continue; + for (String cond + : qf.conditions) { + if (isOptional(cond)) + continue; + String condKey = + stripOptional(cond); + if (!condKey + .contains("打卡") + || !condKey + .contains("群系")) + continue; + String target = + extractBiome(condKey); + boolean hit = false; + if ("ALL".equals(target) + || "风景点" + .equals(target)) { + hit = true; + } else if (!target.isEmpty() + && target.equals( + category)) { + hit = true; + } + if (hit && !matched + .contains(cond)) { + matched.add(cond); + } + } + } + } + + return matched; + } + + + /** + * 从条件文本中提取目标群系分类 + * "打卡上报地狱任意群系" → "下界" + * "打卡任意海洋群系" → "海洋" + * "打卡任意群系" → "ALL" + * "打卡任意风景点" → "" + */ + private String extractBiome(String text) { + if (text == null) return ""; + // 逐个匹配分类关键词 + for (Map.Entry en + : CATEGORY_KEYWORDS.entrySet()) { + if (text.contains(en.getKey())) { + return en.getValue(); + } + } + if (text.contains("风景点") + || text.contains("风景")) { + return "ALL"; + } + + // 有"群系"但无具体分类 → ALL + if (text.contains("群系")) { + return "ALL"; + } + return ""; + } + + + // ========== 标记条件 ========== + + public void markConditionComplete( + String player, + String conditionName) { + if (player == null + || conditionName == null) + return; + storage.setCondition( + player, conditionName); + } + + // ========== 事件回调 ========== + + public void onBlockMined( + String player, String material) { + String key = "MINE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + // 总计数 + setCounter(player, "MINE:ALL", + getCounter(player, "MINE:ALL") + + 1); + } + public boolean onReportVillage( + String player, + String worldName, + double x, double y, double z) { + String dbKey = "village_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + Object lastObj = + storage.getCondition( + player, dbKey); + String lastPos = lastObj != null + ? lastObj.toString() : ""; + if (curPos.equals(lastPos)) { + return false; + } + storage.setConditionRaw( + player, dbKey, curPos); + + String key = "REPORT_VILLAGE"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + return true; + } + + public void onMobKill( + String player, String mobType) { + String key = "KILL:" + mobType; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayerKill( + String player) { + String key = "KILL_PLAYER"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onItemCrafted( + String player) { + String key = "CRAFT"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onFishCaught( + String player) { + String key = "FISH"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayTime( + String player, long ms) { + String key = "PLAYTIME"; + long cur = getCounterLong( + player, key); + setCounter(player, key, cur + ms); + } + + // ========== 内部:条件判定 ========== + + private boolean isStageComplete( + String player, + List conditions) { + if (conditions == null + || conditions.isEmpty()) + return true; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) + return false; + } + return true; + } + + public boolean isConditionMet( + String player, + String rawCond) { + if (player == null + || rawCond == null) + return false; + + String key = stripOptional(rawCond); + // 打卡群系 + if (key.contains("打卡") + && (key.contains("群系") + || key.contains("风景点"))) { + + String biome = + extractBiome(key); + + if ("ALL".equals(biome)) { + int total = getCounter( + player, "biome_ALL"); + return total >= 1; + } + + if (!biome.isEmpty()) { + int catCount = getCounter( + player, + "biomecat_" + biome); + return catCount >= 1; // ← 加这一行 + } + + return getCounter( + player, "biome_ALL") >= 1; + } + + // 注册登录 + + if ("完成注册登录".equals(key)) { + return getDb() != null + && getDb().userExists( + player); + } + + + // 绑定邮箱 + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + return val != null + && !val.toString().isEmpty(); + } + + // 在线时间 + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return cur >= needed * 3600000L; + } + + // 打招呼/发消息/聊天 + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + return getCounter( + player, "CHAT") >= 1; + } + + // 破坏/放置方块 + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return total >= needed; + } + + // 连续登录 + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + // 优先用累计天数,因为 + // streak 可能大于 total(数据不一致时) + int totalDays = getIntField( + player, + "total_checkin_days"); + if (totalDays == 0) { + totalDays = getIntField( + player, + "checkin_streak"); + } + return totalDays >= needed; + } + + // 挖矿 + if (key.startsWith("MINE:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "MINE:" + p[1]) + >= parseInt(p[2]); + } + } + + // 击杀生物 + if (key.startsWith("KILL:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "KILL:" + p[1]) + >= parseInt(p[2]); + } + } + + // 击杀玩家 + if (key.startsWith("KILL_PLAYER")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter(player, + "KILL_PLAYER") >= n; + } + + // 合成 + if (key.startsWith("CRAFT")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "CRAFT") >= n; + } + + // 钓鱼 + if (key.startsWith("FISH")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "FISH") >= n; + } + + // 在线时长 + if (key.startsWith("PLAYTIME")) { + String[] p = key.split(":"); + long n = p.length >= 2 + ? Long.parseLong(p[1]) : 0; + return getCounterLong(player, + "PLAYTIME") >= n; + } + // 上报村庄 + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "REPORT_VILLAGE"); + return cur >= needed; + } + + // 探索村庄 + if (key.contains("探索") + && key.contains("村庄")) { + return getCounter( + player, "REPORT_VILLAGE") + >= 1; + } + // 近7天登录记录 + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + // 优先使用 total_checkin_days + int totalDays = getIntField( + player, + "total_checkin_days"); + if (totalDays == 0) { + totalDays = getIntField( + player, + "checkin_streak"); + } + if (totalDays == 0) { + totalDays = getIntField( + player, "streak"); + return totalDays >= needed; + + } + + + // 诊断:读取数据库所有字段 + try { + var user = getDb().getUser( + player); + if (user != null) { + plugin.getLogger().info( + "[QuestTracker] " + + "登录诊断 " + + player + ": " + + user.toString()); + } else { + plugin.getLogger().info( + "[QuestTracker] " + + "登录诊断: " + + player + + " getUser返回null"); + } + } catch (Exception e) { + plugin.getLogger().info( + "[QuestTracker] " + + "登录诊断异常: " + + e.getMessage()); + } + + int streak = getIntField( + player, + "checkin_streak"); + if (streak == 0) { + streak = getIntField( + player, "streak"); + } + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } + + plugin.getLogger().info( + "[QuestTracker] 登录检测: " + + player + + " 需要=" + needed + + " 实际=" + streak); + + return streak >= needed; + } + + // 完成新手任务全部内容 + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + + // 完成主线任务全部内容 + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + + return false; + } + + public void debugLogin(String player) { + try { + var user = getDb().getUser(player); + if (user != null) { + plugin.getLogger().info( + "[QuestTracker] " + + "=== " + player + + " 数据库全部字段 ==="); + for (var entry + : user.entrySet()) { + plugin.getLogger().info( + "[QuestTracker] " + + entry.getKey() + + " = " + + entry.getValue() + + " (" + + (entry.getValue() + != null + ? entry.getValue() + .getClass() + .getSimpleName() + : "null") + + ")"); + } + } else { + plugin.getLogger().info( + "[QuestTracker] " + + "getUser返回null: " + + player); + } + } catch (Exception e) { + plugin.getLogger().info( + "[QuestTracker] " + + "debug异常: " + + e.getMessage()); + } + } + + // ========== 内部:计数器 ========== + + private int getCounter( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + return 0; + } + + private long getCounterLong( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).longValue(); + } + return 0; + } + + private void setCounter( + String player, String key, + int value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + private void setCounter( + String player, String key, + long value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + // ========== 工具 ========== + + private boolean toBool(Object val) { + if (val == null) return false; + if (val instanceof Boolean) + return (Boolean) val; + if (val instanceof Number) + return ((Number) val) + .intValue() != 0; + String s = String.valueOf(val); + return "1".equals(s) + || "true" + .equalsIgnoreCase(s); + } + + public boolean isOptional(String cond) { + if (cond == null) return false; + String c = cond; + return c.contains("(进阶)") + || c.contains("(可选)") + || c.contains(",可选)") + || c.contains(",可选)") + || c.contains("【可选】") + || c.contains("【进阶】"); + } + + + private String stripOptional(String cond) { + if (cond == null) return ""; + return cond.replace("(进阶)", "") + .replace("(可选)", "") + .replace(",可选)", "") + .replace(",可选)", "") + .replace("【可选】", "") + .replace("【进阶】", "") + .trim(); + } + + private int parseInt(String s) { + + try { + return Integer.parseInt(s.trim()); + } catch (Exception e) { + return 0; + } + } + private Object getDbField( + String player, String field) { + Main main = (Main) plugin; + return main.getDb().getField( + player, field); + } + + /** + * 从文本中提取第一个数字 + * "在线时间≥1小时" → 1 + * "破坏/放置方块100个" → 100 + */ + private int parseNumberFromText( + String text) { + java.util.regex.Matcher m = + java.util.regex.Pattern + .compile("(\\d+)") + .matcher(text); + if (m.find()) { + try { + return Integer.parseInt( + m.group(1)); + } catch (Exception e) { + return 0; + } + } + return 0; + } + public void onBlockPlaced( + String player, String material) { + String key = "PLACE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + setCounter(player, "PLACE:ALL", + getCounter(player, "PLACE:ALL") + + 1); + } + + public void onPlayerChat( + String player) { + String key = "CHAT"; + setCounter(player, key, + getCounter(player, key) + 1); + } + + private String formatMs(long ms) { + long sec = ms / 1000; + long min = sec / 60; + long hr = min / 60; + if (hr > 0) + return hr + "小时" + + (min % 60) + "分"; + if (min > 0) + return min + "分" + + (sec % 60) + "秒"; + return sec + "秒"; + } + + // ========== 数据结构 ========== + + public static class QuestFile { + public String displayName = ""; + public String fileName = ""; + public List details = + new ArrayList<>(); + public List rewards = + new ArrayList<>(); + public List conditions = + new ArrayList<>(); + + public boolean hasConditions() { + return conditions != null + && !conditions.isEmpty(); + } + } +} diff --git a/src/main/java/Sdf1_login/RadioDownloadListener.java b/src/main/java/Sdf1_login/RadioDownloadListener.java new file mode 100644 index 0000000..a9c4d0c --- /dev/null +++ b/src/main/java/Sdf1_login/RadioDownloadListener.java @@ -0,0 +1,118 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerResourcePackStatusEvent; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +public class RadioDownloadListener + implements Listener { + + private final Logger log; + private final Main plugin; + private final Map retryCount = + new ConcurrentHashMap<>(); + private static final int MAX_RETRY = 2; + + public RadioDownloadListener(Main plugin) { + this.plugin = plugin; + this.log = plugin.getLogger(); + } + + @EventHandler + public void onStatus( + PlayerResourcePackStatusEvent e) { + Player p = e.getPlayer(); + UUID uuid = p.getUniqueId(); + String status = e.getStatus().name(); + + log.info("[Radio] " + p.getName() + + " status: " + status); + + switch (status) { + case "ACCEPTED": + break; + + case "DOWNLOADED": + break; + + case "SUCCESS": + log.info("[Radio] " + p.getName() + + " 资源包加载成功"); + retryCount.remove(uuid); + break; + + case "FAILED_DOWNLOAD": + handleRetry(p, uuid); + break; + + case "DECLINED": + log.info("[Radio] " + p.getName() + + " 拒绝下载"); + p.sendMessage( + "§e[Radio] §f你拒绝了资源包"); + retryCount.remove(uuid); + break; + + case "DISCARDED": + log.warning("[Radio] " + p.getName() + + " DISCARDED"); + p.sendMessage( + "§c[Radio] §f资源包下载失败" + + ",可能原因:"); + p.sendMessage( + "§71. CDN/防火墙拦截"); + p.sendMessage( + "§72. SSL证书问题"); + retryCount.remove(uuid); + break; + + case "INVALID_URL": + log.warning("[Radio] " + p.getName() + + " URL无效"); + retryCount.remove(uuid); + break; + + default: + log.info("[Radio] " + p.getName() + + " 未知: " + status); + break; + } + } + + private void handleRetry(Player p, UUID uuid) { + int count = retryCount + .getOrDefault(uuid, 0) + 1; + retryCount.put(uuid, count); + + log.warning("[Radio] " + p.getName() + + " 下载失败 (" + count + + "/" + MAX_RETRY + ")"); + + if (count <= MAX_RETRY) { + p.sendMessage( + "§e[Radio] §f下载失败,重试中... (" + + count + "/" + MAX_RETRY + ")"); + Bukkit.getScheduler().runTaskLater( + plugin, + () -> { + if (p.isOnline() + && plugin.radio != null) { + plugin.radio + .sendResourcePack(p); + } + }, 60L); + } else { + p.sendMessage( + "§c[Radio] §f资源包加载失败," + + "部分音乐无法播放"); + retryCount.remove(uuid); + } + } +} diff --git a/src/main/java/Sdf1_login/RadioManager.java b/src/main/java/Sdf1_login/RadioManager.java new file mode 100644 index 0000000..7e79c0e --- /dev/null +++ b/src/main/java/Sdf1_login/RadioManager.java @@ -0,0 +1,874 @@ +package Sdf1_login; + +import com.sun.net.httpserver.HttpServer; +import org.bukkit.*; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; + +import java.io.*; +import java.net.*; +import java.nio.file.*; +import java.security.MessageDigest; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; +import java.util.zip.*; + +public class RadioManager { + + private final Main plugin; + private File radioDir; + private File oggDir; + private File packDir; + private File configFile; + private BukkitTask mainRadioTask; + private BukkitTask loginRadioTask; + private final Map loginPlayers = + new ConcurrentHashMap<>(); + private int radioIndex = 0; + private HttpServer httpServer; + private byte[] packBytes; + private String packHash = ""; + private String resourcePackUrl = ""; + private int httpPort = 25566; + private final List sourceFiles = + new ArrayList<>(); + private final List oggFiles = + new ArrayList<>(); + private final Set sending = + Collections.newSetFromMap( + new ConcurrentHashMap<>()); + private String adminTag = "admin"; + + public RadioManager(Main plugin) { + this.plugin = plugin; + } + + // ========== init ========== + public void init() { + radioDir = new File( + plugin.getDataFolder(), "radio"); + oggDir = new File( + plugin.getDataFolder(), + "radio/ogg"); + packDir = new File( + plugin.getDataFolder(), + "radiopack"); + radioDir.mkdirs(); + oggDir.mkdirs(); + packDir.mkdirs(); + + loadRadioConfig(); + doConvert(); + buildPack(); + loadOggFiles(); + startHttpServer(); + + plugin.getLogger().info( + "[Radio] init done: " + + oggFiles.size() + " audio(s)"); + } + + // ========== radio_config.txt ========== + private void loadRadioConfig() { + configFile = new File( + plugin.getDataFolder(), + "radio_config.txt"); + if (!configFile.exists()) { + writeDefaultConfig(); + } + try (BufferedReader br = + new BufferedReader( + new FileReader( + configFile))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() + || line.startsWith("#")) + continue; + int eq = line.indexOf('='); + if (eq <= 0) continue; + String key = line.substring(0, eq) + .trim(); + String val = line.substring(eq + 1) + .trim(); + if (key.equals( + "resource-pack-url")) { + resourcePackUrl = val; + } else if (key.equals( + "http-port")) { + try { + httpPort = Integer + .parseInt(val); + } catch (Exception ignored) {} + } + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] config error: " + + e.getMessage()); + } + loadAdminTag(); + plugin.getLogger().info("[Radio] config: url=" + + (resourcePackUrl.isEmpty() + ? "(none)" : resourcePackUrl) + + " port=" + httpPort); + } + /** + * 从插件设置.txt读取管理标签 + */ + private void loadAdminTag() { + File settingsFile = new File( + plugin.getDataFolder(), + "插件设置.txt"); + if (!settingsFile.exists()) return; + try (BufferedReader br = + new BufferedReader( + new FileReader( + settingsFile))) { + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() + || line.startsWith("#")) + continue; + if (line.startsWith("管理标签=") + || line.startsWith("管理标签 =")) { + int eq = line.indexOf('='); + if (eq > 0) { + adminTag = line + .substring(eq + 1) + .trim(); + } + } + } + } catch (Exception e) { + // ignore + } + plugin.getLogger().info( + "[Radio] admin tag: " + adminTag); + } + + /** + * 检查玩家是否有管理标签 + * 控制台不受限制 + */ + public boolean hasRadioPermission( + CommandSender sender) { + // 控制台直接通过 + if (!(sender instanceof Player)) { + return true; + } + Player p = (Player) sender; + + // 检查Bukkit权限节点 + // 格式: sdf1.admin + if (p.hasPermission("sdf1.admin")) { + return true; + } + + // 检查玩家名是否在admin列表中 + // 你可以扩展这里对接你的标签系统 + return false; + } + + private void writeDefaultConfig() { + try { + plugin.getDataFolder().mkdirs(); + String c = + "# ============ Sdf1 Radio " + + "Config ============\n" + + "#\n" + + "# resource-pack-url\n" + + "# 填外部HTTPS地址\n" + + "# 留空 = 使用内置HTTP" + + "服务\n" + + "#\n" + + "# http-port\n" + + "# 内置HTTP端口" + + " (仅url为空时生效)\n" + + "#\n" + + "# ============ Settings" + + " ==============\n" + + "\n" + + "resource-pack-url=\n" + + "http-port=25566\n"; + Files.write(configFile.toPath(), + c.getBytes("UTF-8")); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] config create fail"); + } + } + + // ========== convert ========== + private void doConvert() { + sourceFiles.clear(); + scanSources(radioDir); + for (File src : sourceFiles) { + String name = src.getName().replaceAll( + "\\.(ogg|mp3|wav|flac)$", + ".ogg"); + File dest = new File(oggDir, name); + if (dest.exists() + && dest.lastModified() + >= src.lastModified()) continue; + if (src.getName().toLowerCase() + .endsWith(".ogg")) { + fileCopy(src, dest); + plugin.getLogger().info( + "[Radio] copy: " + + src.getName()); + } else if (runFfmpeg(src, dest)) { + plugin.getLogger().info( + "[Radio] convert: " + + src.getName()); + } else { + plugin.getLogger().warning( + "[Radio] skip(need ffmpeg): " + + src.getName()); + } + } + } + + private void scanSources(File dir) { + if (!dir.exists()) return; + File[] files = dir.listFiles(); + if (files == null) return; + for (File f : files) { + if (f.isDirectory() + && !f.getName().equals("ogg")) { + scanSources(f); + } else if (f.isFile()) { + String n = f.getName().toLowerCase(); + if (n.endsWith(".ogg") + || n.endsWith(".mp3") + || n.endsWith(".wav") + || n.endsWith(".flac")) { + sourceFiles.add(f); + } + } + } + } + + private void fileCopy(File src, File dest) { + try { + dest.getParentFile().mkdirs(); + Files.copy(src.toPath(), + dest.toPath(), + StandardCopyOption + .REPLACE_EXISTING); + } catch (Exception ignored) {} + } + + private boolean runFfmpeg(File src, File dest) { + try { + dest.getParentFile().mkdirs(); + ProcessBuilder pb = new ProcessBuilder( + "ffmpeg", "-y", "-i", + src.getAbsolutePath(), + "-c:a", "libvorbis", + "-q:a", "5", + dest.getAbsolutePath()); + pb.redirectErrorStream(true); + Process p = pb.start(); + p.waitFor(); + return dest.exists(); + } catch (Exception e) { + return false; + } + } + + // ========== pack ========== + private void buildPack() { + deleteDir(packDir); + packDir.mkdirs(); + List allOggs = new ArrayList<>(); + scanOggs(oggDir, allOggs); + if (allOggs.isEmpty()) { + plugin.getLogger().warning( + "[Radio] no ogg found"); + return; + } + + File soundsDir = new File(packDir, + "assets/minecraft/sounds/custom"); + soundsDir.mkdirs(); + + Map unique = + new LinkedHashMap<>(); + for (File ogg : allOggs) { + String name = ogg.getName() + .replace(".ogg", ""); + unique.putIfAbsent( + name.toLowerCase(), ogg); + } + + StringBuilder json = + new StringBuilder("{\n"); + int idx = 0, total = unique.size(); + for (Map.Entry e + : unique.entrySet()) { + String name = e.getKey(); + File ogg = e.getValue(); + File dest = new File(soundsDir, + name + ".ogg"); + try { + Files.copy(ogg.toPath(), + dest.toPath(), + StandardCopyOption + .REPLACE_EXISTING); + plugin.getLogger().info( + "[Radio] pack: " + name + + " (" + dest.length() + + " bytes)"); + } catch (Exception ex) { + plugin.getLogger().warning( + "[Radio] copy fail: " + + name); + continue; + } + json.append(" \"custom.") + .append(name).append("\": {\n"); + json.append( + " \"replace\": false,\n"); + json.append( + " \"sounds\": [{\n"); + json.append( + " \"name\": \"custom/") + .append(name).append("\",\n"); + json.append( + " \"stream\": true\n"); + json.append(" }]\n"); + json.append(" }"); + idx++; + if (idx < total) json.append(","); + json.append("\n"); + } + json.append("}\n"); + + try { + Files.write(new File(packDir, + "assets/minecraft/sounds.json") + .toPath(), + json.toString() + .getBytes("UTF-8")); + plugin.getLogger().info( + "[Radio] sounds.json:\n" + + json.toString()); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] sounds.json fail"); + } + + try { + String meta = + "{\"pack\":{\"pack_format\":54," + + "\"description\":\"Sdf1 Radio\"" + + "}}\n"; + Files.write(new File(packDir, + "pack.mcmeta").toPath(), + meta.getBytes("UTF-8")); + } catch (Exception ignored) {} + + File zipFile = new File( + plugin.getDataFolder(), + "radiopack.zip"); + try { + zipPack(packDir, zipFile); + packBytes = Files.readAllBytes( + zipFile.toPath()); + MessageDigest md = + MessageDigest.getInstance( + "SHA-1"); + md.update(packBytes); + StringBuilder sb = + new StringBuilder(); + for (byte b : md.digest()) + sb.append(String.format( + "%02x", b)); + packHash = sb.toString(); + plugin.getLogger().info( + "[Radio] zip: " + + zipFile.length() + + " bytes, SHA1=" + + packHash); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] zip fail: " + + e.getMessage()); + } + } + + private void scanOggs(File dir, List list) { + File[] files = dir.listFiles(); + if (files == null) return; + for (File f : files) { + if (f.isDirectory()) + scanOggs(f, list); + else if (f.getName() + .endsWith(".ogg")) + list.add(f); + } + } + + private void loadOggFiles() { + oggFiles.clear(); + scanOggs(oggDir, oggFiles); + } + + private void deleteDir(File dir) { + if (!dir.exists()) return; + File[] files = dir.listFiles(); + if (files != null) { + for (File f : files) { + if (f.isDirectory()) + deleteDir(f); + else f.delete(); + } + } + dir.delete(); + } + + private void zipPack(File root, File zipFile) + throws Exception { + try (ZipOutputStream zos = + new ZipOutputStream( + new FileOutputStream( + zipFile))) { + addZipEntries(root, root, zos); + } + } + + private void addZipEntries(File root, File cur, + ZipOutputStream zos) + throws Exception { + File[] files = cur.listFiles(); + if (files == null) return; + for (File f : files) { + if (f.isDirectory()) { + addZipEntries(root, f, zos); + } else { + String path = root.toURI() + .relativize(f.toURI()) + .getPath(); + zos.putNextEntry( + new ZipEntry(path)); + zos.write(Files.readAllBytes( + f.toPath())); + zos.closeEntry(); + } + } + } + + // ========== HTTP server ========== + private void startHttpServer() { + if (packBytes == null) return; + if (!resourcePackUrl.isEmpty()) { + plugin.getLogger().info( + "[Radio] skip HTTP " + + "(external URL)"); + return; + } + try { + httpServer = HttpServer.create( + new InetSocketAddress( + "0.0.0.0", + httpPort), 0); + httpServer.createContext( + "/radiopack.zip", + exchange -> { + if (packBytes != null) { + exchange + .getResponseHeaders() + .set("Content-Type", + "application/zip"); + exchange + .sendResponseHeaders( + 200, + packBytes.length); + exchange + .getResponseBody() + .write(packBytes); + exchange + .getResponseBody() + .close(); + } else { + exchange + .sendResponseHeaders( + 404, 0); + exchange.close(); + } + }); + httpServer.setExecutor(null); + httpServer.start(); + String ip = "127.0.0.1"; + try { + ip = InetAddress.getLocalHost() + .getHostAddress(); + } catch (Exception ignored) {} + plugin.getLogger().info( + "[Radio] HTTP on port " + + httpPort); + plugin.getLogger().info( + "[Radio] url: http://" + ip + + ":" + httpPort + + "/radiopack.zip"); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] HTTP fail: " + + e.getMessage()); + } + } + + // ========== send resource pack ========== + public void sendResourcePack(Player p) { + if (packBytes == null) return; + UUID pid = p.getUniqueId(); + + if (sending.contains(pid)) { + plugin.getLogger().info( + "[Radio] skip duplicate: " + + p.getName()); + return; + } + sending.add(pid); + + UUID uuid = UUID.randomUUID(); + + String url = resourcePackUrl; + if (url.isEmpty()) { + String ip = "127.0.0.1"; + try { + ip = InetAddress.getLocalHost() + .getHostAddress(); + } catch (Exception ignored) {} + url = "http://" + ip + ":" + httpPort + + "/radiopack.zip"; + } + + byte[] hashBytes; + try { + MessageDigest md = + MessageDigest.getInstance( + "SHA-1"); + hashBytes = md.digest(packBytes); + } catch (Exception e) { + sending.remove(pid); + return; + } + + plugin.getLogger().info( + "[Radio] url=" + url); + plugin.getLogger().info( + "[Radio] sha1=" + packHash + + " (" + hashBytes.length + + " bytes)"); + plugin.getLogger().info( + "[Radio] size=" + packBytes.length); + + try { + java.lang.reflect.Method target = null; + for (java.lang.reflect.Method m + : p.getClass().getMethods()) { + if (!m.getName().equals( + "setResourcePack")) + continue; + Class[] pt = + m.getParameterTypes(); + if (pt.length == 5 + && pt[0] == UUID.class + && pt[1] == String.class + && pt[2] == byte[].class + && pt[3] == String.class + && pt[4] == boolean.class) { + target = m; + break; + } + } + + if (target == null) { + plugin.getLogger().warning( + "[Radio] no method"); + sending.remove(pid); + return; + } + + target.invoke(p, uuid, url, + hashBytes, + "Sdf1 Radio", false); + plugin.getLogger().info( + "[Radio] sent to " + + p.getName()); + + } catch (Exception e) { + Throwable c = e.getCause() != null + ? e.getCause() : e; + plugin.getLogger().warning( + "[Radio] failed: " + + c.getMessage()); + } + + org.bukkit.Bukkit.getScheduler() + .runTaskLater(plugin, () -> { + sending.remove(pid); + }, 100L); + } + + // ========== play custom ========== + public boolean playCustom(String name) { + return tryPlay(name, null); + } + + public boolean playCustomTo(String name, + Player target) { + return tryPlay(name, target); + } + + private boolean tryPlay(String name, + Player target) { + for (File f : oggFiles) { + String fn = f.getName() + .replace(".ogg", ""); + if (fn.equalsIgnoreCase(name)) { + String key = + "minecraft:custom." + name; + if (target != null) { + target.playSound( + target.getLocation(), + key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } else { + for (Player p : Bukkit + .getOnlinePlayers()) { + p.playSound( + p.getLocation(), + key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + broadcast( + "§6§l[广播] §f正在播放: §e" + + name); + } + return true; + } + } + return false; + } + + // ========== command ========== + public void handleCommand(CommandSender sender, + String[] args) { + // 无权限 → 静默,不做任何事 + if (!hasRadioPermission(sender)) { + return; + } + + if (args.length == 0) { + stopMainRadio(); + stopAllLoginRadio(); + broadcast("§6§l[广播] §f已停止播放"); + return; + } + String arg = args[0].trim(); + + // /radio reload + if (arg.equalsIgnoreCase("reload")) { + stopMainRadio(); + stopAllLoginRadio(); + loadRadioConfig(); + doConvert(); + buildPack(); + loadOggFiles(); + startHttpServer(); + sender.sendMessage("§a[Radio] reloaded"); + return; + } + + // /radio list + if (arg.equalsIgnoreCase("list")) { + sender.sendMessage("§6音频列表 (" + + oggFiles.size() + "):"); + for (File f : oggFiles) + sender.sendMessage(" §e" + + f.getName() + .replace(".ogg", "")); + return; + } + + // /radio random [玩家] + if (arg.equalsIgnoreCase("random")) { + if (oggFiles.isEmpty()) { + sender.sendMessage( + "§c没有可用的音频文件"); + return; + } + File pick = oggFiles.get( + (int) (Math.random() + * oggFiles.size())); + String name = pick.getName() + .replace(".ogg", ""); + if (args.length >= 2) { + Player target = Bukkit + .getPlayer(args[1]); + if (target != null + && target.isOnline()) { + sendResourcePack(target); + playCustomTo(name, target); + target.sendMessage( + "§6§l[Radio] §f正在播放: §e" + + name); + sender.sendMessage( + "§a[Radio] 已向 " + + target.getName() + + " 播放: " + name); + } else { + sender.sendMessage( + "§c玩家不在线: " + + args[1]); + } + } else { + playCustom(name); + } + return; + } + + // === /radio 音频名 [玩家名] === + String audioArg = arg; + String playerArg = null; + if (args.length >= 2) { + playerArg = args[1].trim(); + } + + Player target = null; + if (playerArg != null) { + target = Bukkit.getPlayer(playerArg); + if (target == null + || !target.isOnline()) { + sender.sendMessage( + "§c玩家不在线: " + playerArg); + return; + } + } + + // 尝试 custom + String base = audioArg.replaceAll( + "\\.(ogg|mp3|wav|flac)$", ""); + if (target != null) { + sendResourcePack(target); + } + if (tryPlay(base, target)) { + if (target != null) { + target.sendMessage( + "§6§l[Radio] §f正在播放: §e" + + base); + sender.sendMessage( + "§a[Radio] 已向 " + + target.getName() + + " 播放: " + base); + } + return; + } + + sender.sendMessage( + "§c未找到: " + audioArg + + " §7/radio list"); + } + + // ========== timer ========== + public void startMainRadio() { + stopMainRadio(); + mainRadioTask = new BukkitRunnable() { + public void run() { + playNext(); + } + }.runTaskTimer(plugin, 100L, 2400L); + } + + private void playNext() { + if (!oggFiles.isEmpty()) { + File f = oggFiles.get( + radioIndex % oggFiles.size()); + playCustom(f.getName() + .replace(".ogg", "")); + radioIndex++; + } + } + + public void stopMainRadio() { + if (mainRadioTask != null) { + mainRadioTask.cancel(); + mainRadioTask = null; + } + } + + public void startLoginRadio(Player p) { + loginPlayers.put(p.getName(), true); + if (loginRadioTask != null) return; + loginRadioTask = new BukkitRunnable() { + public void run() { + if (loginPlayers.isEmpty()) { + cancel(); + loginRadioTask = null; + return; + } + if (oggFiles.isEmpty()) return; + int idx = (int) + (System.currentTimeMillis() + / 12000L + % oggFiles.size()); + for (String name + : loginPlayers.keySet()) { + Player lp = Bukkit + .getPlayer(name); + if (lp != null + && lp.isOnline()) { + String key = + "minecraft:custom." + + oggFiles.get(idx) + .getName() + .replace(".ogg", ""); + lp.playSound( + lp.getLocation(), + key, + SoundCategory.RECORDS, + 1.5f, 1.0f); + } + } + } + }.runTaskTimer(plugin, 0L, 60L); + } + + public void stopLoginRadio(Player p) { + loginPlayers.remove(p.getName()); + if (loginPlayers.isEmpty() + && loginRadioTask != null) { + loginRadioTask.cancel(); + loginRadioTask = null; + } + } + + public void stopAllLoginRadio() { + loginPlayers.clear(); + if (loginRadioTask != null) { + loginRadioTask.cancel(); + loginRadioTask = null; + } + } + + public void stopAll() { + stopMainRadio(); + stopAllLoginRadio(); + if (httpServer != null) + httpServer.stop(0); + } + + private void broadcast(String msg) { + Bukkit.broadcastMessage(msg); + } +} diff --git a/src/main/java/Sdf1_login/TicketManager.java b/src/main/java/Sdf1_login/TicketManager.java index c21edc2..ff05ee7 100644 --- a/src/main/java/Sdf1_login/TicketManager.java +++ b/src/main/java/Sdf1_login/TicketManager.java @@ -1641,4 +1641,4 @@ public class TicketManager { } return it; } -} +} \ No newline at end of file diff --git a/src/main/java/Sdf1_login/UpdateChecker.java b/src/main/java/Sdf1_login/UpdateChecker.java new file mode 100644 index 0000000..c5c01bb --- /dev/null +++ b/src/main/java/Sdf1_login/UpdateChecker.java @@ -0,0 +1,523 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; + +public class UpdateChecker { + + private final Main plugin; + private final String currentVersion; + + // ===== 仓库信息 ===== + private static final String GITHUB_OWNER = + "ypsdf1"; + private static final String GITHUB_REPO = + "sdf1_login"; + private static final String GITEE_OWNER = + "nihaoshidifu"; + private static final String GITEE_REPO = + "sdf1_login"; + + + // API 地址 + private static final String GITHUB_API = + "https://api.github.com/repos/" + + GITHUB_OWNER + "/" + + GITHUB_REPO + + "/tags"; + private static final String GITEE_API = + "https://gitee.com/api/v5/repos/" + + GITEE_OWNER + "/" + + GITEE_REPO + + "/tags"; + + // 仓库页面链接(给用户看的) + private static final String GITHUB_LINK = + "https://github.com/" + + GITHUB_OWNER + "/" + + GITHUB_REPO + "/releases"; + private static final String GITEE_LINK = + "https://gitee.com/" + + GITEE_OWNER + "/" + + GITEE_REPO + "/releases"; + + + // 单次请求超时(毫秒) + private static final int TIMEOUT_MS = 3000; + + // 熔断阈值 + private static final int TRIP_THRESHOLD = 5; + + // ===== 熔断状态(内存态,重启自动清除) ===== + private volatile boolean circuitTripped = + false; + private volatile int failCount = 0; + + // ===== 自动检测禁用(持久化到文件) ===== + private volatile boolean autoCheckDisabled = + false; + private final File disabledFile; + + public UpdateChecker(Main plugin) { + this.plugin = plugin; + this.currentVersion = + plugin.getDescription() + .getVersion(); + this.disabledFile = new File( + plugin.getDataFolder(), + "update_disabled.dat"); + loadDisabledState(); + } + + // ========== 持久化禁用状态 ========== + + private void loadDisabledState() { + if (disabledFile.exists()) { + try (BufferedReader br = + new BufferedReader( + new FileReader( + disabledFile))) { + String line = br.readLine(); + if (line != null) { + autoCheckDisabled = + "true".equals( + line.trim()); + } + } catch (Exception ignored) { + } + } + } + + private void saveDisabledState() { + try (PrintWriter pw = + new PrintWriter( + new FileWriter( + disabledFile))) { + pw.println(autoCheckDisabled + ? "true" : "false"); + } catch (Exception ignored) { + } + } + + // ========== 公开方法 ========== + + /** + * onEnable 调用(异步静默) + * 熔断或禁用则直接跳过 + */ + public void checkOnEnable() { + if (autoCheckDisabled) return; + if (circuitTripped) return; + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> doCheck(null, true)); + } + + /** + * /sdf1_login update 调用(异步) + * 忽略熔断和禁用状态,强制检测 + * 只有拿到版号才解除熔断 + */ + public void checkUpdate( + CommandSender sender) { + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, + () -> + doCheck(sender, + false)); + } + + // ========== 核心检测逻辑 ========== + private void doCheck(CommandSender sender, + boolean isAuto) { + + if (isAuto) { + if (autoCheckDisabled + || circuitTripped) { + return; + } + } + + // 通道1:GitHub + CheckResult gh = + tryChannel(GITHUB_API, + "GitHub", GITHUB_LINK); + + // 通道2:Gitee + CheckResult ge = + tryChannel(GITEE_API, + "Gitee", GITEE_LINK); + + // 选择成功的结果 + CheckResult chosen = null; + String failMsg = null; + + if (gh.isSuccess()) { + chosen = gh; + } else if (ge.isSuccess()) { + chosen = ge; + } else { + // 双通道均失败 + failCount++; + // 拼接两个通道的失败原因 + StringBuilder sb = + new StringBuilder(); + if (gh.error != null) { + sb.append("§7GitHub: §c") + .append(gh.error); + } + if (ge.error != null) { + if (sb.length() > 0) + sb.append("\n"); + sb.append("§7Gitee: §c") + .append(ge.error); + } + if (failCount >= TRIP_THRESHOLD) { + circuitTripped = true; + plugin.getLogger().warning( + "[Sdf1_login] 更新检测" + + "已熔断,连续失败" + + failCount + "次"); + } + if (sender != null) { + final String msg = + sb.toString(); + final boolean tripped = + circuitTripped; + final int fc = failCount; + Bukkit.getScheduler() + .runTask(plugin, () -> { + sender.sendMessage( + "§c§l[更新] " + + "检测失败"); + sender.sendMessage(msg); + sender.sendMessage( + "§e失败次数: " + + fc + "/" + + TRIP_THRESHOLD); + if (tripped) { + sender.sendMessage( + "§c已熔断," + + "请重启服务器" + + "重置"); + } + }); + } + return; + } + + // 至少一个通道成功 + failCount = 0; + + // 检查禁用信号 + if (isDisableSignal(chosen.version)) { + autoCheckDisabled = true; + saveDisabledState(); + plugin.getLogger().info( + "[Sdf1_login] 远程版本信号" + + " \"" + chosen.version + + "\",自动更新检测已禁用"); + if (sender != null) { + Bukkit.getScheduler() + .runTask(plugin, () -> + sender.sendMessage( + "§e[更新] 远程已" + + "禁用自动" + + "更新检测")); + } + return; + } + + // 手动调用:成功才解除熔断和禁用 + if (!isAuto) { + if (circuitTripped) { + circuitTripped = false; + } + if (autoCheckDisabled) { + autoCheckDisabled = false; + saveDisabledState(); + } + } + + final String src = chosen.source; + final String lnk = chosen.link; + final String rv = chosen.version; + + Bukkit.getScheduler() + .runTask(plugin, () -> { + if (sender == null) return; + + sender.sendMessage( + "§e[更新] 检测成功" + + " via: §a" + src); + sender.sendMessage( + "§e[更新] 本地版本:" + + " §f" + + currentVersion); + sender.sendMessage( + "§e[更新] 远程版本:" + + " §f" + rv); + + if (isNewer(rv, + currentVersion)) { + sender.sendMessage( + "§c[更新] 发现新" + + "版本: " + rv); + sender.sendMessage( + "§a请前往仓库下载" + + "更新:"); + sender.sendMessage( + "§a" + lnk); + } else { + sender.sendMessage( + "§a[更新] 已是" + + "最新版本"); + } + + // 显示另一通道的失败原因 + // (仅信息,不影响结果) + CheckResult other = + (src.equals("GitHub")) + ? ge : gh; + if (!other.isSuccess() + && other.error != null) { + sender.sendMessage( + "§7[更新] " + + other.source + + "不可用: " + + other.error); + } + }); + } + + + // ========== 禁用信号判断 ========== + + private boolean isDisableSignal( + String version) { + if (version == null) return false; + String v = version.trim().toLowerCase(); + return "999".equals(v) + || "关闭".equals(v) + || "false".equals(v) + || "禁用".equals(v); + } + + // ========== HTTP 请求(带超时) ========== + + // ========== 检测结果 ========== + + private static class CheckResult { + final String version; + final String source; + final String link; + final String error; + + CheckResult(String version, + String source, + String link, + String error) { + this.version = version; + this.source = source; + this.link = link; + this.error = error; + } + + boolean isSuccess() { + return version != null; + } + } + +// ========== HTTP 请求 ========== + + private CheckResult tryChannel( + String apiUrl, + String sourceName, + String link) { + HttpURLConnection conn = null; + try { + URL url = new URL(apiUrl); + conn = (HttpURLConnection) + url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; " + + "Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) " + + "Chrome/120.0.0.0 Safari/537.36"); + conn.setRequestProperty( + "Accept", + "*/*"); + conn.setRequestProperty( + "Accept-Language", + "zh-CN,zh;q=0.9"); + conn.setConnectTimeout(TIMEOUT_MS); + conn.setReadTimeout(TIMEOUT_MS); + conn.setInstanceFollowRedirects(true); + + int code = conn.getResponseCode(); + + if (code == 404) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 未找到 (HTTP 404)"); + } + if (code == 403) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 被限流 (HTTP 403)"); + } + if (code != 200) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": HTTP " + code); + } + + BufferedReader reader = + new BufferedReader( + new InputStreamReader( + conn + .getInputStream())); + StringBuilder sb = + new StringBuilder(); + String line; + while ((line = reader.readLine()) + != null) { + sb.append(line); + } + reader.close(); + + String tag = + parseTagName(sb.toString()); + if (tag == null) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 无法解析版本号"); + } + + return new CheckResult(tag, + sourceName, link, null); + + } catch (java.net + .SocketTimeoutException e) { + return new CheckResult(null, + sourceName, link, + sourceName + ": 连接超时"); + } catch (java.net.ConnectException e) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": 连接被拒绝"); + } catch (java.net + .UnknownHostException e) { + return new CheckResult(null, + sourceName, link, + sourceName + + ": DNS解析失败"); + } catch (Exception e) { + return new CheckResult(null, + sourceName, link, + sourceName + ": " + + e.getMessage()); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + + + /** + * 从 JSON 中提取 tag_name + */ + private String parseTagName(String json) { + // tags API 返回数组: [{"name":"v1.1",...}] + // releases API 返回对象: {"tag_name":"v1.1"} + + // 尝试 tag_name(releases格式) + int idx = + json.indexOf("\"tag_name\""); + if (idx >= 0) { + int colon = + json.indexOf(":", idx); + int q1 = + json.indexOf("\"", colon + 1); + int q2 = + json.indexOf("\"", q1 + 1); + if (q2 > q1) { + return cleanTag( + json.substring( + q1 + 1, q2)); + } + } + + // 尝试 "name"(tags格式) + idx = json.indexOf("\"name\""); + if (idx >= 0) { + int colon = + json.indexOf(":", idx); + int q1 = + json.indexOf("\"", colon + 1); + int q2 = + json.indexOf("\"", q1 + 1); + if (q2 > q1) { + return cleanTag( + json.substring( + q1 + 1, q2)); + } + } + + return null; + } + + private String cleanTag(String tag) { + if (tag.startsWith("v") + || tag.startsWith("V")) { + tag = tag.substring(1); + } + return tag.isEmpty() ? null : tag; + } + + + // ========== 版本比较 ========== + + private boolean isNewer(String remote, + String local) { + String[] r = remote.split("\\."); + String[] l = local.split("\\."); + int len = Math.max(r.length, l.length); + for (int i = 0; i < len; i++) { + int ri = i < r.length + ? parseNum(r[i]) : 0; + int li = i < l.length + ? parseNum(l[i]) : 0; + if (ri > li) return true; + if (ri < li) return false; + } + return false; + } + + private int parseNum(String s) { + try { + return Integer.parseInt( + s.replaceAll("[^0-9]", "")); + } catch (Exception e) { + return 0; + } + } +} diff --git a/src/main/java/Sdf1_login/WelcomeManager.java b/src/main/java/Sdf1_login/WelcomeManager.java new file mode 100644 index 0000000..68bd3ef --- /dev/null +++ b/src/main/java/Sdf1_login/WelcomeManager.java @@ -0,0 +1,163 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class WelcomeManager { + + private final Main plugin; + private final Random random = new Random(); + private final List registerMsgs = + new ArrayList<>(); + private final List loginMsgs = + new ArrayList<>(); + + public WelcomeManager(Main plugin) { + this.plugin = plugin; + loadMessages(); + } + + public void loadMessages() { + registerMsgs.clear(); + loginMsgs.clear(); + File f = new File( + plugin.getDataFolder(), + "欢迎仪式.txt"); + if (!f.exists()) { + writeDefault(f); + } + try (BufferedReader r = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets + .UTF_8))) { + String section = ""; + String line; + while ((line = r.readLine()) != null) { + if (line.startsWith("\uFEFF")) + line = line.substring(1); + String t = line.trim(); + if (t.isEmpty() + || t.startsWith("#")) + continue; + if (t.equals("注册") + || t.equals("注册:")) { + section = "register"; + continue; + } + if (t.equals("登录") + || t.equals("登录:")) { + section = "login"; + continue; + } + // 支持 = 和 : 两种分隔 + if (section.isEmpty()) { + if (t.startsWith("注册") + && (t.contains("=") + || t.contains(":"))) { + int idx = t.indexOf('='); + int idx2 = t.indexOf(':'); + if (idx < 0) idx = idx2; + else if (idx2 >= 0) + idx = Math.min(idx, idx2); + String v = t.substring(idx + 1) + .trim(); + if (!v.isEmpty()) + registerMsgs.add( + colorize(v)); + continue; + } + if (t.startsWith("登录") + && (t.contains("=") + || t.contains(":"))) { + int idx = t.indexOf('='); + int idx2 = t.indexOf(':'); + if (idx < 0) idx = idx2; + else if (idx2 >= 0) + idx = Math.min(idx, idx2); + String v = t.substring(idx + 1) + .trim(); + if (!v.isEmpty()) + loginMsgs.add( + colorize(v)); + continue; + } + continue; + } + if (!t.isEmpty()) { + if (section.equals("register")) + registerMsgs.add( + colorize(t)); + else + loginMsgs.add( + colorize(t)); + } + } + } catch (Exception e) { + plugin.getLogger().severe( + "[欢迎] 加载失败: " + + e.getMessage()); + } + plugin.getLogger().info( + "[欢迎] 加载完成: 注册=" + + registerMsgs.size() + + " 登录=" + loginMsgs.size()); + } + + public void onRegister(Player p) { + sendRandom(p, registerMsgs, "注册"); + } + + public void onLogin(Player p) { + sendRandom(p, loginMsgs, "登录"); + } + + private void sendRandom(Player p, + List msgs, String tag) { + if (msgs.isEmpty()) return; + String msg = msgs.get( + random.nextInt(msgs.size())); + msg = msg.replace("{player}", + p.getName()); + final String finalMsg = msg; + Bukkit.getScheduler() + .runTaskLater(plugin, + () -> Bukkit + .broadcastMessage( + finalMsg), 10L); + } + + private String colorize(String msg) { + return msg.replace("&", "\u00a7"); + } + + private void writeDefault(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets + .UTF_8))) { + pw.println("# 欢迎仪式配置"); + pw.println("# &为颜色代码"); + pw.println("# {player} = 玩家名"); + pw.println(); + pw.println("注册:"); + pw.println("&a&l欢迎 &f{player} &a加入服务器!&7[注册]"); + pw.println("&6&l{player} &e已完成注册,欢迎入驻!&7[注册]"); + pw.println("&b&l新成员 &f{player} &b已就位!&7[注册]"); + pw.println(); + pw.println("登录:"); + pw.println("&a&l欢迎回来 &f{player}&a!&7[登录]"); + pw.println("&e&l{player} &e已登录,继续冒险吧!&7[登录]"); + pw.println("&b&l{player} &b回来了!&7[登录]"); + } catch (IOException ignored) { + } + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 32ad554..814b24e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -86,4 +86,5 @@ commands: usage: /垃圾清理 menu: description: 雪球菜单开关控制 - usage: /menu <开/关> \ No newline at end of file + usage: /menu <开/关> + commands: \ No newline at end of file diff --git a/txt/AFKManager.txt b/txt/AFKManager.txt new file mode 100644 index 0000000..a65c53f --- /dev/null +++ b/txt/AFKManager.txt @@ -0,0 +1,137 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.*; + +public class AFKManager { + + private final Map lastAction = new HashMap<>(); + private final Map lastStage = new HashMap<>(); + private final ConfigManager config; + private final JavaPlugin plugin; + + public AFKManager(ConfigManager config, JavaPlugin plugin) { + this.config = config; + this.plugin = plugin; + } + + public void startCheck() { + new BukkitRunnable() { + public void run() { tick(); } + }.runTaskTimer(plugin, 20L, 20L); + } + + private void tick() { + if (!config.afkEnabled) return; + long now = System.currentTimeMillis(); + + for (Player p : Bukkit.getOnlinePlayers()) { + String name = p.getName(); + if (!isLoggedIn(name)) continue; + if (config.getAfkWhitelist().contains(name)) continue; + + UUID uuid = p.getUniqueId(); + if (!lastAction.containsKey(uuid)) { + lastAction.put(uuid, now); + lastStage.put(uuid, 0); + continue; + } + + long last = lastAction.get(uuid); + long idle = (now - last) / 1000; + int timeout = config.afkTimeout; + long remain = timeout - idle; + + // 已超时:踢出 + if (remain <= 0) { + p.kickPlayer("§c§l你已挂机 " + fmtSec(idle) + ",被踢出"); + lastAction.remove(uuid); + lastStage.remove(uuid); + continue; + } + + int stage = lastStage.getOrDefault(uuid, 0); + + // === 阶段3:最后10秒 === + // 刷屏倒计时 + 大标题 + 每秒音效 + if (remain <= 10) { + if (stage < 3) { + lastStage.put(uuid, 3); + } + p.sendTitle("§e§l挂机了 " + fmtSec(idle), + "§c还剩 " + remain + " 秒踢出", + 0, 22, 3); + playSound(p); + continue; + } + + // === 阶段2:1分钟~10秒 === + // 持续音效(每秒)+ 标题,聊天仅1次 + if (remain <= 60) { + if (stage < 2) { + lastStage.put(uuid, 2); + p.sendMessage("§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" + fmtSec(idle) + "§f,还剩 §c" + fmtSec(remain) + "§f踢出"); + } + p.sendTitle("§e§l挂机了 " + fmtSec(idle), + "§c还剩 " + fmtSec(remain) + " 踢出", + 5, 25, 5); + playSound(p); + continue; + } + + // === 阶段1:3分钟提醒 === + // 仅1次:聊天 + 标题 + 3次音效循环 + if (remain <= 180 && stage < 1) { + lastStage.put(uuid, 1); + p.sendMessage("§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" + fmtSec(idle) + "§f,还剩 §c" + fmtSec(remain) + "§f踢出"); + p.sendTitle("§e§l挂机了 " + fmtSec(idle), + "§f还剩 " + fmtSec(remain) + " 踢出", + 10, 60, 20); + for (int i = 0; i < 3; i++) { + final int delay = i * 10; + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (p.isOnline()) playSound(p); + }, delay); + } + } + + // 超过3分钟:重置阶段 + if (remain > 180) { + lastStage.remove(uuid); + } + } + } + + + private void playSound(Player p) { + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_CHIME, 1.0f, 0.79f); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0f, 0.79f); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 0.79f); + } + + public void recordAction(UUID uuid) { + lastAction.put(uuid, System.currentTimeMillis()); + lastStage.put(uuid, 0); + } + + public void remove(UUID uuid) { + lastAction.remove(uuid); + lastStage.remove(uuid); + } + + private boolean isLoggedIn(String name) { + return ((Main) plugin).getLoggedIn().contains(name); + } + + private String fmtSec(long sec) { + if (sec < 60) return sec + "秒"; + long m = sec / 60; + long s = sec % 60; + return m + "分" + (s > 0 ? s + "秒" : ""); + } +} diff --git a/txt/AccountRequestManager.txt b/txt/AccountRequestManager.txt new file mode 100644 index 0000000..ae5eaad --- /dev/null +++ b/txt/AccountRequestManager.txt @@ -0,0 +1,146 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class AccountRequestManager { + + public enum ApprovalMode { AUTO, MANUAL, HYBRID } + + public static class Request { + public final int id; + public final String requester; + public final String target; + public final String ip; + public final long time; + public String status; + public String denyReason; + + public Request(int id, String requester, String target, String ip) { + this.id = id; + this.requester = requester; + this.target = target; + this.ip = ip; + this.time = System.currentTimeMillis(); + this.status = "PENDING"; + this.denyReason = ""; + } + } + + private final Main plugin; + private final Map requests = new ConcurrentHashMap<>(); + private final Map pendingByPlayer = new ConcurrentHashMap<>(); + private int nextId = 1; + private ApprovalMode mode = ApprovalMode.MANUAL; + private int autoDelayMinutes = 60; + + public AccountRequestManager(Main plugin) { + this.plugin = plugin; + } + + public ApprovalMode getMode() { return mode; } + + public void setMode(String m) { + try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); } + catch (Exception e) { this.mode = ApprovalMode.MANUAL; } + } + + public void setAutoDelayMinutes(int min) { + this.autoDelayMinutes = min; + } + + public Request createRequest(String requester, String target, String ip) { + int id = nextId++; + Request req = new Request(id, requester, target, ip); + requests.put(id, req); + pendingByPlayer.put(requester, id); + + if (mode == ApprovalMode.AUTO) { + scheduleAutoApprove(id); + } else if (mode == ApprovalMode.HYBRID) { + String targetIp = (String) plugin.getDb().getField(target, "ip_address"); + if (ip != null && ip.equals(targetIp)) { + scheduleAutoApprove(id); + } + } + return req; + } + + private void scheduleAutoApprove(int id) { + long delayTicks = (long) autoDelayMinutes * 60L * 20L; + Request req = requests.get(id); + if (req == null) return; + String requesterName = req.requester; + String targetName = req.target; + plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过"); + + Bukkit.getScheduler().runTaskLater(plugin, () -> { + Request r = requests.get(id); + if (r != null && "PENDING".equals(r.status)) { + approveRequest(id); + plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过"); + Player requester = Bukkit.getPlayer(requesterName); + if (requester != null && requester.isOnline()) { + requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!"); + } + } + }, delayTicks); + } + + public boolean approveRequest(int id) { + Request req = requests.get(id); + if (req == null || !"PENDING".equals(req.status)) return false; + req.status = "APPROVED"; + plugin.getDb().deleteUser(req.target); + pendingByPlayer.remove(req.requester); + // 清背包+传送出生点+踢出 + Player target = Bukkit.getPlayer(req.target); + if (target != null && target.isOnline()) { + target.getInventory().clear(); + target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]); + target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]); + target.teleport(target.getWorld().getSpawnLocation()); + target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册"); + } + return true; + } + + + public boolean denyRequest(int id, String reason) { + Request req = requests.get(id); + if (req == null || !"PENDING".equals(req.status)) return false; + req.status = "DENIED"; + req.denyReason = reason; + pendingByPlayer.remove(req.requester); + return true; + } + + public List getPendingRequests() { + List list = new ArrayList<>(); + for (Request r : requests.values()) { + if ("PENDING".equals(r.status)) list.add(r); + } + return list; + } + + public Request getRequest(int id) { + return requests.get(id); + } + + public boolean hasPending(String player) { + return pendingByPlayer.containsKey(player); + } + + public void cancelPending(String player) { + Integer id = pendingByPlayer.remove(player); + if (id != null) { + Request r = requests.get(id); + if (r != null) r.status = "CANCELLED"; + } + } +} diff --git a/txt/ChatFilterManager.txt b/txt/ChatFilterManager.txt new file mode 100644 index 0000000..1f92245 --- /dev/null +++ b/txt/ChatFilterManager.txt @@ -0,0 +1,594 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.*; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ChatFilterManager { + + private final JavaPlugin plugin; + private final List whitelistUrls = + new ArrayList<>(); + private final Set whitelistPlayers = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Map + punishmentRules = + new LinkedHashMap<>(); + private final Map + punishmentDurations = + new LinkedHashMap<>(); + private final Map + violationCount = + new ConcurrentHashMap<>(); + private final Map + mutedPlayers = + new ConcurrentHashMap<>(); + private final Map messages = + new LinkedHashMap<>(); + private boolean notifyAdmin = true; + private boolean notifyAll = false; + private int muteDuration = 300; + + private static final Pattern URL_PATTERN = + Pattern.compile( + "(?i)(?:https?://\\S+)" + + "|(?:(?:[a-zA-Z0-9]" + + "(?:[a-zA-Z0-9\\-]*" + + "[a-zA-Z0-9])?\\.)+" + + "[a-zA-Z]{2,})" + + "(?:/\\S*)?"); + private static final Pattern DOT_VARIANTS = + Pattern.compile( + "[\uff0e\u3002\u2025\u2026\u00b7]"); + private static final Pattern COLOR_CODES = + Pattern.compile( + "\u00a7[0-9a-fk-orA-FK-OR]"); + + private static final Set FILE_EXT; + static { + Set s = new HashSet<>( + Arrays.asList( + "txt", "yml", "yaml", + "json", "xml", "java", + "class", "jar", "png", + "jpg", "jpeg", "gif", + "bmp", "svg", "ico", + "webp", "mp3", "mp4", + "avi", "mkv", "wav", + "flac", "ogg", "zip", + "rar", "7z", "tar", + "gz", "cfg", "conf", + "ini", "log", "db", + "sql", "sh", "bat", + "html", "htm", "css", + "js", "md", "pdf", + "dll", "so", "exe", + "bin", "c", "cpp", + "py", "rb", "go", + "rs", "kt", "lua", + "php", "asp", "jsp")); + FILE_EXT = Collections.unmodifiableSet(s); + } + + public ChatFilterManager(JavaPlugin plugin) { + this.plugin = plugin; + } + + // ========== 配置加载 ========== + + public void loadConfig() { + whitelistUrls.clear(); + whitelistPlayers.clear(); + punishmentRules.clear(); + punishmentDurations.clear(); + messages.clear(); + + File f = new File( + plugin.getDataFolder(), "chat.txt"); + if (!f.exists()) writeDefaultFile(f); + try (BufferedReader r = + new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8))) { + String section = ""; + String line; + while ((line = r.readLine()) != null) { + if (line.startsWith("\uFEFF")) + line = line.substring(1); + String t = line.trim(); + if (t.isEmpty() + || t.startsWith("#")) + continue; + if (t.equals("白名单") + || t.equals("白名单:")) { + section = "whitelist"; + continue; + } + if (t.equals("处罚规则") + || t.equals("处罚规则:")) { + section = "punishment"; + continue; + } + if (t.equals("白名单玩家") + || t.equals("白名单玩家:")) { + section = "players"; + continue; + } + if (t.contains(":")) { + String[] kv = t.split(":", 2); + String k = kv[0].trim(); + String v = kv[1].trim(); + if (k.equals("通知管理员")) { + notifyAdmin = parseBool(v); + continue; + } + if (k.equals("全服通报")) { + notifyAll = parseBool(v); + continue; + } + if (k.equals("禁言时长")) { + try { + muteDuration = + Integer.parseInt(v); + } catch (Exception ignored) { + } + continue; + } + } + switch (section) { + case "whitelist": + whitelistUrls.add( + t.toLowerCase()); + break; + case "punishment": + String[] parts = t.split(":"); + if (parts.length >= 2) { + try { + int cnt = + Integer.parseInt( + parts[0] + .trim()); + String type = + parts[1].trim() + .toLowerCase(); + int dur = + parts.length >= 3 + ? Integer.parseInt( + parts[2] + .trim()) + : muteDuration; + punishmentRules.put( + cnt, type); + punishmentDurations.put( + cnt, dur); + } catch (Exception ignored) { + } + } + break; + case "players": + whitelistPlayers.add(t); + break; + } + } + } catch (Exception e) { + plugin.getLogger().severe( + "[Sdf1_chat] 配置加载失败: " + + e.getMessage()); + } + + // 从消息.txt读取chat_开头的消息 + loadMessages(); + } + + private void loadMessages() { + File msgFile = new File( + plugin.getDataFolder(), + "消息.txt"); + if (!msgFile.exists()) return; + try (BufferedReader mr = + new BufferedReader( + new InputStreamReader( + new FileInputStream( + msgFile), + StandardCharsets.UTF_8))) { + String mline; + while ((mline = mr.readLine()) != null) { + mline = mline.trim(); + if (mline.isEmpty() + || mline.startsWith("#")) + continue; + // 支持 = 和 : 两种分隔符 + int eqIdx = mline.indexOf('='); + int coIdx = mline.indexOf(':'); + int splitIdx = -1; + if (eqIdx >= 0 && coIdx >= 0) { + splitIdx = Math.min(eqIdx, coIdx); + } else if (eqIdx >= 0) { + splitIdx = eqIdx; + } else if (coIdx >= 0) { + splitIdx = coIdx; + } + if (splitIdx < 0) continue; + String k = mline.substring(0, + splitIdx).trim(); + String v = mline.substring( + splitIdx + 1).trim(); + if (k.startsWith("chat_")) { + messages.put(k, v); + } + } + } catch (Exception ignored) { + } + } + + + private void writeDefaultFile(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# Sdf1_chat 聊天过滤配置"); + pw.println("通知管理员: true"); + pw.println("全服通报: false"); + pw.println("禁言时长: 300"); + pw.println(); + pw.println("白名单:"); + pw.println("*.minecraft.net"); + pw.println("baidu.com"); + pw.println("github.com"); + pw.println(); + pw.println("处罚规则:"); + pw.println("1:warn:0"); + pw.println("2:warn:0"); + pw.println("3:mute:300"); + pw.println("5:mute:600"); + pw.println("8:kick:0"); + pw.println("10:ban:0"); + pw.println(); + pw.println("白名单玩家:"); + } catch (IOException ignored) { + } + } + + public void saveConfig() { + File f = new File( + plugin.getDataFolder(), "chat.txt"); + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.println("# Sdf1_chat 聊天过滤配置"); + pw.println("通知管理员: " + notifyAdmin); + pw.println("全服通报: " + notifyAll); + pw.println("禁言时长: " + muteDuration); + pw.println(); + pw.println("白名单:"); + for (String u : whitelistUrls) + pw.println(u); + pw.println(); + pw.println("处罚规则:"); + for (Map.Entry entry + : punishmentRules.entrySet()) { + int dur = punishmentDurations + .getOrDefault(entry.getKey(), + muteDuration); + pw.println(entry.getKey() + ":" + + entry.getValue() + + ":" + dur); + } + pw.println(); + pw.println("白名单玩家:"); + for (String p : whitelistPlayers) + pw.println(p); + } catch (IOException e) { + plugin.getLogger().severe( + "[Sdf1_chat] 保存失败: " + + e.getMessage()); + } + } + + // ========== 消息 ========== + + public String msg(String key, + String... args) { + String template = messages.getOrDefault( + key, key); + for (int i = 0; i < args.length - 1; + i += 2) { + template = template.replace( + "{" + args[i] + "}", + args[i + 1]); + } + return template; + } + + // ========== URL检测 ========== + + private String normalizeMessage(String msg) { + msg = COLOR_CODES.matcher(msg) + .replaceAll(""); + msg = DOT_VARIANTS.matcher(msg) + .replaceAll("."); + return msg; + } + + public List extractUrls( + String message) { + String normalized = + normalizeMessage(message); + List urls = new ArrayList<>(); + Matcher m = URL_PATTERN + .matcher(normalized); + while (m.find()) { + String url = m.group().toLowerCase(); + url = url.replaceAll( + "[\\s.,;!?,。;!?、)》」』】" + + "\\]\\[\\((]+$", + ""); + if (!url.isEmpty()) urls.add(url); + } + return urls; + } + + public boolean isWhitelisted(String url) { + String clean = url + .replaceAll("^https?://", "") + .toLowerCase(); + int colon = clean.indexOf(':'); + int slash = clean.indexOf('/'); + if (colon > 0 && (slash < 0 + || colon < slash)) { + clean = clean.substring(0, colon); + } + slash = clean.indexOf('/'); + if (slash > 0) { + clean = clean.substring(0, slash); + } + for (String entry : whitelistUrls) { + String e = entry.toLowerCase().trim(); + if (e.startsWith("*.")) { + String suffix = e.substring(1); + if (clean.endsWith(suffix) + || clean.equals( + suffix.substring(1))) + return true; + } else { + if (clean.equals(e) + || clean.endsWith( + "." + e)) + return true; + } + } + return false; + } + + public boolean isLikelyDomain(String url) { + int lastDot = url.lastIndexOf('.'); + if (lastDot < 0 + || lastDot >= url.length() - 1) + return false; + String ext = url.substring(lastDot + 1) + .toLowerCase(); + return !FILE_EXT.contains(ext); + } + + // ========== 玩家操作 ========== + + public boolean isMuted(String name) { + Long expiry = mutedPlayers.get(name); + if (expiry == null) return false; + if (System.currentTimeMillis() + >= expiry) { + mutedPlayers.remove(name); + return false; + } + return true; + } + + public String fmtDuration(int seconds) { + if (seconds <= 0) return "0秒"; + int day = seconds / 86400; + int hr = (seconds % 86400) / 3600; + int min = (seconds % 3600) / 60; + int sec = seconds % 60; + StringBuilder sb = new StringBuilder(); + if (day > 0) sb.append(day).append("天"); + if (hr > 0) + sb.append(hr).append("小时"); + if (min > 0) + sb.append(min).append("分"); + if (sec > 0 || sb.length() == 0) + sb.append(sec).append("秒"); + return sb.toString(); + } + + public void applyPunishment(Player player, + int violation) { + String type = "warn"; + int duration = muteDuration; + int maxRule = 0; + for (Map.Entry entry + : punishmentRules.entrySet()) { + if (violation >= entry.getKey() + && entry.getKey() > maxRule) { + maxRule = entry.getKey(); + type = entry.getValue(); + duration = punishmentDurations + .getOrDefault( + entry.getKey(), + muteDuration); + } + } + if (type.equals("mute")) { + if (isMuted(player.getName())) + return; + mutedPlayers.put(player.getName(), + System.currentTimeMillis() + + (long) duration + * 1000L); + } + final String fType = type; + final int fDur = duration; + final int fV = violation; + final String fName = player.getName(); + Bukkit.getScheduler().runTask(plugin, + () -> { + Player p = Bukkit.getPlayer(fName); + if (p == null) return; + switch (fType) { + case "warn": + // 警告不处罚,只提示 + break; + case "mute": + p.sendMessage( + msg("chat_muted")); + break; + case "kick": + p.kickPlayer( + msg("chat_muted")); + break; + case "ban": + Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), + "ban " + fName + + " §c多次发送违规链接"); + break; + case "banip": + InetSocketAddress addr = + p.getAddress(); + if (addr != null) { + String ip = addr + .getAddress() + .getHostAddress(); + Bukkit.dispatchCommand( + Bukkit.getConsoleSender(), + "ban-ip " + ip + + " §c发送违规链接"); + } + break; + } + }); + } + + public void unmutePlayer(String name) { + mutedPlayers.remove(name); + } + + public void resetPlayer(String name) { + mutedPlayers.remove(name); + violationCount.remove(name); + } + + public boolean isPlayerWhitelisted( + String name) { + return whitelistPlayers.contains(name); + } + + public boolean isNotifyAdmin() { + return notifyAdmin; + } + + public boolean isNotifyAll() { + return notifyAll; + } + + public int getViolationCount(String name) { + return violationCount + .getOrDefault(name, 0); + } + + public void incrementViolation( + String name) { + violationCount.put(name, + getViolationCount(name) + 1); + } + + public List getWhitelistUrls() { + return whitelistUrls; + } + + public Set getWhitelistPlayers() { + return whitelistPlayers; + } + + public Map + getPunishmentRules() { + return punishmentRules; + } + + public Map + getPunishmentDurations() { + return punishmentDurations; + } + + public Map + getMutedPlayers() { + return mutedPlayers; + } + + public void setNotifyAdmin(boolean v) { + notifyAdmin = v; + } + + public void setNotifyAll(boolean v) { + notifyAll = v; + } + + public void setMuteDuration(int v) { + muteDuration = v; + } + + public void addUrl(String url) { + whitelistUrls.add( + url.toLowerCase().trim()); + saveConfig(); + } + + public boolean removeUrl(String url) { + boolean r = whitelistUrls.remove( + url.toLowerCase().trim()); + saveConfig(); + return r; + } + + public void addWhitelistPlayer( + String name) { + whitelistPlayers.add(name); + saveConfig(); + } + + public boolean removeWhitelistPlayer( + String name) { + boolean r = + whitelistPlayers.remove(name); + saveConfig(); + return r; + } + + public void cleanExpired() { + long now = System.currentTimeMillis(); + mutedPlayers.entrySet().removeIf( + e -> e.getValue() <= now); + } + + private boolean parseBool(String v) { + String s = v.toLowerCase().trim(); + return s.equals("true") + || s.equals("on") + || s.equals("yes") + || s.equals("1") + || s.contains("开启") + || s.contains("启用") + || s.contains("是"); + } +} diff --git a/txt/ChatInputManager.txt b/txt/ChatInputManager.txt new file mode 100644 index 0000000..e564b28 --- /dev/null +++ b/txt/ChatInputManager.txt @@ -0,0 +1,823 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; + +public class ChatInputManager { + + public enum InputType { + NONE, + CHANGE_PWD_STEP1, + CHANGE_PWD_STEP2, + SET_EMAIL, + SET_EMAIL_VERIFY, + ADMIN_AUTH, + ADMIN_SET_POINTS, + ADMIN_SET_PWD, + ADMIN_DELETE_CONFIRM, + ADMIN_SET_AFK_TIME, + SMTP_CONFIG, + SMTP_VERIFY, + BACK_CHECKIN, + INVITE_INPUT_CODE, + TICKET_TITLE, + TICKET_DESC, + TICKET_SCORE, + TICKET_REJECT, + TICKET_ASSIGN, + TICKET_INVITE_PROVIDER, + TICKET_REMOVE_PROVIDER, + TICKET_COMPLETE, + TICKET_REPORT_ISSUE, + TICKET_REPLY, + TICKET_ADMIN_TYPE, + TICKET_ADMIN_TITLE, + TICKET_ADMIN_DESC, + TICKET_ADMIN_PROVIDER + } + + public static class PlayerState { + public InputType type = InputType.NONE; + public String targetPlayer = ""; + public String ticketType = ""; + public String ticketTitle = ""; + public String ticketDesc = ""; + public int ticketId = 0; + public int smtpStep = 0; + public int scoreValue = 0; + } + + private final Map states = + new HashMap<>(); + private Main plugin; + + public PlayerState getState(Player p) { + return states.computeIfAbsent( + p.getName(), + k -> new PlayerState()); + } + + public void reset(Player p) { + states.remove(p.getName()); + } + + public void handleInput(Player p, String msg, + Main mainPlugin) { + if (this.plugin == null) + this.plugin = mainPlugin; + PlayerState state = getState(p); + ConfigManager config = + mainPlugin.getConfig2(); + DatabaseManager db = mainPlugin.getDb(); + + switch (state.type) { + + case CHANGE_PWD_STEP1: { + String name = p.getName(); + String salt = (String) db.getField( + name, "password_salt"); + String hash = + PasswordUtils.hash(msg, salt); + boolean mainOk = + db.checkPassword(name, hash); + if (mainOk) { + state.type = + InputType.CHANGE_PWD_STEP2; + state.ticketTitle = "main"; + p.sendMessage( + "§e请输入新密码:"); + return; + } + boolean tempOk = + db.checkPasswordOrTemp( + name, hash); + if (tempOk) { + state.type = + InputType.CHANGE_PWD_STEP2; + state.ticketTitle = "temp"; + p.sendMessage( + "§e请输入新密码:"); + return; + } + p.sendMessage(config.msg( + "password_wrong")); + return; + } + + case CHANGE_PWD_STEP2: { + String name = p.getName(); + if (!PasswordUtils.validate(msg)) { + p.sendMessage(config.msg( + "password_format_error")); + reset(p); + return; + } + String oldHash = + (String) db.getField(name, + "password_hash"); + String oldSalt = + (String) db.getField(name, + "password_salt"); + String newSalt = + PasswordUtils.generateSalt(); + String newHash = + PasswordUtils.hash( + msg, newSalt); + if (newHash.equals(oldHash)) { + p.sendMessage(config.msg( + "password_same")); + reset(p); + return; + } + mainPlugin.recordPasswordChange( + name, oldHash, oldSalt); + db.setField(name, + "password_hash", newHash); + db.setField(name, + "password_salt", newSalt); + mainPlugin + .getNeedsPasswordChange() + .remove(name); + p.sendMessage(config.msg( + "password_changed")); + reset(p); + return; + } + + case SET_EMAIL: { + if (!msg.contains("@") + || !msg.contains(".")) { + p.sendMessage(config.msg( + "email_format_error")); + reset(p); + return; + } + String oldEmail = (String) + db.getField(p.getName(), + "email"); + String emailAddr = msg.trim(); + // 已绑定邮箱:验证登录密码 + if (oldEmail != null + && !oldEmail.isEmpty()) { + state.targetPlayer = emailAddr; + state.type = + InputType.SET_EMAIL_VERIFY; + state.ticketTitle = + "need_pwd"; + p.sendMessage( + "§e您已绑定邮箱: " + + oldEmail); + p.sendMessage( + "§e请验证登录密码以确认修改:"); + return; + } + // 未绑定邮箱:直接发验证码 + String code = String.valueOf( + (int) (Math.random() + * 900000 + + 100000)); + state.targetPlayer = emailAddr; + state.ticketTitle = code; + state.type = + InputType.SET_EMAIL_VERIFY; + final String to = emailAddr; + final String c = code; + final Player fp = p; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> { + boolean sent = mainPlugin + .getEmail() + .sendVerifyCode( + to, c); + Bukkit.getScheduler() + .runTask(mainPlugin, + () -> { + if (sent) { + fp.sendMessage( + "§a验证码已发送到 " + + to); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败"); + reset(fp); + } + }); + }); + return; + } + + + case SET_EMAIL_VERIFY: { + if ("0".equals(msg.trim()) + || "取消".equals( + msg.trim())) { + p.sendMessage( + "§c邮箱设置已取消"); + reset(p); + return; + } + String expected = + state.ticketTitle; + if (msg.trim().equals(expected)) { + db.setField(p.getName(), + "email", + state.targetPlayer); + p.sendMessage( + "§a邮箱已设置: " + + state.targetPlayer); + reset(p); + } else { + p.sendMessage( + "§c验证码错误(0取消):"); + } + return; + } + + case ADMIN_AUTH: { + if (msg.equals( + config.adminPassword)) { + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getGui() + .openAdmin(p)); + } else { + p.sendMessage( + "§c管理密码错误"); + } + reset(p); + return; + } + + case ADMIN_SET_POINTS: { + try { + int pts = Integer.parseInt( + msg.trim()); + db.setField( + state.targetPlayer, + "points", pts); + p.sendMessage("§a已将 " + + state.targetPlayer + + " 积分设为 " + pts); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效数字"); + } + String tgt = + state.targetPlayer; + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getGui() + .openUserDetail( + p, tgt)); + reset(p); + return; + } + + case ADMIN_SET_PWD: { + if (!PasswordUtils.validate(msg)) { + p.sendMessage(config.msg( + "password_format_error")); + reset(p); + return; + } + String tgt = state.targetPlayer; + String salt = + PasswordUtils + .generateSalt(); + String hash = + PasswordUtils.hash( + msg, salt); + db.setField(tgt, + "temp_password", hash); + db.setField(tgt, "temp_pw_expire", + System.currentTimeMillis() + + 300000L); + db.setField(tgt, + "temp_pw_used", 0); + p.sendMessage("§a已为 " + tgt + + " 设置临时密码"); + Player tp = + Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + tp.kickPlayer( + "§c管理员已设置临时密码," + + "请使用临时密码登录"); + // 邮件通知 + String emailAddr = (String) + db.getField(tgt, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String time = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()); + String body = "玩家 " + tgt + + ":\n您的密码已于 " + + time + + " 被管理员重置为临时密码。\n" + + "如果是本人操作请忽略。\n" + + "如非本人操作请立即联系管理员。"; + final String to = emailAddr; + final String fBody = body; + final String fName = tgt; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> + mainPlugin.getEmail() + .sendTempPassword( + to, + fName, + fBody)); + } + reset(p); + return; + } + + case ADMIN_DELETE_CONFIRM: { + if (msg.equals( + state.targetPlayer)) { + db.deleteUser( + state.targetPlayer); + p.sendMessage("§a已删除 " + + state.targetPlayer); + Player tgt = + Bukkit.getPlayer( + state.targetPlayer); + if (tgt != null + && tgt.isOnline()) + tgt.kickPlayer( + "§c账号已被删除"); + } else { + p.sendMessage( + "§c输入不匹配,已取消"); + } + reset(p); + return; + } + + case ADMIN_SET_AFK_TIME: { + try { + int minutes = + Integer.parseInt( + msg.trim()); + config.afkTimeout = + minutes * 60; + config.saveSettings(); + p.sendMessage( + "§a挂机时长已设为 " + + minutes + + " 分钟"); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效数字"); + } + reset(p); + return; + } + + case SMTP_CONFIG: { + String m = msg.trim(); + if ("000".equals(m) + || "取消取消取消" + .equals(m)) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + if ("取消".equals(m)) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + int step = state.smtpStep; + if (step <= 1) { + if (!"0".equals(m)) + config.setSmtp( + "smtp地址", m); + state.smtpStep = 2; + p.sendMessage("§e当前端口: " + + config.getSmtp( + "smtp端口")); + p.sendMessage( + "§e输入端口(0跳过):"); + } else if (step == 2) { + if (!"0".equals(m)) + config.setSmtp( + "smtp端口", m); + state.smtpStep = 3; + p.sendMessage( + "§e输入发件邮箱:"); + } else if (step == 3) { + if (!"0".equals(m)) + config.setSmtp( + "smtp账号", m); + state.smtpStep = 4; + p.sendMessage( + "§e输入SMTP密码(0跳过):"); + } else if (step == 4) { + if (!"0".equals(m)) + config.setSmtp( + "smtp密码", m); + state.smtpStep = 5; + p.sendMessage( + "§e输入接收邮箱(留空发到发件邮箱):"); + } else if (step == 5) { + String recv = m; + if (recv.isEmpty() + || "0".equals(recv)) + recv = config.getSmtp( + "smtp账号"); + config.setSmtp( + "验证码接收邮箱", recv); + if (!config + .canSendSmtpVerify()) { + p.sendMessage( + "§c发送间隔不足1分钟"); + reset(p); + return; + } + String code = config + .generateSmtpVerifyCode(); + String addr = config.getSmtp( + "smtp地址"); + String acct = config.getSmtp( + "smtp账号"); + if (acct.isEmpty() + || addr.isEmpty()) { + p.sendMessage( + "§c地址或账号为空"); + reset(p); + return; + } + config.recordSmtpSendTime(); + final String fRecv = recv; + final String fCode = code; + final Player fp = p; + Bukkit.getScheduler() + .runTaskAsynchronously( + mainPlugin, () -> { + Bukkit.getScheduler() + .runTask( + mainPlugin, + () -> fp.sendMessage( + "§e正在发送验证码...")); + boolean sent = + mainPlugin.getEmail() + .sendVerifyCode( + fRecv, + fCode); + final boolean fSent = sent; + Bukkit.getScheduler() + .runTask( + mainPlugin, + () -> { + if (fSent) { + state.type = + InputType + .SMTP_VERIFY; + state.smtpStep = 0; + fp.sendMessage( + "§a验证码已发送到 " + + fRecv); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败,查看控制台"); + reset(fp); + } + }); + }); + } + return; + } + + case SMTP_VERIFY: { + if ("0".equals(msg.trim()) + || "取消".equals( + msg.trim())) { + p.sendMessage( + "§cSMTP配置已取消"); + reset(p); + return; + } + if (config.checkSmtpVerifyCode( + msg.trim())) { + config.saveSmtp(); + config.clearSmtpVerifyCode(); + String addr = config.getSmtp( + "smtp地址"); + String port = config.getSmtp( + "smtp端口"); + String acct = config.getSmtp( + "smtp账号"); + String recv = config.getSmtp( + "验证码接收邮箱"); + boolean ssl = config.smtpSsl; + p.sendMessage("§a§l=== SMTP配置完成 ==="); + p.sendMessage("§7地址: §f" + addr); + p.sendMessage("§7端口: §f" + port); + p.sendMessage("§7账号: §f" + acct); + p.sendMessage("§7接收: §f" + recv); + p.sendMessage("§7加密: §f" + ssl); + p.sendMessage("§a§l===================="); + reset(p); + } else { + p.sendMessage( + "§c验证码错误(0取消):"); + } + return; + } + + case BACK_CHECKIN: { + int cost = 20; + if (!mainPlugin.getDb() + .deductPoints( + p.getName(), cost)) { + p.sendMessage( + "§c积分不足,补签需 " + + cost + " 积分"); + reset(p); + return; + } + p.sendMessage( + "§a补签成功!扣除 " + + cost + " 积分。" + + "日期: " + msg); + reset(p); + return; + } + + case INVITE_INPUT_CODE: { + String code = msg.trim(); + String myName = p.getName(); + String myCode = + (String) db.getField( + myName, + "invite_code"); + if (code.isEmpty()) { + p.sendMessage( + "§c邀请码不能为空"); + reset(p); + return; + } + if (code.equals(myCode)) { + p.sendMessage( + "§c不能用自己邀请码"); + reset(p); + return; + } + boolean found = false; + for (Map u : + db.getAllUsers()) { + String uc = (String) + u.getOrDefault( + "invite_code", ""); + if (code.equals(uc)) { + String inv = (String) + u.get("player_name"); + db.setField(myName, + "invited_by", inv); + db.addPoints(inv, 50); + p.sendMessage( + "§a邀请成功!邀请人: " + + inv); + Player ip = + Bukkit.getPlayer(inv); + if (ip != null + && ip.isOnline()) + ip.sendMessage("§a" + + myName + + " 使用了您的邀请码"); + found = true; + break; + } + } + if (!found) + p.sendMessage( + "§c邀请码不存在"); + reset(p); + return; + } + + case TICKET_TITLE: { + state.ticketTitle = msg; + state.type = + InputType.TICKET_DESC; + p.sendMessage( + "§e请输入工单详细描述:"); + return; + } + + case TICKET_DESC: { + String tt = state.ticketType; + String tl = state.ticketTitle; + reset(p); + mainPlugin.getTicket() + .submitTicket( + p, tt, tl, msg); + return; + } + + case TICKET_SCORE: { + int tid = state.ticketId; + try { + int sc = Integer.parseInt( + msg.trim()); + reset(p); + mainPlugin.getTicket() + .confirmAndScore( + p, tid, sc); + } catch ( + NumberFormatException e) { + reset(p); + p.sendMessage( + "§c请输入数字1-5"); + } + return; + } + + case TICKET_REJECT: { + int tid = state.ticketId; + reset(p); + mainPlugin.getTicket() + .rejectTicket( + p, tid, msg); + return; + } + + case TICKET_ASSIGN: { + int tid = state.ticketId; + String[] parts = msg.trim() + .split("\\s+"); + String prov = parts[0]; + int price = 0; + if (parts.length >= 2) { + try { + price = Integer.parseInt( + parts[1]); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c价格格式错误"); + } + } + reset(p); + mainPlugin.getTicket() + .assignToProvider( + p, tid, prov, + price); + final int fTid = tid; + Bukkit.getScheduler().runTask( + mainPlugin, () -> + mainPlugin + .getTicket() + .openDetail( + p, fTid)); + return; + } + + case TICKET_INVITE_PROVIDER: { + String tgt = msg.trim(); + reset(p); + if (!db.userExists(tgt)) { + p.sendMessage( + "§c玩家不存在"); + return; + } + db.addServiceProvider( + tgt, "waiter"); + p.sendMessage("§a已邀请 " + tgt + + " 成为服务商"); + return; + } + + case TICKET_REMOVE_PROVIDER: { + String tgt = msg.trim(); + reset(p); + if (!db.isServiceProvider(tgt)) { + p.sendMessage("§c" + tgt + + " 不是服务商"); + return; + } + db.removeServiceProvider(tgt); + p.sendMessage("§a已移除 " + tgt); + return; + } + + case TICKET_COMPLETE: { + try { + int tid = Integer.parseInt( + msg.trim()); + mainPlugin.getTicket() + .completeAsProvider( + p, tid); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c请输入有效工单号"); + } + reset(p); + return; + } + + case TICKET_REPORT_ISSUE: { + String report = msg.trim(); + reset(p); + p.sendMessage("§e异常已上报"); + String tag = config.adminTag; + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.getScoreboardTags() + .contains(tag)) { + op.sendMessage( + "§c§l[工单异常] §f" + + p.getName() + + ": " + report); + } + } + return; + } + + case TICKET_REPLY: { + int replyTid = state.ticketId; + reset(p); + mainPlugin.getTicket() + .replyTicket( + p, replyTid, msg); + return; + } + + case TICKET_ADMIN_TYPE: { + state.ticketType = msg.trim(); + state.type = + InputType + .TICKET_ADMIN_TITLE; + p.sendMessage( + "§e请输入工单标题:"); + return; + } + + case TICKET_ADMIN_TITLE: { + state.ticketTitle = msg; + state.type = + InputType + .TICKET_ADMIN_DESC; + p.sendMessage( + "§e请输入工单描述:"); + return; + } + + case TICKET_ADMIN_DESC: { + state.targetPlayer = msg; + state.type = + InputType + .TICKET_ADMIN_PROVIDER; + p.sendMessage( + "§e输入服务商名和价格:"); + p.sendMessage( + "§7例: playerA 20"); + return; + } + + case TICKET_ADMIN_PROVIDER: { + String[] parts2 = msg.trim() + .split("\\s+"); + String prov2 = parts2[0]; + int price2 = 0; + if (parts2.length >= 2) { + try { + price2 = Integer.parseInt( + parts2[1]); + } catch ( + NumberFormatException e) { + } + } + String t2 = state.ticketType; + String ti2 = state.ticketTitle; + String d2 = state.targetPlayer; + reset(p); + mainPlugin.getTicket() + .adminCreateAndAssign( + p, t2, ti2, + d2, prov2, price2); + return; + } + + default: + reset(p); + } + } +} diff --git a/txt/CheckInManager.txt b/txt/CheckInManager.txt new file mode 100644 index 0000000..79641c6 --- /dev/null +++ b/txt/CheckInManager.txt @@ -0,0 +1,104 @@ +package Sdf1_login; + +import org.bukkit.entity.Player; + +import java.text.SimpleDateFormat; +import java.util.Date; + +public class CheckInManager { + + private final Main plugin; + + public CheckInManager(Main plugin) { + this.plugin = plugin; + } + + public boolean isCheckedInToday(String name) { + String last = (String) plugin.getDb() + .getField(name, "last_checkin_date"); + String today = new SimpleDateFormat("yyyy-MM-dd") + .format(new Date()); + return today.equals(last); + } + + public String checkIn(Player p) { + String name = p.getName(); + if (isCheckedInToday(name)) { + return plugin.getConfig2().msg("checkin_already"); + } + + int streak = ((Number) plugin.getDb() + .getField(name, "checkin_streak")).intValue(); + String lastDate = (String) plugin.getDb() + .getField(name, "last_checkin_date"); + String today = new SimpleDateFormat("yyyy-MM-dd") + .format(new Date()); + + // 检查是否连续 + if (lastDate != null && !lastDate.isEmpty()) { + try { + java.util.Date lastD = new SimpleDateFormat( + "yyyy-MM-dd").parse(lastDate); + java.util.Date todayD = new SimpleDateFormat( + "yyyy-MM-dd").parse(today); + long diff = todayD.getTime() - lastD.getTime(); + if (diff > 86400000L * 2) { + streak = 0; // 断签 + } else if (diff > 86400000L) { + streak++; // 连续 + } + } catch (Exception ignored) {} + } + + if (streak == 0) streak = 1; + else streak++; + + double multi = 1.0; + if (streak >= 21) multi = 1.5; + else if (streak >= 14) multi = 1.3; + else if (streak >= 7) multi = 1.1; + + int points = (int) (1 * multi); + plugin.getDb().addPoints(name, points); + plugin.getDb().setField( + name, "checkin_streak", streak); + plugin.getDb().setField( + name, "last_checkin_date", today); + int total = ((Number) plugin.getDb() + .getField(name, "total_checkin_days")) + .intValue(); + plugin.getDb().setField( + name, "total_checkin_days", total + 1); + + return plugin.getConfig2().msg("checkin_success", + "points", String.valueOf(points), + "streak", String.valueOf(streak), + "multi", String.valueOf(multi)); + } + + public String backCheckIn(Player p, String dateStr) { + String name = p.getName(); + if (!plugin.getDb().deductPoints(name, 10)) { + return plugin.getConfig2().msg("backcheckin_no_point"); + } + try { + java.util.Date target = new SimpleDateFormat( + "yyyy-MM-dd").parse(dateStr); + java.util.Date now = new SimpleDateFormat( + "yyyy-MM-dd").parse( + new SimpleDateFormat("yyyy-MM-dd") + .format(new Date())); + long diff = now.getTime() - target.getTime(); + if (diff < 0 || diff > 3 * 86400000L) { + plugin.getDb().addPoints(name, 10); // 退还 + return plugin.getConfig2().msg( + "backcheckin_expired"); + } + plugin.getDb().addPoints(name, 1); // 补签也给1分 + return plugin.getConfig2().msg("backcheckin_success"); + } catch (Exception e) { + plugin.getDb().addPoints(name, 10); + return "§c[Sdf1_login] §f日期格式错误,请用 yyyy-MM-dd"; + } + } +} diff --git a/txt/ConfigManager.txt b/txt/ConfigManager.txt new file mode 100644 index 0000000..ca90c9e --- /dev/null +++ b/txt/ConfigManager.txt @@ -0,0 +1,451 @@ +package Sdf1_login; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import static java.lang.Double.parseDouble; + +public class ConfigManager { + + private final File dataFolder; + private final Map messages = new LinkedHashMap<>(); + private final Map smtpSettings = new LinkedHashMap<>(); + + public String adminTag = "admin"; + public String adminPassword = "qweasd"; + public int maxAccountsPerIP = 3; + public String approvalMode = "manual"; + public int autoApproveDelayMinutes = 30; + public int loginTimeout = 60; + public boolean afkEnabled = false; + public int afkTimeout = 300; + public boolean smtpSsl = true; + public String smtpVerifyCode = ""; + public long smtpVerifyExpire = 0; + public long smtpLastSendTime = 0; + public String smtpConfiguringPlayer = ""; + + + public ConfigManager(File dataFolder) { + this.dataFolder = dataFolder; + } + + // ==================== 消息 ==================== + + public void loadMessages() { + messages.clear(); + File file = new File(dataFolder, "消息.txt"); + if (!file.exists()) createDefaultMessages(file); + loadMap(file, messages); + } + + public String msg(String key) { + String val = messages.getOrDefault(key, key); + val = val.replace("", "&l"); + val = val.replace("", "&r"); + val = val.replace("
", "\n"); + val = val.replace("\\n", "\n"); + // &颜色码 转换为 §颜色码 + val = val.replaceAll( + "&([0-9a-fk-orA-FK-OR])", "§$1"); + return val; + } + + + private void createDefaultMessages(File f) { + List L = new ArrayList<>(); + L.add("# =========================================="); + L.add("# Sdf1_login 消息配置文件"); + L.add("# =========================================="); + L.add("# 颜色代码使用 § 符号"); + L.add("# §0黑色 §1深蓝 §2深绿 §3深青"); + L.add("# §4深红 §5深紫 §6金色 §7灰色"); + L.add("# §8深灰 §9蓝色 §a绿色 §b浅蓝"); + L.add("# §c红色 §d粉色 §e黄色 §f白色"); + L.add("# §l粗体 §n下划线 §o斜体 §r重置"); + L.add("#"); + L.add("# 占位符用 {xxx} 表示,运行时自动替换"); + L.add("# 支持 粗体 标签"); + L.add("# 支持 \\n 或
换行"); + L.add("# 修改后执行 /sdf1_login reload 重载"); + L.add("# =========================================="); + L.add(""); + + // --- 登录/注册 --- + L.add("# ---- 登录注册相关 ----"); + L.add("# login_timeout: 登录超时被踢出时的提示"); + L.add("login_timeout=§c登录超时,请重新登录"); + L.add("# not_registered: 未注册玩家收到的提示"); + L.add("not_registered=§e您尚未注册,请使用 /reg <密码>"); + L.add("# not_logged_in: 已注册但未登录的提示"); + L.add("not_logged_in=§e您尚未登录,请使用 /login <密码>"); + L.add("# already_registered: 已注册玩家再次注册时的提示"); + L.add("already_registered=§c您已注册过账号"); + L.add("# already_logged_in: 已登录玩家再次登录时的提示"); + L.add("already_logged_in=§c您已登录"); + L.add("# reg_success: 注册成功提示"); + L.add("reg_success=§a注册成功!欢迎来到服务器"); + L.add("# login_success: 登录成功提示"); + L.add("login_success=§a登录成功!"); + L.add("# login_failed: 密码错误提示"); + L.add("login_failed=§c密码错误,请重试"); + L.add(""); + + // --- 密码 --- + L.add("# ---- 密码相关 ----"); + L.add("# password_wrong: 旧密码验证失败"); + L.add("password_wrong=§c密码错误"); + L.add("# password_changed: 密码修改成功"); + L.add("password_changed=§a密码修改成功!"); + L.add("# password_format_error: 密码格式不符合要求"); + L.add("password_format_error=§c密码需6位以上,含大小写字母和数字"); + L.add("# password_same: 新密码与旧密码相同"); + L.add("password_same=§c新密码不能与旧密码相同"); + L.add("# reset_no_email: 找回密码时未绑定邮箱"); + L.add("reset_no_email=§c您未绑定邮箱,无法找回密码"); + L.add("# reset_sent: 临时密码已发送到邮箱"); + L.add("reset_sent=§a临时密码已发送到您的邮箱,请查收"); + L.add(""); + + // --- 签到积分 --- + L.add("# ---- 签到积分相关 ----"); + L.add("# checkin_already: 今日已签到"); + L.add("checkin_already=§c您今日已签到"); + L.add("# checkin_success: 签到成功,{points}=获得积分 {streak}=连续天数 {multi}=倍率"); + L.add("checkin_success=§a签到成功!获得 {points} 积分,连续 {streak} 天,倍率 x{multi}"); + L.add("# points_insufficient: 积分不足"); + L.add("points_insufficient=§c积分不足,当前: {points}"); + L.add("# points_purchase_success: 积分购买成功"); + L.add("points_purchase_success=§a购买成功!消耗 {count} 积分"); + L.add(""); + + // --- 挂机 --- + L.add("# ---- 挂机检测相关 ----"); + L.add("# afk_set_enabled: 挂机检测开启"); + L.add("afk_set_enabled=§a挂机检测已开启"); + L.add("# afk_set_disabled: 挂机检测关闭"); + L.add("afk_set_disabled=§c挂机检测已关闭"); + L.add("# afk_kick: 因挂机被踢出"); + L.add("afk_kick=§c因挂机过久被踢出"); + L.add(""); + + // --- 邮箱 --- + L.add("# ---- 邮箱相关 ----"); + L.add("# email_set: 邮箱设置成功"); + L.add("email_set=§a邮箱设置成功:{email}"); + L.add("# email_format_error: 邮箱格式错误"); + L.add("email_format_error=§c邮箱格式不正确"); + L.add(""); + + // --- 邀请 --- + L.add("# ---- 邀请相关 ----"); + L.add("# invite_code_generated: 邀请码生成"); + L.add("invite_code_generated=§a邀请码:{code}"); + L.add("# invite_used: 使用邀请码成功"); + L.add("invite_used=§a邀请成功,邀请人:{player}"); + L.add("# invite_self: 使用自己的邀请码"); + L.add("invite_self=§c不能使用自己的邀请码"); + L.add("# invite_not_found: 邀请码不存在"); + L.add("invite_not_found=§c邀请码不存在"); + L.add("# invite_referral_bonus: 被邀请人签到后邀请人获得奖励"); + L.add("invite_referral_bonus=§a邀请奖励!获得 {points} 积分"); + L.add(""); + + // --- 礼包 --- + L.add("# ---- 新人礼包相关 ----"); + L.add("# gift_not_ready: 礼包条件未满足"); + L.add("gift_not_ready=§c礼包条件未满足"); + L.add("# gift_claimed: 礼包领取成功"); + L.add("gift_claimed=§a礼包领取成功!"); + L.add(""); + + // --- 审批/IP --- + L.add("# ---- 审批与IP限制 ----"); + L.add("# need_approval: 注册需审批"); + L.add("need_approval=§e注册需审批 (工单#{id})"); + L.add("# ip_max_accounts: IP注册已达上限"); + L.add("ip_max_accounts=§cIP注册已达上限"); + L.add(""); + + // --- 管理员 --- + L.add("# ---- 管理员操作 ----"); + L.add("# admin_delete_confirm: 管理员删除账号确认提示"); + L.add("admin_delete_confirm=§c请在聊天中输入玩家名确认删除"); + L.add(""); + + // --- 聊天过滤 --- + L.add("# ---- 聊天过滤 ----"); + L.add("# chat_muted: 禁言玩家尝试聊天"); + L.add("chat_muted=§c§l[Sdf1_chat] §f你已被禁言,无法聊天"); + L.add("# chat_url_blocked: 发送违规链接被拦截"); + L.add("# {url}=检测到的链接地址"); + L.add("chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规第三方链接: §e{url}"); + L.add("# chat_url_violation: 当前违规次数"); + L.add("# {count}=当前违规次数"); + L.add("chat_url_violation=§7当前违规次数: §e{count}"); + L.add("# chat_url_admin_notify: 通知管理员,{player}=玩家名 {url}=链接 {count}=次数"); + L.add("chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url} (第{count}次)"); + L.add("# chat_url_broadcast: 全服通报,{player}=玩家名"); + L.add("chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚"); + L.add(""); + + // --- 通用 --- + L.add("# ---- 通用 ----"); + L.add("# 未知参数: 未知命令或权限不足时的提示"); + L.add("未知参数=§c未知参数或权限不足"); + L.add("# only_player: 仅玩家可用的命令"); + L.add("# only_player=§c此命令仅玩家可用"); + + writeLines(f, L); + } + + + // ==================== SMTP ==================== + + public void loadSmtp() { + smtpSettings.clear(); + File file = new File(dataFolder, "SMTP设置.txt"); + if (!file.exists()) createDefaultSmtp(file); + loadMap(file, smtpSettings); + } + private double parseDouble(String s, double def) { + try { return Double.parseDouble(s.trim()); } + catch (Exception e) { return def; } + } + + public String getSmtp(String key) { + return smtpSettings.getOrDefault(key, ""); + } + /** 获取挂机白名单列表 */ + public List getAfkWhitelist() { + List list = new ArrayList<>(); + File file = new File(dataFolder, "挂机白名单.txt"); + if (!file.exists()) { + try { + file.createNewFile(); + try (BufferedWriter w = new BufferedWriter( + new OutputStreamWriter( + new FileOutputStream(file), + StandardCharsets.UTF_8))) { + w.write("# 每行一个玩家名,前面加 # 为注释"); + w.newLine(); + w.write("# 以下玩家不会被挂机踢出"); + w.newLine(); + } + } catch (IOException ignored) {} + } + try (BufferedReader r = new BufferedReader( + new InputStreamReader( + new FileInputStream(file), + StandardCharsets.UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) { + list.add(line); + } + } + } catch (IOException ignored) {} + return list; + } + + /** 设置SMTP单项(聊天输入用) */ + public void setSmtp(String key, String value) { + smtpSettings.put(key, value); + } + + /** 将当前SMTP设置写回文件 */ + public void saveSmtp() { + File file = new File(dataFolder, "SMTP设置.txt"); + List L = new ArrayList<>(); + L.add("# ===== SMTP 邮件服务器配置 ====="); + for (Map.Entry e : smtpSettings.entrySet()) { + L.add(e.getKey() + "=" + e.getValue()); + } + writeLines(file, L); + } + + private void createDefaultSmtp(File f) { + List L = new ArrayList<>(); + L.add("# ===== SMTP 邮件服务器配置 ====="); + L.add("smtp地址=smtp.example.com"); + L.add("smtp端口=465"); + L.add("smtp账号=your_email@example.com"); + L.add("smtp密码="); + L.add("发件人名称=Sdf1_login"); + L.add("smtp加密=true"); + writeLines(f, L); + } + + // ==================== 插件设置 ==================== + + public void loadSettings() { + File file = new File(dataFolder, "插件设置.txt"); + if (!file.exists()) createDefaultSettings(file); + Map m = new LinkedHashMap<>(); + loadMap(file, m); + adminTag = m.getOrDefault("管理标签", adminTag); + adminPassword = m.getOrDefault("管理密码", adminPassword); + maxAccountsPerIP = parseInt( + m.getOrDefault("每IP最大账号数", "3"), 3); + approvalMode = m.getOrDefault("审批模式", approvalMode); + autoApproveDelayMinutes = parseInt( + m.getOrDefault("自动审批延迟分钟", "30"), 30); + loginTimeout = parseInt( + m.getOrDefault("登录超时秒数", "60"), 60); + afkEnabled = Boolean.parseBoolean( + m.getOrDefault("挂机踢出", "false")); + afkTimeout = parseInt( + m.getOrDefault("挂机超时秒数", "300"), 300); + smtpSsl = Boolean.parseBoolean( + m.getOrDefault("smtp加密", "true")); + providerPointsPerScore = parseDouble( + m.getOrDefault("服务商积分倍率", "2.0"), + 2.0); + baseEconomyReward = parseInt( + m.getOrDefault("基础经济奖励", "100"), + 100); + providerEconomyPerScore = parseDouble( + m.getOrDefault("经济奖励每评分", "50.0"), + 50.0); + requesterPointsReward = parseInt( + m.getOrDefault("报单人积分奖励", "10"), + 10); + + + } + + + public void saveSettings() { + File file = new File(dataFolder, "插件设置.txt"); + List L = new ArrayList<>(); + L.add("# ===== Sdf1_login 插件设置 ====="); + L.add("管理标签=" + adminTag); + L.add("管理密码=" + adminPassword); + L.add("每IP最大账号数=" + maxAccountsPerIP); + L.add("审批模式=" + approvalMode); + L.add("自动审批延迟分钟=" + autoApproveDelayMinutes); + L.add("登录超时秒数=" + loginTimeout); + L.add("挂机踢出=" + afkEnabled); + L.add("挂机超时秒数=" + afkTimeout); + // 垃圾站设置 + L.add("# ---- 垃圾站 ----"); + L.add("垃圾站_启用: " + + garbageEnabled); + L.add("垃圾站_清理间隔秒: " + + garbageInterval); + L.add("垃圾站_保留轮数: " + + garbageMaxRounds); + + writeLines(file, L); + } + public double providerPointsPerScore = 2.0; + public int baseEconomyReward = 100; + public double providerEconomyPerScore = 50.0; + public int requesterPointsReward = 10; + public boolean garbageEnabled = true; + public int garbageInterval = 300; + public int garbageMaxRounds = 1; + + + private void createDefaultSettings(File f) { + List L = new ArrayList<>(); + L.add("# ===== Sdf1_login 插件设置 ====="); + L.add("管理标签=admin"); + L.add("管理密码=qweasd"); + L.add("每IP最大账号数=3"); + L.add("审批模式=manual"); + L.add("自动审批延迟分钟=30"); + L.add("登录超时秒数=60"); + L.add("挂机踢出=false"); + L.add("挂机超时秒数=300"); + L.add("服务商积分倍率=" + providerPointsPerScore); + L.add("基础经济奖励=" + baseEconomyReward); + L.add("经济奖励每评分=" + providerEconomyPerScore); + L.add("报单人积分奖励=" + requesterPointsReward); + + writeLines(f, L); + } + + // ==================== 通用读写 ==================== + + private void loadMap(File file, Map map) { + try (BufferedReader r = new BufferedReader( + new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) continue; + int eq = line.indexOf('='); + if (eq > 0) map.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim()); + } + } catch (IOException e) { + System.err.println("[Sdf1_login] 读取失败: " + file.getName()); + } + } + + private void writeLines(File file, List lines) { + try (BufferedWriter w = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) { + for (String l : lines) { w.write(l); w.newLine(); } + } catch (IOException e) { + System.err.println("[Sdf1_login] 写入失败: " + file.getName()); + } + } + + private int parseInt(String s, int def) { + try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; } + } + /** + * 生成6位验证码(字母+数字) + */ + public String generateSmtpVerifyCode() { + String chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789"; + StringBuilder sb = new StringBuilder(); + java.util.Random r = new java.util.Random(); + for (int i = 0; i < 6; i++) { + sb.append(chars.charAt(r.nextInt( + chars.length()))); + } + smtpVerifyCode = sb.toString(); + smtpVerifyExpire = + System.currentTimeMillis() + 300000L; + return smtpVerifyCode; + } + + /** + * 检查验证码是否有效 + */ + public boolean checkSmtpVerifyCode(String code) { + if (System.currentTimeMillis() > smtpVerifyExpire) { + smtpVerifyCode = ""; + return false; + } + return smtpVerifyCode.equalsIgnoreCase(code); + } + + /** + * 清除验证码 + */ + public void clearSmtpVerifyCode() { + smtpVerifyCode = ""; + smtpVerifyExpire = 0; + } + + /** + * 检查发送间隔是否>=1分钟 + */ + public boolean canSendSmtpVerify() { + return System.currentTimeMillis() + - smtpLastSendTime >= 60000L; + } + + public void recordSmtpSendTime() { + smtpLastSendTime = System.currentTimeMillis(); + } + +} diff --git a/txt/DatabaseManager.txt b/txt/DatabaseManager.txt new file mode 100644 index 0000000..5fb6bf1 --- /dev/null +++ b/txt/DatabaseManager.txt @@ -0,0 +1,1406 @@ +package Sdf1_login; + +import java.io.File; +import java.sql.*; +import java.util.*; +import java.util.logging.Logger; + +public class DatabaseManager { + + private Connection db; + private final File dataFolder; + private final Logger logger; + + public DatabaseManager(File dataFolder) { + this.dataFolder = dataFolder; + this.logger = Logger.getLogger("Sdf1_login"); + } + + // ==================== 初始化 ==================== + + public void init() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File(dataFolder, + "login.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + + // 用户表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "users (" + + "player_name TEXT PRIMARY KEY," + + "password_hash TEXT NOT NULL," + + "password_salt TEXT NOT NULL," + + "email TEXT DEFAULT ''," + + "register_time INTEGER DEFAULT 0," + + "last_login_time INTEGER DEFAULT 0," + + "total_online_time INTEGER DEFAULT 0," + + "last_online_check INTEGER DEFAULT 0," + + "is_logged_in INTEGER DEFAULT 0," + + "blocks_broken INTEGER DEFAULT 0," + + "blocks_placed INTEGER DEFAULT 0," + + "gift_stage INTEGER DEFAULT 0," + + "gift_claimed TEXT DEFAULT ''," + + "invite_code TEXT DEFAULT ''," + + "invited_by TEXT DEFAULT ''," + + "monthly_invite_count INTEGER DEFAULT 0," + + "invite_month TEXT DEFAULT ''," + + "points INTEGER DEFAULT 0," + + "checkin_streak INTEGER DEFAULT 0," + + "last_checkin_date TEXT DEFAULT ''," + + "total_checkin_days INTEGER DEFAULT 0," + + "temp_password TEXT DEFAULT ''," + + "temp_pw_expire INTEGER DEFAULT 0," + + "temp_pw_used INTEGER DEFAULT 0," + + "ip_address TEXT DEFAULT ''," + + "tasks_completed TEXT DEFAULT ''," + + "premium_uuid TEXT DEFAULT ''," + + "register_ip TEXT DEFAULT ''," + + "register_type TEXT DEFAULT 'manual'" + + ")"); + + // 安全添加缺失列 + safeAdd(st, "tasks_completed", + "TEXT DEFAULT ''"); + safeAdd(st, "temp_password", + "TEXT DEFAULT ''"); + safeAdd(st, "temp_pw_expire", + "INTEGER DEFAULT 0"); + safeAdd(st, "temp_pw_used", + "INTEGER DEFAULT 0"); + safeAdd(st, "ip_address", + "TEXT DEFAULT ''"); + safeAdd(st, "premium_uuid", + "TEXT DEFAULT ''"); + safeAdd(st, "register_ip", + "TEXT DEFAULT ''"); + safeAdd(st, "register_type", + "TEXT DEFAULT 'manual'"); + // 在 init 方法的 try 中添加 + // 如果表存在但没有save_time列,添加它 + try { + st.execute("ALTER TABLE " + + "inventory_backups " + + "ADD COLUMN save_time " + + "INTEGER DEFAULT 0"); + } catch (SQLException ignored) { + // 列已存在 + } + + // 背包备份表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "inventory_backups (" + + "player_name TEXT PRIMARY KEY," + + "inventory_data TEXT DEFAULT ''," + + "armor_data TEXT DEFAULT ''," + + "extra_data TEXT DEFAULT ''," + + "player_level INTEGER DEFAULT 0," + + "player_exp REAL DEFAULT 0.0)"); +// 确保 inventory_backups 表结构正确 + st.execute("CREATE TABLE IF NOT EXISTS " + + "inventory_backups (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "contents_b64 TEXT," + + "armor_b64 TEXT," + + "extra_b64 TEXT," + + "level INTEGER DEFAULT 0," + + "experience REAL DEFAULT 0," + + "save_time INTEGER DEFAULT 0" + + ")"); + // 安全报警表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "security_alerts (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "player_name TEXT NOT NULL," + + "alert_ip TEXT NOT NULL," + + "alert_time INTEGER NOT NULL," + + "alert_date TEXT NOT NULL," + + "notified INTEGER DEFAULT 0," + + "notified_time INTEGER DEFAULT 0)"); + + // 工单表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "tickets (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "type TEXT NOT NULL," + + "status TEXT DEFAULT 'pending'," + + "priority TEXT DEFAULT 'normal'," + + "requester TEXT NOT NULL," + + "assigned_to TEXT DEFAULT ''," + + "title TEXT DEFAULT ''," + + "description TEXT DEFAULT ''," + + "reward_amount INTEGER DEFAULT 0," + + "score INTEGER DEFAULT 0," + + "target_player TEXT DEFAULT ''," + + "create_time INTEGER NOT NULL," + + "update_time INTEGER DEFAULT 0," + + "complete_time INTEGER DEFAULT 0," + + "admin_confirmed INTEGER DEFAULT 0," + + "reject_reason TEXT DEFAULT ''" + + ")"); + // 工单回复表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "ticket_replies (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "ticket_id INTEGER NOT NULL," + + "sender TEXT NOT NULL," + + "message TEXT NOT NULL," + + "create_time INTEGER NOT NULL" + + ")"); + + // 服务商表 + st.execute("CREATE TABLE IF NOT EXISTS " + + "service_providers (" + + "player_name TEXT PRIMARY KEY," + + "role TEXT DEFAULT 'waiter'," + + "active INTEGER DEFAULT 1," + + "join_time INTEGER DEFAULT 0" + + ")"); + // 安全迁移 inventory_backups 表 + // 不删除旧数据,只补缺失列 + try { + st.execute("ALTER TABLE " + + "inventory_backups " + + "ADD COLUMN " + + "save_time INTEGER " + + "DEFAULT 0"); + } catch (SQLException ignored) { + } + try { + st.execute("ALTER TABLE " + + "inventory_backups " + + "ADD COLUMN " + + "level INTEGER " + + "DEFAULT 0"); + } catch (SQLException ignored) { + } + try { + st.execute("ALTER TABLE " + + "inventory_backups " + + "ADD COLUMN " + + "experience REAL " + + "DEFAULT 0"); + } catch (SQLException ignored) { + } + try { + st.execute("ALTER TABLE " + + "inventory_backups " + + "ADD COLUMN " + + "armor_b64 TEXT " + + "DEFAULT ''"); + } catch (SQLException ignored) { + } + try { + st.execute("ALTER TABLE " + + "inventory_backups " + + "ADD COLUMN " + + "extra_b64 TEXT " + + "DEFAULT ''"); + } catch (SQLException ignored) { + } + try { + st.execute("ALTER TABLE " + + "inventory_backups " + + "ADD COLUMN " + + "contents_b64 TEXT " + + "DEFAULT ''"); + } catch (SQLException ignored) { + } + + + st.close(); + logger.info("[Sdf1_login] 数据库初始化完成"); + } catch (Exception e) { + throw new RuntimeException( + "[Sdf1_login] DB初始化失败: " + + e.getMessage(), e); + } + } + + private void safeAdd(Statement st, + String col, String def) { + try { + st.executeUpdate("ALTER TABLE users " + + "ADD COLUMN " + col + " " + def); + } catch (SQLException ignored) { + } + } + + public void close() { + try { + if (db != null && !db.isClosed()) + db.close(); + } catch (Exception ignored) { + } + } + + // ==================== 用户操作 ==================== + + public boolean userExists(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + public void createUser(String name, String hash, + String salt) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO users " + + "(player_name, " + + "password_hash, " + + "password_salt, " + + "register_time) " + + "VALUES (?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, hash); + ps.setString(3, salt); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + // ========== 临时密码管理 ========== + + public void clearTempPassword(String name) { + setField(name, "temp_password", null); + setField(name, "temp_pw_expire", 0); + setField(name, "temp_pw_used", 1); + } + + public void clearAllTempPasswords() { + try { + Statement st = db.createStatement(); + st.executeUpdate( + "UPDATE users SET " + + "temp_password=NULL," + + "temp_pw_expire=0," + + "temp_pw_used=1 " + + "WHERE temp_password " + + "IS NOT NULL"); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public String getPlayerIPByName( + String name) { + Object ip = getField(name, "ip"); + return ip != null + ? String.valueOf(ip) : null; + } + + public boolean checkPasswordOrTemp( + String name, String hash) { + // 先检查主密码 + try { + PreparedStatement ps = db.prepareStatement( + "SELECT password_hash " + + "FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String mainHash = + rs.getString("password_hash"); + rs.close(); + ps.close(); + if (hash.equals(mainHash)) + return true; + } else { + rs.close(); + ps.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + // 再检查临时密码 + try { + PreparedStatement ps2 = db.prepareStatement( + "SELECT temp_password, " + + "temp_pw_expire, " + + "temp_pw_used " + + "FROM users " + + "WHERE player_name=?"); + ps2.setString(1, name); + ResultSet rs2 = ps2.executeQuery(); + if (rs2.next()) { + String tempHash = + rs2.getString( + "temp_password"); + long expire = + rs2.getLong( + "temp_pw_expire"); + int used = + rs2.getInt( + "temp_pw_used"); + rs2.close(); + ps2.close(); + if (tempHash != null + && !tempHash.isEmpty() + && hash.equals(tempHash) + && System.currentTimeMillis() + < expire + && used != 1) { + return true; + } + } else { + rs2.close(); + ps2.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + + public Map getUser(String name) { + Map r = + new LinkedHashMap<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + r.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return r; + } + + public void setField(String name, String field, + Object val) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE users SET " + field + + "=? " + + "WHERE player_name=?"); + if (val instanceof Integer) + ps.setInt(1, (Integer) val); + else if (val instanceof Long) + ps.setLong(1, (Long) val); + else + ps.setString(1, + String.valueOf(val)); + ps.setString(2, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Object getField(String name, + String field) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT " + field + + " FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + Object val = rs.next() + ? rs.getObject(1) : null; + rs.close(); + ps.close(); + return val; + } catch (SQLException e) { + return null; + } + } + + public boolean checkPassword(String name, + String hash) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT password_hash " + + "FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String stored = + rs.getString("password_hash"); + rs.close(); + ps.close(); + return hash.equals(stored); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + + + public boolean isUsingTempPassword(String name) { + String h = (String) getField(name, + "temp_password"); + return h != null && !h.isEmpty(); + } + + public void setLoggedIn(String name, + boolean val) { + setField(name, "is_logged_in", + val ? 1 : 0); + } + + public void addPoints(String name, int amount) { + Object cur = getField(name, "points"); + int curVal = cur instanceof Number + ? ((Number) cur).intValue() : 0; + setField(name, "points", curVal + amount); + } + + public boolean deductPoints(String name, + int amount) { + Object cur = getField(name, "points"); + int curVal = cur instanceof Number + ? ((Number) cur).intValue() : 0; + if (curVal < amount) return false; + setField(name, "points", + curVal - amount); + return true; + } + public List> + getAllUsers() { + List> list = + new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT * FROM users"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + public List> + getTicketsByProviderAll( + String provider) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE assigned_to=? " + + "ORDER BY create_time DESC"); + ps.setString(1, provider); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void deleteUser(String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM users " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void recordIP(String name, String ip) { + if (ip != null && !ip.isEmpty()) { + setField(name, "ip_address", ip); + } + } + + // ==================== IP查询 ==================== + + /** + * 统计指定IP注册的账号数量 + */ + public int countIPAccounts(String ip) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT COUNT(*) FROM users " + + "WHERE ip_address=?"); + ps.setString(1, ip); + ResultSet rs = ps.executeQuery(); + int count = 0; + if (rs.next()) count = rs.getInt(1); + rs.close(); + ps.close(); + return count; + } catch (SQLException e) { + return 0; + } + } + + /** + * 获取指定IP下所有账号详情 + */ + public List> + getAccountDetailsByIP(String ip) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT player_name, " + + "register_time " + + "FROM users " + + "WHERE ip_address=? " + + "ORDER BY register_time"); + ps.setString(1, ip); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("player_name", + rs.getString("player_name")); + row.put("register_time", + rs.getLong("register_time")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // ==================== 背包备份 ==================== + + public void saveInventoryBackup(String name, + String contents, String armor, + String extra, int level, + double experience) { + try { + PreparedStatement del = + db.prepareStatement( + "DELETE FROM " + + "inventory_backups " + + "WHERE player_name=?"); + del.setString(1, name); + del.executeUpdate(); + del.close(); + PreparedStatement ps = + db.prepareStatement( + "INSERT INTO " + + "inventory_backups" + + " (player_name, " + + "contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience, " + + "save_time) " + + "VALUES " + + "(?,?,?,?,?,?,?)"); + ps.setString(1, name); + ps.setString(2, contents); + ps.setString(3, armor); + ps.setString(4, extra); + ps.setInt(5, level); + ps.setDouble(6, experience); + ps.setLong(7, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public String[] loadInventoryBackup( + String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM " + + "inventory_backups " + + "WHERE player_name=? " + + "ORDER BY save_time " + + "DESC LIMIT 1"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = + new String[5]; + result[0] = rs.getString( + "contents_b64"); + result[1] = rs.getString( + "armor_b64"); + result[2] = rs.getString( + "extra_b64"); + result[3] = String.valueOf( + rs.getInt("level")); + result[4] = String.valueOf( + rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public void deleteInventoryBackup( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM inventory_backups " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean hasInventoryBackup( + String name) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id FROM " + + "inventory_backups " + + "WHERE player_name=? " + + "LIMIT 1"); + ps.setString(1, name); + ResultSet rs = ps.executeQuery(); + boolean has = rs.next(); + rs.close(); + ps.close(); + return has; + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + + // ==================== 安全报警 ==================== + + public void recordSecurityAlert( + String playerName, String ip) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO security_alerts " + + "(player_name, alert_ip, " + + "alert_time, alert_date) " + + "VALUES (?,?,?,?)"); + ps.setString(1, playerName); + ps.setString(2, ip); + long now = System.currentTimeMillis(); + ps.setLong(3, now); + ps.setString(4, + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date( + now))); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> + getUnnotifiedAlerts( + String playerName) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM security_alerts " + + "WHERE player_name=? " + + "AND notified=0 " + + "ORDER BY alert_time ASC"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void markAlertNotified(int alertId) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE security_alerts " + + "SET notified=1, " + + "notified_time=? " + + "WHERE id=?"); + ps.setLong(1, + System.currentTimeMillis()); + ps.setInt(2, alertId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + /** + * 获取玩家最近N条背包备份 + */ + public List> + getInventoryBackups(String name, + int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id, save_time " + + "FROM inventory_backups " + + "WHERE player_name=? " + + "ORDER BY save_time DESC " + + "LIMIT ?"); + ps.setString(1, name); + ps.setInt(2, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", + rs.getInt("id")); + row.put("save_time", + rs.getLong("save_time")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + + /** + * 获取指定ID的备份数据 + */ + public String[] getInventoryBackupById( + int backupId) { + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT contents_b64, " + + "armor_b64, " + + "extra_b64, " + + "level, " + + "experience " + + "FROM inventory_backups " + + "WHERE id=?"); + ps.setInt(1, backupId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + String[] result = + new String[5]; + result[0] = rs.getString( + "contents_b64"); + result[1] = rs.getString( + "armor_b64"); + result[2] = rs.getString( + "extra_b64"); + result[3] = String.valueOf( + rs.getInt("level")); + result[4] = String.valueOf( + rs.getDouble("experience")); + rs.close(); + ps.close(); + return result; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + + // ==================== 工单操作 ==================== + + public int createTicket(String type, + String status, String priority, + String requester, String title, + String description, + int rewardAmount, + String targetPlayer) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO tickets " + + "(type, status, priority, " + + "requester, title, " + + "description, " + + "reward_amount, " + + "target_player, " + + "create_time) " + + "VALUES (?,?,?,?,?,?,?,?,?)"); + ps.setString(1, type); + ps.setString(2, status); + ps.setString(3, priority); + ps.setString(4, requester); + ps.setString(5, title); + ps.setString(6, description); + ps.setInt(7, rewardAmount); + ps.setString(8, targetPlayer); + ps.setLong(9, + System.currentTimeMillis()); + ps.executeUpdate(); + ResultSet rs = + ps.getGeneratedKeys(); + int id = -1; + if (rs.next()) id = rs.getInt(1); + rs.close(); + ps.close(); + return id; + } catch (SQLException e) { + e.printStackTrace(); + return -1; + } + } + + public void updateTicketStatus(int ticketId, + String status) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET status=?, " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, status); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void assignTicket(int ticketId, + String provider) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "assigned_to=?, " + + "status='processing', " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, provider); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void completeTicket(int ticketId) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "status='completed', " + + "complete_time=?, " + + "update_time=? " + + "WHERE id=?"); + long now = System.currentTimeMillis(); + ps.setLong(1, now); + ps.setLong(2, now); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void confirmTicket(int ticketId, + int score) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "admin_confirmed=1, " + + "score=?, " + + "status='resolved', " + + "update_time=? " + + "WHERE id=?"); + ps.setInt(1, score); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + + public void rejectTicketComplete(int ticketId, + String reason) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + + "status='rejected', " + + "reject_reason=?, " + + "update_time=? " + + "WHERE id=?"); + ps.setString(1, reason); + ps.setLong(2, + System.currentTimeMillis()); + ps.setInt(3, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + /** + * 更新工单任意字段 + */ + public void updateTicketField(int ticketId, + String field, Object value) { + try { + PreparedStatement ps = db.prepareStatement( + "UPDATE tickets SET " + field + + "=? WHERE id=?"); + if (value instanceof Integer) + ps.setInt(1, (Integer) value); + else if (value instanceof Long) + ps.setLong(1, (Long) value); + else + ps.setString(1, + String.valueOf(value)); + ps.setInt(2, ticketId); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Map getTicket( + int ticketId) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE id=?"); + ps.setInt(1, ticketId); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + rs.close(); + ps.close(); + return row; + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } + + public List> + getTicketsByStatus(String status) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status=? " + + "ORDER BY create_time DESC"); + ps.setString(1, status); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public void addTicketReply(int ticketId, + String sender, String message) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT INTO ticket_replies " + + "(ticket_id, sender, " + + "message, create_time) " + + "VALUES (?,?,?,?)"); + ps.setInt(1, ticketId); + ps.setString(2, sender); + ps.setString(3, message); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + // 更新工单状态为已回复 + updateTicketStatus(ticketId, + "replied"); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public List> + getTicketReplies(int ticketId) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM ticket_replies " + + "WHERE ticket_id=? " + + "ORDER BY create_time ASC"); + ps.setInt(1, ticketId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + /** + * 获取已回复但超时未互动的工单 + */ + public List> + getRepliedStaleTickets( + long timeoutMs) { + List> list = + new ArrayList<>(); + try { + long cutoff = + System.currentTimeMillis() + - timeoutMs; + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status='replied' " + + "AND update_time < ?"); + ps.setLong(1, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTicketsByPlayer(String playerName) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE requester=? " + + "ORDER BY create_time DESC"); + ps.setString(1, playerName); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getTicketsByProvider(String provider) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE assigned_to=? " + + "AND (status='processing' " + + "OR status='replied') " + + "ORDER BY create_time DESC"); + ps.setString(1, provider); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs + .getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public List> + getUnconfirmedCompleted( + long timeoutMs) { + List> list = + new ArrayList<>(); + try { + long cutoff = + System.currentTimeMillis() + - timeoutMs; + PreparedStatement ps = db.prepareStatement( + "SELECT * FROM tickets " + + "WHERE status='completed' " + + "AND admin_confirmed=0 " + + "AND complete_time < ?"); + ps.setLong(1, cutoff); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + // ==================== 服务商操作 ==================== + + public void addServiceProvider(String name, + String role) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO " + + "service_providers " + + "(player_name, role, " + + "active, join_time) " + + "VALUES (?,?,1,?)"); + ps.setString(1, name); + ps.setString(2, role); + ps.setLong(3, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void removeServiceProvider( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM service_providers " + + "WHERE player_name=?"); + ps.setString(1, name); + ps.executeUpdate(); + ps.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public boolean isServiceProvider( + String name) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT 1 FROM service_providers " + + "WHERE player_name=? " + + "AND active=1"); + ps.setString(1, name); + boolean r = ps.executeQuery().next(); + ps.close(); + return r; + } catch (SQLException e) { + return false; + } + } + + public List> + getAllServiceProviders() { + List> list = + new ArrayList<>(); + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT * FROM service_providers " + + "WHERE active=1"); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + for (int i = 1; i <= rs.getMetaData() + .getColumnCount(); i++) { + row.put(rs.getMetaData() + .getColumnName(i), + rs.getObject(i)); + } + list.add(row); + } + rs.close(); + st.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return list; + } + + public Connection getDb() { + return db; + } +} diff --git a/txt/EmailManager.txt b/txt/EmailManager.txt new file mode 100644 index 0000000..3b37bb1 --- /dev/null +++ b/txt/EmailManager.txt @@ -0,0 +1,210 @@ +package Sdf1_login; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.Base64; +import java.util.logging.Logger; + +public class EmailManager { + + private final ConfigManager config; + private final Logger logger; + /** + * 发送SMTP验证码 + */ + public boolean sendVerifyCode(String to, String code) { + if (!isConfigured()) { + plugin.getLogger().severe( + "[Sdf1_login] SMTP未配置完整," + + "请检查SMTP设置.txt"); + return false; + } + String subject = + "[Sdf1_login] SMTP配置验证码"; + String body = "您的SMTP配置验证码是: " + + code + "\n" + + "验证码5分钟内有效。\n" + + "如非本人操作请忽略此邮件。"; + try { + return sendEmail(to, subject, body); + } catch (Exception e) { + plugin.getLogger().severe( + "[Sdf1_login] 邮件发送异常: " + + e.getClass().getSimpleName() + + ": " + e.getMessage()); + if (e.getCause() != null) { + plugin.getLogger().severe( + "[Sdf1_login] 根因: " + + e.getCause().getMessage()); + } + return false; + } + } + + + public EmailManager(ConfigManager config) { + this.config = config; + this.logger = Logger.getLogger("Sdf1_login"); + } + + /** + * 检查SMTP是否已配置 + */ + public boolean isConfigured() { + String host = config.getSmtp("smtp地址"); + String user = config.getSmtp("发件邮箱"); + String pass = config.getSmtp("授权码"); + return host != null && !host.isEmpty() + && user != null && !user.isEmpty() + && pass != null && !pass.isEmpty(); + } + + /** + * 发送验证码,返回是否成功发起 + */ + public boolean sendVerifyCode(String to, String player, String code) { + if (!isConfigured()) { + logger.warning("[Sdf1_login] SMTP未配置,无法发送验证码到 " + to); + return false; + } + logger.info("[Sdf1_login] 发送验证码到 " + to + " (玩家: " + player + ")"); + return sendEmailSync(to, "Sdf1_login 验证码", + "玩家 " + player + " 的验证码: " + code); + } + + /** + * 发送临时密码,返回是否成功发起 + */ + public boolean sendTempPassword(String to, String player, String tempPwd) { + if (!isConfigured()) { + logger.warning("[Sdf1_login] SMTP未配置,无法发送临时密码到 " + to); + return false; + } + logger.info("[Sdf1_login] 发送临时密码到 " + to + " (玩家: " + player + ")"); + return sendEmailSync(to, "Sdf1_login 临时密码", + "玩家 " + player + " 的临时密码: " + tempPwd + "\n请尽快登录并修改密码!"); + } + + private boolean sendEmailSync(String to, String subject, String body) { + final boolean[] result = {false}; + Thread t = new Thread(() -> { + try { + result[0] = sendEmail(to, subject, body); + } catch (Exception e) { + logger.severe("[Sdf1_login] 邮件线程异常: " + e.getMessage()); + } + }, "Sdf1_login-SMTP"); + t.setDaemon(true); + t.start(); + try { t.join(15000); } catch (InterruptedException ignored) {} + if (t.isAlive()) { + t.interrupt(); + logger.severe("[Sdf1_login] SMTP超时(15秒)"); + return false; + } + return result[0]; + } + + private boolean sendEmail(String to, String subject, String body) { + String host = config.getSmtp("smtp地址"); + String portStr = config.getSmtp("smtp端口"); + String user = config.getSmtp("发件邮箱"); + String pass = config.getSmtp("授权码"); + String fromName = config.getSmtp("发件人名"); + + int port; + try { port = Integer.parseInt(portStr); } catch (Exception e) { port = 465; } + + try { + String encSubject = "=?UTF-8?B?" + Base64.getEncoder().encodeToString(subject.getBytes("UTF-8")) + "?="; + String encBody = Base64.getEncoder().encodeToString(body.getBytes("UTF-8")); + + if (port == 465) { + SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + SSLSocket socket = (SSLSocket) factory.createSocket(host, port); + socket.setSoTimeout(10000); + doSmtp(socket, host, user, pass, to, encSubject, encBody, fromName); + socket.close(); + } else { + Socket plainSocket = new Socket(host, port); + plainSocket.setSoTimeout(10000); + BufferedReader preReader = new BufferedReader(new InputStreamReader(plainSocket.getInputStream(), "UTF-8")); + PrintWriter preWriter = new PrintWriter(plainSocket.getOutputStream(), true); + checkResponse(preReader, "220"); + sendLine(preWriter, "EHLO " + host); + checkResponse(preReader, "250"); + sendLine(preWriter, "STARTTLS"); + checkResponse(preReader, "220"); + SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + SSLSocket sslSocket = (SSLSocket) factory.createSocket(plainSocket, host, port, true); + sslSocket.startHandshake(); + doSmtp(sslSocket, host, user, pass, to, encSubject, encBody, fromName); + sslSocket.close(); + } + logger.info("[Sdf1_login] 邮件发送成功 -> " + to); + return true; + } catch (Exception e) { + logger.severe("[Sdf1_login] 邮件异常: " + e.getMessage()); + return false; + } + } + + private void doSmtp(SSLSocket socket, String host, String user, String pass, + String to, String subject, String body, String fromName) throws Exception { + BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); + OutputStream rawOut = socket.getOutputStream(); + PrintWriter writer = new PrintWriter(new java.io.BufferedOutputStream(rawOut), true); + + checkResponse(reader, "220"); + sendLine(writer, "EHLO " + host); + checkResponse(reader, "250"); + sendLine(writer, "AUTH LOGIN"); + checkResponse(reader, "334"); + sendLine(writer, Base64.getEncoder().encodeToString(user.getBytes("UTF-8"))); + checkResponse(reader, "334"); + sendLine(writer, Base64.getEncoder().encodeToString(pass.getBytes("UTF-8"))); + checkResponse(reader, "235"); + sendLine(writer, "MAIL FROM:<" + user + ">"); + checkResponse(reader, "250"); + sendLine(writer, "RCPT TO:<" + to + ">"); + checkResponse(reader, "250"); + sendLine(writer, "DATA"); + checkResponse(reader, "354"); + sendLine(writer, "From: " + fromName + " <" + user + ">"); + sendLine(writer, "To: <" + to + ">"); + sendLine(writer, "Reply-To: <" + user + ">"); + sendLine(writer, "Subject: " + subject); + sendLine(writer, "MIME-Version: 1.0"); + sendLine(writer, "Content-Type: text/plain; charset=UTF-8"); + sendLine(writer, "Content-Transfer-Encoding: base64"); + sendLine(writer, ""); + sendLine(writer, body); + sendLine(writer, "."); + checkResponse(reader, "250"); + sendLine(writer, "QUIT"); + } + + private void sendLine(PrintWriter writer, String line) { + writer.print(line + "\r\n"); + writer.flush(); + } + + private void checkResponse(BufferedReader reader, String expected) throws Exception { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + if (line.length() >= 4 && Character.isDigit(line.charAt(0)) && line.charAt(3) == ' ') break; + } + String resp = sb.toString().trim(); + if (!resp.startsWith(expected)) { + if (resp.startsWith("535")) logger.severe("[Sdf1_login] SMTP 535 认证失败"); + throw new Exception("SMTP响应异常: 期望 " + expected + ",实际: " + resp); + } + } +} diff --git a/txt/GUIManager.txt b/txt/GUIManager.txt new file mode 100644 index 0000000..6c60ac6 --- /dev/null +++ b/txt/GUIManager.txt @@ -0,0 +1,861 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.*; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.*; + +public class GUIManager { + + private final Main plugin; + + public static final String T_MAIN = + "§6§l登录面板"; + public static final String T_MY_INFO = + "§a§l我的信息"; + public static final String T_ADMIN = + "§c§l管理员面板"; + public static final String T_USER_MGMT = + "§e§l用户管理"; + public static final String T_INVITE = + "§b§l邀请数据"; + public static final String T_TASK_CENTER = "§d§l任务中心"; + public static final String T_GIFT_STAGES = "§d§l新人任务"; + + + public GUIManager(Main plugin) { + this.plugin = plugin; + } + + public void openMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_MAIN); + fillBg(g); + g.setItem(10, mkItem(Material.PLAYER_HEAD, + "§a§l我的信息")); + g.setItem(12, mkItem(Material.PAPER, + "§b§l邀请数据")); + g.setItem(14, mkItem(Material.EMERALD, + "§d§l积分商城")); + g.setItem(16, mkItem(Material.CHEST, + "§e§l任务中心")); + g.setItem(18, mkItem(Material.BOOK, + "§6工单系统", + "§7提交bug、求助、举报")); + if (p.isOp() || isAdmin(p)) { + g.setItem(20, mkItem(Material.CHEST, + "§6垃圾回收站", + "§7查看和取回清理的物品")); + } + if (isAdmin(p)) { + g.setItem(22, mkItem(Material.REDSTONE_BLOCK, + "§c§l管理员面板")); + } + p.openInventory(g); + } + + public void openMyInfo(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_MY_INFO); + fillBg(g); + Map user = plugin.getDb() + .getUser(p.getName()); + boolean checkedIn = plugin.getCheckIn() + .isCheckedInToday(p.getName()); + Material checkMat = checkedIn + ? Material.EMERALD : Material.COAL; + String checkName = checkedIn + ? "§a已签到" : "§7点击签到"; + int streak = ((Number) user.getOrDefault( + "checkin_streak", 0)).intValue(); + int points = ((Number) user.getOrDefault( + "points", 0)).intValue(); + int totalDays = ((Number) user.getOrDefault( + "total_checkin_days", 0)).intValue(); + String email = (String) user.getOrDefault( + "email", ""); + g.setItem(4, mkItem(Material.NAME_TAG, + "§e" + p.getName())); + g.setItem(10, mkItem(checkMat, checkName, + "§7连续签到: " + streak + "天", + "§7累计签到: " + totalDays + "天", + "§7当前积分: " + points)); + g.setItem(11, mkItem(Material.PAPER, + "§e修改密码", "§7双击修改密码")); + g.setItem(12, mkItem(Material.CLOCK, + "§e补签", "§7消耗10积分补签3天内")); + g.setItem(13, mkItem(Material.ENDER_PEARL, + "§e邀请码", + "§7你的邀请码: " + + user.getOrDefault( + "invite_code", "无"), + "§7点击生成/查看")); + g.setItem(14, mkItem(Material.BOOK, + "§e邮箱: " + + (email.isEmpty() + ? "未绑定" : email), + "§7点击绑定邮箱")); + g.setItem(15, mkItem(Material.EMERALD_BLOCK, + "§a§l积分商城", + "§7当前: " + points + "积分")); + g.setItem(26, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + /** + * 打开账号删除选择面板 + * 展示该玩家IP下所有账号,供选择删除 + */ + public void openAccountDelete(Player p) { + String ip = plugin.getPlayerIP(p); + if (ip == null) { + p.sendMessage("§c无法获取你的IP地址"); + return; + } + + List> accounts = + plugin.getDb().getAccountDetailsByIP(ip); + + // 54格大箱子 + org.bukkit.inventory.Inventory g = + org.bukkit.Bukkit.createInventory( + null, 54, + "§c§l选择要删除的账号"); + + // 填充玻璃面板 + org.bukkit.inventory.ItemStack glass = + new org.bukkit.inventory.ItemStack( + org.bukkit.Material + .RED_STAINED_GLASS_PANE); + org.bukkit.inventory.meta.ItemMeta gm = + glass.getItemMeta(); + if (gm != null) { + gm.setDisplayName(" "); + glass.setItemMeta(gm); + } + for (int i = 0; i < 54; i++) { + g.setItem(i, glass); + } + + // 展示账号列表(从0号位开始放) + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm"); + int slot = 0; + for (Map acc : accounts) { + if (slot >= 45) break; + String accName = (String) acc + .get("player_name"); + long regTime = ((Number) acc + .get("register_time")).longValue(); + String dateStr = regTime > 0 + ? sdf.format( + new java.util.Date(regTime)) + : "未知"; + + // 当前登录的账号用不同颜色标记 + boolean isCurrent = + accName.equalsIgnoreCase( + p.getName()); + + org.bukkit.inventory.ItemStack item = + new org.bukkit.inventory.ItemStack( + isCurrent + ? org.bukkit.Material + .RED_WOOL + : org.bukkit.Material + .ORANGE_WOOL); + org.bukkit.inventory.meta.ItemMeta im = + item.getItemMeta(); + if (im != null) { + im.setDisplayName( + (isCurrent ? "§c" : "§e") + + accName); + im.setLore(java.util.Arrays.asList( + "§7注册时间: " + dateStr, + "§7IP: " + ip, + isCurrent + ? "§c§l(当前账号)" + : "§a点击选择删除此账号")); + item.setItemMeta(im); + } + g.setItem(slot, item); + slot++; + } + + // 49号位:返回 + org.bukkit.inventory.ItemStack back = + new org.bukkit.inventory.ItemStack( + org.bukkit.Material.ARROW); + org.bukkit.inventory.meta.ItemMeta backMeta = + back.getItemMeta(); + if (backMeta != null) { + backMeta.setDisplayName("§7返回"); + back.setItemMeta(backMeta); + } + g.setItem(49, back); + + p.openInventory(g); + } + + /** + * 打开删除确认面板 + */ + public void openDeleteConfirm(Player p, + String targetName) { + org.bukkit.inventory.Inventory g = + org.bukkit.Bukkit.createInventory( + null, 27, + "§c§l确认删除: " + targetName); + + org.bukkit.inventory.ItemStack glass = + new org.bukkit.inventory.ItemStack( + org.bukkit.Material + .GRAY_STAINED_GLASS_PANE); + org.bukkit.inventory.meta.ItemMeta gm = + glass.getItemMeta(); + if (gm != null) { + gm.setDisplayName(" "); + glass.setItemMeta(gm); + } + for (int i = 0; i < 27; i++) { + g.setItem(i, glass); + } + + // 11号位:确认删除 + org.bukkit.inventory.ItemStack confirm = + new org.bukkit.inventory.ItemStack( + org.bukkit.Material.LIME_WOOL); + org.bukkit.inventory.meta.ItemMeta cm = + confirm.getItemMeta(); + if (cm != null) { + cm.setDisplayName("§a§l确认删除"); + cm.setLore(java.util.Arrays.asList( + "§7删除账号: §c" + targetName, + "§c此操作不可撤销!")); + confirm.setItemMeta(cm); + } + g.setItem(11, confirm); + + // 15号位:取消 + org.bukkit.inventory.ItemStack cancel = + new org.bukkit.inventory.ItemStack( + org.bukkit.Material.RED_WOOL); + org.bukkit.inventory.meta.ItemMeta cml = + cancel.getItemMeta(); + if (cml != null) { + cml.setDisplayName("§c§l取消"); + cancel.setItemMeta(cml); + } + g.setItem(15, cancel); + + p.openInventory(g); + } + + public void openShopAdmin(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§c§l商城管理"); + ItemStack glass = new ItemStack( + Material.GRAY_STAINED_GLASS_PANE); + ItemMeta glm = glass.getItemMeta(); + if (glm != null) { + glm.setDisplayName(" "); + glass.setItemMeta(glm); + } + for (int i = 0; i < 54; i++) + g.setItem(i, glass); + g.setItem(49, mkItem(Material.ARROW, + "§7返回商城")); + g.setItem(50, mkItem(Material.EMERALD, + "§a添加商品", + "§7格式: /sdf1_login shopadd <名称> <价格> <命令>")); + g.setItem(51, mkItem(Material.BARRIER, + "§c删除商品", + "§7格式: /sdf1_login shopdel <序号>")); + p.openInventory(g); + } + + + public void openAdmin(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_ADMIN); + fillBg(g); + g.setItem(4, mkItem(Material.NAME_TAG, + "§e§l管理员面板")); + g.setItem(10, mkItem(Material.REDSTONE, + "§6SMTP配置", + "§7配置邮件服务器")); + g.setItem(11, mkItem(Material.ARMOR_STAND, + "§a用户管理", + "§7管理玩家数据")); + g.setItem(12, mkItem(Material.PLAYER_HEAD, + "§a§l我的信息")); + g.setItem(14, mkItem(Material.EMERALD, + "§d§l积分商城")); + boolean afkOn = plugin.getConfig2().afkEnabled; + int afkMin = plugin.getConfig2().afkTimeout / 60; + g.setItem(16, mkItem( + afkOn ? Material.REDSTONE_TORCH + : Material.TORCH, + afkOn ? "§a§l挂机踢出: 开启" + : "§c§l挂机踢出: 关闭", + "§7左键: 切换开关", + "§7右键: 设置时长", + "§7当前: " + afkMin + "分钟")); + g.setItem(22, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + + public void openInvite(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_INVITE); + fillBg(g); + String code = (String) plugin.getDb() + .getField(p.getName(), "invite_code"); + if (code == null || code.isEmpty()) { + code = plugin.getInvite().generateCode(p); + } + int monthly = ((Number) plugin.getDb() + .getField(p.getName(), + "monthly_invite_count")) + .intValue(); + g.setItem(10, mkItem(Material.PAPER, + "§b§l我的邀请码", + "§7" + code, + "", + "§e点击刷新邀请码")); + g.setItem(12, mkItem(Material.PLAYER_HEAD, + "§a本月邀请", + "§7已邀请 §e" + monthly + " §7人")); + g.setItem(16, mkItem(Material.BOOK, + "§e输入邀请码", + "§7绑定他人的邀请码", + "", + "§e点击输入")); + g.setItem(26, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + + private String getInviteRate(int count) { + if (count > 15) return "60%"; + if (count > 10) return "35%"; + if (count > 5) return "15%"; + if (count > 0) return "10%"; + return "0%"; + } + + public void openUserManagement(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_USER_MGMT); + fillBg(g); + List> users = plugin.getDb() + .getAllUsers(); + int slot = 0; + for (Map user : users) { + if (slot >= 45) break; + String name = (String) user.get("player_name"); + int pts = ((Number) user.getOrDefault( + "points", 0)).intValue(); + int stage = ((Number) user.getOrDefault( + "gift_stage", 0)).intValue(); + g.setItem(slot, mkItem(Material.PLAYER_HEAD, + "§e" + name, + "§7积分: " + pts + + " 礼包阶段: " + stage, + "§7双击管理")); + slot++; + } + + g.setItem(53, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + public void openTaskCenter(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_TASK_CENTER); + fillBg(g); + + QuestTracker qt = + plugin.getQuestTracker(); + String player = p.getName(); + + // 新人任务进度 + List newQuests = + qt.getQuests("新人任务"); + int newTotal = newQuests.size(); + int newDone = + qt.getCompletedStageCount( + player, newQuests); + QuestTracker.QuestFile newCurrent = + qt.getCurrentStage( + player, newQuests); + + String newInfo = newDone + "/" + newTotal; + List newLore = new ArrayList<>(); + newLore.add("§7进度: §e" + newInfo); + if (newCurrent != null) { + newLore.add("§7当前: §f" + + newCurrent.displayName); + if (newCurrent.hasConditions()) { + List unmet = + qt.getUnmetConditions( + player, + newCurrent.conditions); + if (!unmet.isEmpty()) { + for (String c : unmet) { + newLore.add("§7 " + + qt.getProgress( + player, c)); + } + } else { + newLore.add("§a✓ 条件已满足"); + } + } + } else { + newLore.add("§a§l全部完成!"); + } + + g.setItem(10, mkItem(Material.BOOK, + "§e§l新人任务 §7[" + + newInfo + "]", + newLore.toArray(new String[0]))); + + // 主线任务 + List mainQuests = + qt.getQuests("主线任务"); + g.setItem(12, mkItem(Material.COMPASS, + "§b§l主线任务 §7[" + + qt.getCompletedStageCount( + player, mainQuests) + + "/" + mainQuests.size() + + "]", + "§7查看主线任务列表")); + + // 支线任务 + List sideQuests = + qt.getQuests("支线任务"); + g.setItem(14, mkItem(Material.PAPER, + "§a§l支线任务 §7[" + + qt.getCompletedStageCount( + player, sideQuests) + + "/" + sideQuests.size() + + "]", + "§7查看支线任务列表")); + + g.setItem(16, mkItem(Material.EMERALD, + "§d§l每日签到", + "§7签到获取积分")); + + // 打卡上报按钮(slot 21) + g.setItem(21, mkItem( + Material.EMERALD, + "§a§l打卡上报", + "§7验证当前阶段任务完成情况", + "§7满足条件将自动标记完成")); + + // 返回按钮(slot 26) + g.setItem(26, mkItem( + Material.ARROW, "§7返回")); + p.openInventory(g); + } + + + public void openGiftStages(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_GIFT_STAGES); + fillBg(g); + + QuestTracker qt = + plugin.getQuestTracker(); + List quests = + qt.getQuests("新人任务"); + int total = quests.size(); + int completed = + qt.getCompletedStageCount( + p.getName(), quests); + QuestTracker.QuestFile current = + qt.getCurrentStage( + p.getName(), quests); + + g.setItem(4, mkItem(Material.BOOK, + "§e§l新人任务进度", + "§7已完成: §e" + completed + + "/" + total, + current != null + ? "§7当前: §f" + + current.displayName + : "§a§l全部完成!")); + + for (int i = 0; + i < quests.size() && i < 44; i++) { + QuestTracker.QuestFile qf = + quests.get(i); + boolean done = + qt.isStageCompleted( + p.getName(), qf); + boolean isCurrent = + (current != null + && current == qf); + + Material mat; + List lore = new ArrayList<>(); + + if (done) { + mat = Material.LIME_WOOL; + } else if (isCurrent) { + mat = Material.YELLOW_WOOL; + } else { + mat = Material.RED_WOOL; + } + + if (!qf.details.isEmpty()) { + lore.add("§7---- 任务详情 ----"); + for (String d : qf.details) { + lore.add("§f" + d); + } + } + + if (qf.hasConditions() + && (done || isCurrent)) { + lore.add(""); + for (String c : qf.conditions) { + if (qt.isOptional(c)) + continue; + if (qt.isConditionMet( + p.getName(), c)) { + lore.add("§7 §a✓ " + + qt.getProgress( + p.getName(), c)); + } else { + lore.add("§7 §c✗ " + + qt.getProgress( + p.getName(), c)); + } + } + } + + if (!qf.rewards.isEmpty()) { + lore.add(""); + lore.add("§7---- 奖励 ----"); + for (String r : qf.rewards) { + lore.add("§a" + r); + } + } + + if (done + && !qf.rewards.isEmpty() + && !qt.hasClaimed( + p.getName(), qf)) { + lore.add(""); + lore.add("§e§l点击领取奖励"); + } + if (done && qt.hasClaimed( + p.getName(), qf)) { + lore.add(""); + lore.add("§7已领取奖励"); + } + + g.setItem(10 + i, mkItem(mat, + "§e" + qf.displayName, + lore.toArray(new String[0]))); + } + // 返回按钮 + g.setItem(53, mkItem( + Material.ARROW, "§7返回")); + p.openInventory(g); + } + + + + + public void openTaskList(Player p, + String folderName) { + QuestTracker qt = + plugin.getQuestTracker(); + List tasks = + qt.getQuests(folderName); + + int size = Math.min(54, + Math.max(54, + ((tasks.size() / 9) + 1) * 9)); + Inventory g = Bukkit.createInventory( + null, size, + "§d§l" + folderName); + fillBg(g); + + for (int i = 0; + i < tasks.size() + && i < 45; i++) { + QuestTracker.QuestFile qf = + tasks.get(i); + boolean done = + qt.isStageCompleted( + p.getName(), qf); + Material mat = done + ? Material.LIME_WOOL + : Material.BOOK; + + List lore = new ArrayList<>(); + + // 解锁条件 + if (qf.hasConditions()) { + lore.add("§7──── 解锁条件 ────"); + for (String c : qf.conditions) { + if (qt.isOptional(c)) + continue; + if (qt.isConditionMet( + p.getName(), c)) { + lore.add("§a ✓ " + + qt.getProgress( + p.getName(), c)); + } else { + lore.add("§c ✗ " + + qt.getProgress( + p.getName(), c)); + } + } + } + + // 任务详情 + if (!qf.details.isEmpty()) { + lore.add(""); + lore.add("§7──── 任务详情 ────"); + for (String d : qf.details) { + lore.add("§f" + d); + } + } + + // 任务奖励 + if (!qf.rewards.isEmpty()) { + lore.add(""); + lore.add("§7──── 任务奖励 ────"); + for (String r : qf.rewards) { + lore.add("§a" + r); + } + } + + g.setItem(i, mkItem(mat, + (done ? "§a✓ " : "§e") + + qf.displayName, + lore.toArray(new String[0]))); + } + + if (tasks.isEmpty()) { + g.setItem(22, mkItem( + Material.BARRIER, + "§7暂无任务")); + } + g.setItem(size - 1, mkItem( + Material.ARROW, "§7返回")); + p.openInventory(g); + } + + + public void openUserDetail(Player p, String target) { + Inventory g = Bukkit.createInventory( + null, 27, "§e§l管理: " + target); + fillBg(g); + Map user = plugin.getDb() + .getUser(target); + int pts = ((Number) user.getOrDefault( + "points", 0)).intValue(); + String email = (String) user.getOrDefault( + "email", ""); + g.setItem(4, mkItem(Material.NAME_TAG, + "§e" + target)); + g.setItem(10, mkItem(Material.EMERALD, + "§a积分: " + pts, + "§7点击设置积分")); + g.setItem(11, mkItem(Material.PAPER, + "§e邮箱: " + + (email.isEmpty() ? "无" : email))); + g.setItem(12, mkItem(Material.BOOK, + "§e设置密码")); + g.setItem(16, mkItem(Material.BARRIER, + "§c删除用户", "§7二次验证删除")); + // 雪球菜单检测(slot 14) + g.setItem(14, mkItem( + Material.SNOWBALL, + "§e§l雪球菜单", + "§7点击为该玩家发放菜单雪球")); + g.setItem(16, mkItem( + Material.ENDER_EYE, + "§b§l打卡记录", + "§7查看该玩家的群系打卡记录")); + + g.setItem(22, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } + + public void openCheckInRecords( + Player p, String target) { + Inventory g = Bukkit.createInventory( + null, 54, + "§b§l打卡记录: " + target); + fillBg(g); + + QuestTracker qt = + plugin.getQuestTracker(); + List records = + qt.getPlayerRecords(target); + + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss"); + + int slot = 0; + for (int i = records.size() - 1; + i >= 0 && slot < 45; i--) { + String r = records.get(i); + String[] parts = r.split("\\|"); + if (parts.length < 7) continue; + + String world = parts[1]; + String biome = parts[2]; + String category = parts[3]; + String x = parts[4]; + String y = parts[5]; + String z = parts[6]; + String timeStr = ""; + if (parts.length >= 8) { + try { + long ts = Long.parseLong( + parts[7]); + timeStr = sdf.format( + new java.util.Date( + ts)); + } catch (Exception e) { + timeStr = parts[7]; + } + } + + g.setItem(slot, mkItem( + Material.PAPER, + "§e" + category + " - " + + biome, + "§7时间: " + timeStr, + "§7世界: " + world, + "§7坐标: " + x + + ", " + y + + ", " + z)); + slot++; + } + + if (slot == 0) { + g.setItem(22, mkItem( + Material.BARRIER, + "§7暂无打卡记录")); + } + + g.setItem(53, mkItem( + Material.ARROW, "§7返回")); + p.openInventory(g); + } + + + /** + * 打开账号请求面板 + * 玩家通过 /oa 命令触发 + */ + public void openAccountRequest(Player p) { + org.bukkit.inventory.Inventory g = + org.bukkit.Bukkit.createInventory( + null, 27, + "§e§l账号请求"); + + // 填充玻璃面板 + org.bukkit.inventory.ItemStack glass = + new org.bukkit.inventory.ItemStack( + org.bukkit.Material + .YELLOW_STAINED_GLASS_PANE); + org.bukkit.inventory.meta.ItemMeta gm = + glass.getItemMeta(); + if (gm != null) { + gm.setDisplayName(" "); + glass.setItemMeta(gm); + } + for (int i = 0; i < 27; i++) { + g.setItem(i, glass); + } + + // 10号位:申请删除账号 + g.setItem(10, makeItem( + org.bukkit.Material.BARRIER, + "§c§l申请删除账号", + "§7点击后确认删除您的账号", + "§7此操作不可撤销")); + + // 13号位:找回密码 + g.setItem(13, makeItem( + org.bukkit.Material.TRIPWIRE_HOOK, + "§e§l找回密码", + "§7通过绑定邮箱获取临时密码", + "§7需要先绑定邮箱")); + + // 16号位:联系管理员 + g.setItem(16, makeItem( + org.bukkit.Material.EMERALD, + "§a§l联系管理员", + "§7遇到问题?向管理员提交工单")); + + // 22号位:返回主菜单 + g.setItem(22, makeItem( + org.bukkit.Material.ARROW, + "§7返回")); + + p.openInventory(g); + } + + /** + * 创建物品的工具方法 + */ + private org.bukkit.inventory.ItemStack makeItem( + org.bukkit.Material mat, + String displayName, + String... lore) { + org.bukkit.inventory.ItemStack item = + new org.bukkit.inventory.ItemStack(mat); + org.bukkit.inventory.meta.ItemMeta meta = + item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(displayName); + if (lore.length > 0) { + meta.setLore( + java.util.Arrays.asList(lore)); + } + item.setItemMeta(meta); + } + return item; + } + + + private boolean isAdmin(Player p) { + return p.getScoreboardTags().contains( + plugin.getConfig2().adminTag); + } + + private void fillBg(Inventory inv) { + ItemStack gl = mkItem( + Material.GRAY_STAINED_GLASS_PANE, " "); + for (int i = 0; i < inv.getSize(); i++) + inv.setItem(i, gl); + } + + private ItemStack mkItem(Material mat, String name, + String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) + im.setLore(Arrays.asList(lore)); + it.setItemMeta(im); + } + return it; + } +} diff --git a/txt/GarbageManager.txt b/txt/GarbageManager.txt new file mode 100644 index 0000000..b4da32c --- /dev/null +++ b/txt/GarbageManager.txt @@ -0,0 +1,568 @@ +package Sdf1_login; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Item; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.scheduler.BukkitRunnable; + +import java.io.*; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.sql.*; +import java.sql.Date; +import java.text.SimpleDateFormat; +import java.util.*; + +public class GarbageManager { + + private final Main plugin; + private Connection db; + private int cleanupInterval = 1800; + private int maxRounds = 1; + private boolean enabled = true; + private int currentRound = 0; + private final Map + recyclePages = new HashMap<>(); + + + public GarbageManager(Main plugin) { + this.plugin = plugin; + } + + public void init() { + try { + Class.forName("org.sqlite.JDBC"); + File dbFile = new File( + plugin.getDataFolder(), + "garbage.db"); + db = DriverManager.getConnection( + "jdbc:sqlite:" + + dbFile.getAbsolutePath()); + Statement st = db.createStatement(); + st.execute("PRAGMA journal_mode=WAL"); + st.execute("CREATE TABLE IF NOT EXISTS " + + "items (" + + "id INTEGER PRIMARY KEY " + + "AUTOINCREMENT," + + "item_data TEXT NOT NULL," + + "amount INTEGER DEFAULT 1," + + "round INTEGER DEFAULT 0," + + "create_time INTEGER NOT NULL" + + ")"); + st.execute("CREATE TABLE IF NOT EXISTS " + + "config (" + + "key TEXT PRIMARY KEY," + + "value TEXT NOT NULL" + + ")"); + st.close(); + } catch (Exception e) { + plugin.getLogger().severe( + "[Garbage] DB失败: " + + e.getMessage()); + } + } + + public void loadConfig() { + // 从 ConfigManager 读取 + cleanupInterval = + plugin.getConfig2() + .garbageInterval; + enabled = + plugin.getConfig2() + .garbageEnabled; + maxRounds = + plugin.getConfig2() + .garbageMaxRounds; + + // 从数据库读取当前轮次 + try { + String val = getConfigValue("round"); + if (val != null) + currentRound = + Integer.parseInt(val); + } catch (Exception ignored) { + } + + plugin.getLogger().info( + "[Garbage] 间隔: " + + cleanupInterval + "秒, " + + "轮次保留: " + + maxRounds + "轮, " + + "启用: " + enabled); + } + + + private String getConfigValue(String key) { + try { + PreparedStatement ps = db.prepareStatement( + "SELECT value FROM config " + + "WHERE key=?"); + ps.setString(1, key); + ResultSet rs = ps.executeQuery(); + String val = rs.next() + ? rs.getString("value") : null; + rs.close(); + ps.close(); + return val; + } catch (Exception e) { + return null; + } + } + + private void setConfigValue(String key, + String value) { + try { + PreparedStatement ps = db.prepareStatement( + "INSERT OR REPLACE INTO config " + + "(key, value) " + + "VALUES (?, ?)"); + ps.setString(1, key); + ps.setString(2, value); + ps.executeUpdate(); + ps.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void startAutoCleanup() { + if (!enabled) return; + new BukkitRunnable() { + int countdown = 0; + + @Override + public void run() { + // 在线人数≤0时暂停 + if (Bukkit.getOnlinePlayers() + .isEmpty()) { + countdown = 0; + return; + } + + // 重新读取配置(支持热更) + cleanupInterval = + plugin.getConfig2() + .garbageInterval; + + int remain = + cleanupInterval - countdown; + if (remain == 300) + broadcast("§e§l[垃圾站] §f距离清理还有 §e5分钟"); + else if (remain == 180) + broadcast("§e§l[垃圾站] §f距离清理还有 §e3分钟"); + else if (remain == 60) + broadcast("§e§l[垃圾站] §f距离清理还有 §e1分钟"); + else if (remain == 30) + broadcast("§e§l[垃圾站] §f距离清理还有 §e30秒"); + else if (remain == 10) + broadcast("§e§l[垃圾站] §f距离清理还有 §e10秒"); + else if (remain <= 5 && remain > 0) + broadcast("§e§l[垃圾站] §c" + remain + "§f..."); + else if (remain == 0) { + doCleanup(); + countdown = 0; + return; + } + countdown++; + } + }.runTaskTimer(plugin, 20L, 20L); + } + + private void doCleanup() { + // 删除过期轮次的物品 + int deleteRound = + currentRound - maxRounds; + if (deleteRound >= 0) { + int deleted = deleteOldItems( + deleteRound); + if (deleted > 0) { + broadcast("§7[垃圾站] §f永久清理了 §e" + + deleted + " §f个过期物品"); + } + } + + // 递增轮次 + currentRound++; + setConfigValue("round", + String.valueOf(currentRound)); + + // 收集当前掉落物 + int count = collectItems(); + broadcast("§a§l[垃圾站] §f清理完成," + + "共清理 §e" + count + + " §f个掉落物,存入回收站"); + + // 打印下次清理时间 + long nextMs = System.currentTimeMillis() + + (long) cleanupInterval * 1000L; + String nextTime = + new SimpleDateFormat("HH:mm:ss") + .format(new Date(nextMs)); + broadcast("§e§l[垃圾站] §f下次清理时间: §a" + + nextTime); + } + + public int collectItems() { + int count = 0; + for (org.bukkit.World w : + Bukkit.getWorlds()) { + Collection items = + w.getEntitiesByClass(Item.class); + for (Item item : items) { + ItemStack stack = + item.getItemStack(); + if (stack == null + || stack.getType() + == Material.AIR) + continue; + saveItem(stack); + item.remove(); + count++; + } + } + return count; + } + + public void saveItem(ItemStack stack) { + { + try { + String data = + stackToBase64(stack); + if (data.isEmpty()) return; + PreparedStatement ps = db.prepareStatement( + "INSERT INTO items " + + "(item_data, amount, " + + "round, create_time) " + + "VALUES (?,?,?,?)"); + ps.setString(1, data); + ps.setInt(2, stack.getAmount()); + ps.setInt(3, currentRound); + ps.setLong(4, + System.currentTimeMillis()); + ps.executeUpdate(); + ps.close(); + } catch (Exception e) { + e.printStackTrace(); + } + }} + + private int deleteOldItems(int beforeRound) { + try { + PreparedStatement ps = db.prepareStatement( + "DELETE FROM items " + + "WHERE round <= ?"); + ps.setInt(1, beforeRound); + int rows = ps.executeUpdate(); + ps.close(); + return rows; + } catch (Exception e) { + return 0; + } + } + + // ========== 序列化 ========== + + private String stackToBase64(ItemStack stack) { + try { + Map map = + stack.serialize(); + String json = new Gson().toJson(map); + return Base64.getEncoder() + .encodeToString( + json.getBytes( + StandardCharsets.UTF_8)); + } catch (Exception e) { + return ""; + } + } + + private ItemStack base64ToStack(String base64) { + try { + byte[] bytes = Base64.getDecoder() + .decode(base64); + String json = new String(bytes, + StandardCharsets.UTF_8); + Type type = new TypeToken< + Map>() {} + .getType(); + Map map = + new Gson().fromJson(json, type); + map = fixNumberTypes(map); + return ItemStack.deserialize(map); + } catch (Exception e) { + return null; + } + } + + @SuppressWarnings("unchecked") + private Map fixNumberTypes( + Map map) { + Map result = + new LinkedHashMap<>(); + for (Map.Entry e + : map.entrySet()) { + Object v = e.getValue(); + if (v instanceof Double) { + double d = (Double) v; + if (d == (long) d) { + result.put(e.getKey(), (long) d); + } else { + result.put(e.getKey(), d); + } + } else if (v instanceof Map) { + result.put(e.getKey(), + fixNumberTypes( + (Map) v)); + } else if (v instanceof List) { + List list = + new ArrayList<>(); + for (Object item : (List) v) { + if (item instanceof Map) { + list.add(fixNumberTypes( + (Map) item)); + } else { + list.add(item); + } + } + result.put(e.getKey(), list); + } else { + result.put(e.getKey(), v); + } + } + return result; + } + + // ========== 回收站GUI ========== + public void openRecycle(Player p) { + openRecyclePage(p, 0); + } + + public void openRecyclePage(Player p, int page) { + + Inventory g = Bukkit.createInventory( + null, 54, "§6§l垃圾回收站"); + + // 获取全部存储物品 + List> allItems = + getStoredItems(999); + int total = allItems.size(); + int pageSize = 40; + int totalPages = Math.max(1, + (int) Math.ceil( + (double) total / pageSize)); + + // 修正页码 + if (page < 0) page = 0; + if (page >= totalPages) + page = totalPages - 1; + + recyclePages.put(p.getName(), page); + + // 填充物品(0-39 格 = 第1页,40-79 = 第2页...) + int start = page * pageSize; + int end = Math.min(start + pageSize, + total); + int slot = 0; + for (int i = start; i < end; i++) { + if (slot >= 40) break; + Map item = + allItems.get(i); + int id = ((Number) item.get("id")) + .intValue(); + String data = (String) item + .get("item_data"); + int amount = ((Number) item + .get("amount")).intValue(); + ItemStack stack = base64ToStack(data); + if (stack != null) { + stack.setAmount(amount); + ItemMeta im = + stack.getItemMeta(); + if (im != null) { + List lore = + new ArrayList<>(); + lore.add("§7ID: #" + id); + lore.add("§a点击取出"); + im.setLore(lore); + stack.setItemMeta(im); + } + g.setItem(slot, stack); + } + slot++; + } + + // 45-48 格:额外投放区(空位) + // 52-53 格:额外投放区(空位) + + // 49 格:翻页-上一页 + if (page > 0) { + g.setItem(49, mk(Material.ARROW, + "§e上一页 §7(" + + page + "/" + + totalPages + ")")); + } else { + g.setItem(49, mk(Material.ARROW, + "§e第一页 §7(" + + "1/" + totalPages + ")")); + } + + // 50 格:统计信息 + g.setItem(50, mk(Material.CHEST, + "§e回收站统计", + "§7总存量: §f" + total + " 个", + "§7当前页: §f" + (page + 1) + + "/" + totalPages)); + + // 51 格:翻页-下一页 + boolean canNext = total >= pageSize + && page < totalPages - 1; + if (canNext) { + g.setItem(51, mk(Material.ARROW, + "§e下一页 §7(" + + (page + 2) + "/" + + totalPages + ")")); + } else { + g.setItem(51, mk(Material.ARROW, + "§7已是最后一页")); + } + + p.openInventory(g); + } + public int getRecyclePage(String name) { + return recyclePages + .getOrDefault(name, 0); + } + + + public boolean handleClick(Player p, + String title, int slot) { + if (!title.equals("§6§l垃圾回收站")) + return false; + // 49:上一页 + if (slot == 49) { + int page = recyclePages + .getOrDefault(p.getName(), 0); + if (page > 0) + openRecyclePage(p, page - 1); + return true; + } + // 51:下一页 + if (slot == 51) { + int page = recyclePages + .getOrDefault(p.getName(), 0); + openRecyclePage(p, page + 1); + return true; + } + // 50/52/53:不处理 + if (slot >= 49 && slot <= 53) + return true; + return true; + } + + private List> + getStoredItems(int limit) { + List> list = + new ArrayList<>(); + try { + PreparedStatement ps = + db.prepareStatement( + "SELECT id, item_data, " + + "amount " + + "FROM items " + + "ORDER BY " + + "create_time " + + "DESC LIMIT ?"); + ps.setInt(1, limit); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Map row = + new LinkedHashMap<>(); + row.put("id", + rs.getInt("id")); + row.put("item_data", + rs.getString("item_data")); + row.put("amount", + rs.getInt("amount")); + list.add(row); + } + rs.close(); + ps.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return list; + } + + public boolean removeItem(int id) { + try { + PreparedStatement ps = + db.prepareStatement( + "DELETE FROM items " + + "WHERE id=?"); + ps.setInt(1, id); + int rows = ps.executeUpdate(); + ps.close(); + return rows > 0; + } catch (Exception e) { + return false; + } + } + + public int getItemCount() { + try { + Statement st = db.createStatement(); + ResultSet rs = st.executeQuery( + "SELECT COUNT(*) FROM items"); + int count = 0; + if (rs.next()) + count = rs.getInt(1); + rs.close(); + st.close(); + return count; + } catch (Exception e) { + return 0; + } + } + + private void broadcast(String msg) { + // 同步打印到控制台 + plugin.getLogger().info( + msg.replaceAll("§[0-9a-fk-orA-FK-OR]", "")); + // 同步打印到聊天栏 + Bukkit.broadcastMessage(msg); + } + + + private ItemStack mk(Material mat, + String name, String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) { + List list = + new ArrayList<>(); + for (String l : lore) + if (!l.isEmpty()) list.add(l); + if (!list.isEmpty()) + im.setLore(list); + } + it.setItemMeta(im); + } + return it; + } + + public void close() { + try { + if (db != null && !db.isClosed()) + db.close(); + } catch (Exception ignored) { + } + } +} diff --git a/txt/GiftManager.txt b/txt/GiftManager.txt new file mode 100644 index 0000000..524ece8 --- /dev/null +++ b/txt/GiftManager.txt @@ -0,0 +1,296 @@ +package Sdf1_login; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; + + +public class GiftManager { + + private final File giftFolder; + private final Main plugin; + private final List> stages = + new ArrayList<>(); + + public GiftManager(Main plugin) { + this.plugin = plugin; + this.giftFolder = new File( + plugin.getDataFolder(), "新人礼包"); + } + + public void loadStages() { + stages.clear(); + if (!giftFolder.exists()) { + giftFolder.mkdirs(); + writeDefaultStages(); + } + File[] files = giftFolder.listFiles( + (dir, name) -> name.endsWith(".md")); + if (files == null) return; + Arrays.sort(files); + for (File f : files) { + stages.add(parseMarkdown(f)); + } + } + + private Map parseMarkdown(File f) { + Map stage = new LinkedHashMap<>(); + stage.put("stage", stages.size() + 1); + stage.put("name", + f.getName().replace(".md", "")); + stage.put("rewards", new ArrayList()); + try { + BufferedReader r = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String line; + List rewards = new ArrayList<>(); + while ((line = r.readLine()) != null) { + if (line.contains("礼包内容") + || line.contains("奖励") + || line.contains("任务内容")) { + int colon = line.indexOf(':'); + if (colon < 0) + colon = line.indexOf('\uff1a'); + if (colon >= 0) { + rewards.add( + line.substring(colon + 1) + .trim()); + } + } + } + stage.put("rewards", rewards); + r.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return stage; + } + private void writeStage(String name, String content) { + File f = new File(giftFolder, name); + if (f.exists()) return; + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + StandardCharsets.UTF_8))) { + pw.print(content); + } catch (IOException ignored) {} + } + + private void writeDefaultStages() { + writeStage("第一阶段.md", + "# 第一阶段\n\n## 领取条件\n\n- 无前级条件\n\n## 礼包内容\n\n- 皮革套装 × 1\n- 木制武器工具套装 × 1\n"); + writeStage("第二阶段.md", + "# 第二阶段\n\n## 领取条件\n\n- 在线时长>1小时\n\n## 礼包内容\n\n- 500 游戏币\n"); + writeStage("第三阶段.md", + "# 第三阶段\n\n## 领取条件\n\n- 破坏100个方块\n- 在线>1天\n\n## 礼包内容\n\n- 1500 游戏币\n- 铁质工具套装 × 1\n- 铁质防具 × 1\n"); + writeStage("第四阶段.md", + "# 第四阶段\n\n## 领取条件\n\n- 破坏/放置300方块\n- 在线≥7天\n\n## 礼包内容\n\n- 钻石套装 × 1\n- 2000 游戏币\n"); + } + + /** 从任意文件夹加载任务列表 */ + public List> loadTasksFromFolder( + File folder) { + List> tasks = + new ArrayList<>(); + if (!folder.exists() || !folder.isDirectory()) + return tasks; + File[] files = folder.listFiles( + (dir, name) -> name.endsWith(".md")); + if (files == null) return tasks; + Arrays.sort(files); + for (File f : files) { + Map task = + new LinkedHashMap<>(); + task.put("name", + f.getName().replace(".md", "")); + task.put("conditions", + new ArrayList()); + task.put("rewards", + new ArrayList()); + try { + BufferedReader r = new BufferedReader( + new InputStreamReader( + new FileInputStream(f), + StandardCharsets.UTF_8)); + String line; + List current = null; + while ((line = r.readLine()) != null) { + if (line.startsWith("# ")) { + task.put("name", + line.substring(2) + .trim()); + } else if (line.contains("条件") + || line.contains("领取条件") + || line.contains("完成条件")) { + current = (List) task + .get("conditions"); + } else if (line.contains("内容") + || line.contains("奖励") + || line.contains("任务内容") + || line.contains("礼包内容")) { + current = (List) task + .get("rewards"); + } else if (line.startsWith("- ") + && current != null) { + current.add(line.substring(2) + .trim()); + } + } + r.close(); + } catch (Exception e) { + e.printStackTrace(); + } + tasks.add(task); + } + return tasks; + } + + public boolean canClaim(Player p, int stage) { + Map user = plugin.getDb() + .getUser(p.getName()); + if (user.isEmpty()) return false; + int curStage = ((Number) user.getOrDefault( + "gift_stage", 0)).intValue(); + if (stage > curStage + 1) return false; + if (stage < 1 || stage > stages.size()) + return false; + String claimed = (String) user.getOrDefault( + "gift_claimed", ""); + if (claimed.contains("[" + stage + "]")) + return false; + + // debug 绕过:检查 tasks_completed + String completed = (String) user.getOrDefault( + "tasks_completed", ""); + if (completed.contains("[" + stage + "]")) + return true; + + // 正常条件检查 + long dbTime = ((Number) user.getOrDefault( + "total_online_time", 0L)).longValue(); + long sessionTime = 0; + Object lastObj = plugin.getDb().getField( + p.getName(), "last_online_check"); + if (lastObj != null && plugin.getLoggedIn() + .contains(p.getName())) { + long last = ((Number) lastObj).longValue(); + if (last > 0) { + sessionTime = + System.currentTimeMillis() - last; + } + } + long onlineTime = dbTime + sessionTime; + int broken = ((Number) user.getOrDefault( + "blocks_broken", 0)).intValue(); + int placed = ((Number) user.getOrDefault( + "blocks_placed", 0)).intValue(); + + switch (stage) { + case 1: return true; + case 2: return onlineTime >= 3600000; + case 3: return broken >= 100 + && onlineTime >= 86400000L; + case 4: return (broken + placed) >= 300 + && onlineTime >= 604800000L; + default: return false; + } + } + + public void claimReward(Player p, int stage) { + if (!canClaim(p, stage)) return; + Map user = plugin.getDb() + .getUser(p.getName()); + String claimed = (String) user.getOrDefault( + "gift_claimed", ""); + claimed += "[" + stage + "]"; + plugin.getDb().setField( + p.getName(), "gift_claimed", claimed); + plugin.getDb().setField( + p.getName(), "gift_stage", + Math.max(stage, + ((Number) user.getOrDefault( + "gift_stage", 0)) + .intValue())); + + if (stage == 1) { + giveItem(p, Material.LEATHER_HELMET); + giveItem(p, Material.LEATHER_CHESTPLATE); + giveItem(p, Material.LEATHER_LEGGINGS); + giveItem(p, Material.LEATHER_BOOTS); + giveItem(p, Material.WOODEN_SWORD); + giveItem(p, Material.WOODEN_PICKAXE); + giveItem(p, Material.WOODEN_AXE); + giveItem(p, Material.WOODEN_SHOVEL); + } else if (stage == 2) { + if (plugin.getEconomy() != null) { + double before = plugin.getEconomy() + .getBalance(p); + plugin.getEconomy() + .depositPlayer(p, 500); + double after = plugin.getEconomy() + .getBalance(p); + p.sendMessage("§a§l[礼包] §f游戏币: §e" + + (long) before + + "§f → §a" + (long) after); + } + } else if (stage == 3) { + if (plugin.getEconomy() != null) { + double before = plugin.getEconomy() + .getBalance(p); + plugin.getEconomy() + .depositPlayer(p, 1500); + double after = plugin.getEconomy() + .getBalance(p); + p.sendMessage("§a§l[礼包] §f游戏币: §e" + + (long) before + + "§f → §a" + (long) after); + } + giveItem(p, Material.IRON_PICKAXE); + giveItem(p, Material.IRON_AXE); + giveItem(p, Material.IRON_SWORD); + giveItem(p, Material.IRON_HELMET); + giveItem(p, Material.IRON_CHESTPLATE); + giveItem(p, Material.IRON_LEGGINGS); + giveItem(p, Material.IRON_BOOTS); + } else if (stage == 4) { + if (plugin.getEconomy() != null) { + double before = plugin.getEconomy() + .getBalance(p); + plugin.getEconomy() + .depositPlayer(p, 2000); + double after = plugin.getEconomy() + .getBalance(p); + p.sendMessage("§a§l[礼包] §f游戏币: §e" + + (long) before + + "§f → §a" + (long) after); + } + giveItem(p, Material.DIAMOND_HELMET); + giveItem(p, Material.DIAMOND_CHESTPLATE); + giveItem(p, Material.DIAMOND_LEGGINGS); + giveItem(p, Material.DIAMOND_BOOTS); + } + p.sendMessage(plugin.getConfig2().msg( + "gift_claimed")); + } + + private void giveItem(Player p, Material mat) { + p.getInventory().addItem(new ItemStack(mat)); + } + + public List> getStages() { + return stages; + } + + public int getMaxStage() { + return stages.size(); + } +} diff --git a/txt/IPGroupManager.txt b/txt/IPGroupManager.txt new file mode 100644 index 0000000..041e184 --- /dev/null +++ b/txt/IPGroupManager.txt @@ -0,0 +1,36 @@ +package Sdf1_login; + +import java.util.List; + +public class IPGroupManager { + + private final Main plugin; + private int maxAccounts; + + public IPGroupManager(Main plugin, int maxAccounts) { + this.plugin = plugin; + this.maxAccounts = maxAccounts; + } + + public int getMaxAccounts() { + return maxAccounts; + } + + public void setMaxAccounts(int max) { + this.maxAccounts = max; + } + + public List getAccountsByIP(String ip) { + return plugin.getDb().getAccountsByIP(ip); + } + + public int getAccountCount(String ip) { + if (ip == null) return 0; + return getAccountsByIP(ip).size(); + } + + public boolean canRegister(String ip) { + if (maxAccounts <= 0) return true; + return getAccountCount(ip) < maxAccounts; + } +} diff --git a/txt/InventorySerializer.txt b/txt/InventorySerializer.txt new file mode 100644 index 0000000..32bc632 --- /dev/null +++ b/txt/InventorySerializer.txt @@ -0,0 +1,46 @@ +package Sdf1_login; + +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.io.BukkitObjectInputStream; +import org.bukkit.util.io.BukkitObjectOutputStream; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Base64; + +public class InventorySerializer { + + public static String toBase64(ItemStack[] items) throws IOException { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream); + dataOutput.writeInt(items.length); + for (ItemStack item : items) { + dataOutput.writeObject(item); + } + dataOutput.close(); + return Base64.getEncoder().encodeToString(outputStream.toByteArray()); + } catch (Exception e) { + throw new IOException("Unable to serialize item stacks.", e); + } + } + + public static ItemStack[] fromBase64(String data) throws IOException { + if (data == null || data.isEmpty()) { + return new ItemStack[0]; + } + try { + ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data)); + BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream); + ItemStack[] items = new ItemStack[dataInput.readInt()]; + for (int i = 0; i < items.length; i++) { + items[i] = (ItemStack) dataInput.readObject(); + } + dataInput.close(); + return items; + } catch (ClassNotFoundException e) { + throw new IOException("Unable to deserialize item stacks.", e); + } + } +} diff --git a/txt/InviteManager.txt b/txt/InviteManager.txt new file mode 100644 index 0000000..a9ca6ea --- /dev/null +++ b/txt/InviteManager.txt @@ -0,0 +1,91 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.text.SimpleDateFormat; +import java.util.*; + +public class InviteManager { + + private final Main plugin; + + public InviteManager(Main plugin) { + this.plugin = plugin; + } + + public String generateCode(Player p) { + String code = PasswordUtils.generateInviteCode(); + plugin.getDb().setField( + p.getName(), "invite_code", code); + return code; + } + + public void onInviteeCheckIn(String inviteeName) { + String invitedBy = (String) plugin.getDb() + .getField(inviteeName, "invited_by"); + if (invitedBy == null || invitedBy.isEmpty()) return; + + String month = new SimpleDateFormat("yyyy-MM") + .format(new Date()); + String lastMonth = (String) plugin.getDb() + .getField(invitedBy, "invite_month"); + + if (!month.equals(lastMonth)) { + plugin.getDb().setField( + invitedBy, "invite_month", month); + plugin.getDb().setField( + invitedBy, "monthly_invite_count", 0); + } + + int count = ((Number) plugin.getDb() + .getField(invitedBy, + "monthly_invite_count")).intValue(); + plugin.getDb().setField( + invitedBy, "monthly_invite_count", count + 1); + + double rate = 0.10; + if (count + 1 > 15) { + rate = 0.60; + } else if (count + 1 > 10) { + rate = 0.35; + } else if (count + 1 > 5) { + rate = 0.15; + } + + int bonus = (int) (1 * rate); + if (bonus > 0) { + plugin.getDb().addPoints(invitedBy, bonus); + Player inviterPlayer = Bukkit + .getPlayerExact(invitedBy); + if (inviterPlayer != null + && inviterPlayer.isOnline()) { + inviterPlayer.sendMessage( + plugin.getConfig2().msg( + "invite_referral_bonus", + "points", + String.valueOf(bonus))); + } + } + } + + public List> getAllInviteData() { + List> result = new ArrayList<>(); + for (Map user + : plugin.getDb().getAllUsers()) { + String code = (String) user.getOrDefault( + "invite_code", ""); + if (!code.isEmpty()) { + Map row = new LinkedHashMap<>(); + row.put("player", + user.get("player_name")); + row.put("code", code); + row.put("monthly", + user.getOrDefault( + "monthly_invite_count", 0)); + result.add(row); + } + } + return result; + } +} diff --git a/txt/LoginManager.txt b/txt/LoginManager.txt new file mode 100644 index 0000000..4a50dc1 --- /dev/null +++ b/txt/LoginManager.txt @@ -0,0 +1,301 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.entity.Player; + +public class LoginManager { + + private final Main plugin; + + public LoginManager(Main plugin) { + this.plugin = plugin; + } + + public boolean handleRegister(Player p, String[] args) { + String name = p.getName(); + + if (plugin.getplugin.getDb()().userExists(name)) { + p.sendMessage(plugin.getConfig2().msg("already_registered")); + return true; + } + + // ===== IP注册限制检查 ===== + String ip = plugin.getPlayerIP(p); + if (ip != null && plugin.getConfig2().maxAccountsPerIP > 0) { + if (!plugin.getIPGroup().canRegister(ip)) { + int count = plugin.getIPGroup().getAccountCount(ip); + int max = plugin.getConfig2().maxAccountsPerIP; + p.sendMessage("§c[Sdf1_login] §f该IP注册已达上限(" + count + "/" + max + ")"); + return true; + } + } + + if (args.length < 1) { + p.sendMessage("§c用法: /reg <密码>"); + p.sendMessage("§7密码要求: 6位以上,包含大小写字母和数字"); + return true; + } + + String pwd = args[0]; + if (pwd.length() < 6) { + p.sendMessage("§c[Sdf1_login] §f密码长度不足6位"); + return true; + } + if (!pwd.matches(".*[a-z].*") || !pwd.matches(".*[A-Z].*") || !pwd.matches(".*[0-9].*")) { + p.sendMessage("§c[Sdf1_login] §f密码必须包含大写字母、小写字母和数字"); + return true; + } + + String salt = PasswordUtils.generateSalt(); + String hash = PasswordUtils.hash(pwd, salt); + plugin.getplugin.getDb()().createUser(name, hash, salt); + plugin.getLoggedIn().add(name); + plugin.getplugin.getDb()().setLoggedIn(name, true); + plugin.getplugin.getDb()().setField(name, "last_login_time", System.currentTimeMillis()); + plugin.getplugin.getDb()().setField(name, "last_online_check", System.currentTimeMillis()); + plugin.getplugin.getDb()().recordIP(name, ip); + plugin.getplugin.getDb()().setField(name, "register_time", System.currentTimeMillis()); + plugin.restoreInventory(p); + plugin.recordIPLogin(p); + + p.sendMessage(plugin.getConfig2().msg("reg_success")); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_CHIME, 1.0f, 0.79f); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0f, 0.79f); + p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 0.79f); + + try { + org.bukkit.plugin.Plugin cy = org.bukkit.Bukkit.getPluginManager().getPlugin("CY_beibao"); + if (cy != null && cy.isEnabled()) { + cy.getClass().getMethod("onSdf1Activation", String.class, int.class, int.class) + .invoke(cy, name, 9, 0); + } + } catch (Exception ignored) { + } + return true; + } + + public boolean handleLogin(Player p, + String[] args) { + String name = p.getName(); + if (plugin.getLoggedIn().contains(name)) { + p.sendMessage(plugin.getConfig2().msg( + "already_logged_in")); + return true; + } + if (!plugin.getplugin.getDb()().userExists(name)) { + p.sendMessage(plugin.getConfig2().msg( + "not_registered")); + return true; + } + if (args.length < 1) { + p.sendMessage("§c用法: /login <密码>"); + return true; + } + String pwd = args[0]; + + // 诊断:显示账号信息 + String salt = (String) plugin.getplugin.getDb()() + .getField(name, "password_salt"); + String regType = (String) plugin.getplugin.getDb()() + .getField(name, "register_type"); + String storedHash = (String) plugin.getplugin.getDb()() + .getField(name, "password_hash"); + plugin.getLogger().info( + "[Sdf1_login] [登录诊断] 玩家: " + name); + plugin.getLogger().info( + "[Sdf1_login] [登录诊断] 注册类型: " + + regType); + plugin.getLogger().info( + "[Sdf1_login] [登录诊断] 盐值: " + salt); + plugin.getLogger().info( + "[Sdf1_login] [登录诊断] 存储哈希: " + + (storedHash != null + ? storedHash.substring(0, + Math.min(16, + storedHash.length())) + + "..." + : "null")); + + if (salt == null) { + p.sendMessage( + "§c[Sdf1_login] §f数据异常,请联系管理员"); + plugin.getLogger().severe( + "[Sdf1_login] 玩家 " + name + + " 盐值为null"); + return true; + } + + String hash = PasswordUtils.hash(pwd, salt); + + plugin.getLogger().info( + "[Sdf1_login] [登录诊断] 输入密码哈希: " + + hash.substring(0, + Math.min(16, hash.length())) + + "..."); + + // 检查是否匹配主密码 + boolean matchMain = + plugin.getplugin.getDb()().checkPassword(name, hash); + plugin.getLogger().info( + "[Sdf1_login] [登录诊断] 主密码匹配: " + + matchMain); + + if (matchMain) { + // 主密码匹配 → 正常登录 + plugin.getLoggedIn().add(name); + plugin.getplugin.getDb()().setLoggedIn(name, true); + plugin.getplugin.getDb()().setField(name, + "last_login_time", + System.currentTimeMillis()); + plugin.getplugin.getDb()().setField(name, + "last_online_check", + System.currentTimeMillis()); + plugin.restoreInventory(p); + plugin.recordIPLogin(p); + p.sendMessage(plugin.getConfig2().msg("login_success")); + plugin.getLogger().info( + "[Sdf1_login] " + name + + " 主密码登录成功"); + try { + org.bukkit.plugin.Plugin cy = + org.bukkit.Bukkit + .getPluginManager() + .getPlugin("CY_beibao"); + if (cy != null && cy.isEnabled()) { + java.lang.reflect.Method m = + cy.getClass().getMethod( + "onSdf1Activation", + String.class, + int.class, int.class); + m.invoke(cy, name, 0, 0); + } + } catch (Exception ignored) { + } + return true; + } + + // 主密码不匹配 → 检查临时密码 + boolean matchTemp = + plugin.getplugin.getDb()().checkPasswordOrTemp( + name, hash); + plugin.getLogger().info( + "[Sdf1_login] [登录诊断] 临时密码匹配: " + + matchTemp); + + if (matchTemp) { + // 临时密码匹配 → 登录 + 强制改密 + plugin.getLoggedIn().add(name); + plugin.getplugin.getDb()().setLoggedIn(name, true); + plugin.getplugin.getDb()().setField(name, + "last_login_time", + System.currentTimeMillis()); + plugin.getplugin.getDb()().setField(name, + "last_online_check", + System.currentTimeMillis()); + plugin.restoreInventory(p); + plugin.recordIPLogin(p); + plugin.getNeedsPasswordChange().add(name); + p.sendMessage(plugin.getConfig2().msg("login_success")); + p.sendMessage( + "§c§l[警告] §f您使用的是临时密码," + + "请尽快修改密码!"); + p.sendMessage( + "§7用法: /sdf1_login pw"); + plugin.getLogger().info( + "[Sdf1_login] " + name + + " 临时密码登录成功," + + "强制要求改密"); + try { + org.bukkit.plugin.Plugin cy = + org.bukkit.Bukkit + .getPluginManager() + .getPlugin("CY_beibao"); + if (cy != null && cy.isEnabled()) { + java.lang.reflect.Method m = + cy.getClass().getMethod( + "onSdf1Activation", + String.class, + int.class, int.class); + m.invoke(cy, name, 0, 0); + } + } catch (Exception ignored) { + } + return true; + } + + // 都不匹配 → 登录失败 + plugin.getLogger().info( + "[Sdf1_login] " + name + + " 密码验证失败"); + p.sendMessage(plugin.getConfig2().msg( + "login_failed")); + return true; + } + + public void handleReset(Player p) { + String name = p.getName(); + String plugin.getEmail()Addr = (String) plugin.getDb().getField( + name, "plugin.getEmail()"); + if (plugin.getEmail()Addr == null + || plugin.getEmail()Addr.isEmpty()) { + p.sendMessage( + "§c未设置邮箱,无法重置密码"); + return; + } + String tempPwd = PasswordUtils + .generateTempPassword(); + String salt = (String) plugin.getDb().getField( + name, "password_salt"); + String hash = PasswordUtils.hash( + tempPwd, salt); + plugin.getDb().setField(name, + "temp_password", hash); + plugin.getDb().setField(name, + "temp_pw_expire", + System.currentTimeMillis() + + 3600000L); + plugin.getDb().setField(name, + "temp_pw_used", 0); + p.sendMessage("§e正在发送临时密码..."); + final String to = plugin.getEmail()Addr; + final String fName = name; + final String pwd = tempPwd; + Bukkit.getScheduler() + .Bukkit.getScheduler().runTask(plugin, ...)Asynchronously(this, + () -> { + final boolean[] sent = {false}; + Thread t = new Thread(() -> { + sent[0] = plugin.getEmail() + .sendTempPassword( + to, fName, pwd); + }, "Sdf1_login-reset"); + t.setDaemon(true); + t.start(); + try { + t.join(15000); + } catch ( + InterruptedException ignored) { + } + if (t.isAlive()) { + t.interrupt(); + Bukkit.getScheduler().Bukkit.getScheduler().runTask(plugin, ...)( + this, () -> + p.sendMessage( + "§c邮件发送超时")); + return; + } + Bukkit.getScheduler().Bukkit.getScheduler().runTask(plugin, ...)( + this, () -> { + if (sent[0]) { + p.sendMessage( + "§a临时密码已发送到 " + + to); + } else { + p.sendMessage( + "§c邮件发送失败"); + } + }); + }); + } +} \ No newline at end of file diff --git a/txt/Main.txt b/txt/Main.txt new file mode 100644 index 0000000..6c24d45 --- /dev/null +++ b/txt/Main.txt @@ -0,0 +1,3712 @@ +package Sdf1_login; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import org.bukkit.event.player.PlayerInteractEvent; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Villager; +import Sdf1_login.ChatInputManager.InputType; + + + + +import java.util.TreeSet; +import java.util.UUID; +import java.util.LinkedHashMap; +import java.util.concurrent.ConcurrentHashMap; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.inventory.CraftItemEvent; +import org.bukkit.event.player.PlayerFishEvent; + + +import net.milkbowl.vault.economy.Economy; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.player.AsyncPlayerChatEvent; +import org.bukkit.event.player.PlayerDropItemEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerPickupItemEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.scheduler.BukkitRunnable; + +public class Main extends JavaPlugin + implements CommandExecutor, Listener, + TabCompleter { + + private DatabaseManager db; + private ConfigManager config; + private EmailManager email; + private LoginManager loginMgr; + private AFKManager afk; + private GiftManager gift; + private CheckInManager checkIn; + private InviteManager invite; + private PointsManager points; + private GUIManager gui; + private ChatInputManager chatInput; + private Economy economy; + private VerificationManager verification; + private IPGroupManager ipGroup; + private AccountRequestManager accountRequest; + private TicketManager ticket; + private ChatFilterManager chatFilter; + private GarbageManager garbage; + + private final Set loggedIn = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Set needsPasswordChange = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Set frozenFlightPlayers = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private final Map joinTime = + new ConcurrentHashMap<>(); + private final Map joinLoc = + new HashMap<>(); + private final Map ipAutoLogin = + new HashMap<>(); + private static final long IP_SKIP_MS = 300000L; + + private final Map lastActivity = + new ConcurrentHashMap<>(); + // 登录后首次回收飞行(仅一次) + private final Set removeFlightOnce = + new TreeSet<>( + String.CASE_INSENSITIVE_ORDER); + private RadioManager radio; + public RadioManager getRadio() { + return radio; + } + + + private static class PwdRollback { + final String hash; + final String salt; + final long time; + PwdRollback(String h, String s, long t) { + hash = h; + salt = s; + time = t; + } + } + + private final Map + pwdRollback = + new ConcurrentHashMap<>(); + + public void recordPasswordChange(String name, + String oldHash, String oldSalt) { + pwdRollback.put(name, + new PwdRollback(oldHash, oldSalt, + System.currentTimeMillis())); + } + + // ===== Getters ===== + public DatabaseManager getDb() { + return db; + } + public ConfigManager getConfig2() { return config; } + public EmailManager getEmail() { return email; } + public Economy getEconomy() { return economy; } + public LoginManager getLoginMgr() { return loginMgr; } + public AFKManager getAfk() { return afk; } + public GiftManager getGift() { return gift; } + public CheckInManager getCheckIn() { return checkIn; } + public InviteManager getInvite() { return invite; } + public PointsManager getPoints() { return points; } + public GUIManager getGui() { return gui; } + public ChatInputManager getChatInput() { return chatInput; } + public VerificationManager getVerification() { return verification; } + public IPGroupManager getIPGroup() { return ipGroup; } + public AccountRequestManager getAccountRequest() { return accountRequest; } + public TicketManager getTicket() { return ticket; } + public ChatFilterManager getChatFilter() { return chatFilter; } + public GarbageManager getGarbage() { return garbage; } + public Set getLoggedIn() { return loggedIn; } + public Set getNeedsPasswordChange() { return needsPasswordChange; } + private WelcomeManager welcome; + private UpdateChecker updateChecker; + private QuestTracker questTracker; + private CommissionManager commission; + public CommissionManager getCommission() { + return commission; + } + + + + // ===== Enable/Disable ===== + @Override + public void onEnable() { + radio = new RadioManager(this); + getDataFolder().mkdirs(); + config = new ConfigManager(getDataFolder()); + config.loadMessages(); + config.loadSmtp(); + config.loadSettings(); + getCommand("oa").setExecutor(this); + + db = new DatabaseManager(getDataFolder()); + db.init(); + email = new EmailManager(config); + loginMgr = new LoginManager(this); + afk = new AFKManager(config, this); + gift = new GiftManager(this); + gift.loadStages(); + checkIn = new CheckInManager(this); + invite = new InviteManager(this); + points = new PointsManager(this); + gui = new GUIManager(this); + chatInput = new ChatInputManager(); + verification = new VerificationManager(this); + ipGroup = new IPGroupManager(this, + config.maxAccountsPerIP); + accountRequest = + new AccountRequestManager(this); + accountRequest.setMode(config.approvalMode); + accountRequest.setAutoDelayMinutes( + config.autoApproveDelayMinutes); + ticket = new TicketManager(this); + garbage = new GarbageManager(this); + garbage.init(); + garbage.loadConfig(); + garbage.startAutoCleanup(); + chatFilter = new ChatFilterManager(this); + chatFilter.loadConfig(); + welcome = new WelcomeManager(this); + updateChecker = new UpdateChecker(this); + questTracker = new QuestTracker(this); + commission = new CommissionManager(this); + + + setupEconomy(); + + if (getCommand("sdf1_login") != null) { + getCommand("sdf1_login").setExecutor(this); + getCommand("sdf1_login").setTabCompleter(this); + } + if (getCommand("reg") != null) + getCommand("reg").setExecutor(this); + if (getCommand("login") != null) + getCommand("login").setExecutor(this); + if (getCommand("l") != null) + getCommand("l").setExecutor(this); + if (getCommand("签到") != null) + getCommand("签到").setExecutor(this); + if (getCommand("recycle") != null) + getCommand("recycle").setExecutor(this); + if (getCommand("垃圾清理") != null) + getCommand("垃圾清理").setExecutor(this); + if (getCommand("绑定邮箱") != null) + getCommand("绑定邮箱").setExecutor(this); + if (getCommand("找回密码") != null) + getCommand("找回密码").setExecutor(this); + if (getCommand("玩家信息") != null) + getCommand("玩家信息").setExecutor(this); + if (getCommand("sdf1debug") != null) + getCommand("sdf1debug").setExecutor(this); + if (getCommand("oa") != null) + getCommand("oa").setExecutor(this); + if (getCommand("menu") != null) + getCommand("menu").setExecutor(this); + if (getCommand("radio") != null) + getCommand("radio").setExecutor(this); + getServer().getPluginManager() + .registerEvents(this, this); + + afk.startCheck(); + startLoginReminder(); + startTimeoutCheck(); + startPasswordReminder(); + startTicketAutoProcess(); + startPlayTimeTracker(); + // 启动后异步检查更新 + updateChecker.checkOnEnable(); + + getLogger().info( + "Sdf1_login v1.0 | 就绪"); + for (Player online + : Bukkit.getOnlinePlayers()) { + radio.sendResourcePack(online); + } + + + } + + @Override + public void onDisable() { + if (questTracker != null) { + questTracker.shutdown(); + } + if (radio != null) { + radio.stopAllLoginRadio(); + radio.stopMainRadio(); + } + + if (questTracker != null) { + } + } + +// 没替换,只是粗略看了下,有几个问题 + private void setupEconomy() { + if (getServer().getPluginManager() + .getPlugin("Vault") == null) return; + RegisteredServiceProvider rsp = + getServer().getServicesManager() + .getRegistration( + Economy.class); + if (rsp != null) + economy = rsp.getProvider(); + } + public WelcomeManager getWelcome() { + return welcome; + } + public UpdateChecker getUpdateChecker() { + return updateChecker; + } + public QuestTracker getQuestTracker() { + return questTracker; + } + + // ===== Timers ===== + private void startTimeoutCheck() { + new BukkitRunnable() { + public void run() { + long now = System.currentTimeMillis(); + Iterator> it = + joinTime.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = + it.next(); + if (now - entry.getValue() + > config.loginTimeout + * 1000L) { + Player p = Bukkit.getPlayer( + entry.getKey()); + if (p != null && p.isOnline() + && !loggedIn.contains( + p.getName())) + p.kickPlayer(config.msg( + "login_timeout")); + it.remove(); + } + } + } + }.runTaskTimer(this, 80L, 80L); + } + + private void startLoginReminder() { + new BukkitRunnable() { + public void run() { + Iterator it = + joinTime.keySet().iterator(); + while (it.hasNext()) { + UUID uuid = it.next(); + Player p = Bukkit.getPlayer(uuid); + if (p == null || !p.isOnline()) { + it.remove(); + continue; + } + if (loggedIn.contains( + p.getName())) { + it.remove(); + continue; + } + // 已注册→提示登录,未注册→提示注册 + if (db.userExists(p.getName())) + p.sendMessage(config.msg( + "not_logged_in")); + else + p.sendMessage(config.msg( + "not_registered")); + } + } + }.runTaskTimer(this, 200L, 200L); + } + + private void startPasswordReminder() { + new BukkitRunnable() { + public void run() { + for (Player p : + Bukkit.getOnlinePlayers()) { + if (needsPasswordChange.contains( + p.getName())) { + p.sendTitle("§c§l请修改密码", + "§f使用 /sdf1_login pw 修改密码", + 10, 40, 10); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_PLING, + 1.0f, 1.0f); + } + } + } + }.runTaskTimer(this, 200L, 200L); + } + + private void startTicketAutoProcess() { + new BukkitRunnable() { + public void run() { + if (ticket != null) + ticket.autoProcessCompleted(); + } + }.runTaskTimer(this, 6000L, 6000L); + } + + // ===== Utils ===== + public String getPlayerIP(Player p) { + InetSocketAddress addr = p.getAddress(); + if (addr == null) return null; + return addr.getAddress().getHostAddress(); + } + + + public void resetActivity(Player p) { + lastActivity.put(p.getUniqueId(), + System.currentTimeMillis()); + } + + public boolean canAutoLogin(Player p) { + String ip = getPlayerIP(p); + if (ip == null) return false; + Long last = ipAutoLogin.get(ip); + return last != null + && System.currentTimeMillis() - last + < IP_SKIP_MS; + } + + public void recordIPLogin(Player p) { + String ip = getPlayerIP(p); + if (ip != null) + ipAutoLogin.put(ip, + System.currentTimeMillis()); + } + + public boolean isFrozen(Player p) { + return !loggedIn.contains(p.getName()); + } + + private boolean isAdmin(CommandSender sender) { + if (sender instanceof Player) + return ((Player) sender) + .getScoreboardTags() + .contains(config.adminTag); + return true; + } + + private void clearPlayer(Player p) { + p.getInventory().clear(); + p.getInventory().setArmorContents( + new ItemStack[4]); + p.getInventory().setExtraContents( + new ItemStack[1]); + p.setLevel(0); + p.setExp(0f); + } + + public void restoreInventory(Player p) { + String name = p.getName(); + try { + String[] backup = + db.loadInventoryBackup(name); + if (backup == null + || !isValidBackup(backup)) { + return; + } + + debugPrintBase64("恢复前", name, + backup[0]); + + ItemStack[] contents = + InventorySerializer + .fromBase64(backup[0]); + if (contents != null) + p.getInventory() + .setContents(contents); + + if (backup[1] != null + && !backup[1].isEmpty()) { + ItemStack[] armor = + InventorySerializer + .fromBase64( + backup[1]); + if (armor != null) + p.getInventory() + .setArmorContents( + armor); + } + + if (backup[2] != null + && !backup[2].isEmpty()) { + ItemStack[] extra = + InventorySerializer + .fromBase64( + backup[2]); + if (extra != null) + p.getInventory() + .setExtraContents( + extra); + } + + int lv = Integer.parseInt( + backup[3]); + if (lv > 0) p.setLevel(lv); + double xp = Double.parseDouble( + backup[4]); + if (xp > 0) + p.setExp((float) xp); + + debugPrintBase64("恢复后", name, + InventorySerializer.toBase64( + p.getInventory() + .getContents())); + + getLogger().info( + "[Sdf1_login] " + name + + " 背包已恢复"); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] 还原背包失败: " + + name); + e.printStackTrace(); + } + + } + + + @EventHandler + public void onBlockPlace_Quest( + BlockPlaceEvent e) { + if (questTracker == null) return; + Player p = e.getPlayer(); + if (isFrozen(p)) return; + questTracker.onBlockPlaced( + p.getName(), + e.getBlock().getType().name()); + } + + @EventHandler + public void onMoveBiome( + PlayerMoveEvent e) { + if (questTracker == null) return; + Player p = e.getPlayer(); + if (isFrozen(p)) return; + Location to = e.getTo(); + if (to == null) return; + String fromKey = e.getFrom().getBlock() + .getBiome().getKey().getKey(); + String toKey = to.getBlock() + .getBiome().getKey().getKey(); + if (fromKey.equals(toKey)) return; + questTracker.onPlayerMove( + p.getName(), toKey); + } + + + public void backupInventory(Player p) { + String name = p.getName(); + int cnt = countItems(p) + + countArmor(p) + + countExtra(p); + if (cnt == 0) { + getLogger().warning( + "[Sdf1_login] " + name + + " 背包为空,跳过备份"); + return; + } + try { + String invB64 = + InventorySerializer.toBase64( + p.getInventory() + .getContents()); + String armorB64 = + InventorySerializer.toBase64( + p.getInventory() + .getArmorContents()); + String extraB64 = + InventorySerializer.toBase64( + p.getInventory() + .getExtraContents()); + db.saveInventoryBackup(name, invB64, + armorB64, extraB64, + p.getLevel(), p.getExp()); + getLogger().info( + "[Sdf1_login] " + name + + " 背包已备份 (物品:" + + cnt + ")"); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] 背包备份失败: " + + name); + e.printStackTrace(); + } + } + + private void hideInventory(Player p) { + String name = p.getName(); + try { + debugPrintBase64("清空前", name, + InventorySerializer.toBase64( + p.getInventory() + .getContents())); + + // 清空背包(备份已在上次退出时保存) + p.getInventory().clear(); + p.getInventory() + .setArmorContents(null); + p.getInventory() + .setExtraContents(null); + p.setLevel(0); + p.setExp(0); + + debugPrintBase64("清空后", name, + InventorySerializer.toBase64( + p.getInventory() + .getContents())); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] hideInventory失败: " + + name); + e.printStackTrace(); + } + } + + private void hideInventoryDelayed(Player p, + int attempt) { + String name = p.getName(); + if (!p.isOnline()) return; + if (!isFrozen(p)) return; + + // 从数据库加载已有备份 + String[] existing = + db.loadInventoryBackup(name); + + // 调试打印 + debugPrintBase64("加入-加载DB", name, + existing != null + ? existing[0] : null); + + // 无有效备份 → 跳过清空背包 + if (existing == null + || !isValidBackup(existing)) { + getLogger().info( + "[Sdf1_login] " + name + + " 无有效备份," + + "跳过清空背包"); + return; + } + + // 有有效备份 → 清空当前背包 + hideInventory(p); + } + + + public void autoLogin(Player p, + String registerType) { + String name = p.getName(); + String ip = getPlayerIP(p); + String uuid = + p.getUniqueId().toString(); + String defaultPwd = "123456aA"; + + if (!db.userExists(name)) { + if (config.maxAccountsPerIP > 0 + && ip != null + && !ipGroup.canRegister(ip)) { + p.kickPlayer( + buildIPKickMessage(p, ip)); + return; + } + if (!"manual".equals( + config.approvalMode)) { + AccountRequestManager.Request req = + accountRequest.createRequest( + name, name, ip); + if (req != null) { + p.kickPlayer( + config.msg("need_approval") + .replace("{id}", + String.valueOf( + req.id))); + return; + } + radio.stopLoginRadio(p); + + } + String salt = + PasswordUtils.generateSalt(); + String hash = PasswordUtils.hash( + defaultPwd, salt); + db.createUser(name, hash, salt); + db.setField(name, + "premium_uuid", uuid); + db.setField(name, + "register_ip", ip); + db.setField(name, + "register_type", registerType); + db.recordIP(name, ip); + } + + loggedIn.add(name); + removeFlightOnce.add(name); + db.setLoggedIn(name, true); + db.setField(name, + "last_login_time", + System.currentTimeMillis()); + db.setField(name, + "last_online_check", + System.currentTimeMillis()); + db.recordIP(name, ip); + joinTime.remove(p.getUniqueId()); + recordIPLogin(p); + + // 标记注册登录条件完成 + if (questTracker != null) { + questTracker.markConditionComplete( + name, "完成注册登录"); + } + + // 欢迎信息 + this.getWelcome().onLogin(p); + + // 只在有有效备份时恢复 + restoreInventory(p); + + giveMenuSnowball(p); + p.sendMessage("§a[Sdf1_login] §f您已自动登录!"); + activateBeibao(p); + pushPendingAlerts(p); + } + + // ===== 任务追踪事件 ===== + + @EventHandler + public void onBlockMine(BlockBreakEvent event) { + if (questTracker == null) return; // ← 新增 + Player p = event.getPlayer(); + if (isFrozen(p)) return; + questTracker.onBlockMined( + p.getName(), + event.getBlock().getType().name()); + } + + @EventHandler + public void onMobDeath(EntityDeathEvent e) { + if (questTracker == null) return; // ← 新增 + if (e.getEntity().getKiller() == null) + return; + Player p = e.getEntity().getKiller(); + questTracker.onMobKill( + p.getName(), + e.getEntity().getType().name()); + } + + @EventHandler + public void onPlayerDeath( + PlayerDeathEvent e) { + if (questTracker == null) return; // ← 新增 + Player killer = + e.getEntity().getKiller(); + if (killer == null) return; + questTracker.onPlayerKill( + killer.getName()); + } + + @EventHandler + public void onCraft( + CraftItemEvent e) { + if (questTracker == null) return; // ← 新增 + if (!(e.getWhoClicked() + instanceof Player)) return; + Player p = (Player) e.getWhoClicked(); + if (isFrozen(p)) return; + questTracker.onItemCrafted( + p.getName()); + } + + @EventHandler + public void onFish( + PlayerFishEvent e) { + if (questTracker == null) return; // ← 新增 + if (e.getState() + != PlayerFishEvent + .State.CAUGHT_FISH) return; + if (!(e.getCaught() + instanceof + org.bukkit.entity.Item)) return; + Player p = e.getPlayer(); + questTracker.onFishCaught( + p.getName()); + } + + // 在线时长追踪(每分钟) + public void startPlayTimeTracker() { + Bukkit.getScheduler() + .runTaskTimer(this, + () -> { + if (questTracker == null) return; + for (Player p : + Bukkit.getOnlinePlayers()) { + if (isFrozen(p)) continue; + questTracker.onPlayTime( + p.getName(), + 60000L); + } + }, 1200L, 1200L); + } + + private void activateBeibao(Player p) { + try { + org.bukkit.plugin.Plugin cy = + Bukkit.getPluginManager() + .getPlugin("CY_beibao"); + if (cy != null && cy.isEnabled()) + cy.getClass().getMethod( + "onSdf1Activation", + String.class, + int.class, int.class) + .invoke(cy, p.getName(), + 0, 0); + } catch (Exception ignored) { + } + } + + public void triggerSecurityAlert(Player player, + String suspiciousIP) { + String name = player.getName(); + db.recordSecurityAlert(name, suspiciousIP); + Bukkit.broadcastMessage( + "§c§l[安全警告] §e" + name + + " §f账号疑似遭到 §c" + + suspiciousIP + + " §f的盗号尝试"); + String emailAddr = + (String) db.getField(name, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String body = "玩家 " + name + + ":\n您的账号在 " + + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()) + + " 疑似遭到 IP: " + + suspiciousIP + + " 的盗号尝试。\n" + + "建议立即修改密码。"; + final String to = emailAddr; + final String finalBody = body; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> email + .sendTempPassword( + to, name, + finalBody)); + } + } + + public void pushPendingAlerts(Player p) { + String name = p.getName(); + List> alerts = + db.getUnnotifiedAlerts(name); + if (alerts.isEmpty()) return; + p.sendMessage("§c§l========== 安全报警 =========="); + for (Map alert : alerts) { + p.sendMessage("§e时间: §f" + + alert.get("alert_date")); + p.sendMessage("§e可疑IP: §c" + + alert.get("alert_ip")); + db.markAlertNotified( + ((Number) alert.get("id")) + .intValue()); + } + p.sendMessage( + "§c建议使用 §e/sdf1_login pw §c修改密码"); + p.sendMessage( + "§c§l=================================="); + } + + public void handleReset(Player p) { + loginMgr.handleReset(p); + } + + public boolean dailySign(Player p) { + if (checkIn.isCheckedInToday(p.getName())) + return false; + p.sendMessage(checkIn.checkIn(p)); + invite.onInviteeCheckIn(p.getName()); + return true; + } + + + public void openAccountRequestPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, "§e§l账号请求"); + ItemStack gl = new ItemStack( + Material.GRAY_STAINED_GLASS_PANE); + ItemMeta glm = gl.getItemMeta(); + if (glm != null) { + glm.setDisplayName(" "); + gl.setItemMeta(glm); + } + for (int i = 0; i < 27; i++) + g.setItem(i, gl); + g.setItem(10, mkItem(Material.BARRIER, + "§c申请删除账号", + "§7申请删除自己名下的账号")); + g.setItem(13, mkItem(Material.PAPER, + "§6提交工单", + "§7提交bug反馈、求助、举报等")); + g.setItem(16, mkItem(Material.REDSTONE, + "§e找回密码", + "§7通过邮箱找回密码")); + g.setItem(22, mkItem(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + private void playSuccessSound(Player p) { + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_CHIME, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_BASS, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound.BLOCK_NOTE_BLOCK_PLING, + 1.0f, 0.79f); + } + + private int countItems(Player p) { + int c = 0; + for (ItemStack it : p.getInventory() + .getContents()) + if (it != null && it.getType() + != Material.AIR) c++; + return c; + } + + private int countArmor(Player p) { + int c = 0; + for (ItemStack it : p.getInventory() + .getArmorContents()) + if (it != null && it.getType() + != Material.AIR) c++; + return c; + } + + private int countExtra(Player p) { + int c = 0; + for (ItemStack it : p.getInventory() + .getExtraContents()) + if (it != null && it.getType() + != Material.AIR) c++; + return c; + } + + /** + * 检查备份是否有效(包含非雪球的真实物品) + */ + public boolean isValidBackup(String[] backup) { + if (backup == null + || backup[0] == null + || backup[0].isEmpty()) { + return false; + } + try { + ItemStack[] items = + InventorySerializer + .fromBase64(backup[0]); + if (items == null) return false; + for (ItemStack item : items) { + if (item == null + || item.getType() + == Material.AIR) + continue; + if (isMenuSnowball(item)) + continue; + return true; + } + } catch (Exception e) { + return false; + } + return false; + } + + /** + * 控制台打印 base64 前10位 + */ + private void debugPrintBase64(String label, + String name, String base64) { + if (base64 == null || base64.isEmpty()) { + getLogger().info( + "[BACKUP_DEBUG] " + label + + " " + name + + ": "); + return; + } + String preview = + base64.length() > 10 + ? base64.substring(0, 10) + : base64; + getLogger().info( + "[BACKUP_DEBUG] " + label + " " + + name + ": " + preview); + } + + private ItemStack mkItem(Material mat, + String name, String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) { + List list = + new ArrayList<>(); + for (String l : lore) + if (!l.isEmpty()) list.add(l); + if (!list.isEmpty()) + im.setLore(list); + } + it.setItemMeta(im); + } + return it; + } + + private String buildIPKickMessage(Player p, + String ip) { + List> accounts = + db.getAccountDetailsByIP(ip); + StringBuilder sb = new StringBuilder(); + sb.append("§c§l======== IP注册已满 ========\n"); + sb.append("§e您的IP §f").append(ip) + .append(" §e下已注册 §f") + .append(accounts.size()) + .append(" §e个账号:\n"); + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm"); + int idx = 1; + for (Map acc : accounts) { + long regTime = ((Number) acc.get( + "register_time")).longValue(); + String dateStr = regTime > 0 + ? sdf.format(new java.util.Date( + regTime)) + : "未知"; + sb.append("§7").append(idx) + .append(". §f") + .append(acc.get("player_name")) + .append(" §7注册于: §e") + .append(dateStr).append("\n"); + idx++; + } + sb.append("§7-----------------------------\n"); + sb.append("§e请使用已有账号登录\n"); + sb.append("§e登录后输入 §f/oa §e提交工单\n"); + sb.append("§c§l============================"); + return sb.toString(); + } + /** + * 读取备份,返回物品名预览 + */ + private String getBackupItemPreview( + int backupId) { + try { + String[] backup = + db.getInventoryBackupById( + backupId); + if (backup == null + || backup[0] == null + || backup[0].isEmpty()) + return "§7空"; + ItemStack[] items = + InventorySerializer + .fromBase64( + backup[0]); + if (items == null) + return "§7空"; + Map counts = + new LinkedHashMap<>(); + for (ItemStack it : items) { + if (it == null + || it.getType() + == Material.AIR) + continue; + String name = translateMaterial( + it.getType().name()); + counts.merge(name, + it.getAmount(), + Integer::sum); + } + if (counts.isEmpty()) + return "§7空"; + StringBuilder sb = + new StringBuilder(); + int shown = 0; + for (Map.Entry en + : counts.entrySet()) { + if (shown >= 5) { + sb.append(" §7..."); + break; + } + if (shown > 0) + sb.append("§7, "); + sb.append("§f").append(en.getKey()) + .append("§7x") + .append(en.getValue()); + shown++; + } + return sb.toString(); + } catch (Exception e) { + return "§7解析失败"; + } + } + + private String translateMaterial( + String name) { + switch (name) { + case "GRASS_BLOCK": + return "草方块"; + case "OAK_LOG": + return "橡木原木"; + case "OAK_PLANKS": + return "橡木木板"; + case "STONE": + return "石头"; + case "COBBLESTONE": + return "圆石"; + case "DIRT": + return "泥土"; + case "SAND": + return "沙子"; + case "GRAVEL": + return "砂砾"; + case "DIAMOND": + return "钻石"; + case "DIAMOND_ORE": + return "钻石矿石"; + case "IRON_INGOT": + return "铁锭"; + case "GOLD_INGOT": + return "金锭"; + case "COAL": + return "煤炭"; + case "COAL_ORE": + return "煤矿石"; + case "RAW_IRON": + return "粗铁"; + case "RAW_GOLD": + return "粗金"; + case "OAK_LEAVES": + return "橡树树叶"; + case "OAK_SAPLING": + return "橡树树苗"; + case "STICK": + return "木棍"; + case "CRAFTING_TABLE": + return "工作台"; + case "FURNACE": + return "熔炉"; + case "CHEST": + return "箱子"; + case "TORCH": + return "火把"; + case "BEDROCK": + return "基岩"; + case "TNT": + return "TNT"; + case "REDSTONE": + return "红石"; + case "REDSTONE_ORE": + return "红石矿石"; + case "LAPIS_LAZULI": + return "青金石"; + case "EMERALD": + return "绿宝石"; + case "EMERALD_ORE": + return "绿宝石矿石"; + case "OBSIDIAN": + return "黑曜石"; + case "GLOWSTONE": + return "萤石"; + case "ARROW": + return "箭"; + case "BOW": + return "弓"; + case "IRON_SWORD": + return "铁剑"; + case "DIAMOND_SWORD": + return "钻石剑"; + case "IRON_PICKAXE": + return "铁镐"; + case "DIAMOND_PICKAXE": + return "钻石镐"; + case "IRON_AXE": + return "铁斧"; + case "DIAMOND_AXE": + return "钻石斧"; + case "IRON_HELMET": + return "铁头盔"; + case "IRON_CHESTPLATE": + return "铁胸甲"; + case "IRON_LEGGINGS": + return "铁护腿"; + case "IRON_BOOTS": + return "铁靴子"; + case "DIAMOND_HELMET": + return "钻石头盔"; + case "DIAMOND_CHESTPLATE": + return "钻石胸甲"; + case "DIAMOND_LEGGINGS": + return "钻石护腿"; + case "DIAMOND_BOOTS": + return "钻石靴子"; + case "BREAD": + return "面包"; + case "COOKED_BEEF": + return "牛排"; + case "COOKED_PORKCHOP": + return "熟猪排"; + case "COOKED_CHICKEN": + return "熟鸡肉"; + case "GOLDEN_APPLE": + return "金苹果"; + case "ENDER_PEARL": + return "末影珍珠"; + case "BLAZE_ROD": + return "烈焰棒"; + case "GHAST_TEAR": + return "恶魂之泪"; + case "NETHER_STAR": + return "下界之星"; + case "SHULKER_SHELL": + return "潜影壳"; + case "ELYTRA": + return "鞘翅"; + case "TOTEM_OF_UNDYING": + return "不死图腾"; + case "TRIDENT": + return "三叉戟"; + case "ENCHANTED_GOLDEN_APPLE": + return "附魔金苹果"; + default: + // 转中文名格式:SANDSTONE -> 沙石 + return name.toLowerCase() + .replace("_", " "); + } + } + + @EventHandler + public void onTeleportBiome( + PlayerTeleportEvent e) { + if (questTracker == null) return; + Player p = e.getPlayer(); + if (isFrozen(p)) return; + Location to = e.getTo(); + if (to == null) return; + String toKey = to.getBlock() + .getBiome().getKey().getKey(); + questTracker.onPlayerMove( + p.getName(), toKey); + } + + @EventHandler + public void onJoin(PlayerJoinEvent e) { + Player p = e.getPlayer(); + String name = p.getName(); + String ip = getPlayerIP(p); + // 重置聊天输入状态(防止上次退出时残留) + chatInput.reset(p); + if (needsPasswordChange.contains(name)) + needsPasswordChange.remove(name); + lastActivity.put(p.getUniqueId(), + System.currentTimeMillis()); + if (needsPasswordChange.contains(name)) + needsPasswordChange.remove(name); + + boolean isBedrock = + verification.isBedrockPlayer(p); + boolean isOnlineMode = + verification.isOnlineMode(); + + if (!db.userExists(name)) { + if (config.maxAccountsPerIP > 0 + && ip != null + && !ipGroup.canRegister(ip)) { + p.kickPlayer( + buildIPKickMessage(p, ip)); + return; + } + } + if (isFrozen(p)) { + p.setAllowFlight(true); + frozenFlightPlayers.add(p.getName()); + } + if (questTracker != null) { + questTracker.debugLogin(p.getName()); + } + + if (!"manual".equals(config.approvalMode) + && !db.userExists(name)) { + AccountRequestManager.Request req = + accountRequest.createRequest( + name, name, ip); + if (req != null) { + p.kickPlayer( + config.msg("need_approval") + .replace("{id}", + String.valueOf( + req.id))); + return; + } + } + + if (isOnlineMode) { + autoLogin(p, "premium"); + return; + } + if (isBedrock) { + autoLogin(p, "bedrock"); + return; + } + if (canAutoLogin(p)) { + autoLogin(p, "ip_reconnect"); + return; + } + afk.remove(p.getUniqueId()); + afk.recordAction(p.getUniqueId()); + hideInventoryDelayed(p, 0); + + + verification.verifyPremiumAsync(p, + isPremium -> { + if (isPremium) { + Bukkit.getScheduler() + .runTask(this, + () -> autoLogin(p, + "premium")); + } else { + Bukkit.getScheduler() + .runTask(this, () -> { + joinTime.put( + p.getUniqueId(), + System.currentTimeMillis()); + if (!db.userExists(name)) + p.sendMessage(config.msg( + "not_registered")); + else + p.sendMessage(config.msg( + "not_logged_in")); + }); + } + }); + } + + @EventHandler + public void onQuit(PlayerQuitEvent e) { + Player p = e.getPlayer(); + // 记录冻结状态的飞行 + if (!db.userExists(p.getName()) + || !loggedIn.contains(p.getName())) { + p.setAllowFlight(true); + frozenFlightPlayers.add(p.getName()); + } + + String name = p.getName(); + chatInput.reset(p); + + boolean wasLoggedIn = + loggedIn.contains(name); + + boolean hasRealItems = false; + for (ItemStack it : p.getInventory() + .getContents()) { + if (it != null + && it.getType() + != Material.AIR + && !isMenuSnowball(it)) { + hasRealItems = true; + break; + } + } + + if (wasLoggedIn || hasRealItems) { + try { + String contents = + InventorySerializer + .toBase64( + p.getInventory() + .getContents()); + String armor = + InventorySerializer + .toBase64( + p.getInventory() + .getArmorContents()); + String extra = + InventorySerializer + .toBase64( + p.getInventory() + .getExtraContents()); + int level = p.getLevel(); + double exp = p.getExp(); + + debugPrintBase64( + "退出保存", name, contents); + db.saveInventoryBackup(name, + contents, armor, extra, + level, exp); + } catch (Exception ex) { + getLogger().severe( + "[Sdf1_login] " + + "退出保存背包失败: " + + name); + } + } else { + getLogger().info( + "[BACKUP_DEBUG] 退出跳过 " + + name + ": 背包为空" + + "(插件已清空)"); + } + + if (wasLoggedIn) { + loggedIn.remove(name); + db.setLoggedIn(name, false); + recordIPLogin(p); + } + + frozenFlightPlayers.remove(name); // ← 新增 + lastActivity.remove(p.getUniqueId()); + joinTime.remove(p.getUniqueId()); + joinLoc.remove(p.getUniqueId()); + needsPasswordChange.remove(name); + } + + private static final String MENU_SNOWBALL_TAG = + "sdf1_menu"; + + // 雪球菜单 + public void giveMenuSnowball(Player p) { + // 检查开关 + Object val = db.getField( + p.getName(), "menu_snowball"); + if (val != null) { + int on = val instanceof Number + ? ((Number) val).intValue() + : 1; + if (on == 0) { + return; // 开关关了,不发放 + } + } + // 检查是否已有 + for (ItemStack it : p.getInventory() + .getContents()) { + if (isMenuSnowball(it)) { + return; + } + } + // 发放 + ItemStack snow = new ItemStack( + Material.SNOWBALL); + ItemMeta im = snow.getItemMeta(); + if (im != null) { + im.setDisplayName( + "§e§l[菜单] §f右键打开主菜单"); + List lore = + new ArrayList<>(); + lore.add("§7右键点击打开功能菜单"); + lore.add("§8" + MENU_SNOWBALL_TAG); + im.setLore(lore); + snow.setItemMeta(im); + } + int slot = p.getInventory() + .firstEmpty(); + if (slot >= 0) { + p.getInventory().setItem( + slot, snow); + } else { + p.getWorld().dropItemNaturally( + p.getLocation(), snow); + } + } + + /** + * 手动发放雪球菜单(绕过开关检查) + */ + public void forceGiveMenuSnowball( + Player p) { + // 只检查是否已有 + for (ItemStack it : p.getInventory() + .getContents()) { + if (isMenuSnowball(it)) { + return; + } + } + ItemStack snow = new ItemStack( + Material.SNOWBALL); + ItemMeta im = snow.getItemMeta(); + if (im != null) { + im.setDisplayName( + "§e§l[菜单] §f右键打开主菜单"); + List lore = + new ArrayList<>(); + lore.add("§7右键点击打开功能菜单"); + lore.add("§8" + MENU_SNOWBALL_TAG); + im.setLore(lore); + snow.setItemMeta(im); + } + int slot = p.getInventory() + .firstEmpty(); + if (slot >= 0) { + p.getInventory().setItem( + slot, snow); + } else { + p.getWorld().dropItemNaturally( + p.getLocation(), snow); + } + } + + private boolean isMenuSnowball(ItemStack item) { + if (item == null + || item.getType() + != Material.SNOWBALL) + return false; + if (!item.hasItemMeta()) + return false; + ItemMeta im = item.getItemMeta(); + if (im == null) return false; + if (!im.hasLore()) return false; + List lore = im.getLore(); + if (lore == null) return false; + for (String line : lore) { + if (line.contains(MENU_SNOWBALL_TAG)) + return true; + } + return false; + } + + @EventHandler + public void onMove(PlayerMoveEvent e) { + Player p = e.getPlayer(); + + // 已登录:一次性关闭飞行 + if (!isFrozen(p)) { + if (frozenFlightPlayers.remove( + p.getName())) { + p.setAllowFlight(false); + p.setFlying(false); + } + return; + } + + + // 未登录:检查是否能移动 + Location from = e.getFrom(); + Location to = e.getTo(); + if (to == null) return; + + // 允许转头(无位移) + if (from.getBlockX() == to.getBlockX() + && from.getBlockY() == to.getBlockY() + && from.getBlockZ() == to.getBlockZ()) { + return; + } + + // 有位移 → 取消移动 + e.setCancelled(true); + + // 确保允许飞行(防止被踢出) + if (!p.getAllowFlight()) { + p.setAllowFlight(true); + } + + // 确保已记录(首次加入时未触发移动的情况) + if (!frozenFlightPlayers.contains( + p.getName())) { + frozenFlightPlayers.add(p.getName()); + } + } + + @EventHandler + public void onInteract(PlayerInteractEvent e) { + Player p = e.getPlayer(); + ItemStack item = e.getItem(); + if (item == null + || item.getType() + != Material.SNOWBALL) + return; + if (!isMenuSnowball(item)) return; + if (isFrozen(p)) return; + if (!loggedIn.contains(p.getName())) return; + e.setCancelled(true); + gui.openMain(p); + } + + @EventHandler + public void onBlockBreak(BlockBreakEvent e) { + Player p = e.getPlayer(); + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + afk.recordAction(p.getUniqueId()); + } + + @EventHandler + public void onBlockPlace(BlockPlaceEvent e) { + if (isFrozen(e.getPlayer())) + e.setCancelled(true); + } + + @EventHandler + public void onDrop(PlayerDropItemEvent e) { + if (isFrozen(e.getPlayer())) { + e.setCancelled(true); + return; + } + // 禁止丢弃菜单雪球 + ItemStack drop = + e.getItemDrop().getItemStack(); + if (isMenuSnowball(drop)) { + e.setCancelled(true); + } + } + + @EventHandler + public void onPickup(PlayerPickupItemEvent e) { + if (isFrozen(e.getPlayer())) + e.setCancelled(true); + } + + @EventHandler + public void onDamage(EntityDamageEvent e) { + if (e.getEntity() instanceof Player) { + if (isFrozen((Player) e.getEntity())) + e.setCancelled(true); + } + } + + @EventHandler + public void onEntityAttack( + EntityDamageByEntityEvent e) { + if (e.getDamager() instanceof Player + && isFrozen( + (Player) e.getDamager())) + e.setCancelled(true); + if (e.getEntity() instanceof Player + && isFrozen( + (Player) e.getEntity())) + e.setCancelled(true); + } + + @EventHandler(priority = EventPriority.LOWEST) + public void onChat(AsyncPlayerChatEvent e) { + Player p = e.getPlayer(); + String msg = e.getMessage(); + + if (chatInput.getState(p).type + != ChatInputManager.InputType.NONE) { + e.setCancelled(true); + ChatInputManager.InputType inputType = + chatInput.getState(p).type; + if (ticket != null + && ticket.handleChatInput( + p, msg, inputType)) { + return; + } + chatInput.handleInput(p, msg, this); + return; + } + + if (isFrozen(p)) { + e.setCancelled(true); + return; + } +// 在 onChat 方法中,isFrozen 检查之后 +// (在 chatFilter 检查之前) + if (!isFrozen(p) + && questTracker != null) { + final String pn = p.getName(); + Bukkit.getScheduler().runTask(this, + () -> questTracker + .onPlayerChat(pn)); + } + + if (chatFilter == null) return; + // 安全兜底:任何输入状态下跳过过滤 + if (chatInput.getState(p).type + != ChatInputManager.InputType.NONE) { + return; + } + + if (chatFilter.isPlayerWhitelisted( + p.getName())) return; + // 联控:绑定邮箱/配置SMTP时跳过URL过滤 + InputType inType = + chatInput.getState(p).type; + if (inType == InputType.SET_EMAIL + || inType == InputType.SET_EMAIL_VERIFY + || inType == InputType.SMTP_CONFIG + || inType == InputType.SMTP_VERIFY) { + return; + } + + if (chatFilter.isMuted(p.getName())) { + e.setCancelled(true); + p.sendMessage( + chatFilter.msg("chat_muted")); + return; + } + + List urls = + chatFilter.extractUrls(msg); + if (urls.isEmpty()) return; + + for (String u : urls) { + if (chatFilter.isPlayerWhitelisted(u)) + continue; + if (!chatFilter.isLikelyDomain(u)) + continue; + + e.setCancelled(true); + chatFilter.incrementViolation( + p.getName()); + int count = chatFilter + .getViolationCount(p.getName()); + + p.sendMessage(chatFilter + .msg("chat_url_blocked") + .replace("{url}", u)); + p.sendMessage(chatFilter + .msg("chat_url_violation") + .replace("{count}", + String.valueOf(count))); + + chatFilter.applyPunishment(p, count); + + if (chatFilter.isNotifyAdmin()) { + String tag = config.adminTag; + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.getScoreboardTags() + .contains(tag) + && !op.equals(p)) { + op.sendMessage(chatFilter + .msg("chat_url_admin_notify") + .replace("{player}", + p.getName()) + .replace("{url}", u) + .replace("{count}", + String.valueOf( + count))); + } + } + } + + if (chatFilter.isNotifyAll()) { + Bukkit.broadcastMessage(chatFilter + .msg("chat_url_broadcast") + .replace("{player}", + p.getName())); + } + } + } + + + + // ===== Inventory Click ===== + // ===== Inventory Click ===== + @EventHandler + public void onInvClick(InventoryClickEvent e) { + if (!(e.getWhoClicked() instanceof Player)) + return; + Player p = (Player) e.getWhoClicked(); + if (isFrozen(p)) { + e.setCancelled(true); + return; + } + String title = e.getView().getTitle(); + int slot = e.getRawSlot(); + + // Shift+点击背包物品 → 存入垃圾箱 + if (e.isShiftClick() + && e.getClickedInventory() != null + && e.getClickedInventory() + != e.getView().getTopInventory() + && e.getCurrentItem() != null + && e.getCurrentItem().getType() + != Material.AIR + && !isMenuSnowball( + e.getCurrentItem()) + && title.equals( + "§6§l垃圾回收站")) { + e.setCancelled(true); + + ItemStack clicked = + e.getCurrentItem().clone(); + + getLogger().info( + "[垃圾站] Shift存入: " + + clicked.getType() + + " x" + clicked.getAmount()); + + // 先存入数据库 + garbage.saveItem(clicked); + + // 关闭背包强制同步服务器与客户端状态 + // (解决Paper的shift-click取消后仍移动物品的问题) + p.closeInventory(); + + // 1 tick后:移除物品 + 重新打开垃圾站 + Bukkit.getScheduler() + .runTaskLater(this, + () -> { + // 此时物品状态已同步 + // 搜索并移除对应物品 + boolean found = false; + for (int i = 0; + i < p.getInventory() + .getSize(); + i++) { + ItemStack is = + p.getInventory() + .getItem(i); + if (is != null + && is.getType() + == clicked + .getType() + && is.getAmount() + == clicked + .getAmount()) { + p.getInventory() + .setItem( + i, + null); + found = true; + getLogger() + .info( + "[垃圾站] " + + "移除成功: slot=" + + i); + break; + } + } + if (!found) { + getLogger() + .info( + "[垃圾站] " + + "物品已被移走"); + } + + p.updateInventory(); + + // 重新打开垃圾站 + garbage + .openRecyclePage( + p, + garbage + .getRecyclePage( + p + .getName())); + }, 2L); + return; + } + + + if (needsPasswordChange + .contains(p.getName()) + && !title.equals( + "§6§l任务面板")) { + e.setCancelled(true); + p.sendMessage("§c请先修改密码!"); + return; + } + // 任务中心 + if (title.equals( + GUIManager.T_TASK_CENTER)) { + e.setCancelled(true); + if (slot == 26) gui.openMain(p); + else if (slot == 10) + gui.openGiftStages(p); + else if (slot == 12) + gui.openTaskList(p, "主线任务"); + else if (slot == 14) + gui.openTaskList(p, "支线任务"); + else if (slot == 16) { + if (dailySign(p)) { + gui.openTaskCenter(p); + playSuccessSound(p); + } else + p.sendMessage(config.msg( + "checkin_already")); + } + else if (slot == 21) { + handleCheckInReport(p); + } + return; + } + + // 工单GUI + if (title.equals( + TicketManager.T_TICKET_MAIN) + || title.equals( + TicketManager.T_TICKET_CREATE) + || title.equals( + TicketManager.T_TICKET_MY) + || title.equals( + TicketManager.T_TICKET_ADMIN) + || title.equals( + TicketManager.T_TICKET_DETAIL) + || title.equals( + "§d§l服务商面板") + || title.equals( + "§6§l抢单大厅") + || title.equals( + "§b§l我处理中的工单")) { + e.setCancelled(true); + ticket.handleClick( + p, title, slot); + return; + } + + // 垃圾回收站 + if (title.equals("§6§l垃圾回收站")) { + if (slot < 0 || slot >= 54) return; + + // 功能格 49-53 + if (slot >= 49 && slot <= 53) { + e.setCancelled(true); + garbage.handleClick( + p, title, slot); + return; + } + + ItemStack inSlot = + e.getInventory().getItem(slot); + ItemStack cursor = e.getCursor(); + + // ===== 取出 ===== + if (inSlot != null + && inSlot.hasItemMeta() + && inSlot.getItemMeta().hasLore()) { + List lore = + inSlot.getItemMeta().getLore(); + if (lore != null && !lore.isEmpty() + && lore.get(0) + .startsWith("§7ID: #")) { + e.setCancelled(true); + try { + String numStr = lore.get(0) + .replace("§7ID: #", "") + .trim(); + int dbId = + Integer.parseInt(numStr); + if (garbage.removeItem(dbId)) { + ItemStack give = + inSlot.clone(); + ItemMeta gm = + give.getItemMeta(); + gm.setLore(null); + give.setItemMeta(gm); + p.getInventory() + .addItem(give); + e.getInventory() + .setItem(slot, null); + } + } catch (Exception ex) { + p.sendMessage("§c取出失败"); + } + return; + } + } + + // ===== 光标存入 ===== + if (cursor != null + && cursor.getType() + != Material.AIR + && !isMenuSnowball(cursor)) { + ItemStack toSave = cursor.clone(); + getLogger().info( + "[垃圾站] 存入: " + + toSave.getType() + + " x" + toSave.getAmount()); + garbage.saveItem(toSave); + e.setCancelled(true); + Bukkit.getScheduler().runTaskLater( + this, () -> { + p.getOpenInventory() + .setCursor(null); + garbage.openRecyclePage(p, + garbage.getRecyclePage( + p.getName())); + }, 1L); + return; + } + + // 默认取消 + e.setCancelled(true); + return; + } + + // 主菜单 + if (title.equals(GUIManager.T_MAIN)) { + e.setCancelled(true); + if (slot == 10) gui.openMyInfo(p); + else if (slot == 12) gui.openInvite(p); + else if (slot == 14) + p.openInventory( + points.createShopGUI(p)); + else if (slot == 16) + gui.openTaskCenter(p); + else if (slot == 18) ticket.openMain(p); + else if (slot == 20) { + if (p.isOp() || p.getScoreboardTags() + .contains(config.adminTag)) { + garbage.openRecycle(p); + } + } + + else if (slot == 22 && isAdmin(p)) { + p.closeInventory(); + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_AUTH; + p.sendMessage("请输入管理密码:"); + } + return; + } + + + // 我的信息 + if (title.equals(GUIManager.T_MY_INFO)) { + e.setCancelled(true); + if (slot == 26) gui.openMain(p); + else if (slot == 10) { + if (checkIn.isCheckedInToday( + p.getName())) + p.sendMessage(config.msg( + "checkin_already")); + else { + p.sendMessage( + checkIn.checkIn(p)); + invite.onInviteeCheckIn( + p.getName()); + playSuccessSound(p); + gui.openMyInfo(p); + } + } else if (slot == 11) { + chatInput.getState(p).type = + ChatInputManager.InputType + .CHANGE_PWD_STEP1; + p.closeInventory(); + p.sendMessage("§e请输入当前密码:"); + } else if (slot == 12) { + chatInput.getState(p).type = + ChatInputManager.InputType + .BACK_CHECKIN; + p.closeInventory(); + p.sendMessage("§e请输入补签日期:"); + } else if (slot == 13) { + String code = (String) db.getField( + p.getName(), "invite_code"); + if (code == null || code.isEmpty()) + code = invite.generateCode(p); + p.sendMessage("§e邀请码: " + code); + } else if (slot == 14) { + chatInput.getState(p).type = + ChatInputManager.InputType + .SET_EMAIL; + p.closeInventory(); + p.sendMessage("§e请输入邮箱:"); + } else if (slot == 15) + p.openInventory( + points.createShopGUI(p)); + return; + } + + // 管理面板 + if (title.equals(GUIManager.T_ADMIN)) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 10) { + chatInput.getState(p).type = + ChatInputManager.InputType + .SMTP_CONFIG; + p.closeInventory(); + p.sendMessage("§e当前SMTP: " + + config.getSmtp("smtp地址")); + p.sendMessage("§e输入新地址(0跳过):"); + } else if (slot == 11) + gui.openUserManagement(p); + else if (slot == 12) gui.openMyInfo(p); + else if (slot == 14) + p.openInventory( + points.createShopGUI(p)); + else if (slot == 16) { + if (e.getClick().isLeftClick()) { + config.afkEnabled = + !config.afkEnabled; + p.sendMessage(config.msg( + config.afkEnabled + ? "afk_set_enabled" + : "afk_set_disabled")); + config.saveSettings(); + gui.openAdmin(p); + } else if (e.getClick() + .isRightClick()) { + p.closeInventory(); + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_SET_AFK_TIME; + p.sendMessage( + "§e输入挂机时长(分钟):"); + } + } + return; + } + + // 任务中心 + if (title.equals( + GUIManager.T_TASK_CENTER)) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 10) + gui.openGiftStages(p); + else if (slot == 12) + gui.openTaskList(p, "主线任务"); + else if (slot == 14) + gui.openTaskList(p, "支线任务"); + else if (slot == 16) { + if (dailySign(p)) { + gui.openTaskCenter(p); + playSuccessSound(p); + } else + p.sendMessage(config.msg( + "checkin_already")); + } + return; + } + + // 新人任务 + if (title.equals( + GUIManager.T_GIFT_STAGES)) { + e.setCancelled(true); + if (slot == 53) { + gui.openTaskCenter(p); + } else if (slot == 52) { + // 打卡上报 + handleCheckInReport(p); + } else if (slot == 4) { + // 进度标题 + } else if (slot >= 10 && slot < 53) { + int idx = slot - 10; + QuestTracker qt = questTracker; + List quests = + qt.getQuests("新人任务"); + if (idx < quests.size()) { + QuestTracker.QuestFile qf = + quests.get(idx); + boolean done = + qt.isStageCompleted( + p.getName(), qf); + if (done + && !qt.hasClaimed( + p.getName(), qf)) { + qt.markClaimed( + p.getName(), qf); + qt.claimRewards(p, qf); + gui.openGiftStages(p); + } else if (done + && qt.hasClaimed( + p.getName(), qf)) { + p.sendMessage( + "§c该阶段奖励已领取"); + } else { + p.sendMessage( + "§c该阶段尚未完成"); + } + } + } + return; + } + + + + // 邀请 + if (title.equals(GUIManager.T_INVITE)) { + e.setCancelled(true); + if (slot == 26) gui.openMain(p); + else if (slot == 10) { + String code = + invite.generateCode(p); + p.sendMessage("§e邀请码: " + code); + gui.openInvite(p); + } else if (slot == 16) { + chatInput.getState(p).type = + ChatInputManager.InputType + .INVITE_INPUT_CODE; + p.closeInventory(); + p.sendMessage("§e请输入邀请码:"); + } + + return; + } + + // 用户管理 + if (title.equals( + GUIManager.T_USER_MGMT)) { + e.setCancelled(true); + if (slot == 53) + gui.openAdmin(p); + else if (slot >= 0 && slot < 45) { + ItemStack item = + e.getInventory().getItem(slot); + if (item != null + && item.hasItemMeta() + && item.getItemMeta() + .hasDisplayName()) + gui.openUserDetail(p, + item.getItemMeta() + .getDisplayName() + .replace("§e", "") + .replace("§a", "")); + } + + return; + } + + // 用户详情 + if (title.startsWith("§e§l管理: ")) { + e.setCancelled(true); + String tgt = title.substring( + "§e§l管理: ".length()); + if (slot == 22) + gui.openUserManagement(p); + else if (slot == 10) { + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_SET_POINTS; + chatInput.getState(p) + .targetPlayer = tgt; + p.closeInventory(); + p.sendMessage("§e输入" + tgt + + "的新积分:"); + } else if (slot == 12) { + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_SET_PWD; + chatInput.getState(p) + .targetPlayer = tgt; + p.closeInventory(); + p.sendMessage("§e输入" + tgt + + "的临时密码:"); + } else if (slot == 13) { + gui.openCheckInRecords(p, tgt); + } else if (slot == 14) { + Player tp = Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) { + forceGiveMenuSnowball(tp); + p.sendMessage("§a已发放雪球菜单给 " + + tgt); + } else { + p.sendMessage("§c玩家不在线"); + } + } else if (slot == 16) { + + } else if (slot == 16) { + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_DELETE_CONFIRM; + chatInput.getState(p) + .targetPlayer = tgt; + p.closeInventory(); + p.sendMessage(config.msg( + "admin_delete_confirm")); + } + + return; + + } + + + // 积分商城 + if (title.startsWith("§d§l积分商城")) { + e.setCancelled(true); + if (slot == 49) gui.openMain(p); + else if (slot == 50 && isAdmin(p)) { + gui.openShopAdmin(p); + } else + points.handleClick(p, slot); + return; + } + + // 商城管理 + if (title.equals("§c§l商城管理")) { + e.setCancelled(true); + if (slot == 49) { + p.openInventory( + points.createShopGUI(p)); + return; + } + return; + } + + // CY背包商城 + if (title.startsWith("§d§lCY背包商城")) { + e.setCancelled(true); + int size = e.getInventory().getSize(); + if (slot == size - 1) + p.openInventory( + points.createShopGUI(p)); + else points.handleCYClick(p, slot); + return; + } + + // 账号请求面板 + if (title.equals("§e§l账号请求")) { + e.setCancelled(true); + if (slot == 22) gui.openMain(p); + else if (slot == 10) { + p.closeInventory(); + p.sendMessage("§c§l[确认] §f请输入:"); + p.sendMessage("§e/sdf1_login del " + + p.getName()); + } else if (slot == 13) { + ticket.openCreate(p); + } else if (slot == 16) { + p.closeInventory(); + loginMgr.handleReset(p); + } + return; + } + + // 主线/支线任务 + if (title.equals("§d§l主线任务") + || title.equals("§d§l支线任务")) { + e.setCancelled(true); + if (slot == e.getInventory().getSize() - 1) + gui.openTaskCenter(p); + return; + } + } + /** + * 打卡上报:验证当前阶段条件 + */ + + private void handleCheckInReport(Player p) { + QuestTracker qt = questTracker; + if (qt == null) { + p.sendMessage("§c任务系统未加载"); + return; + } + + p.sendMessage("§e§l[打卡] §f正在检测环境..."); + + // === 检测当前群系 === + Location loc = p.getLocation(); + String biomeKey = loc.getBlock() + .getBiome().getKey().getKey(); + String worldName = loc.getWorld().getName(); + + List biomeMatched = + qt.checkInBiome( + p.getName(), + biomeKey, worldName, + loc.getX(), loc.getY(), + loc.getZ()); + + qt.recordCheckIn( + p.getName(), biomeKey, + qt.mapBiomeCategory( + biomeKey, worldName), + worldName, + loc.getX(), loc.getY(), + loc.getZ()); + + p.sendMessage("§7 群系: §e" + + biomeKey); + p.sendMessage("§7 分类: §e" + + qt.mapBiomeCategory( + biomeKey, worldName)); + p.sendMessage("§7 世界: §e" + + worldName); + p.sendMessage("§7 坐标: §e" + + String.format("%.0f, %.0f, %.0f", + loc.getX(), loc.getY(), + loc.getZ())); + + if (!biomeMatched.isEmpty()) { + for (String m : biomeMatched) { + p.sendMessage("§7 §a✓ " + m); + } + } + + // === 检测附近村庄(50格内有村民) === + boolean nearVillage = false; + for (Entity entity + : p.getNearbyEntities(50, 50, 50)) { + if (entity instanceof Villager) { + nearVillage = true; + break; + } + } + if (nearVillage) { + boolean counted = + qt.onReportVillage( + p.getName(), + worldName, + loc.getX(), + loc.getY(), + loc.getZ()); + if (counted) { + p.sendMessage( + "§7 §a村庄上报计数成功!"); + } else { + p.sendMessage( + "§7 §e村庄32格内已上报过"); + } + } + + + // === 验证所有未完成的任务 === + p.sendMessage(""); + p.sendMessage("§e§l[打卡] §f正在验证" + + "未完成的任务..."); + + List categories = + Arrays.asList( + "新人任务", + "主线任务", + "支线任务"); + + int totalMet = 0; + int totalChecked = 0; + int finishedCount = 0; + + for (String cat : categories) { + List quests = + qt.getQuests(cat); + for (QuestTracker.QuestFile qf + : quests) { + if (qt.isStageCompleted( + p.getName(), qf)) { + finishedCount++; + continue; + } + if (!qf.hasConditions()) + continue; + + p.sendMessage("§e§l[" + + qf.displayName + "]"); + + for (String cond + : qf.conditions) { + if (qt.isOptional(cond)) + continue; + totalChecked++; + + boolean met = + qt.isConditionMet( + p.getName(), + cond); + + if (met) { + totalMet++; + p.sendMessage("§7 §a✓ " + + qt.getProgress( + p.getName(), + cond)); + } else { + p.sendMessage("§7 §c✗ " + + qt.getProgress( + p.getName(), + cond)); + } + } + } + } + + p.sendMessage(""); + if (finishedCount > 0) { + p.sendMessage("§7已跳过 " + + finishedCount + + " 个已完成的阶段"); + } + if (totalChecked == 0) { + p.sendMessage( + "§a§l[打卡] §f没有" + + "未完成的条件"); + } else if (totalMet >= totalChecked) { + p.sendMessage( + "§a§l[打卡] §f全部通过!(" + + totalMet + "/" + + totalChecked + ")"); + playSuccessSound(p); + } else { + p.sendMessage( + "§c§l[打卡] §f未通过 (" + + totalMet + "/" + + totalChecked + + ") §c请继续完成"); + } + + gui.openTaskCenter(p); + } + + + + @EventHandler + public void onInvDrag(InventoryDragEvent e) { + if (!(e.getWhoClicked() instanceof Player)) + return; + Player p = (Player) e.getWhoClicked(); + if (isFrozen(p)) { + frozenFlightPlayers.add(p.getName()); + e.setCancelled(true); + return; + } + String t = e.getView().getTitle(); + + // 回收站拖拽 + if (t.equals("§6§l垃圾回收站")) { + for (int rawSlot : + e.getRawSlots()) { + if (rawSlot >= 49) { + e.setCancelled(true); + return; + } + } + // 0-48允许拖入,不取消 + // 延迟2tick后扫描格子 + final Player fp = p; + Bukkit.getScheduler() + .runTaskLater(this, + () -> { + Inventory top = fp + .getOpenInventory() + .getTopInventory(); + for (int rawSlot : + e.getRawSlots()) { + if (rawSlot < 0 + || rawSlot >= 49) + continue; + ItemStack item = + top.getItem(rawSlot); + if (item != null + && item.getType() + != Material.AIR + && !isMenuSnowball( + item)) { + garbage.saveItem( + item.clone()); + top.setItem( + rawSlot, null); + } + } + }, 2L); + return; + } + + //服务商工单系统 + if (t.equals(TicketManager.T_TICKET_MAIN) + || t.equals( + TicketManager.T_TICKET_CREATE) + || t.equals( + TicketManager.T_TICKET_MY) + || t.equals( + TicketManager.T_TICKET_ADMIN) + || t.equals( + TicketManager.T_TICKET_DETAIL) + || t.equals("§d§l服务商面板") + || t.equals("§6§l抢单大厅") + || t.equals( + "§b§l我处理中的工单") + || t.equals("§6§l我的历史工单") + || t.equals("§c§l商城管理")) { + e.setCancelled(true); + } + } + + // ===== Commands ===== + @Override + public boolean onCommand(CommandSender sender, + Command cmd, String label, + String[] args) { + + String cmdName = cmd.getName().toLowerCase(); + + if (cmdName.equals("recycle")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player pr = (Player) sender; + if (!pr.isOp() + && !pr.getScoreboardTags() + .contains(config.adminTag)) { + pr.sendMessage("§c权限不足"); + return true; + } + garbage.openRecycle(pr); + return true; + } + + if (cmd.getName().equalsIgnoreCase("radio")) { + getLogger().info( + "[Radio] 收到命令: " + + Arrays.toString(args)); + + // 统一交给 RadioManager 处理 + radio.handleCommand(sender, args); + return true; + } + + + // /垃圾清理 + if (cmdName.equals("垃圾清理")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + int count = garbage.collectItems(); + sender.sendMessage("§a已清理 " + + count + " 个掉落物"); + return true; + } + // /menu 开关雪球菜单 + if (cmdName.equals("menu")) { + if (!(sender instanceof Player)) { + sender.sendMessage( + "§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length == 0) { + p2.sendMessage( + "§e用法: /menu <开/关>"); + return true; + } + String arg = args[0]; + if (arg.equals("开")) { + db.setField(p2.getName(), + "menu_snowball", 1); + p2.sendMessage( + "§a雪球菜单已开启," + + "下次进入将发放"); + } else if (arg.equals("关")) { + db.setField(p2.getName(), + "menu_snowball", 0); + p2.sendMessage( + "§c雪球菜单已关闭," + + "下次进入不再发放"); + } else { + p2.sendMessage( + "§e用法: /menu <开/关>"); + } + return true; + } +// /reg + if (cmdName.equals("reg") + || cmdName.equals("注册")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + chatInput.reset(p); + if (db.userExists(p.getName())) { + p.sendMessage( + "§c您已注册过,请使用 /login"); + return true; + } + try { + loginMgr.handleRegister(p, args); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] reg命令异常: " + + e.getMessage()); + e.printStackTrace(); + p.sendMessage("§c注册出错,请联系管理员"); + } + // 注册/登录成功后统一处理 + if (loggedIn.contains(p.getName())) { + frozenFlightPlayers.remove(p.getName()); + p.setAllowFlight(false); + p.setFlying(false); + // 仅关闭冻结期间的飞行 + if (frozenFlightPlayers.remove( + p.getName())) { + p.setAllowFlight(false); + p.setFlying(false); + removeFlightOnce.add(p.getName()); + if (questTracker != null) { + questTracker.markConditionComplete( + p.getName(), + "完成注册登录"); + } + this.getWelcome().onLogin(p); + restoreInventory(p); + giveMenuSnowball(p); + } + return true; + } +} + + if (cmdName.equals("login") + || cmdName.equals("l") + || cmdName.equals("登录")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p = (Player) sender; + chatInput.reset(p); + if (loggedIn.contains(p.getName())) { + frozenFlightPlayers.remove(p.getName()); + p.setAllowFlight(false); + p.setFlying(false); + removeFlightOnce.add(p.getName()); + p.sendMessage("§c您已登录"); + return true; + } + try { + loginMgr.handleLogin(p, args); + } catch (Exception e) { + getLogger().severe( + "[Sdf1_login] login命令异常: " + + e.getMessage()); + e.printStackTrace(); + p.sendMessage("§c登录出错,请联系管理员"); + } + // 登录成功后统一处理 + if (loggedIn.contains(p.getName())) { + if (questTracker != null) { + questTracker.markConditionComplete( + p.getName(), + "完成注册登录"); + } + this.getWelcome().onLogin(p); + restoreInventory(p); + giveMenuSnowball(p); + } + return true; + } + + // /找回密码 + if (cmdName.equals("找回密码")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + handleReset((Player) sender); + return true; + } + + // /签到 + if (cmdName.equals("签到")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + if (dailySign((Player) sender)) + playSuccessSound((Player) sender); + else + ((Player) sender).sendMessage( + config.msg("checkin_already")); + return true; + } + + // 绑定邮箱 + if (cmdName.equals("绑定邮箱")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length >= 1) { + String emailAddr = args[0]; + if (!emailAddr.contains("@") + || !emailAddr.contains(".")) { + p2.sendMessage("§c邮箱格式不正确"); + return true; + } + String oldEmail = (String) db.getField( + p2.getName(), "email"); + if (oldEmail != null + && !oldEmail.isEmpty()) { + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = "need_pwd"; + p2.sendMessage("§e您已绑定邮箱: " + + oldEmail); + p2.sendMessage( + "§e请输入登录密码以确认修改:"); + } else { + String code = String.valueOf( + (int) (Math.random() + * 900000 + 100000)); + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = code; + final String to = emailAddr; + final String c = code; + final Player fp = p2; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> { + boolean sent = email + .sendVerifyCode(to, c); + Bukkit.getScheduler().runTask( + this, () -> { + if (sent) { + fp.sendMessage( + "§a验证码已发送到 " + + to); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败"); + } + }); + }); + } + } else { + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL; + p2.sendMessage("§e请输入邮箱:"); + } + return true; + } + + + // /sdf1debug + if (cmdName.equals("sdf1debug")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (!isAdmin(sender)) { + p2.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + p2.sendMessage("§e用法: /sdf1debug <玩家> <阶段>"); + return true; + } + String tgt = args[0]; + try { + int stage = Integer.parseInt(args[1]); + db.setField(tgt, "gift_stage", stage); + p2.sendMessage("§a已将 " + tgt + + " 的任务阶段设为 " + stage); + } catch (NumberFormatException ex) { + p2.sendMessage("§c请输入有效数字"); + } + return true; + } + + // /oa + if (cmdName.equals("oa")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + openAccountRequestPanel( + (Player) sender); + return true; + } + + // /玩家信息 + if (cmdName.equals("玩家信息")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + gui.openMain((Player) sender); + return true; + } + + // ===== /sdf1_login ===== + if (!cmdName.equals("sdf1_login")) + return false; + + if (args.length == 0) { + if (!(sender instanceof Player)) { + sender.sendMessage( + "§e用法: /sdf1_login <子命令>"); + return true; + } + gui.openMain((Player) sender); + return true; + } + + String sub = args[0].toLowerCase(); + // update - 检查更新(控制台+玩家均可) + if (sub.equals("update")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + sender.sendMessage("§e[更新] 正在检测..."); + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> updateChecker + .checkUpdate(sender)); + return true; + } + + // stop + if (sub.equals("stop")) { + if (!(sender instanceof Player)) { + // 控制台 + if (args.length >= 2) { + db.clearTempPassword(args[1]); + sender.sendMessage("§a已报废 " + + args[1] + " 的临时密码"); + } else { + db.clearAllTempPasswords(); + sender.sendMessage( + "§a已报废所有临时密码"); + } + return true; + } + Player p2 = (Player) sender; + if (args.length >= 2) { + String tgt = args[1]; + String myIP = + getPlayerIP(p2); + String targetIP = + db.getPlayerIPByName(tgt); + if (targetIP == null) { + Player tp = + Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + targetIP = + getPlayerIP(tp); + } + if (targetIP == null + || !targetIP.equals(myIP)) { + p2.sendMessage( + "§c只能操作同IP下的玩家"); + return true; + } + db.clearTempPassword(tgt); + p2.sendMessage("§a已报废 " + tgt + + " 的临时密码"); + } else { + db.clearTempPassword(p2.getName()); + p2.sendMessage( + "§a已报废您的临时密码"); + } + return true; + } + + // reload + if (sub.equals("reload")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + config.loadMessages(); + config.loadSmtp(); + config.loadSettings(); + chatFilter.loadConfig(); + sender.sendMessage("§a配置已重载!"); + return true; + } + // back - 恢复背包备份 + if (sub.equals("back")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login back <玩家> [#编号]"); + return true; + } + String backTarget = args[1]; + if (args.length >= 3 + && args[2].startsWith("#")) { + // /sdf1_login back Aplayer #1 + int backupId; + try { + backupId = Integer.parseInt( + args[2].replace("#", "")); + } catch (NumberFormatException ex) { + sender.sendMessage( + "§c无效编号"); + return true; + } + String[] backup = + db.getInventoryBackupById( + backupId); + if (backup == null) { + sender.sendMessage( + "§c备份 #" + backupId + + " 不存在"); + return true; + } + // 如果玩家在线,直接恢复 + Player tp = Bukkit.getPlayer( + backTarget); + if (tp != null && tp.isOnline()) { + try { + tp.getInventory() + .setContents( + InventorySerializer + .fromBase64( + backup[0])); + if (backup[1] != null + && !backup[1] + .isEmpty()) + tp.getInventory() + .setArmorContents( + InventorySerializer + .fromBase64( + backup[1])); + if (backup[2] != null + && !backup[2] + .isEmpty()) + tp.getInventory() + .setExtraContents( + InventorySerializer + .fromBase64( + backup[2])); + int lv = Integer.parseInt( + backup[3]); + if (lv > 0) + tp.setLevel(lv); + tp.sendMessage( + "§a管理员已恢复您的背包"); + sender.sendMessage( + "§a已恢复 " + backTarget + + " 的背包 (备份#" + + backupId + ")"); + } catch (Exception e) { + sender.sendMessage( + "§c恢复失败: " + + e.getMessage()); + } + } else { + // 玩家离线:保存到数据库供下次登录恢复 + db.saveInventoryBackup( + backTarget, + backup[0], + backup[1], + backup[2], + Integer.parseInt( + backup[3]), + Double.parseDouble( + backup[4])); + sender.sendMessage( + "§a" + backTarget + + " 离线,备份已写入数据库," + + "下次登录时恢复"); + } + } else { + // /sdf1_login back Aplayer + // 罗列最近5条备份 + List> backups = + db.getInventoryBackups( + backTarget, 5); + if (backups.isEmpty()) { + sender.sendMessage( + "§c" + backTarget + + " 没有背包备份"); + return true; + } + sender.sendMessage( + "§e§l=== " + backTarget + + " 背包备份 ==="); + java.text.SimpleDateFormat sdf = + new java.text.SimpleDateFormat( + "MM-dd HH:mm:ss"); + for (Map b : + backups) { + int bid = ((Number) + b.get("id")) + .intValue(); + Object stObj = + b.get("save_time"); + long st = stObj != null + ? ((Number) stObj) + .longValue() : 0; + String time = st > 0 + ? sdf.format( + new java.util.Date(st)) + : "未知时间"; + String itemNames = + getBackupItemPreview( + bid); + sender.sendMessage( + "§7#" + bid + + " §f" + time + + " §7: " + + itemNames); + } + sender.sendMessage( + "§7使用 §e/sdf1_login back " + + backTarget + + " #编号 §7恢复"); + sender.sendMessage( + "§e§l================================"); + } + } + + // pw / password + if (sub.equals("pw") + || sub.equals("password")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + + if (args.length >= 3) { + // /sdf1_login pw 旧密码 新密码 + String oldPwd = args[1]; + String newPwd = args[2]; + if (!PasswordUtils.validate( + newPwd)) { + p2.sendMessage(config.msg( + "password_format_error")); + return true; + } + String salt = (String) db.getField( + p2.getName(), + "password_salt"); + String hash = + PasswordUtils.hash( + oldPwd, salt); + boolean mainOk = + db.checkPassword( + p2.getName(), hash); + boolean tempOk = + db.checkPasswordOrTemp( + p2.getName(), hash); + if (!mainOk && !tempOk) { + p2.sendMessage(config.msg( + "password_wrong")); + return true; + } + String oldHash = + (String) db.getField( + p2.getName(), + "password_hash"); + String newSalt = + PasswordUtils.generateSalt(); + String newHash = + PasswordUtils.hash( + newPwd, newSalt); + if (newHash.equals(oldHash)) { + p2.sendMessage(config.msg( + "password_same")); + return true; + } + recordPasswordChange( + p2.getName(), + oldHash, salt); + db.setField(p2.getName(), + "password_hash", newHash); + db.setField(p2.getName(), + "password_salt", newSalt); + if (!mainOk && tempOk) { + db.clearTempPassword( + p2.getName()); + } + frozenFlightPlayers.remove(p2.getName()); + p2.setAllowFlight(false); + p2.setFlying(false); + needsPasswordChange.remove( + p2.getName()); + p2.sendMessage(config.msg( + "password_changed")); + return true; + } + + if (args.length == 2) { + // /sdf1_login pw 旧密码 + // 验证旧密码,然后聊天输入新密码 + String oldPwd = args[1]; + String salt = (String) db.getField( + p2.getName(), + "password_salt"); + String hash = + PasswordUtils.hash( + oldPwd, salt); + boolean mainOk = + db.checkPassword( + p2.getName(), hash); + boolean tempOk = + db.checkPasswordOrTemp( + p2.getName(), hash); + if (!mainOk && !tempOk) { + p2.sendMessage(config.msg( + "password_wrong")); + return true; + } + chatInput.getState(p2).type = + ChatInputManager.InputType + .CHANGE_PWD_STEP2; + chatInput.getState(p2) + .ticketTitle = + mainOk ? "main" : "temp"; + p2.sendMessage( + "§e请输入新密码:"); + return true; + } + + // /sdf1_login pw (无参数) + chatInput.getState(p2).type = + ChatInputManager.InputType + .CHANGE_PWD_STEP1; + p2.sendMessage( + "§e请输入当前密码:"); + return true; + } + + // email + if (sub.equals("email")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + if (args.length >= 2) { + String emailAddr = args[1]; + if (!emailAddr.contains("@") + || !emailAddr.contains(".")) { + p2.sendMessage("§c邮箱格式不正确"); + return true; + } + String oldEmail = (String) db.getField( + p2.getName(), "email"); + if (oldEmail != null + && !oldEmail.isEmpty()) { + // 已绑定:先验证密码 + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = "need_pwd"; + p2.sendMessage("§e您已绑定邮箱: " + + oldEmail); + p2.sendMessage( + "§e请输入登录密码以确认修改:"); + } else { + // 未绑定:直接发验证码 + String code = String.valueOf( + (int) (Math.random() + * 900000 + 100000)); + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL_VERIFY; + chatInput.getState(p2) + .targetPlayer = emailAddr; + chatInput.getState(p2) + .ticketTitle = code; + final String to = emailAddr; + final String c = code; + final Player fp = p2; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> { + boolean sent = email + .sendVerifyCode(to, c); + Bukkit.getScheduler().runTask( + this, () -> { + if (sent) { + fp.sendMessage( + "§a验证码已发送到 " + + to); + fp.sendMessage( + "§e输入6位验证码(0取消):"); + } else { + fp.sendMessage( + "§c发送失败"); + } + }); + }); + } + } else { + chatInput.getState(p2).type = + ChatInputManager.InputType + .SET_EMAIL; + p2.sendMessage("§e请输入邮箱:"); + } + return true; + } + + + // sign + if (sub.equals("sign")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + if (dailySign((Player) sender)) + playSuccessSound((Player) sender); + else + ((Player) sender).sendMessage( + config.msg("checkin_already")); + return true; + } + + // reset + if (sub.equals("reset")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + loginMgr.handleReset( + (Player) sender); + return true; + } + + // undo + if (sub.equals("undo")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + Player p2 = (Player) sender; + PwdRollback rb = + pwdRollback.get(p2.getName()); + if (rb == null) { + p2.sendMessage("§c无可撤销记录"); + return true; + } + if (System.currentTimeMillis() - rb.time + > 600000L) { + pwdRollback.remove(p2.getName()); + p2.sendMessage("§c撤销已过期"); + return true; + } + db.setField(p2.getName(), + "password_hash", rb.hash); + db.setField(p2.getName(), + "password_salt", rb.salt); + pwdRollback.remove(p2.getName()); + p2.sendMessage("§a密码已撤销!"); + return true; + } + + // take + if (sub.equals("take")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 3) { + sender.sendMessage( + "§e用法: /sdf1_login take <玩家> <积分>"); + return true; + } + int amount; + try { + amount = Integer.parseInt(args[2]); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + return true; + } + if (!db.deductPoints(args[1], amount)) { + sender.sendMessage("§c积分不足"); + return true; + } + sender.sendMessage("§a已扣除 " + args[1] + + " " + amount + " 积分"); + return true; + } + + // set + if (sub.equals("set")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 3) { + sender.sendMessage( + "§e用法: /sdf1_login set <玩家> <临时密码>"); + return true; + } + String tgt = args[1]; + String tempPwd = args[2]; + if (!PasswordUtils.validate(tempPwd)) { + sender.sendMessage("§c密码格式不符合要求"); + return true; + } + if (!db.userExists(tgt)) { + sender.sendMessage("§c玩家不存在"); + return true; + } + String salt = (String) db.getField( + tgt, "password_salt"); + String hash = PasswordUtils.hash( + tempPwd, salt); + db.setField(tgt, "temp_password", hash); + db.setField(tgt, "temp_pw_expire", + System.currentTimeMillis() + + 300000L); + + db.setField(tgt, "temp_pw_used", 0); + sender.sendMessage("§a已为 " + tgt + + " 设置临时密码"); + Player tp = Bukkit.getPlayer(tgt); + if (tp != null && tp.isOnline()) + tp.kickPlayer( + "§c管理员已设置临时密码"); + // 邮件通知 + String emailAddr = (String) db.getField( + tgt, "email"); + if (emailAddr != null + && !emailAddr.isEmpty()) { + String time = + new java.text.SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss") + .format(new java.util.Date()); + String body = "玩家 " + tgt + + ":\n您的密码已于 " + time + + " 被管理员重置为临时密码。\n" + + "如果是本人操作请忽略。\n" + + "如非本人操作请立即联系管理员。"; + final String to = emailAddr; + final String fName = tgt; + final String bodyF = body; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> email + .sendTempPassword( + to, fName, + bodyF)); + } + return true; + } + // give - 手动发放雪球菜单 + if (sub.equals("give")) { + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login give <玩家名>"); + return true; + } + Player tp = Bukkit.getPlayer( + args[1]); + if (tp == null || !tp.isOnline()) { + sender.sendMessage( + "§c玩家不在线: " + + args[1]); + return true; + } + forceGiveMenuSnowball(tp); + tp.sendMessage( + "§a管理员已为您发放菜单雪球"); + sender.sendMessage( + "§a已发放雪球菜单给 " + + args[1]); + return true; + } + + // shopadd + if (sub.equals("shopadd")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 4) { + sender.sendMessage( + "§e用法: /sdf1_login shopadd <名称> <价格> <命令>"); + return true; + } + String shopName = args[1]; + int price; + try { + price = Integer.parseInt(args[2]); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + return true; + } + String shopCmd = args[3]; + points.addItem(shopName, price, shopCmd); + sender.sendMessage("§a已添加商品: " + + shopName + " (" + price + + "积分)"); + return true; + } + + // shopdel + if (sub.equals("shopdel")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login shopdel <序号>"); + return true; + } + try { + int idx = Integer.parseInt(args[1]); + points.removeItem(idx); + sender.sendMessage("§a已删除商品 #" + idx); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + } + return true; + } + + // get + if (sub.equals("get")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login get <玩家>"); + return true; + } + Map user = + db.getUser(args[1]); + if (user.isEmpty()) { + sender.sendMessage("§c玩家不存在"); + return true; + } + sender.sendMessage( + "§e=== " + args[1] + " ==="); + sender.sendMessage("§7积分: §e" + + user.getOrDefault("points", 0)); + sender.sendMessage("§7邮箱: §e" + + user.getOrDefault("email", "无")); + sender.sendMessage("§7签到天数: §e" + + user.getOrDefault( + "total_checkin_days", 0)); + sender.sendMessage("§7邀请码: §e" + + user.getOrDefault( + "invite_code", "无")); + return true; + } + + // del + if (sub.equals("del")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login del <玩家>"); + return true; + } + db.deleteUser(args[1]); + sender.sendMessage("§a已删除 " + args[1]); + Player tp = Bukkit.getPlayer(args[1]); + if (tp != null && tp.isOnline()) + tp.kickPlayer("§c账号已被管理员删除"); + return true; + } + + // kick (挂机) + if (sub.equals("kick")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 2) { + sender.sendMessage( + "§e用法: /sdf1_login kick <开关> [分钟]"); + return true; + } + String mode = args[1].toLowerCase(); + if (mode.equals("on") + || mode.equals("开") + || mode.equals("启用")) { + config.afkEnabled = true; + if (args.length >= 3) { + try { + config.afkTimeout = + Integer.parseInt( + args[2]) * 60; + } catch (NumberFormatException ignored) { + } + } + config.saveSettings(); + int minutes = + config.afkTimeout / 60; + sender.sendMessage( + "§a挂机踢出已开启,超时: " + + minutes + " 分钟"); + } else if (mode.equals("off") + || mode.equals("关") + || mode.equals("停用")) { + config.afkEnabled = false; + config.saveSettings(); + sender.sendMessage( + "§c挂机踢出已关闭"); + } else { + sender.sendMessage( + "§c无效参数 on/off"); + } + return true; + } + + // add + if (sub.equals("add")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + if (args.length < 3) { + sender.sendMessage( + "§e用法: /sdf1_login add <玩家> <积分>"); + return true; + } + int amount; + try { + amount = Integer.parseInt(args[2]); + } catch (NumberFormatException ex) { + sender.sendMessage("§c无效数字"); + return true; + } + db.addPoints(args[1], amount); + sender.sendMessage("§a已为 " + args[1] + + " 增加 " + amount + " 积分"); + return true; + } + + // ticket + if (sub.equals("ticket") + || sub.equals("工单")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + ticket.openMain((Player) sender); + return true; + } + + sender.sendMessage("§c未知参数"); + return true; + } + /** + * 是否是垃圾站展示物品(带ID lore) + */ + private boolean isTrashDisplayItem( + ItemStack item) { + if (item == null + || item.getType() + == Material.AIR) + return false; + if (!item.hasItemMeta()) + return false; + ItemMeta im = item.getItemMeta(); + if (im == null || !im.hasLore()) + return false; + List lore = im.getLore(); + if (lore == null || lore.isEmpty()) + return false; + return lore.get(0) + .startsWith("§7ID: #"); + } + + private void sendChatHelp(Player p) { + p.sendMessage( + "§e§l======== 聊天过滤帮助 =========="); + p.sendMessage("§e/chat reload §7- 重载配置"); + p.sendMessage("§e/chat add §7- 添加白名单URL"); + p.sendMessage("§e/chat remove §7- 移除白名单URL"); + p.sendMessage("§e/chat addplayer <玩家> §7- 添加白名单玩家"); + p.sendMessage("§e/chat takeplayer <玩家> §7- 移除白名单玩家"); + p.sendMessage("§e/chat unmute <玩家> §7- 解禁玩家"); + p.sendMessage("§e/chat reset <玩家> §7- 重置违规记录"); + p.sendMessage( + "§e§l=================================="); + } + private int findEmptySlot(Inventory inv) { + for (int i = 0; i < 49; i++) { + ItemStack item = inv.getItem(i); + if (item == null + || item.getType() + == Material.AIR) + return i; + } + return -1; + } + + // ===== TabComplete ===== + @Override + public List onTabComplete( + CommandSender sender, Command cmd, + String label, String[] args) { + List list = new ArrayList<>(); + if (cmd.getName().equalsIgnoreCase( + "sdf1_login") + && args.length == 1) { + if (isAdmin(sender)) { + list.addAll(Arrays.asList( + "reload", "pw", "email", + "sign", "reset", "undo", + "get", "del", "set", + "take", "add", "kick", + "ticket", "oa", "stop", + "shopadd", "shopdel","back","update")); + } else { + list.addAll(Arrays.asList( + "pw", "email", "sign", + "reset", "undo", + "ticket", "oa", "stop")); + } + } + if (cmd.getName().equalsIgnoreCase("chat") + && args.length == 1 + && isAdmin(sender)) { + list.addAll(Arrays.asList( + "reload", "add", "remove", + "addplayer", "takeplayer", + "unmute", "reset")); + } + return list; + } + +} \ No newline at end of file diff --git a/txt/PasswordUtils.txt b/txt/PasswordUtils.txt new file mode 100644 index 0000000..4029ed4 --- /dev/null +++ b/txt/PasswordUtils.txt @@ -0,0 +1,72 @@ +package Sdf1_login; + +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; + +public class PasswordUtils { + + private static final SecureRandom RANDOM = + new SecureRandom(); + + public static String generateSalt() { + byte[] salt = new byte[16]; + RANDOM.nextBytes(salt); + return Base64.getEncoder().encodeToString(salt); + } + + public static String hash(String password, String salt) { + try { + MessageDigest md = + MessageDigest.getInstance("SHA-256"); + md.update(Base64.getDecoder().decode(salt)); + byte[] hash = + md.digest(password.getBytes("UTF-8")); + return Base64.getEncoder().encodeToString(hash); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static boolean validate(String password) { + if (password == null) return false; + if (password.length() < 6 || password.length() > 20) + return false; + boolean hasUpper = false; + boolean hasLower = false; + boolean hasDigit = false; + for (char c : password.toCharArray()) { + if (Character.isUpperCase(c)) hasUpper = true; + if (Character.isLowerCase(c)) hasLower = true; + if (Character.isDigit(c)) hasDigit = true; + } + return hasUpper && hasLower && hasDigit; + } + + public static String generateTempPassword() { + String chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder sb = new StringBuilder(8); + for (int i = 0; i < 8; i++) { + sb.append(chars.charAt( + RANDOM.nextInt(chars.length()))); + } + return sb.toString(); + } + + public static String generateInviteCode() { + String chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + StringBuilder sb = new StringBuilder(6); + for (int i = 0; i < 6; i++) { + sb.append(chars.charAt( + RANDOM.nextInt(chars.length()))); + } + return sb.toString(); + } + + public static String generateVerifyCode() { + int code = 100000 + RANDOM.nextInt(900000); + return String.valueOf(code); + } +} diff --git a/txt/PointsManager.txt b/txt/PointsManager.txt new file mode 100644 index 0000000..07e865e --- /dev/null +++ b/txt/PointsManager.txt @@ -0,0 +1,328 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.lang.reflect.Method; +import java.util.*; + +public class PointsManager { + + private final Main plugin; + private Object cyInstance = null; + private Method cyActivationMethod = null; + private Method cyGetShopItemsMethod = null; + + private final List> cyItems = + new ArrayList<>(); + + public PointsManager(Main plugin) { + this.plugin = plugin; + discoverCY(); + } + + private void discoverCY() { + try { + org.bukkit.plugin.Plugin p = Bukkit + .getPluginManager() + .getPlugin("CY_beibao"); + if (p != null && p.isEnabled()) { + cyInstance = p; + cyGetShopItemsMethod = p.getClass() + .getMethod("onSdf1GetShopItems"); + cyActivationMethod = p.getClass() + .getMethod("onSdf1Activation", + String.class, + int.class, int.class); + plugin.getLogger().info( + "[Sdf1_login] CY_beibao 联控已连接"); + } + } catch (Exception ignored) {} + } + + // ===== CY 商品拉取 ===== + + private void refreshCYItems() { + cyItems.clear(); + if (cyInstance == null + || cyGetShopItemsMethod == null) return; + try { + Object result = + cyGetShopItemsMethod.invoke(cyInstance); + if (result == null) return; + String data = result.toString(); + if (data.isEmpty()) return; + String[] items = data.split(";"); + for (String itemStr : items) { + String[] p = itemStr.split("\\|"); + if (p.length < 8) continue; + Map item = + new LinkedHashMap<>(); + item.put("id", p[0]); + item.put("name", p[1]); + item.put("days", + Integer.parseInt(p[2])); + item.put("slots", + Integer.parseInt(p[3])); + item.put("price", + Integer.parseInt(p[4])); + item.put("stock", + Integer.parseInt(p[5])); + item.put("icon", p[6]); + item.put("lifetime", + "1".equals(p[7])); + cyItems.add(item); + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 拉取CY商品失败: " + + e.getMessage()); + } + } + + private void activateCY(String name, + int slots, int days) { + try { + if (cyInstance == null + || cyActivationMethod == null) return; + cyActivationMethod.invoke( + cyInstance, name, slots, days); + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] CY激活失败: " + + e.getMessage()); + } + } + + // ===== 积分查询 ===== + + public int getPoints(String name) { + return ((Number) plugin.getDb() + .getField(name, "points")).intValue(); + } + + // ===== 主面板 ===== + + public Inventory createShopGUI(Player p) { + int points = getPoints(p.getName()); + Inventory inv = Bukkit.createInventory( + null, 54, + "§d§l积分商城 | 当前积分: " + points); + fillBg(inv); + inv.setItem(10, mkItem(Material.EMERALD, + "§a游戏币充值", + "§7消耗积分兑换游戏币", + "§7100积分 = $1000", + "", + "§e点击兑换")); + inv.setItem(12, mkItem(Material.ENDER_CHEST, + "§b背包空间", + "§7消耗积分兑换背包格子", + "§750积分 = 9格永久空间", + "", + "§e点击兑换")); + inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE, + "§e经验瓶", + "§7消耗积分兑换经验", + "§720积分 = 10级经验", + "", + "§e点击兑换")); + if (cyInstance != null) { + inv.setItem(31, mkItem(Material.CHEST, + "§6背包商城", + "§7来自CY_beibao的商品", + "", + "§e点击查看")); + } + inv.setItem(49, mkItem(Material.ARROW, "§7返回")); + return inv; + } + + // ===== CY 商品面板 ===== + + public Inventory createCYShopGUI(Player p) { + refreshCYItems(); + int points = getPoints(p.getName()); + int size = Math.max(54, + ((cyItems.size() / 9) + 1) * 9); + if (size > 54) size = 54; + Inventory inv = Bukkit.createInventory( + null, size, + "§d§lCY背包商城 | 当前积分: " + points); + fillBg(inv); + for (int i = 0; i < cyItems.size() + && i < 45; i++) { + Map item = cyItems.get(i); + String name = (String) item.get("name"); + int price = ((Number) item.get("price")) + .intValue(); + int stock = ((Number) item.get("stock")) + .intValue(); + int days = ((Number) item.get("days")) + .intValue(); + int slots = ((Number) item.get("slots")) + .intValue(); + boolean lifetime = (boolean) item + .get("lifetime"); + + List lore = new ArrayList<>(); + lore.add("§7价格: §e" + price + " 积分"); + if (lifetime) { + lore.add("§7格子: §e终身 +" + slots); + } else { + lore.add("§7格子: §e+" + slots); + lore.add("§7时间: §e" + days + "天"); + } + if (stock == 0) { + lore.add("§7库存: §a无限"); + } else if (stock == -1) { + lore.add("§7库存: §c已下架"); + } else { + lore.add("§7库存: §e" + stock); + } + lore.add(""); + if (points >= price && stock != -1) { + lore.add("§e点击购买"); + } else if (stock == -1) { + lore.add("§c已下架"); + } else { + lore.add("§c积分不足"); + } + inv.setItem(i, mkItem(Material.CHEST, + "§a" + name, + lore.toArray(new String[0]))); + } + inv.setItem(size - 1, + mkItem(Material.ARROW, "§7返回")); + return inv; + } + + // ===== 主面板点击 ===== + + public void handleClick(Player p, int slot) { + int points = getPoints(p.getName()); + switch (slot) { + case 10: + if (points >= 100) { + plugin.getDb().deductPoints( + p.getName(), 100); + if (plugin.getEconomy() != null) { + plugin.getEconomy() + .depositPlayer(p, 1000); + } + p.sendMessage(plugin.getConfig2().msg( + "points_purchase_success", + "count", "100")); + } else { + p.sendMessage(plugin.getConfig2().msg( + "points_insufficient", + "points", + String.valueOf(points))); + } + break; + case 12: + if (points >= 50) { + plugin.getDb().deductPoints( + p.getName(), 50); + activateCY(p.getName(), 9, 0); + p.sendMessage(plugin.getConfig2().msg( + "points_purchase_success", + "count", "50")); + } else { + p.sendMessage(plugin.getConfig2().msg( + "points_insufficient", + "points", + String.valueOf(points))); + } + break; + case 14: + if (points >= 20) { + plugin.getDb().deductPoints( + p.getName(), 20); + p.setLevel(p.getLevel() + 10); + p.sendMessage(plugin.getConfig2().msg( + "points_purchase_success", + "count", "20")); + } else { + p.sendMessage(plugin.getConfig2().msg( + "points_insufficient", + "points", + String.valueOf(points))); + } + break; + case 31: + if (cyInstance != null) { + p.openInventory( + createCYShopGUI(p)); + } + break; + } + } + + // ===== CY 商品面板点击 ===== + + public void handleCYClick(Player p, int slot) { + if (slot < 0 || slot >= cyItems.size() + || slot >= 45) return; + Map item = cyItems.get(slot); + int price = ((Number) item.get("price")) + .intValue(); + int stock = ((Number) item.get("stock")) + .intValue(); + int days = ((Number) item.get("days")) + .intValue(); + int slots = ((Number) item.get("slots")) + .intValue(); + boolean lifetime = (boolean) item + .get("lifetime"); + + if (stock == -1) { + p.sendMessage( + "§c[Sdf1_login] §f该商品已下架"); + return; + } + int points = getPoints(p.getName()); + if (points < price) { + p.sendMessage(plugin.getConfig2().msg( + "points_insufficient", + "points", + String.valueOf(points))); + return; + } + + plugin.getDb().deductPoints(p.getName(), price); + int actDays = lifetime ? 0 : days; + activateCY(p.getName(), slots, actDays); + + String tl = lifetime + ? "终身" : (days + "天"); + p.sendMessage("§a[Sdf1_login] §f购买成功!获得 " + + tl + " +" + slots + "格 背包空间"); + } + + // ===== 工具 ===== + + private void fillBg(Inventory inv) { + ItemStack gl = mkItem( + Material.GRAY_STAINED_GLASS_PANE, " "); + for (int i = 0; i < inv.getSize(); i++) + inv.setItem(i, gl); + } + + private ItemStack mkItem(Material mat, String name, + String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) + im.setLore(Arrays.asList(lore)); + it.setItemMeta(im); + } + return it; + } +} diff --git a/txt/QuestTracker.txt b/txt/QuestTracker.txt new file mode 100644 index 0000000..b6c9147 --- /dev/null +++ b/txt/QuestTracker.txt @@ -0,0 +1,2493 @@ +package Sdf1_login; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + + +public class QuestTracker { + + private final Plugin plugin; + private final PlayerStorage storage; + private final Map + playerBiome = new HashMap<>(); + private final Map> + questCache = new HashMap<>(); + private final List + checkInRecords = new ArrayList<>(); + private final Map + lastCheckInPos = new HashMap<>(); + + public void recordCheckIn( + String player, + String biomeKey, + String category, + String worldName, + double x, double y, double z) { + String record = player + + "|" + worldName + + "|" + biomeKey + + "|" + category + + "|" + String.format( + "%.1f", x) + + "|" + String.format( + "%.1f", y) + + "|" + String.format( + "%.1f", z) + + "|" + System.currentTimeMillis(); + checkInRecords.add(record); + plugin.getLogger().info( + "[QuestTracker] 打卡记录: " + + record); + } + + public List getCheckInRecords() { + return new ArrayList<>(checkInRecords); + } + + public List getPlayerRecords( + String player) { + List list = new ArrayList<>(); + for (String r : checkInRecords) { + if (r.startsWith( + player.toLowerCase() + + "|")) { + list.add(r); + } + } + return list; + } + + + public QuestTracker(Plugin plugin) { + this.plugin = plugin; + this.storage = new PlayerStorage( + plugin.getDataFolder(), + plugin.getLogger()); + loadAllQuests(); + } + + public void debugPlayer(String player) { + plugin.getLogger().info( + "[QuestTracker] === " + + player + " 任务数据 ==="); + } + public void shutdown() { + if (storage != null) { + storage.close(); + } + } + + /** + * 玩家移动时记录当前群系 + */ + public void onPlayerMove( + String player, + String biomeName) { + playerBiome.put(player, biomeName); + } + /** + * 标记玩家已打卡该群系 + */ + // ========== 群系分类映射表 ========== + private static final Map + BIOME_CATEGORY = new HashMap<>(); + static { + // 海洋群系 + BIOME_CATEGORY.put("minecraft:ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_warm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_lukewarm_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_cold_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:frozen_ocean", "海洋"); + BIOME_CATEGORY.put("minecraft:deep_frozen_ocean", "海洋"); + // 平原群系 + BIOME_CATEGORY.put("minecraft:plains", "平原"); + BIOME_CATEGORY.put("minecraft:sunflower_plains", "平原"); + BIOME_CATEGORY.put("minecraft:meadow", "平原"); + // 森林群系 + BIOME_CATEGORY.put("minecraft:forest", "森林"); + BIOME_CATEGORY.put("minecraft:flower_forest", "森林"); + BIOME_CATEGORY.put("minecraft:birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_birch_forest", "森林"); + BIOME_CATEGORY.put("minecraft:dark_forest", "森林"); + BIOME_CATEGORY.put("minecraft:taiga", "森林"); + BIOME_CATEGORY.put("minecraft:snowy_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_pine_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:old_growth_spruce_taiga", "森林"); + BIOME_CATEGORY.put("minecraft:cherry_grove", "森林"); + BIOME_CATEGORY.put("minecraft:jungle", "森林"); + BIOME_CATEGORY.put("minecraft:sparse_jungle", "森林"); + BIOME_CATEGORY.put("minecraft:bamboo_jungle", "森林"); + // 沙漠群系 + BIOME_CATEGORY.put("minecraft:desert", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna", "沙漠"); + BIOME_CATEGORY.put("minecraft:savanna_plateau", "沙漠"); + BIOME_CATEGORY.put("minecraft:beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:snowy_beach", "沙漠"); + BIOME_CATEGORY.put("minecraft:windswept_savanna", "沙漠"); + // 恶地群系 + BIOME_CATEGORY.put("minecraft:badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:wooded_badlands", "恶地"); + BIOME_CATEGORY.put("minecraft:eroded_badlands", "恶地"); + // 雪原群系 + BIOME_CATEGORY.put("minecraft:snowy_plains", "雪原"); + BIOME_CATEGORY.put("minecraft:grove", "雪原"); + BIOME_CATEGORY.put("minecraft:ice_spikes", "雪原"); + BIOME_CATEGORY.put("minecraft:jagged_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_river", "雪原"); + BIOME_CATEGORY.put("minecraft:frozen_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:stony_peaks", "雪原"); + BIOME_CATEGORY.put("minecraft:snowy_slopes", "雪原"); + // 沙砾群系 + BIOME_CATEGORY.put("minecraft:windswept_hills", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_forest", "沙砾"); + BIOME_CATEGORY.put("minecraft:windswept_gravelly_hills", "沙砾"); + // 沼泽群系 + BIOME_CATEGORY.put("minecraft:swamp", "沼泽"); + BIOME_CATEGORY.put("minecraft:mangrove_swamp", "沼泽"); + // 河流 + BIOME_CATEGORY.put("minecraft:river", "河流"); + // 蘑菇岛 + BIOME_CATEGORY.put("minecraft:mushroom_fields", "蘑菇岛"); + // 地下群系 + BIOME_CATEGORY.put("minecraft:dripstone_caves", "地下"); + BIOME_CATEGORY.put("minecraft:lush_caves", "地下"); + BIOME_CATEGORY.put("minecraft:deep_dark", "地下"); + // 下界群系 + BIOME_CATEGORY.put("minecraft:nether_wastes", "下界"); + BIOME_CATEGORY.put("minecraft:crimson_forest", "下界"); + BIOME_CATEGORY.put("minecraft:warped_forest", "下界"); + BIOME_CATEGORY.put("minecraft:soul_sand_valley", "下界"); + BIOME_CATEGORY.put("minecraft:basalt_deltas", "下界"); + // 末地群系 + BIOME_CATEGORY.put("minecraft:the_end", "末地"); + BIOME_CATEGORY.put("minecraft:the_void", "末地"); + BIOME_CATEGORY.put("minecraft:end_highlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_midlands", "末地"); + BIOME_CATEGORY.put("minecraft:end_barrens", "末地"); + } + + // 所有群系大类列表 + private static final String[] ALL_CATEGORIES = + {"海洋", "平原", "森林", "沙漠", + "恶地", "雪原", "沙砾", "沼泽", + "河流", "蘑菇岛", "地下", + "下界", "末地"}; + + // 所有群系分类的条件关键词映射 + private static final Map + CATEGORY_KEYWORDS = new HashMap<>(); + static { + CATEGORY_KEYWORDS.put("海洋", "海洋"); + CATEGORY_KEYWORDS.put("平原", "平原"); + CATEGORY_KEYWORDS.put("森林", "森林"); + CATEGORY_KEYWORDS.put("沙漠", "沙漠"); + CATEGORY_KEYWORDS.put("恶地", "恶地"); + CATEGORY_KEYWORDS.put("雪原", "雪原"); + CATEGORY_KEYWORDS.put("沙砾", "沙砾"); + CATEGORY_KEYWORDS.put("沼泽", "沼泽"); + CATEGORY_KEYWORDS.put("河流", "河流"); + CATEGORY_KEYWORDS.put("蘑菇岛", "蘑菇岛"); + CATEGORY_KEYWORDS.put("地下", "地下"); + CATEGORY_KEYWORDS.put("下界", "下界"); + CATEGORY_KEYWORDS.put("末地", "末地"); + // 条件文本中的别名映射 + CATEGORY_KEYWORDS.put("地狱", "下界"); + CATEGORY_KEYWORDS.put("下届", "下界"); + CATEGORY_KEYWORDS.put("主岛", "末地"); + } + + /** + * 用世界维度名分类(最可靠) + * 世界名含 the_nether → 下界 + * 世界名含 the_end → 末地 + * 其余 → 主世界 + */ + public String mapBiomeCategory( + String rawKey, String worldName) { + // 下界维度优先用世界名 + if (worldName != null) { + String w = worldName.toLowerCase(); + if (w.contains("nether")) + return "下界"; + if (w.contains("end")) + return "末地"; + } + // 用群系映射表 + if (rawKey != null) { + String cat = + BIOME_CATEGORY.get( + rawKey.toLowerCase()); + if (cat != null) return cat; + } + return "主世界"; + } + + +// ========== 解析为区块 ========== + + private List
parseIntoSections( + List lines) { + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + return parseHashSections(lines); + } else if (hasDash) { + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + continue; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseHashSections(converted); + } + return new ArrayList<>(); + } + + private List
parseHashSections( + List lines) { + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add(new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } else if (t.isEmpty()) { + // skip + } else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty() + && curTitle != null) { + curLines.add(cond); + } + } + } + if (curTitle != null) { + sections.add(new Section( + curTitle, curLines)); + } + return sections; + } + + private List
buildFlatSections( + List lines, + String category) { + List conditions = + new ArrayList<>(); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + conditions.add(cond); + } + } + if (conditions.isEmpty()) + return new ArrayList<>(); + List
list = + new ArrayList<>(); + list.add(new Section( + "第一阶段", conditions)); + return list; + } + + // ========== 文件加载入口 ========== + + private void loadAllQuests() { + loadCategory("新人任务"); + loadCategory("主线任务"); + loadCategory("支线任务"); + } + + private void loadCategory(String name) { + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 扫描中..."); + File folder = new File( + plugin.getDataFolder(), + name); + String path = "plugins/Sdf1_login/" + + name + "/"; + + if (!folder.exists()) { + folder.mkdirs(); + plugin.getLogger().warning( + "[QuestTracker] 已创建目录: " + + path); + questCache.put(name, + new ArrayList<>()); + return; + } + + File[] mdFiles = folder.listFiles( + (d, n) -> n.endsWith(".md")); + + if (mdFiles == null + || mdFiles.length == 0) { + plugin.getLogger().warning( + "[QuestTracker] " + path + + " 下无 .md 文件"); + questCache.put(name, + new ArrayList<>()); + return; + } + Arrays.sort(mdFiles); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 发现 " + mdFiles.length + + " 个文件,开始扫描..."); + + // 合并所有文件的阶段 + TreeMap merged = + new TreeMap<>(); + int fileCount = 0; + + for (int fi = 0; + fi < mdFiles.length; fi++) { + File file = mdFiles[fi]; + List lines = + readFileLines(file); + + if (lines.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 空文件,跳过"); + continue; + } + + List
sections = + parseIntoSections(lines); + + if (sections.isEmpty()) { + sections = buildFlatSections( + lines, name); + } + + if (sections.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + + file.getName() + + ": 无法识别,跳过"); + continue; + } + + // 每个文件独立处理阶段号 + int defaultNum = fi + 1; + buildSingleFileStages( + sections, name, + merged, defaultNum); + + fileCount++; + } + + if (merged.isEmpty()) { + plugin.getLogger().warning( + "[QuestTracker] " + name + + ": 无有效任务数据"); + questCache.put(name, + new ArrayList<>()); + return; + } + + // 日志 + for (Map.Entry en + : merged.entrySet()) { + int num = en.getKey() >= 1000 + ? en.getKey() - 999 + : en.getKey(); + plugin.getLogger().info( + "[QuestTracker] " + name + + " 阶段" + num + + " \"" + + en.getValue().displayName + + "\" → 条件" + + en.getValue() + .conditions.size() + + "项, 详情" + + en.getValue() + .details.size() + + "项, 奖励" + + en.getValue() + .rewards.size() + + "项"); + } + + List list = + new ArrayList<>( + merged.values()); + questCache.put(name, list); + + plugin.getLogger().info( + "[QuestTracker] " + name + + ": 解析完成 → " + + list.size() + " 个阶段" + + " (来自 " + fileCount + + " 个文件)"); + } + + private Integer extractStageNumber( + String title) { + if (title == null) return null; + String t = title.toLowerCase().trim(); + + // 含阶段/stage/关/chapter 关键词 + boolean isStage = + t.contains("阶段") + || t.contains("stage") + || t.contains("关卡") + || t.contains("章节") + || t.contains("chapter"); + + if (isStage) { + String digits = title + .replaceAll("[^0-9]", ""); + if (!digits.isEmpty()) { + try { + return Integer.parseInt(digits); + } catch (NumberFormatException e) { + // fall through + } + } + return parseChineseNumber(title); + } + + // 无关键词但以数字开头(如 "1. 挖掘") + String trimmed = t + .replaceAll("^[.、\\s]+", "") + .trim(); + if (trimmed.length() > 0 + && Character.isDigit( + trimmed.charAt(0))) { + String num = trimmed + .replaceAll("[^0-9]", ""); + if (!num.isEmpty()) { + try { + return Integer.parseInt(num); + } catch (NumberFormatException e) { + // fall through + } + } + } + + return null; + } +// ========== 领取记录 ========== + + /** + * 标记已领取某阶段奖励 + */ + public void markClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return; + // 用阶段编号代替显示名,保证稳定 + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + storage.setCondition(player, key); + } + + public boolean hasClaimed( + String player, QuestFile qf) { + if (player == null || qf == null) + return false; + int num = getStageNumber(qf); + String key = "claimed_" + + qf.fileName + "_" + num; + return toBool( + storage.getCondition( + player, key)); + } + + private int parseChineseNumber( + String title) { + String[] cn = {"一", "二", "三", + "四", "五", "六", "七", + "八", "九", "十"}; + if (title.contains("十")) { + for (int j = 0; j < 9; j++) { + if (title.contains(cn[j])) { + return 10 + j + 1; + } + } + return 10; + } + for (int i = 0; i < 9; i++) { + if (title.contains(cn[i])) { + return i + 1; + } + } + return 0; + } + + // ========== md 解析核心 ========== + + /** + * 读取文件所有行 + */ + private List readFileLines( + File file) { + List lines = + new ArrayList<>(); + try { + BufferedReader br = + new BufferedReader( + new FileReader(file)); + String line; + while ((line = br.readLine()) + != null) { + lines.add(line); + } + br.close(); + } catch (Exception e) { + plugin.getLogger().warning( + "[QuestTracker] 读取 " + + file.getName() + + " 失败: " + + e.getMessage()); + } + return lines; + } + + /** + * 自动检测格式并解析单个文件 + */ + private List parseMdFile( + File file, String category) { + List lines = + readFileLines(file); + if (lines.isEmpty()) + return new ArrayList<>(); + + // 检测分隔符 + boolean hasHash = false; + boolean hasDash = false; + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("#")) + hasHash = true; + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) + hasDash = true; + } + + if (hasHash) { + // # 分隔格式 + return parseSections( + lines, category); + } else if (hasDash) { + // ---- 分隔格式:转换为 # 后复用 + List converted = + new ArrayList<>(); + for (String l : lines) { + String t = l.trim(); + if (t.startsWith("----") + || t.startsWith("====") + || t.startsWith("****")) { + String title = t + .replaceAll( + "^[-=*]+\\s*", + "") + .replaceAll( + "\\s*[-=*]+$", + "") + .trim(); + if (title.isEmpty()) + title = "separator"; + converted.add("# " + title); + } else { + converted.add(l); + } + } + return parseSections( + converted, category); + } else { + // 无分隔符:整文件一个阶段 + return parseFlat(lines, category); + } + } + + /** + * # 分隔格式 → 区块 → 阶段 + */ + private List parseSections( + List lines, + String category) { + // 拆分为区块 + List
sections = + new ArrayList<>(); + String curTitle = null; + List curLines = + new ArrayList<>(); + + for (String raw : lines) { + String t = raw.trim(); + + // # 开头 → 新区块 + if (t.startsWith("#")) { + if (curTitle != null) { + sections.add( + new Section( + curTitle, + new ArrayList<>( + curLines))); + } + curTitle = t.replaceFirst( + "^#+\\s*", "").trim(); + curLines = new ArrayList<>(); + } + // 空行/分隔线 → 跳过 + else if (t.isEmpty() + || t.startsWith("----") + || t.startsWith("====") + || t.startsWith("***")) { + // skip + } + // 普通行 → 区块内容 + else { + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + curLines.add(cond); + } + } + } + // 保存最后一个区块 + if (curTitle != null) { + sections.add( + new Section( + curTitle, curLines)); + } + + // 直接转为 QuestFile 列表 + List result = + new ArrayList<>(); + int num = 1; + for (Section sec : sections) { + QuestFile qf = new QuestFile(); + qf.displayName = + extractDisplayName( + sec.title); + qf.fileName = category; + qf.conditions = + new ArrayList<>(sec.lines); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + result.add(qf); + num++; + } + return result; + } + + /** + * 无分隔符:整文件 = 一个阶段 + */ + private List parseFlat( + List lines, + String category) { + List list = + new ArrayList<>(); + QuestFile qf = makeDefaultStage( + category); + for (String raw : lines) { + String t = raw.trim(); + if (t.isEmpty()) continue; + String cond = t.replaceFirst( + "^-\\s+", "").trim(); + if (!cond.isEmpty()) { + qf.conditions.add(cond); + } + } + if (!qf.conditions.isEmpty()) { + list.add(qf); + } + return list; + } +// ========== Vault 经济奖励发放 ========== + + /** + * 发放阶段奖励 + * 含经济内容的走 Vault API + */ + public void claimRewards( + Player player, + QuestFile qf) { + if (qf == null + || qf.rewards == null + || qf.rewards.isEmpty()) { + player.sendMessage( + "§c无可领取的奖励"); + return; + } + + net.milkbowl.vault.economy.Economy econ = + ((Main) plugin).getEconomy(); + + int sentCount = 0; + + for (String reward : qf.rewards) { + String rt = reward.trim(); + if (rt.isEmpty()) continue; + + // 检查可选奖励标记 + if (hasOptionalMarker(rt)) { + if (!areAllConditionsMet(player.getName(), qf)) + player.sendMessage( + "§7[可选奖励] " + + "需完成所有可选条件: " + + rt); + continue; + } + + + // 按 + 分割子项 + String[] parts = + rt.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 尝试发经济 + double amt = + extractEconomyAmount(part); + if (amt > 0 && econ != null) { + double before = + econ.getBalance(player); + econ.depositPlayer( + player, amt); + player.sendMessage( + "§a§l[任务奖励] §f" + + part + + " §7(余额: §e" + + String.format( + "%.2f", before) + + " §7→ §a" + + String.format( + "%.2f", + econ.getBalance( + player)) + + "§7)"); + // 通知提成系统 + if (plugin instanceof Main) { + ((Main) plugin) + .getCommission() + .payCommission( + player + .getName(), + amt); + } + + sentCount++; + continue; + } + + // 尝试发物品 + java.util.List items = + parseRewardItems(part); + if (!items.isEmpty()) { + for (ItemStack is : items) { + player.getInventory() + .addItem(is); + } + player.sendMessage( + "§a§l[任务奖励] §f" + + part); + sentCount++; + continue; + } + + // 非经济非物品 → 仅消息 + player.sendMessage( + "§a§l[任务奖励] §f" + + part); + sentCount++; + } + } + + plugin.getLogger().info( + "[QuestTracker] " + + player.getName() + + " 领取了 \"" + + qf.displayName + + "\" 共 " + sentCount + + " 项奖励"); + } + /** + * 检查奖励文本是否含可选标记 + */ + private boolean hasOptionalMarker( + String text) { + return text.contains("(可选)") + || text.contains("(进阶)") + || text.contains("【可选】") + || text.contains("【进阶】"); + } + + /** + * 检查阶段所有条件(含可选)是否全部满足 + * 用于决定可选奖励是否发放 + */ + private boolean areAllConditionsMet( + String player, QuestFile qf) { + if (qf.conditions == null + || qf.conditions.isEmpty()) + return true; + for (String cond : qf.conditions) { + if (!isConditionMet(player, cond)) + return false; + } + return true; + } + + /** + * 从奖励文本中提取经济金额 + * 支持: "500游戏币" "500" "金币500" + * "$500" "1000-5000"(随机) "1000~5000" + */ + private double extractEconomyAmount( + String text) { + String t = text.replaceAll(" ", ""); + + // 必须包含经济关键词才算经济奖励 + boolean hasEcon = + t.contains("游戏币") + || t.contains("金币") + || t.contains("积分") + || t.contains("块钱") + || t.contains("元") + || t.contains("$") + || t.contains("经济") + || t.contains("余额") + || t.contains("发钱"); + + if (!hasEcon) return 0; + + // 范围: "1000-5000" 或 "1000~5000" + Matcher rm = Pattern + .compile("(\\d+)[-~](\\d+)") + .matcher(t); + if (rm.find()) { + double min = Double.parseDouble( + rm.group(1)); + double max = Double.parseDouble( + rm.group(2)); + return min + Math.random() + * (max - min); + } + + // 纯数字(含 $ 前缀) + Matcher nm = Pattern + .compile("\\$?(\\d+(\\.\\d+)?)") + .matcher(t); + if (nm.find()) { + return Double.parseDouble( + nm.group(1)); + } + + return 0; + } + /** + * 解析奖励物品文本 + * "皮革套装*1" → 4件皮革装备 + * "木剑*1" → 1把木剑 + * "钻石*64" → 64颗钻石 + */ + private List parseRewardItems( + String text) { + List result = + new ArrayList<>(); + + // 去掉条件前缀 + // 如 "完成注册登录:皮革套装*1" + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + if (text.contains(":")) { + text = text.substring( + text.indexOf(":") + 1); + } + + // 按 + 分割多个物品 + String[] parts = + text.split("[+,]"); + for (String part : parts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // 跳过经济文本 + if (extractEconomyAmount(part) > 0) + continue; + + int amount = 1; + String itemName = part; + + // 解析 *N 数量 + if (part.contains("*")) { + String[] sp = + part.split("\\*"); + itemName = sp[0].trim(); + try { + amount = Integer.parseInt( + sp[1].trim()); + } catch (Exception e) { + amount = 1; + } + } + + List mats = + matchMaterials(itemName); + for (Material m : mats) { + int give = m.name() + .contains("HELMET") + || m.name() + .contains("CHESTPLATE") + || m.name() + .contains("LEGGINGS") + || m.name() + .contains("BOOTS") + ? 1 : amount; + result.add( + new ItemStack(m, give)); + } + } + return result; + } + + /** + * 中文物品名 → Material 列表 + * 套装返回多个部件 + */ + private List matchMaterials( + String name) { + List list = + new ArrayList<>(); + String n = name.toLowerCase().trim(); + + // 套装 + if (n.contains("皮革套装") + || n.contains("皮套装")) { + list.add(Material.LEATHER_HELMET); + list.add(Material.LEATHER_CHESTPLATE); + list.add(Material.LEATHER_LEGGINGS); + list.add(Material.LEATHER_BOOTS); + return list; + } + if (n.contains("铁套装") + || n.contains("铁甲")) { + list.add(Material.IRON_HELMET); + list.add(Material.IRON_CHESTPLATE); + list.add(Material.IRON_LEGGINGS); + list.add(Material.IRON_BOOTS); + return list; + } + if (n.contains("钻石套装") + || n.contains("钻甲")) { + list.add(Material.DIAMOND_HELMET); + list.add(Material.DIAMOND_CHESTPLATE); + list.add(Material.DIAMOND_LEGGINGS); + list.add(Material.DIAMOND_BOOTS); + return list; + } + // 木制工具套装 + if (n.contains("木制工具") + || n.contains("木工具")) { + list.add(Material.WOODEN_SWORD); + list.add(Material.WOODEN_PICKAXE); + list.add(Material.WOODEN_AXE); + list.add(Material.WOODEN_SHOVEL); + return list; + } + if (n.contains("石制工具") + || n.contains("石工具")) { + list.add(Material.STONE_SWORD); + list.add(Material.STONE_PICKAXE); + list.add(Material.STONE_AXE); + list.add(Material.STONE_SHOVEL); + return list; + } + if (n.contains("铁工具")) { + list.add(Material.IRON_SWORD); + list.add(Material.IRON_PICKAXE); + list.add(Material.IRON_AXE); + list.add(Material.IRON_SHOVEL); + return list; + } + + // 单个物品 + if (n.contains("木剑")) + list.add(Material.WOODEN_SWORD); + else if (n.contains("木镐")) + list.add(Material.WOODEN_PICKAXE); + else if (n.contains("铁剑")) + list.add(Material.IRON_SWORD); + else if (n.contains("铁镐")) + list.add(Material.IRON_PICKAXE); + else if (n.contains("钻石剑")) + list.add(Material.DIAMOND_SWORD); + else if (n.contains("钻石镐")) + list.add(Material.DIAMOND_PICKAXE); + else if (n.contains("钻石")) + list.add(Material.DIAMOND); + else if (n.contains("铁锭")) + list.add(Material.IRON_INGOT); + else if (n.contains("金锭") + || n.contains("金块")) + list.add(Material.GOLD_INGOT); + else if (n.contains("煤炭")) + list.add(Material.COAL); + else if (n.contains("木棍")) + list.add(Material.STICK); + else if (n.contains("弓")) + list.add(Material.BOW); + else if (n.contains("箭")) + list.add(Material.ARROW); + else if (n.contains("床")) + list.add(Material.RED_BED); + else { + // 尝试直接匹配 Material 名 + try { + list.add(Material.valueOf( + n.toUpperCase() + .replaceAll("[^A-Z_]", + ""))); + } catch (Exception e) { + // 未知物品 + } + } + return list; + } + + private QuestFile getOrCreateStage( + TreeMap map, + int key, + String category) { + if (!map.containsKey(key)) { + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + key; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.details = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + } + return map.get(key); + } + + /** + * 单个文件的区块 → 阶段 + * defaultStageNum: 文件无阶段标题时的默认编号 + */ + private void buildSingleFileStages( + List
sections, + String category, + TreeMap stageMap, + int defaultStageNum) { + Integer curKey = null; + + for (Section sec : sections) { + Integer stageNum = + extractStageNumber(sec.title); + + if (stageNum != null) { + curKey = stageNum; + } else if (curKey == null) { + curKey = defaultStageNum; + } + + QuestFile qf = getOrCreateStage( + stageMap, curKey, category); + + String displayName = + extractDisplayName(sec.title); + + // 设置显示名 + if (!displayName.isEmpty() + && qf.displayName + .startsWith("阶段 ")) { + qf.displayName = displayName; + } + + // 路由 + if (stageNum != null + && sec.lines.isEmpty()) { + if (!displayName.isEmpty()) { + qf.displayName = displayName; + } + } + else if (isRewardTitle(sec.title)) { + // 奖励 → 只进 rewards + qf.rewards.addAll(sec.lines); + } + else if (isDetailsTitle(sec.title)) { + // 详情 → 进 details + conditions + qf.details.addAll(sec.lines); + for (String line : sec.lines) { + if (!qf.conditions + .contains(line)) { + qf.conditions.add(line); + } + } + } + else if (isConditionsTitle( + sec.title)) { + // 条件 → 只进 conditions + qf.conditions.addAll(sec.lines); + } + else { + // 其他 → 条件 + qf.conditions.addAll(sec.lines); + } + } + } + + /** + * "主线任务-斗志昂扬" → "斗志昂扬" + * "支线-钓鱼" → "钓鱼" + * "阶段 1" → "阶段 1" + * "第一阶段" → "第一阶段" + */ + private String extractDisplayName( + String title) { + if (title == null) return ""; + // 按 - 或 — 或 – 分割 + String[] parts = title.split( + "\\s*[-—–]\\s*", 2); + if (parts.length >= 2) { + String right = parts[1].trim(); + if (!right.isEmpty()) { + return right; + } + } + return title; + } + private DatabaseManager getDb() { + return ((Main) plugin).getDb(); + } + + private boolean isDetailsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("任务详情") + || t.contains("详情") + || t.contains("任务说明") + || t.contains("任务目标") + || t.contains("description") + || t.contains("任务要求"); + } + + private boolean isConditionsTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("解锁条件") + || t.contains("条件") + || t.contains("前置条件") + || t.contains("前置要求") + || t.contains("解锁") + || t.contains("condition"); + } + + + private void ensureStage( + TreeMap map, + String category, + QuestFile current, + int counter) { + if (current != null) return; + // 用大数字确保排在有编号阶段后面 + int key = 1000 + counter; + QuestFile qf = new QuestFile(); + qf.displayName = "阶段 " + (counter + 1); + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + map.put(key, qf); + // 更新调用方的引用(通过返回值) + // 这里用 map 操作,current 无法直接更新 + // 所以改用返回值方式 + } + /** + * 自动将条件中的奖励内容移到 rewards + */ + private void separateRewards(QuestFile qf) { + List remaining = + new ArrayList<>(); + for (String line : qf.conditions) { + if (isRewardContent(line)) { + if (!qf.rewards.contains(line)) { + qf.rewards.add(line); + } + } else { + remaining.add(line); + } + } + qf.conditions = remaining; + } + private boolean checkStageDependency( + String player, + String condition) { + Matcher m = Pattern.compile( + "第([一二三四五六七八九十\\d]+)" + + "阶段") + .matcher(condition); + + while (m.find()) { + String numStr = m.group(1); + int stageNum = + parseChineseNumber(numStr); + if (stageNum == 0) { + try { + stageNum = + Integer.parseInt(numStr); + } catch (Exception e) { + return false; + } + } + + boolean stageDone = false; + for (List quests + : questCache.values()) { + for (QuestFile qf : quests) { + Integer qfNum = + extractStageNumber( + qf.displayName); + if (qfNum != null + && qfNum == stageNum) { + if (isStageComplete(player, + qf.conditions)) { + stageDone = true; + } + break; + } + } + if (stageDone) break; + } + + if (!stageDone) return false; + } + return true; + } + + /** + * 判断一行内容是否为奖励 + */ + private boolean isRewardContent( + String line) { + String l = line.toLowerCase(); + // 含货币关键词 + if (l.contains("游戏币") + || l.contains("金币") + || l.contains("积分") + || l.contains("钻石") + || l.contains("发钱") + || l.contains("发放") + || l.contains("经验") + || l.contains("等级") + || l.contains("经济") + || l.contains("余额") + || l.contains("块钱")) + return true; + // 纯数字+单位(如 "500游戏币") + if (l.matches( + ".*\\d+\\s*(游戏币|金币|元|积分|" + + "钻石|块钱|经验|级|经济|余额).*")) + return true; + // $前缀(如 "$500") + if (l.matches(".*\\$\\d+.*")) + return true; + // 范围格式(如 "1000-5000" 或 "1000~5000") + if (l.matches(".*\\d+[-~]\\d+.*") + && !l.contains("阶段") + && !l.contains("stage")) + return true; + return false; + } + + // ========== 标题分类 ========== + + private boolean isStageTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("阶段") + || t.contains("stage") + || t.contains("第一") + || t.contains("第二") + || t.contains("第三") + || t.contains("第四") + || t.contains("第五"); + } + + private boolean isRewardTitle( + String title) { + String t = title.toLowerCase(); + return t.contains("奖励") + || t.contains("奖品") + || t.contains("报酬") + || t.contains("reward") + || t.contains("奖赏") + || t.contains("礼包内容") + || t.contains("礼包") + || t.contains("物品奖励"); + } + + + private QuestFile makeDefaultStage( + String category) { + QuestFile qf = new QuestFile(); + qf.displayName = "第一阶段"; + qf.fileName = category; + qf.conditions = new ArrayList<>(); + qf.rewards = new ArrayList<>(); + return qf; + } + + private static class Section { + final String title; + final List lines; + Section(String title, + List lines) { + this.title = title; + this.lines = lines; + } + } + + // ========== 公开查询 ========== + + public List getQuests( + String category) { + List list = + questCache.get(category); + return list != null ? list + : new ArrayList<>(); + } + + public int getCompletedStageCount( + String player, + List quests) { + int count = 0; + if (quests == null) return 0; + for (QuestFile qf : quests) { + if (isStageComplete(player, + qf.conditions)) { + count++; + } + } + return count; + } + + public QuestFile getCurrentStage( + String player, + List quests) { + if (quests == null) return null; + for (QuestFile qf : quests) { + if (!isStageComplete( + player, qf.conditions)) { + return qf; + } + } + return null; + } + + public int getStageNumber( + List quests, + QuestFile qf) { + if (quests == null || qf == null) + return 0; + return quests.indexOf(qf) + 1; + } + + public int getStageNumber( + QuestFile qf) { + if (qf == null) return 0; + List quests = + getQuests(qf.fileName); + return getStageNumber(quests, qf); + } + + public boolean isStageCompleted( + String player, + QuestFile qf) { + if (qf == null) return false; + return isStageComplete( + player, qf.conditions); + } + + public List getUnmetConditions( + String player, + List conditions) { + List unmet = + new ArrayList<>(); + if (conditions == null) return unmet; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) { + unmet.add( + getProgress( + player, cond)); + } + } + return unmet; + } + + public String getProgress( + String player, + String rawCond) { + if (player == null + || rawCond == null) + return "§7未知"; + + String key = stripOptional(rawCond); + // 打卡群系进度 + if (key.contains("打卡") + && key.contains("群系")) { + String biome = + extractBiome(key); + + if ("ALL".equals(biome)) { + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + + if (!biome.isEmpty()) { + int catCount = getCounter( + player, + "biomecat_" + biome); + return "§7打卡 " + biome + + "群系: §e" + + catCount + "/1"; + } + + int total = getCounter( + player, "biome_ALL"); + return "§7群系打卡: §e" + + total + "/1"; + } + + + // 阶段依赖 + if (key.contains("完成") + && key.contains("阶段")) { + boolean done = checkStageDependency( + player, key); + return done + ? "§a✓ " + key + : "§c✗ " + key; + } + + + // 注册登录(检查主数据库是否已注册) + // 注册登录(检查主数据库是否已注册) + if ("完成注册登录".equals(key)) { + boolean done = getDb() != null + && getDb().userExists( + player); + return done + ? "§a✓ 完成注册登录" + : "§c✗ 完成注册登录"; + } + + // 绑定邮箱 + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + boolean done = val != null + && !val.toString().isEmpty(); + return done + ? "§a✓ 绑定Email" + : "§c✗ 绑定Email"; + } + + // 在线时间 + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + needed + "小时"; + } + + // 打招呼/发消息/聊天 + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + int cur = getCounter( + player, "CHAT"); + return "§7聊天: §e" + cur + "/1"; + } + + // 破坏/放置方块 + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return "§7方块操作: §e" + + total + "/" + needed; + } + + // 连续登录 + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + Object val = + getDb().getField( + player, + "checkin_streak"); + int streak = val instanceof Number + ? ((Number) val).intValue() + : 0; + return "§7连续登录: §e" + + streak + "/" + needed + "天"; + } + + // 挖矿 + if (key.startsWith("MINE:")) { + String[] parts = key.split(":"); + if (parts.length >= 3) { + int needed = + parseInt(parts[2]); + int cur = getCounter( + player, + "MINE:" + parts[1]); + return "§7挖 " + parts[1] + + ": §e" + cur + + "/" + needed; + } + } + + // 击杀生物 + if (key.startsWith("KILL:")) { + String[] parts = key.split(":"); + if (parts.length >= 3) { + int needed = + parseInt(parts[2]); + int cur = getCounter( + player, + "KILL:" + parts[1]); + return "§7杀 " + parts[1] + + ": §e" + cur + + "/" + needed; + } + } + + // 击杀玩家 + if (key.startsWith("KILL_PLAYER")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "KILL_PLAYER"); + return "§7击杀玩家: §e" + + cur + "/" + needed; + } + + // 合成 + if (key.startsWith("CRAFT")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "CRAFT"); + return "§7合成: §e" + + cur + "/" + needed; + } + + // 钓鱼 + if (key.startsWith("FISH")) { + String[] parts = key.split(":"); + int needed = parts.length >= 2 + ? parseInt(parts[1]) : 1; + int cur = getCounter( + player, "FISH"); + return "§7钓鱼: §e" + + cur + "/" + needed; + } + + // 在线时长(毫秒) + if (key.startsWith("PLAYTIME")) { + String[] parts = key.split(":"); + long needed = parts.length >= 2 + ? Long.parseLong(parts[1]) + : 0; + long cur = getCounterLong( + player, "PLAYTIME"); + return "§7在线: §e" + + formatMs(cur) + "/" + + formatMs(needed); + } + // 上报村庄 + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7上报村庄: §e" + + cur + "/" + needed; + } + + // 探索村庄 + if (key.contains("探索") + && key.contains("村庄")) { + int cur = getCounter( + player, "REPORT_VILLAGE"); + return "§7探索村庄: §e" + + cur + "/1"; + } + // 近7天登录 + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int streak = getIntField( + player, + "checkin_streak"); + if (streak == 0) { + streak = getIntField( + player, "streak"); + } + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } + return "§7近7天登录: §e" + + streak + "/" + + needed + "天"; + } + + + // 完成新手任务全部 + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7新手任务: §e" + + done + "/" + + quests.size(); + } + + // 完成主线任务全部 + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + int done = 0; + for (QuestFile qf : quests) { + if (isStageComplete( + player, + qf.conditions)) + done++; + } + return "§7主线任务: §e" + + done + "/" + + quests.size(); + } + + return "§7" + key; + } + private int getIntField( + String player, String field) { + try { + Object val = getDb().getField( + player, field); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + if (val != null) { + return Integer.parseInt( + val.toString().trim()); + } + } catch (Exception e) { + // 忽略 + } + return 0; + } + + public List checkInBiome( + String player, + String rawBiomeKey, + String worldName, + double x, double y, double z) { + List matched = + new ArrayList<>(); + + // 32格去重 + String posKey = + player + "_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + String lastPos = + lastCheckInPos.get(posKey); + if (curPos.equals(lastPos)) { + plugin.getLogger().info( + "[QuestTracker] 打卡忽略: " + + player + + " 32格内重复"); + return matched; + } + lastCheckInPos.put(posKey, curPos); + + String category = + mapBiomeCategory( + rawBiomeKey, worldName); + + // 记录总打卡 + int allCur = getCounter( + player, "biome_ALL"); + setCounter(player, "biome_ALL", + allCur + 1); + + // 记录分类打卡 + if (!category.isEmpty() + && !"主世界" + .equals(category)) { + String catKey = + "biomecat_" + category; + int catCur = getCounter( + player, catKey); + setCounter(player, catKey, + catCur + 1); + } + + // 记录具体群系 + String bioKey = + "biome_" + rawBiomeKey; + int bioCur = getCounter( + player, bioKey); + setCounter(player, bioKey, + bioCur + 1); + + // 检查所有任务条件是否匹配 + List categories = + Arrays.asList( + "新人任务", + "主线任务", + "支线任务"); + + for (String cat : categories) { + List quests = + getQuests(cat); + for (QuestFile qf : quests) { + if (isStageCompleted( + player, qf)) + continue; + for (String cond + : qf.conditions) { + if (isOptional(cond)) + continue; + String condKey = + stripOptional(cond); + if (!condKey + .contains("打卡") + || !condKey + .contains("群系")) + continue; + String target = + extractBiome(condKey); + boolean hit = false; + if ("ALL".equals(target) + || "风景点" + .equals(target)) { + hit = true; + } else if (!target.isEmpty() + && target.equals( + category)) { + hit = true; + } + if (hit && !matched + .contains(cond)) { + matched.add(cond); + } + } + } + } + + return matched; + } + + + /** + * 从条件文本中提取目标群系分类 + * "打卡上报地狱任意群系" → "下界" + * "打卡任意海洋群系" → "海洋" + * "打卡任意群系" → "ALL" + * "打卡任意风景点" → "" + */ + private String extractBiome(String text) { + if (text == null) return ""; + // 逐个匹配分类关键词 + for (Map.Entry en + : CATEGORY_KEYWORDS.entrySet()) { + if (text.contains(en.getKey())) { + return en.getValue(); + } + } + if (text.contains("风景点") + || text.contains("风景")) { + return "ALL"; + } + + // 有"群系"但无具体分类 → ALL + if (text.contains("群系")) { + return "ALL"; + } + return ""; + } + + + // ========== 标记条件 ========== + + public void markConditionComplete( + String player, + String conditionName) { + if (player == null + || conditionName == null) + return; + storage.setCondition( + player, conditionName); + } + + // ========== 事件回调 ========== + + public void onBlockMined( + String player, String material) { + String key = "MINE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + // 总计数 + setCounter(player, "MINE:ALL", + getCounter(player, "MINE:ALL") + + 1); + } + public boolean onReportVillage( + String player, + String worldName, + double x, double y, double z) { + String dbKey = "village_lastPos"; + String curPos = worldName + + "|" + String.format( + "%.0f", x / 32) + + "|" + String.format( + "%.0f", y / 32) + + "|" + String.format( + "%.0f", z / 32); + Object lastObj = + storage.getCondition( + player, dbKey); + String lastPos = lastObj != null + ? lastObj.toString() : ""; + if (curPos.equals(lastPos)) { + return false; + } + storage.setConditionRaw( + player, dbKey, curPos); + + String key = "REPORT_VILLAGE"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + return true; + } + + public void onMobKill( + String player, String mobType) { + String key = "KILL:" + mobType; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayerKill( + String player) { + String key = "KILL_PLAYER"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onItemCrafted( + String player) { + String key = "CRAFT"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onFishCaught( + String player) { + String key = "FISH"; + int cur = getCounter(player, key); + setCounter(player, key, cur + 1); + } + + public void onPlayTime( + String player, long ms) { + String key = "PLAYTIME"; + long cur = getCounterLong( + player, key); + setCounter(player, key, cur + ms); + } + + // ========== 内部:条件判定 ========== + + private boolean isStageComplete( + String player, + List conditions) { + if (conditions == null + || conditions.isEmpty()) + return true; + for (String cond : conditions) { + if (isOptional(cond)) continue; + if (!isConditionMet( + player, cond)) + return false; + } + return true; + } + + public boolean isConditionMet( + String player, + String rawCond) { + if (player == null + || rawCond == null) + return false; + + String key = stripOptional(rawCond); + // 打卡群系 + if (key.contains("打卡") + && (key.contains("群系") + || key.contains("风景点"))) { + + String biome = + extractBiome(key); + + if ("ALL".equals(biome)) { + // 打卡任意群系 + int total = getCounter( + player, "biome_ALL"); + return total >= 1; + } + + if (!biome.isEmpty()) { + // 打卡特定分类群系 + int catCount = getCounter( + player, + "biomecat_" + biome); + } + + // 无特定要求 + return getCounter( + player, "biome_ALL") >= 1; + } + + + // 阶段依赖 + if (key.contains("完成") + && key.contains("阶段")) { + return checkStageDependency( + player, key); + } + + // 注册登录 + + if ("完成注册登录".equals(key)) { + return getDb() != null + && getDb().userExists( + player); + } + + + // 绑定邮箱 + if (key.contains("绑定Email") + || key.contains("绑定邮箱")) { + Object val = + getDb().getField( + player, "email"); + return val != null + && !val.toString().isEmpty(); + } + + // 在线时间 + if (key.contains("在线") + && key.contains("小时")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + long cur = getCounterLong( + player, "PLAYTIME"); + return cur >= needed * 3600000L; + } + + // 打招呼/发消息/聊天 + if (key.contains("打招呼") + || key.contains("打个招呼") + || key.contains("发消息") + || key.contains("聊天")) { + return getCounter( + player, "CHAT") >= 1; + } + + // 破坏/放置方块 + if ((key.contains("破坏") + || key.contains("放置")) + && key.contains("方块")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int total = getCounter( + player, "MINE:ALL") + + getCounter( + player, "PLACE:ALL"); + return total >= needed; + } + + // 连续登录 + if (key.contains("连续") + && key.contains("登录")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + Object val = + getDb().getField( + player, + "checkin_streak"); + int streak = val instanceof Number + ? ((Number) val).intValue() + : 0; + return streak >= needed; + } + + // 挖矿 + if (key.startsWith("MINE:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "MINE:" + p[1]) + >= parseInt(p[2]); + } + } + + // 击杀生物 + if (key.startsWith("KILL:")) { + String[] p = key.split(":"); + if (p.length >= 3) { + return getCounter(player, + "KILL:" + p[1]) + >= parseInt(p[2]); + } + } + + // 击杀玩家 + if (key.startsWith("KILL_PLAYER")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter(player, + "KILL_PLAYER") >= n; + } + + // 合成 + if (key.startsWith("CRAFT")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "CRAFT") >= n; + } + + // 钓鱼 + if (key.startsWith("FISH")) { + String[] p = key.split(":"); + int n = p.length >= 2 + ? parseInt(p[1]) : 1; + return getCounter( + player, "FISH") >= n; + } + + // 在线时长 + if (key.startsWith("PLAYTIME")) { + String[] p = key.split(":"); + long n = p.length >= 2 + ? Long.parseLong(p[1]) : 0; + return getCounterLong(player, + "PLAYTIME") >= n; + } + // 上报村庄 + if (key.contains("上报") + && key.contains("村庄")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + int cur = getCounter( + player, "REPORT_VILLAGE"); + return cur >= needed; + } + + // 探索村庄 + if (key.contains("探索") + && key.contains("村庄")) { + return getCounter( + player, "REPORT_VILLAGE") + >= 1; + } + // 近7天登录记录 + if (key.contains("7天") + || key.contains("七天")) { + int needed = parseNumberFromText(key); + if (needed <= 0) needed = 1; + + // 诊断:读取数据库所有字段 + try { + var user = getDb().getUser( + player); + if (user != null) { + plugin.getLogger().info( + "[QuestTracker] " + + "登录诊断 " + + player + ": " + + user.toString()); + } else { + plugin.getLogger().info( + "[QuestTracker] " + + "登录诊断: " + + player + + " getUser返回null"); + } + } catch (Exception e) { + plugin.getLogger().info( + "[QuestTracker] " + + "登录诊断异常: " + + e.getMessage()); + } + + int streak = getIntField( + player, + "checkin_streak"); + if (streak == 0) { + streak = getIntField( + player, "streak"); + } + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } + + plugin.getLogger().info( + "[QuestTracker] 登录检测: " + + player + + " 需要=" + needed + + " 实际=" + streak); + + return streak >= needed; + } + + // 完成新手任务全部内容 + if (key.contains("新手任务") + && key.contains("全部")) { + List quests = + getQuests("新人任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + + // 完成主线任务全部内容 + if (key.contains("主线任务") + && key.contains("全部")) { + List quests = + getQuests("主线任务"); + for (QuestFile qf : quests) { + if (!isStageComplete( + player, + qf.conditions)) + return false; + } + return true; + } + + return false; + } + + public void debugLogin(String player) { + try { + var user = getDb().getUser(player); + if (user != null) { + plugin.getLogger().info( + "[QuestTracker] " + + "=== " + player + + " 数据库全部字段 ==="); + for (var entry + : user.entrySet()) { + plugin.getLogger().info( + "[QuestTracker] " + + entry.getKey() + + " = " + + entry.getValue() + + " (" + + (entry.getValue() + != null + ? entry.getValue() + .getClass() + .getSimpleName() + : "null") + + ")"); + } + } else { + plugin.getLogger().info( + "[QuestTracker] " + + "getUser返回null: " + + player); + } + } catch (Exception e) { + plugin.getLogger().info( + "[QuestTracker] " + + "debug异常: " + + e.getMessage()); + } + } + + // ========== 内部:计数器 ========== + + private int getCounter( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).intValue(); + } + return 0; + } + + private long getCounterLong( + String player, String key) { + Object val = storage.getCondition( + player, "cnt_" + key); + if (val instanceof Number) { + return ((Number) val).longValue(); + } + return 0; + } + + private void setCounter( + String player, String key, + int value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + private void setCounter( + String player, String key, + long value) { + storage.setConditionRaw( + player, "cnt_" + key, + String.valueOf(value)); + } + + // ========== 工具 ========== + + private boolean toBool(Object val) { + if (val == null) return false; + if (val instanceof Boolean) + return (Boolean) val; + if (val instanceof Number) + return ((Number) val) + .intValue() != 0; + String s = String.valueOf(val); + return "1".equals(s) + || "true" + .equalsIgnoreCase(s); + } + + public boolean isOptional(String cond) { + if (cond == null) return false; + String c = cond; + return c.contains("(进阶)") + || c.contains("(可选)") + || c.contains(",可选)") + || c.contains(",可选)") + || c.contains("【可选】") + || c.contains("【进阶】"); + } + + + private String stripOptional(String cond) { + if (cond == null) return ""; + return cond.replace("(进阶)", "") + .replace("(可选)", "") + .replace(",可选)", "") + .replace(",可选)", "") + .replace("【可选】", "") + .replace("【进阶】", "") + .trim(); + } + + private int parseInt(String s) { + + try { + return Integer.parseInt(s.trim()); + } catch (Exception e) { + return 0; + } + } + private Object getDbField( + String player, String field) { + Main main = (Main) plugin; + return main.getDb().getField( + player, field); + } + + /** + * 从文本中提取第一个数字 + * "在线时间≥1小时" → 1 + * "破坏/放置方块100个" → 100 + */ + private int parseNumberFromText( + String text) { + java.util.regex.Matcher m = + java.util.regex.Pattern + .compile("(\\d+)") + .matcher(text); + if (m.find()) { + try { + return Integer.parseInt( + m.group(1)); + } catch (Exception e) { + return 0; + } + } + return 0; + } + public void onBlockPlaced( + String player, String material) { + String key = "PLACE:" + material; + setCounter(player, key, + getCounter(player, key) + 1); + setCounter(player, "PLACE:ALL", + getCounter(player, "PLACE:ALL") + + 1); + } + + public void onPlayerChat( + String player) { + String key = "CHAT"; + setCounter(player, key, + getCounter(player, key) + 1); + } + + private String formatMs(long ms) { + long sec = ms / 1000; + long min = sec / 60; + long hr = min / 60; + if (hr > 0) + return hr + "小时" + + (min % 60) + "分"; + if (min > 0) + return min + "分" + + (sec % 60) + "秒"; + return sec + "秒"; + } + + // ========== 数据结构 ========== + + public static class QuestFile { + public String displayName = ""; + public String fileName = ""; + public List details = + new ArrayList<>(); + public List rewards = + new ArrayList<>(); + public List conditions = + new ArrayList<>(); + + public boolean hasConditions() { + return conditions != null + && !conditions.isEmpty(); + } + } +} diff --git a/txt/RadioConverter.txt b/txt/RadioConverter.txt new file mode 100644 index 0000000..d072efb --- /dev/null +++ b/txt/RadioConverter.txt @@ -0,0 +1,113 @@ +package Sdf1_login; + +import java.io.*; +import java.nio.file.*; +import java.util.logging.Logger; + +public class RadioConverter { + + private final Logger logger; + private final boolean ffmpegAvailable; + + public RadioConverter(Logger logger) { + this.logger = logger; + this.ffmpegAvailable = checkFfmpeg(); + } + + /** + * 转换音频文件为 ogg 格式 + * 支持: mp3, wav, flac, ogg + */ + public boolean convert(File input, + File output) { + String name = + input.getName().toLowerCase(); + + // ogg 文件直接复制 + if (name.endsWith(".ogg")) { + return copyFile(input, output); + } + + // 有 ffmpeg 则转换 + if (ffmpegAvailable) { + return convertFfmpeg(input, output); + } + + // 无 ffmpeg 无法转换 + logger.warning( + "[Radio] 无法转换: " + + input.getName() + + " (需要安装ffmpeg)"); + return false; + } + + private boolean copyFile(File src, + File dest) { + try { + dest.getParentFile().mkdirs(); + Files.copy(src.toPath(), + dest.toPath(), + StandardCopyOption + .REPLACE_EXISTING); + return true; + } catch (Exception e) { + return false; + } + } + + private boolean convertFfmpeg( + File input, File output) { + try { + output.getParentFile().mkdirs(); + ProcessBuilder pb = + new ProcessBuilder( + "ffmpeg", "-y", + "-i", + input + .getAbsolutePath(), + "-c:a", + "libvorbis", + "-q:a", "5", + output + .getAbsolutePath()); + pb.redirectErrorStream(true); + Process p = pb.start(); + p.waitFor(); + return output.exists(); + } catch (Exception e) { + logger.warning( + "[Radio] ffmpeg转换失败: " + + input.getName()); + return false; + } + } + + private boolean checkFfmpeg() { + try { + Process p = + new ProcessBuilder( + "ffmpeg", + "-version") + .start(); + boolean ok = p.waitFor() == 0; + if (ok) { + logger.info( + "[Radio] ffmpeg可用"); + } else { + logger.info( + "[Radio] ffmpeg不可用" + + " (仅支持ogg文件)"); + } + return ok; + } catch (Exception e) { + logger.info( + "[Radio] ffmpeg未安装" + + " (仅支持ogg文件)"); + return false; + } + } + + public boolean isFfmpegAvailable() { + return ffmpegAvailable; + } +} diff --git a/txt/RadioManager.txt b/txt/RadioManager.txt new file mode 100644 index 0000000..4756823 --- /dev/null +++ b/txt/RadioManager.txt @@ -0,0 +1,563 @@ +package Sdf1_login; + +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; + +import java.io.*; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; +import java.util.zip.*; + +public class RadioManager { + + private final Main plugin; + private File radioDir; + private File oggDir; + private File packDir; + private BukkitTask mainRadioTask; + private BukkitTask loginRadioTask; + private final Map + loginPlayers = + new ConcurrentHashMap<>(); + private int radioIndex = 0; + private RadioConverter converter; + + private final List mainFiles = + new ArrayList<>(); + private final List loginFiles = + new ArrayList<>(); + + public RadioManager(Main plugin) { + this.plugin = plugin; + this.radioDir = new File( + plugin.getDataFolder(), + "radio"); + this.oggDir = new File( + plugin.getDataFolder(), + "radio/ogg"); + this.packDir = new File( + plugin.getDataFolder(), + "radiopack"); + radioDir.mkdirs(); + oggDir.mkdirs(); + packDir.mkdirs(); + } + + // ========== 初始化 ========== + + public void init() { + converter = new RadioConverter( + plugin.getLogger()); + + convertAll(); + buildResourcePack(); + loadFiles(); + + plugin.getLogger().info( + "[Radio] 初始化完成: 全服=" + + mainFiles.size() + + " 登录=" + loginFiles.size()); + } + + // ========== 格式转换 ========== + + private void convertAll() { + // 根目录 + convertDir(radioDir, + new File(oggDir, "radio")); + // login 目录 + File loginSrc = new File( + radioDir, "login"); + if (loginSrc.exists()) { + convertDir(loginSrc, + new File(oggDir, + "radio/login")); + } + // reg 目录 + File regSrc = new File( + radioDir, "reg"); + if (regSrc.exists()) { + convertDir(regSrc, + new File(oggDir, + "radio/reg")); + } + } + + private void convertDir(File src, + File dest) { + dest.mkdirs(); + File[] files = src.listFiles( + (d, n) -> { + String l = + n.toLowerCase(); + return l.endsWith(".mp3") + || l.endsWith(".wav") + || l.endsWith(".flac") + || l.endsWith(".ogg"); + }); + if (files == null) return; + + for (File f : files) { + String oggName = f.getName() + .replaceAll( + "\\.(mp3|wav|flac" + + "|ogg)$", + ".ogg"); + File oggFile = + new File(dest, oggName); + if (oggFile.exists() + && oggFile.lastModified() + >= f.lastModified()) { + continue; + } + boolean ok = converter.convert( + f, oggFile); + if (ok) { + plugin.getLogger().info( + "[Radio] 转换成功: " + + f.getName()); + } else { + plugin.getLogger().warning( + "[Radio] 转换失败: " + + f.getName()); + } + } + } + + // ========== 资源包 ========== + private void buildResourcePack() { + // 清理旧文件 + deleteDir(packDir); + packDir.mkdirs(); + + List allOggs = new ArrayList<>(); + collectOggs(oggDir, allOggs); + + if (allOggs.isEmpty()) { + plugin.getLogger().warning( + "[Radio] 无 ogg 文件,跳过资源包"); + return; + } + + // assets/sdf1radio/sounds/ 下面直接放文件 + File soundsDir = new File( + packDir, + "assets/sdf1radio/sounds"); + soundsDir.mkdirs(); + + // 构建 sounds.json + StringBuilder json = + new StringBuilder("{\n"); + + for (int i = 0; + i < allOggs.size(); i++) { + File ogg = allOggs.get(i); + // 简化路径:只取文件名(去掉所有目录前缀) + String simpleName = + ogg.getName() + .replace(".ogg", ""); + // 检查是否有同名文件,加前缀区分 + String soundKey = simpleName; + + // 复制 ogg 到资源包 + File dest = + new File(soundsDir, + simpleName + ".ogg"); + try { + Files.copy(ogg.toPath(), + dest.toPath(), + StandardCopyOption + .REPLACE_EXISTING); + } catch (Exception e) { + // 忽略 + } + + // 写 JSON + json.append(" \"sdf1radio:") + .append(soundKey) + .append("\": {\n"); + json.append(" \"sounds\": [{\n"); + json.append(" \"name\": \"") + .append("sdf1radio/") + .append(soundKey) + .append("\",\n"); + json.append( + " \"stream\": true\n"); + json.append(" }]\n"); + json.append(" }"); + if (i < allOggs.size() - 1) + json.append(","); + json.append("\n"); + } + json.append("}"); + + // 写 sounds.json + try { + Files.write( + new File(packDir, + "assets/sdf1radio/" + + "sounds.json") + .toPath(), + json.toString() + .getBytes("UTF-8")); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] sounds.json写入失败"); + } + + // 写 pack.mcmeta + try { + Files.write( + new File(packDir, + "pack.mcmeta") + .toPath(), + ("{\n" + + " \"pack\": {\n" + + " \"pack_format\": 34,\n" + + " \"description\": " + + "\"Sdf1 Radio\"\n" + + " }\n" + + "}") + .getBytes("UTF-8")); + } catch (Exception e) { + // 忽略 + } + + // 打包 zip + try { + File zipFile = new File( + plugin.getDataFolder(), + "radiopack.zip"); + packZip(packDir, zipFile); + plugin.getLogger().info( + "[Radio] 资源包生成: " + + allOggs.size() + + " 个音频"); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] zip打包失败: " + + e.getMessage()); + } + } + + private void deleteDir(File dir) { + if (!dir.exists()) return; + File[] files = dir.listFiles(); + if (files != null) { + for (File f : files) { + if (f.isDirectory()) { + deleteDir(f); + } else { + f.delete(); + } + } + } + dir.delete(); + } + + private void collectOggs(File dir, + List list) { + File[] files = dir.listFiles(); + if (files == null) return; + for (File f : files) { + if (f.isDirectory()) { + collectOggs(f, list); + } else if (f.getName() + .endsWith(".ogg")) { + list.add(f); + } + } + } + + private String getRelativePath( + File base, File file) { + return base.toURI() + .relativize(file.toURI()) + .getPath(); + } + + private void packZip(File root, + File zipFile) throws Exception { + try (ZipOutputStream zos = + new ZipOutputStream( + new FileOutputStream( + zipFile))) { + zipDir(root, root, zos); + } + } + + private void zipDir(File root, + File current, + ZipOutputStream zos) + throws Exception { + File[] files = current.listFiles(); + if (files == null) return; + for (File f : files) { + if (f.isDirectory()) { + zipDir(root, f, zos); + } else { + String path = + root.toURI() + .relativize(f.toURI()) + .getPath(); + zos.putNextEntry( + new ZipEntry(path)); + zos.write(Files.readAllBytes( + f.toPath())); + zos.closeEntry(); + } + } + } + + // ========== 加载文件列表 ========== + + private void loadFiles() { + mainFiles.clear(); + loginFiles.clear(); + + File radioOgg = new File( + oggDir, "radio"); + loadOggs(radioOgg, mainFiles); + + File loginOgg = new File( + oggDir, "radio/login"); + loadOggs(loginOgg, loginFiles); + + File regOgg = new File( + oggDir, "radio/reg"); + loadOggs(regOgg, loginFiles); + } + + private void loadOggs(File dir, + List list) { + File[] files = dir.listFiles( + (d, n) -> n.toLowerCase() + .endsWith(".ogg")); + if (files != null) { + Arrays.sort(files); + for (File f : files) + list.add(f); + } + } + + // ========== 资源包发送 ========== + public void sendResourcePack(Player p) { + File zip = new File( + plugin.getDataFolder(), + "radiopack.zip"); + if (!zip.exists()) return; + try { + byte[] data = + Files.readAllBytes( + zip.toPath()); + java.security.MessageDigest md = + java.security.MessageDigest + .getInstance("SHA-1"); + md.update(data); + byte[] hashBytes = md.digest(); + StringBuilder sb = + new StringBuilder(); + for (byte b : hashBytes) + sb.append(String.format( + "%02x", b)); + String hash = sb.toString(); + UUID uuid = UUID.randomUUID(); + + // 用反射兼容不同版本 + try { + java.lang.reflect.Method m = + p.getClass().getMethod( + "setResourcePack", + UUID.class, + byte[].class, + String.class, + String.class, + boolean.class); + m.invoke(p, uuid, data, + hash, + "Sdf1 Radio", + false); + } catch (NoSuchMethodException ex) { + try { + java.lang.reflect.Method m2 = + p.getClass().getMethod( + "setResourcePack", + UUID.class, + byte[].class, + String.class); + m2.invoke(p, uuid, + data, hash); + } catch (Exception ex2) { + plugin.getLogger().warning( + "[Radio] 服务端不支持" + + "发送资源包"); + } + } + } catch (Exception e) { + } + } + + // ========== 全服广播 ========== + + public void startMainRadio() { + stopMainRadio(); + if (mainFiles.isEmpty()) { + plugin.getLogger().warning( + "[Radio] radio/ 目录" + + "无 .ogg 文件"); + return; + } + plugin.getLogger().info( + "[Radio] 启动全服广播" + + " (" + mainFiles.size() + + " 个文件)"); + + mainRadioTask = new BukkitRunnable() { + @Override + public void run() { + playNext(); + } + }.runTaskTimer( + plugin, 100L, 2400L); + } + private void playNext() { + if (mainFiles.isEmpty()) return; + File file = mainFiles.get( + radioIndex % mainFiles.size()); + String name = file.getName() + .replace(".ogg", ""); + String key = "sdf1radio:" + name; + + for (Player p + : Bukkit.getOnlinePlayers()) { + p.playSound( + p.getLocation(), + key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + + Bukkit.broadcastMessage( + "§6§l[广播] §f正在播放: §e" + + name); + radioIndex++; + } + + + public void stopMainRadio() { + if (mainRadioTask != null) { + mainRadioTask.cancel(); + mainRadioTask = null; + } + } + + // ========== 登录阶段广播 ========== + + public void startLoginRadio( + Player p) { + loginPlayers.put(p.getName(), true); + if (loginRadioTask != null) return; + if (loginFiles.isEmpty()) return; + + loginRadioTask = new BukkitRunnable() { + @Override + public void run() { + if (loginPlayers.isEmpty()) { + cancel(); + loginRadioTask = null; + return; + } + int idx = (int) + (System.currentTimeMillis() + / 2400L + % loginFiles.size()); + File file = + loginFiles.get(idx); + String rel = + getRelativePath( + oggDir, file) + .replace("\\", "/"); + String key = + "sdf1radio:" + + rel.replace(".ogg", ""); + + for (String name + : loginPlayers.keySet()) { + Player lp = Bukkit + .getPlayer(name); + if (lp != null + && lp.isOnline()) { + lp.playSound( + lp.getLocation(), + key, + SoundCategory + .RECORDS, + 1.5f, 1.0f); + } + } + } + }.runTaskTimer( + plugin, 0L, 24L); + } + + public void stopLoginRadio( + Player p) { + loginPlayers.remove(p.getName()); + if (loginPlayers.isEmpty() + && loginRadioTask != null) { + loginRadioTask.cancel(); + loginRadioTask = null; + } + } + + public void stopAllLoginRadio() { + loginPlayers.clear(); + if (loginRadioTask != null) { + loginRadioTask.cancel(); + loginRadioTask = null; + } + } + + // ========== 手动播放 ========== + public boolean playSpecific( + Player sender, String name) { + List all = new ArrayList<>(); + collectOggs(oggDir, all); + for (File f : all) { + String fn = f.getName() + .replace(".ogg", ""); + if (fn.equalsIgnoreCase(name)) { + String key = "sdf1radio:" + name; + for (Player p + : Bukkit.getOnlinePlayers()) { + p.playSound( + p.getLocation(), + key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + Bukkit.broadcastMessage( + "§6§l[广播] §f正在播放: §e" + + name); + return true; + } + } + sender.sendMessage("§c未找到: " + name); + return false; + } + + // ========== 重载 ========== + + public void reload() { + stopMainRadio(); + stopAllLoginRadio(); + convertAll(); + buildResourcePack(); + loadFiles(); + // 不自动启动,由管理员手动启动 + } } + diff --git a/txt/RiskControlManager.txt b/txt/RiskControlManager.txt new file mode 100644 index 0000000..5671a0d --- /dev/null +++ b/txt/RiskControlManager.txt @@ -0,0 +1,276 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class RiskControlManager { + + private final Main plugin; + + // 玩家当前是否处于风控状态 + private final Map riskActive = new ConcurrentHashMap<>(); + // 密码验证连续错误次数 + private final Map passwordAttempts = new ConcurrentHashMap<>(); + // 邮箱验证码连续错误次数 + private final Map emailAttempts = new ConcurrentHashMap<>(); + // 风控触发时间(5分钟过期) + private final Map riskTriggerTime = new ConcurrentHashMap<>(); + // 临时封禁到期时间 + private final Map tempBanExpiry = new ConcurrentHashMap<>(); + + private static final long RISK_TIMEOUT = 5 * 60 * 1000L; // 5分钟 + private static final int MAX_ATTEMPTS = 3; + private static final long TEMP_BAN_DURATION = 10 * 60 * 1000L; // 10分钟 + + public RiskControlManager(Main plugin) { + this.plugin = plugin; + } + + /** + * 检查玩家是否被临时封禁 + */ + public boolean isTempBanned(String name) { + Long expiry = tempBanExpiry.get(name); + if (expiry == null) return false; + if (System.currentTimeMillis() >= expiry) { + tempBanExpiry.remove(name); + return false; + } + return true; + } + + /** + * 获取临时封禁剩余秒数 + */ + public int getTempBanRemaining(String name) { + Long expiry = tempBanExpiry.get(name); + if (expiry == null) return 0; + return (int) ((expiry - System.currentTimeMillis()) / 1000); + } + + /** + * 检查玩家是否需要风控验证 + * 条件:IP变化 + 有背包数据(老玩家) + */ + public boolean shouldTrigger(String name, String currentIP) { + if (isTempBanned(name)) return false; + if (riskActive.getOrDefault(name, false)) return false; + + // 获取上次记录的IP + String lastIP = (String) plugin.getDb().getField(name, "last_ip"); + if (lastIP == null || lastIP.isEmpty()) return false; // 新玩家不触发 + + // IP相同不触发 + if (lastIP.equals(currentIP)) return false; + + // 有背包数据的老玩家 + IP变化 → 触发风控 + String invData = (String) plugin.getDb().getField(name, "inventory_data"); + if (invData != null && !invData.isEmpty()) { + return true; + } + return false; + } + + /** + * 触发风控:冻结玩家,保存并隐藏背包 + */ + public void trigger(Player p, String currentIP) { + String name = p.getName(); + riskActive.put(name, true); + riskTriggerTime.put(name, System.currentTimeMillis()); + passwordAttempts.put(name, 0); + emailAttempts.put(name, 0); + + // 保存背包并冻结 + plugin.hideInventory(p); + + p.sendMessage("§c§l[风控系统] §f检测到异常登录!"); + p.sendMessage("§e当前IP: §f" + currentIP); + p.sendMessage("§e请选择验证方式:"); + p.sendMessage("§a1. §f密码验证 §7(输入: 密码 <你的密码>)"); + p.sendMessage("§a2. §f邮箱验证 §7(输入: 邮箱)"); + p.sendMessage("§7风控有效期: 5分钟"); + } + + /** + * 处理密码验证 + */ + public boolean handlePassword(Player p, String input) { + String name = p.getName(); + if (!riskActive.getOrDefault(name, false)) return false; + + // 检查过期 + if (isExpired(name)) { expireRisk(name); return false; } + + int attempts = passwordAttempts.getOrDefault(name, 0) + 1; + passwordAttempts.put(name, attempts); + + String salt = (String) plugin.getDb().getField(name, "password_salt"); + if (salt == null) return false; + String hash = PasswordUtils.hash(input, salt); + + if (plugin.getDb().checkPassword(name, hash)) { + passRisk(p); + return true; + } + + if (attempts >= MAX_ATTEMPTS) { + failRisk(p); + } else { + p.sendMessage("§c§l[风控系统] §f密码错误!剩余 " + (MAX_ATTEMPTS - attempts) + " 次机会"); + } + return true; + } + + /** + * 发送邮箱验证码 + */ + public void sendEmailCode(Player p) { + String name = p.getName(); + if (!riskActive.getOrDefault(name, false)) return; + if (isExpired(name)) { expireRisk(name); return; } + + String emailAddr = (String) plugin.getDb().getField(name, "email"); + if (emailAddr == null || emailAddr.isEmpty()) { + p.sendMessage("§c§l[风控系统] §f你未绑定邮箱,无法使用邮箱验证"); + return; + } + + if (!plugin.getEmail().isConfigured()) { + p.sendMessage("§c§l[风控系统] §f邮件服务未配置,请使用密码验证"); + return; + } + + String code = PasswordUtils.generateVerifyCode(); + plugin.getChatInput().setVerifyCode(p, code, "risk_control"); + boolean sent = plugin.getEmail().sendVerifyCode(emailAddr, name, code); + if (sent) { + p.sendMessage("§a§l[风控系统] §f验证码已发送到 " + emailAddr); + p.sendMessage("§e请输入: 验证 <验证码>"); + } else { + p.sendMessage("§c§l[风控系统] §f验证码发送失败,请使用密码验证"); + } + } + + /** + * 处理邮箱验证码 + */ + public boolean handleEmailCode(Player p, String code) { + String name = p.getName(); + if (!riskActive.getOrDefault(name, false)) return false; + if (isExpired(name)) { expireRisk(name); return false; } + + int attempts = emailAttempts.getOrDefault(name, 0) + 1; + emailAttempts.put(name, attempts(p, code, "risk_control")) { + passRisk(p); + return true; + } + + if (attempts >= MAX_ATTEMPTS) { + failRisk(p); + } else { + p.sendMessage("§c§l[风控系统] §f验证码); + + if (plugin.getChatInput().checkVerifyCode错误!剩余 " + (MAX_ATTEMPTS - attempts) + " 次机会"); + } + return true; + } + + /** + * 验证通过:解冻,记录新IP + */ + private void passRisk(Player p) { + String name = p.getName(); + riskActive.remove(name); + passwordAttempts.remove(name); + emailAttempts.remove(name); + riskTriggerTime.remove(name); + + // 记录新IP + String ip = plugin.getPlayerIP(p); + if (ip != null) plugin.getDb().setField(name, "last_ip", ip); + plugin.recordIPLogin(p); + + // 还原背包 + plugin.restoreInventory(p); + plugin.getLoggedIn().add(name); + plugin.getDb().setLoggedIn(name, true); + + p.sendMessage("§a§l[风控系统] §f验证通过!已解除风控限制"); + p.playSound(p.getLocation(), org.bukkit.Sound.BLOCK_NOTE_BLOCK_CHIME, 1.0f, 0.79f); + } + + /** + * 验证失败3次:临时封禁 + 邮件报警 + */ + private void failRisk(Player p) { + String name = p.getName(); + riskActive.remove(name); + passwordAttempts.remove(name); + emailAttempts.remove(name); + riskTriggerTime.remove(name); + + // 临时封禁10分钟 + tempBanExpiry.put(name, System.currentTimeMillis() + TEMP_BAN_DURATION); + + p.sendMessage("§c§l[风控系统] §f验证失败次数过多,临时封禁10分钟"); + + // 邮件报警 + String emailAddr = (String) plugin.getDb().getField(name, "email"); + if (emailAddr != null && !emailAddr.isEmpty() && plugin.getEmail().isConfigured()) { + String ip = plugin.getPlayerIP(p); + plugin.getEmail().sendVerifyCode(emailAddr, name, + "[Sdf1_login] 风控报警: 玩家 " + name + " IP(" + ip + ")验证失败3次,已临时封禁10分钟"); + } + + // 踢出 + p.kickPlayer("§c§l[风控系统] §f验证失败,临时封禁10分钟,请稍后再试"); + } + + /** + * 检查风控是否过期 + */ + private boolean isExpired(String name) { + Long trigger = riskTriggerTime.get(name); + if (trigger == null) return true; + return System.currentTimeMillis() - trigger > RISK_TIMEOUT; + } + + /** + * 风控过期处理 + */ + private void expireRisk(String name) { + riskActive.remove(name); + passwordAttempts.remove(name); + emailAttempts.remove(name); + riskTriggerTime.remove(name); + } + + /** + * 玩家是否处于风控状态 + */ + public boolean isRiskActive(String name) { + return riskActive.getOrDefault(name, false); + } + + /** + * 定时清理过期数据 + */ + public void cleanup() { + long now = System.currentTimeMillis(); + riskTriggerTime.entrySet().removeIf(e -> { + if (now - e.getValue() > RISK_TIMEOUT) { + riskActive.remove(e.getKey()); + passwordAttempts.remove(e.getKey()); + emailAttempts.remove(e.getKey()); + return true; + } + return false; + }); + tempBanExpiry.entrySet().removeIf(e -> now >= e.getValue()); + } +} diff --git a/txt/TicketManager.txt b/txt/TicketManager.txt new file mode 100644 index 0000000..18f3392 --- /dev/null +++ b/txt/TicketManager.txt @@ -0,0 +1,1650 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TicketManager { + + private final Main plugin; + + public static final String T_TICKET_MAIN = + "§6§l工单中心"; + public static final String T_TICKET_CREATE = + "§e§l创建工单"; + public static final String T_TICKET_MY = + "§b§l我的工单"; + public static final String T_TICKET_ADMIN = + "§c§l工单管理"; + public static final String T_TICKET_DETAIL = + "§a§l工单详情"; + + public static final String TYPE_BUG = + "bug反馈"; + public static final String TYPE_HELP = + "求助"; + public static final String TYPE_REPORT = + "举报"; + public static final String TYPE_APPLY = + "申请"; + public static final String TYPE_OTHER = + "其他"; + public static final String TYPE_DELETE = + "删号"; + public static final String TYPE_PASSWORD = + "修改密码"; + + + + private static final Map + PRICES = new LinkedHashMap<>(); + static { + PRICES.put(TYPE_BUG, 10); + PRICES.put(TYPE_HELP, 5); + PRICES.put(TYPE_REPORT, 8); + PRICES.put(TYPE_APPLY, 15); + PRICES.put(TYPE_OTHER, 5); + PRICES.put(TYPE_DELETE, 0); + PRICES.put(TYPE_PASSWORD, 0); + } + + + public TicketManager(Main plugin) { + this.plugin = plugin; + } + + // ========== GUI ========== + + public void openMain(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, T_TICKET_MAIN); + fillBg(g); + g.setItem(10, mk(Material.PAPER, + "§e创建工单", + "§7提交bug、求助、举报等")); + g.setItem(12, mk(Material.BOOK, + "§b我的工单", + "§7查看已提交的工单")); + g.setItem(14, mk(Material.CHEST, + "§6工单记录", + "§7查看历史工单")); + if (adm(p)) { + g.setItem(16, mk(Material.REDSTONE, + "§c工单管理", + "§7管理员专用")); + } + if (plugin.getDb() + .isServiceProvider( + p.getName())) { + g.setItem(18, mk(Material.EMERALD, + "§a服务商面板", + "§7接单、处理工单")); + } + g.setItem(22, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openCreate(Player p) { + Inventory g = Bukkit.createInventory( + null, 36, T_TICKET_CREATE); + fillBg(g); + g.setItem(10, mk(Material.BOOK, + "§cbug反馈")); + g.setItem(11, mk(Material.EMERALD, + "§a求助")); + g.setItem(12, mk(Material.BLAZE_POWDER, + "§6举报")); + g.setItem(13, mk(Material.NAME_TAG, + "§e申请")); + g.setItem(14, mk(Material.PAPER, + "§7其他")); + g.setItem(20, mk(Material.BARRIER, + "§c申请删号", + "§7申请删除账号")); + g.setItem(21, mk(Material.NETHER_WART, + "§d申请改密", + "§7请求管理员协助改密")); + g.setItem(31, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openMyTickets(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_TICKET_MY); + fillBg(g); + List> list = + plugin.getDb() + .getTicketsByPlayer( + p.getName()); + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openDetail(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + Inventory g = Bukkit.createInventory( + null, 27, T_TICKET_DETAIL); + fillBg(g); + + String type = sv(t, "type"); + String status = sv(t, "status"); + String requester = sv(t, "requester"); + String title = sv(t, "title"); + String desc = sv(t, "description"); + String assigned = sv(t, "assigned_to"); + int reward = nv(t, "reward_amount"); + int score = nv(t, "score"); + int confirmed = nv(t, "admin_confirmed"); + + g.setItem(10, mk(Material.PAPER, + "§e§l" + (title.isEmpty() + ? type : title), + "§7类型: §f" + type, + "§7状态: " + stxt(status), + "§7提交者: §f" + requester, + "§7奖励: §e" + reward + " 积分")); + + g.setItem(12, mk(Material.BOOK, + "§7详细描述", + desc.isEmpty() ? "§7无描述" + : "§7" + (desc.length() > 40 + ? desc.substring(0, 40) + + "..." : desc))); + + if (!assigned.isEmpty()) { + g.setItem(14, mk(Material.PLAYER_HEAD, + "§7处理人: §f" + assigned, + confirmed == 1 + ? "§7评分: §e" + score + : "§7处理中")); + } else { + g.setItem(14, mk(Material.BARRIER, + "§7未分配")); + } + + // 驳回原因 + if ("closed".equals(status)) { + String reason = sv(t, "reject_reason"); + if (!reason.isEmpty()) { + g.setItem(13, mk(Material.BARRIER, + "§c驳回原因", + "§7" + reason)); + } + } + + // 回复按钮(非关闭/已完结状态) + if (!"closed".equals(status) + && !"withdrawn".equals(status) + && !"resolved".equals(status)) { + g.setItem(15, mk( + Material.WRITABLE_BOOK, + "§b回复工单", + "§7向对方发送消息")); + g.setItem(16, mk(Material.BOOK, + "§7查看回复记录")); + } + + // ===== 管理员按钮 ===== + if (adm(p)) { + if ("pending".equals(status)) { + // 待分配:可分配给服务商 + g.setItem(19, mk(Material.EMERALD, + "§a分配工单", + "§7指定服务商处理")); + // 也可自己处理 + g.setItem(20, mk(Material.LIME_DYE, + "§a我来处理", + "§7管理员直接处理此工单")); + } + if ("processing".equals(status) + && !assigned.equals("admin") + && !assigned.isEmpty()) { + // 服务商在处理:可取消指派收回 + g.setItem(19, mk(Material.BARRIER, + "§c取消指派", + "§7取消服务商指派,收回工单")); + // 也可自己接手处理 + g.setItem(20, mk(Material.LIME_DYE, + "§a我来处理", + "§7管理员直接处理此工单")); + } + if ("processing".equals(status) + && assigned.equals("admin")) { + // 管理员自己在处理:可标记完结 + g.setItem(19, mk(Material.LIME_DYE, + "§a标记完结", + "§7确认工单处理完成")); + } + if ("replied".equals(status) + && assigned.equals("admin")) { + g.setItem(19, mk(Material.LIME_DYE, + "§a标记完结", + "§7确认工单处理完成")); + } + // 驳回(非已关闭/已完结状态) + if (!"closed".equals(status) + && !"resolved".equals(status) + && !"withdrawn".equals(status)) { + g.setItem(21, mk(Material.RED_DYE, + "§c驳回工单", + "§7驳回并关闭此工单")); + } + } + + // ===== 服务商按钮 ===== + if (plugin.getDb() + .isServiceProvider(p.getName()) + && !TYPE_DELETE.equals(type) + && !TYPE_PASSWORD.equals(type)) { + if ("pending".equals(status) + && assigned.isEmpty()) { + // 待分配无人:抢单 + g.setItem(19, mk( + Material.GOLD_INGOT, + "§6抢单", + "§7接取此工单")); + } + if (assigned.equals(p.getName()) + && ("processing".equals(status) + || "replied".equals(status))) { + // 我在处理:标记完结/上报异常 + g.setItem(19, mk( + Material.LIME_DYE, + "§a标记完结", + "§7上报处理完成")); + g.setItem(20, mk( + Material.REDSTONE, + "§c上报异常", + "§7报告处理中的问题")); + } + } + + g.setItem(22, mk(Material.ARROW, + "§7返回")); + + ItemStack info = g.getItem(10); + if (info != null) { + ItemMeta im = info.getItemMeta(); + if (im != null) { + im.setCustomModelData(tid); + info.setItemMeta(im); + } + } + p.openInventory(g); + } + + + public void openAdminList(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, T_TICKET_ADMIN); + fillBg(g); + g.setItem(45, mk(Material.EMERALD, + "§a邀请服务商")); + g.setItem(46, mk(Material.BARRIER, + "§c移除服务商")); + g.setItem(47, mk(Material.CLOCK, + "§e服务商列表")); + g.setItem(48, mk(Material.NETHER_STAR, + "§e直接派单", + "§7管理员直接创建并派单")); + List> all = + new ArrayList<>(); + all.addAll(plugin.getDb() + .getTicketsByStatus("pending")); + all.addAll(plugin.getDb() + .getTicketsByStatus("processing")); + all.addAll(plugin.getDb() + .getTicketsByStatus("replied")); + all.addAll(plugin.getDb() + .getTicketsByStatus("completed")); + all.addAll(plugin.getDb() + .getTicketsByStatus("resolved")); + all.addAll(plugin.getDb() + .getTicketsByStatus("closed")); + fillList(g, all); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + + public void openProviderPanel(Player p) { + Inventory g = Bukkit.createInventory( + null, 27, "§d§l服务商面板"); + fillBg(g); + g.setItem(10, mk(Material.CHEST, + "§e抢单大厅", + "§7查看可接工单")); + g.setItem(12, mk(Material.CLOCK, + "§b我的工单", + "§7我正在处理的工单")); + g.setItem(14, mk(Material.PAPER, + "§6历史工单", + "§7已完成的工单")); + g.setItem(22, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openGrabList(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, "§6§l抢单大厅"); + fillBg(g); + List> pending = + plugin.getDb() + .getTicketsByStatus( + "pending"); + List> list = + new ArrayList<>(); + for (Map t : pending) { + if (sv(t, "assigned_to").isEmpty()) { + list.add(t); + } + } + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + public void openMyAssigned(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, + "§b§l我处理中的工单"); + fillBg(g); + List> list = + plugin.getDb() + .getTicketsByProvider( + p.getName()); + fillList(g, list); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + // ========== 业务 ========== + + public int submitTicket(Player p, + String type, String title, + String desc) { + int price = PRICES + .getOrDefault(type, 5); + int id = plugin.getDb().createTicket( + type, "pending", "normal", + p.getName(), title, desc, + price, ""); + if (id > 0) { + if (TYPE_DELETE.equals(type) + || TYPE_PASSWORD.equals(type)) { + plugin.getDb().updateTicketField( + id, "assigned_to", "admin"); + // 播放派单音效 + if (plugin.getTicketSound() != null) { + plugin.getTicketSound() + .playSound(p, "assign"); + } + + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage( + "§a§l[工单] §f工单 #" + + id + " 已提交," + + "等待管理员处理"); + } else { + p.sendMessage( + "§a§l[工单] §f创建成功!" + + " #§e" + id + + " 奖励: §e" + + price + "积分"); + } + notifyAdmins(p.getName(), id); + } else { + p.sendMessage("§c创建失败"); + } + return id; + } + + + public void adminCreateAndAssign( + Player p, String type, + String title, String desc, + String provider, int price) { + if (!plugin.getDb() + .isServiceProvider(provider)) { + p.sendMessage("§c" + provider + + " 不是服务商"); + return; + } + int id = plugin.getDb().createTicket( + type, "processing", "normal", + p.getName(), title, desc, + price, ""); + if (id <= 0) { + p.sendMessage("§c创建失败"); + return; + } + plugin.getDb().assignTicket( + id, provider); + if (price > 0) { + plugin.getDb().updateTicketField( + id, "reward_amount", price); + } + p.sendMessage("§a工单 #" + id + + " 已创建并分配给 " + provider); + Player tp = Bukkit.getPlayer(provider); + if (tp != null && tp.isOnline()) { + tp.sendMessage( + "§e§l[工单] §f管理员指派工单 #" + + id + " 给您"); + playAssignedSound(tp); + } + } + + public void grabTicket(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!"pending".equals( + sv(t, "status"))) { + p.sendMessage("§c该工单已被接单"); + return; + } + if (!sv(t, "assigned_to").isEmpty()) { + p.sendMessage("§c该工单已被分配"); + return; + } + plugin.getDb().assignTicket( + tid, p.getName()); + p.sendMessage("§a抢单成功!工单 #" + tid); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f您的工单 #" + + tid + " 已被 " + + p.getName() + + " 接单"); + } + } + + public void assignToProvider(Player p, + int tid, String provider, + int price) { + if (!plugin.getDb() + .isServiceProvider(provider)) { + p.sendMessage("§c" + provider + + " 不是服务商"); + return; + } + plugin.getDb().assignTicket( + tid, provider); + if (price > 0) { + plugin.getDb().updateTicketStatus( + tid, "processing"); + plugin.getDb().updateTicketField( + tid, "reward_amount", price); + } + p.sendMessage("§a工单 #" + tid + + " 已分配给 " + provider); + Player tp = Bukkit.getPlayer(provider); + if (tp != null && tp.isOnline()) { + tp.sendMessage( + "§e§l[工单] §f您有新工单: #" + + tid); + playAssignedSound(tp); + } + } + + public void confirmAndScore(Player p, + int tid, int score) { + if (score < 1 || score > 5) { + p.sendMessage("§c评分1-5"); + return; + } + plugin.getDb().confirmTicket( + tid, score); + p.sendMessage("§a工单 #" + tid + + " 已确认,评分: " + score); + Map t = + plugin.getDb().getTicket(tid); + if (t != null) { + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 已确认完结"); + } + } + settleTicket(tid); + } + + public void completeAsProvider(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!sv(t, "assigned_to") + .equals(p.getName())) { + p.sendMessage("§c这不是你的工单"); + return; + } + plugin.getDb().completeTicket(tid); + p.sendMessage("§a工单 #" + tid + + " 已标记完结,等待确认"); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 已处理完成"); + } + } + + public void rejectTicket(Player p, + int tid, String reason) { + plugin.getDb().updateTicketStatus( + tid, "closed"); + plugin.getDb().updateTicketField( + tid, "reject_reason", reason); + p.sendMessage("§c工单 #" + tid + + " 已驳回并关闭"); + Map t = + plugin.getDb().getTicket(tid); + if (t != null) { + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§c§l[工单] §f工单 #" + + tid + + " 被驳回: " + + reason); + } + } + } + + public void replyTicket(Player p, + int tid, String message) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + String status = sv(t, "status"); + if ("closed".equals(status) + || "withdrawn".equals(status) + || "resolved".equals(status)) { + p.sendMessage("§c该工单已关闭"); + return; + } + plugin.getDb().addTicketReply( + tid, p.getName(), message); + p.sendMessage("§a回复成功"); + String requester = + sv(t, "requester"); + String assigned = + sv(t, "assigned_to"); + String sender = p.getName(); + if (!requester.equals(sender)) { + Player rp = Bukkit.getPlayer( + requester); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f工单 #" + + tid + " 新回复"); + } + } + if (!assigned.isEmpty() + && !assigned.equals(sender)) { + Player ap = Bukkit.getPlayer( + assigned); + if (ap != null && ap.isOnline()) { + ap.sendMessage( + "§e§l[工单] §f工单 #" + + tid + " 新回复"); + } + } + } + + public void showReplies(Player p, + int tid) { + List> replies = + plugin.getDb() + .getTicketReplies(tid); + if (replies.isEmpty()) { + p.sendMessage("§7暂无回复"); + return; + } + p.sendMessage("§e§l=== 回复记录 ==="); + SimpleDateFormat sdf = + new SimpleDateFormat( + "MM-dd HH:mm"); + for (Map r : replies) { + String sender = sv(r, "sender"); + String msg2 = sv(r, "message"); + long time = ((Number) r.get( + "create_time")).longValue(); + p.sendMessage("§7[" + sdf.format( + new Date(time)) + "] §f" + + sender + "§7: " + msg2); + } + p.sendMessage( + "§e§l================="); + } + + public void withdrawTicket(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!sv(t, "requester") + .equals(p.getName()) + && !adm(p)) { + p.sendMessage("§c无权操作"); + return; + } + plugin.getDb().updateTicketStatus( + tid, "withdrawn"); + p.sendMessage("§a工单 #" + tid + + " 已撤回"); + } + + public void showProviderList(Player p) { + List> list = + plugin.getDb() + .getAllServiceProviders(); + if (list.isEmpty()) { + p.sendMessage("§7暂无服务商"); + return; + } + p.sendMessage( + "§e§l=== 服务商列表 ==="); + for (Map pro : list) { + p.sendMessage("§7- §f" + + pro.get("player_name") + + " §7角色: §e" + + pro.get("role")); + } + p.sendMessage( + "§e§l==================="); + } + + public void autoProcessCompleted() { + long timeoutMs = 10 * 60 * 1000L; + + // 自动处理删号工单(5分钟自动通过) + List> deleteTickets = + plugin.getDb() + .getTicketsByStatus( + "processing"); + for (Map t : deleteTickets) { + String dtype = sv(t, "type"); + if (!TYPE_DELETE.equals(dtype)) continue; + int did = nv(t, "id"); + int dconf = nv(t, "admin_confirmed"); + // 已经处理过了跳过 + if (dconf != 0) continue; + String dasg = sv(t, "assigned_to"); + // 只处理分配给admin的 + if (!"admin".equals(dasg)) continue; + long createTime = ((Number) t.get( + "create_time")).longValue(); + if (System.currentTimeMillis() + - createTime >= 5 * 60 * 1000L) { + processDeleteAccount(did, t); + } + } + + + // 已完结未确认→10分钟自动完结 + List> list = + plugin.getDb() + .getUnconfirmedCompleted( + timeoutMs); + for (Map t : list) { + int id = nv(t, "id"); + plugin.getDb().confirmTicket(id, 3); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) + rp.sendMessage( + "§e§l[工单] §f工单 #" + + id + " 已自动完结"); + String assigned = + sv(t, "assigned_to"); + if (!assigned.isEmpty()) { + Player pp = + Bukkit.getPlayer(assigned); + if (pp != null && pp.isOnline()) + pp.sendMessage( + "§e§l[工单] §f工单 #" + + id + + " 已自动完结"); + } + settleTicket(id); + } + + // 已回复但10分钟无互动→自动完结 + List> stale = + plugin.getDb() + .getRepliedStaleTickets( + timeoutMs); + for (Map t : stale) { + int id = nv(t, "id"); + plugin.getDb().confirmTicket(id, 3); + Player rp = Bukkit.getPlayer( + sv(t, "requester")); + if (rp != null && rp.isOnline()) + rp.sendMessage( + "§e§l[工单] §f工单 #" + + id + " 超时自动完结"); + settleTicket(id); + } + } + + + + // ========== GUI点击 ========== + public boolean handleClick(Player p, + String title, int slot) { + + if (title.equals(T_TICKET_MAIN)) { + if (slot == 10) openCreate(p); + else if (slot == 12) + openMyTickets(p); + else if (slot == 14) + openMyTickets(p); + else if (slot == 16) { + if (adm(p)) openAdminList(p); + else p.sendMessage("§c权限不足"); + } + else if (slot == 18) { + if (plugin.getDb() + .isServiceProvider(p.getName())) { + openProviderPanel(p); + } else { + p.sendMessage("§c权限不足"); + } + } + else if (slot == 22) + plugin.getGui().openMain(p); + return true; + } + + if (title.equals(T_TICKET_CREATE)) { + if (slot == 31) { + openMain(p); + return true; + } + String type = null; + if (slot == 10) type = TYPE_BUG; + else if (slot == 11) type = TYPE_HELP; + else if (slot == 12) type = TYPE_REPORT; + else if (slot == 13) type = TYPE_APPLY; + else if (slot == 14) type = TYPE_OTHER; + else if (slot == 20) type = TYPE_DELETE; + else if (slot == 21) type = TYPE_PASSWORD; + if (type != null) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_TITLE; + plugin.getChatInput() + .getState(p) + .ticketType = type; + if (TYPE_DELETE.equals(type)) { + p.sendMessage("§e请描述删号原因:"); + } else if (TYPE_PASSWORD.equals(type)) { + p.sendMessage("§e请描述需要协助的内容:"); + } else { + p.sendMessage("§e请输入工单标题:"); + } + } + return true; + } + + + if (title.equals(T_TICKET_MY)) { + if (slot == 49) { + openMain(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals(T_TICKET_DETAIL)) { + return handleClickDetail(p, slot); + } + + if (title.equals(T_TICKET_ADMIN)) { + if (slot == 49) { + openMain(p); + return true; + } + if (slot == 45) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_INVITE_PROVIDER; + p.sendMessage("§e输入服务商玩家名:"); + return true; + } + if (slot == 46) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REMOVE_PROVIDER; + p.sendMessage("§e输入要移除的服务商:"); + return true; + } + if (slot == 47) { + showProviderList(p); + return true; + } + if (slot == 48) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_ADMIN_TYPE; + p.sendMessage("§e选择工单类型:"); + p.sendMessage("§7bug/求助/举报/申请/其他"); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals("§d§l服务商面板")) { + if (slot == 22) { + plugin.getGui().openMain(p); + return true; + } + if (slot == 10) openGrabList(p); + else if (slot == 12) openMyAssigned(p); + else if (slot == 14) openMyTickets(p); + return true; + } + + if (title.equals("§6§l抢单大厅")) { + if (slot == 49) { + openProviderPanel(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + if (title.equals("§b§l我处理中的工单")) { + if (slot == 49) { + openProviderPanel(p); + return true; + } + if (slot >= 0 && slot < 45) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(slot); + int id = gt(it); + if (id > 0) openDetail(p, id); + } + return true; + } + + return false; + } + + private boolean handleClickDetail(Player p, + int slot) { + if (slot == 22) { + openMyTickets(p); + return true; + } + int id = gtFromDetail(p); + if (id <= 0) return true; + Map t = + plugin.getDb().getTicket(id); + if (t == null) return true; + + String st = sv(t, "status"); + int conf = nv(t, "admin_confirmed"); + String asg = sv(t, "assigned_to"); + + // 回复按钮 + if (slot == 15) { + if (!"closed".equals(st) + && !"withdrawn".equals(st) + && !"resolved".equals(st)) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REPLY; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入回复内容:"); + } + return true; + } + + // 查看回复记录 + if (slot == 16) { + showReplies(p, id); + return true; + } + + // slot 19 + if (slot == 19) { + // 管理员:分配工单 + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_ASSIGN; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage( + "§e输入: 服务商名 价格"); + return true; + } + // 管理员:取消指派(服务商在处理中) + if (adm(p) && "processing".equals(st) + && !asg.equals("admin") + && !asg.isEmpty()) { + cancelTicket(p, id); + openDetail(p, id); + return true; + } + // 管理员:自己处理(pending) + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 管理员:标记完结(已回复状态) + if (adm(p) && "replied".equals(st) + && asg.equals("admin")) { + + // 管理员:标记完结(已回复状态) + if (adm(p) && "replied".equals(st) + && asg.equals("admin")) { + Map tk = + plugin.getDb().getTicket(id); + String tkType = sv(tk, "type"); + if (TYPE_DELETE.equals(tkType)) { + approveDeleteTicket(p, id); + return true; + } + plugin.getDb().completeTicket(id); + p.sendMessage("§a工单 #" + id + + " 已标记完结"); + openDetail(p, id); + return true; + } + } + // 服务商:抢单 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && "pending".equals(st) + && asg.isEmpty()) { + grabTicket(p, id); + openDetail(p, id); + return true; + } + // 服务商:标记完结 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && asg.equals(p.getName()) + && ("processing".equals(st) + || "replied".equals(st))) { + completeAsProvider(p, id); + openDetail(p, id); + return true; + } + return true; + } + + // slot 20 + if (slot == 20) { + // 管理员:自己处理(pending) + if (adm(p) && "pending".equals(st) + && asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + plugin.getDb().updateTicketStatus( + id, "processing"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 管理员:自己接手(服务商在处理中) + if (adm(p) && "processing".equals(st) + && !asg.equals("admin") + && !asg.isEmpty()) { + plugin.getDb().assignTicket( + id, "admin"); + p.sendMessage("§a工单 #" + id + + " 已由您接手处理"); + openDetail(p, id); + return true; + } + // 服务商:上报异常 + if (plugin.getDb() + .isServiceProvider(p.getName()) + && asg.equals(p.getName()) + && ("processing".equals(st) + || "replied".equals(st))) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REPORT_ISSUE; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入异常描述:"); + return true; + } + return true; + } + + // slot 21:驳回 + if (slot == 21) { + if (adm(p) && !"closed".equals(st) + && !"resolved".equals(st) + && !"withdrawn".equals(st)) { + p.closeInventory(); + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_REJECT; + plugin.getChatInput() + .getState(p).ticketId = id; + p.sendMessage("§e输入驳回原因:"); + } + return true; + } + + return true; + } + + // ========== 聊天输入 ========== + + public boolean handleChatInput(Player p, + String msg, + ChatInputManager.InputType type) { + switch (type) { + case TICKET_TITLE: { + plugin.getChatInput() + .getState(p) + .ticketTitle = msg; + plugin.getChatInput() + .getState(p).type = + ChatInputManager.InputType + .TICKET_DESC; + p.sendMessage( + "§e请输入工单详细描述:"); + return true; + } + case TICKET_DESC: { + String tt = plugin.getChatInput() + .getState(p).ticketType; + String tl = plugin.getChatInput() + .getState(p).ticketTitle; + plugin.getChatInput().reset(p); + submitTicket(p, tt, tl, msg); + return true; + } + case TICKET_SCORE: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + try { + int sc = Integer.parseInt( + msg.trim()); + Map tk = + plugin.getDb() + .getTicket(tid2); + String st = tk != null + ? sv(tk, "status") + : ""; + if ("completed".equals(st)) { + plugin.getChatInput() + .reset(p); + confirmAndScore( + p, tid2, sc); + } else { + plugin.getChatInput() + .reset(p); + p.sendMessage( + "§c工单尚未处理完成"); + } + } catch ( + NumberFormatException e) { + plugin.getChatInput() + .reset(p); + p.sendMessage( + "§c请输入数字1-5"); + } + return true; + } + case TICKET_REJECT: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + plugin.getChatInput().reset(p); + rejectTicket(p, tid2, msg); + return true; + } + case TICKET_ASSIGN: { + int tid2 = plugin.getChatInput() + .getState(p).ticketId; + String[] parts = msg.trim() + .split("\\s+"); + String prov = parts[0]; + int price = 0; + if (parts.length >= 2) { + try { + price = Integer.parseInt( + parts[1]); + } catch ( + NumberFormatException e) { + p.sendMessage( + "§c价格格式错误"); + } + } + plugin.getChatInput().reset(p); + assignToProvider(p, tid2, + prov, price); + runSync(() -> + openDetail(p, tid2)); + return true; + } + case TICKET_INVITE_PROVIDER: { + String tgt = msg.trim(); + plugin.getChatInput().reset(p); + if (!plugin.getDb() + .userExists(tgt)) { + p.sendMessage( + "§c玩家不存在"); + return true; + } + plugin.getDb() + .addServiceProvider( + tgt, "waiter"); + p.sendMessage("§a已邀请 " + tgt + + " 成为服务商"); + return true; + } + case TICKET_REMOVE_PROVIDER: { + String tgt = msg.trim(); + plugin.getChatInput().reset(p); + if (!plugin.getDb() + .isServiceProvider( + tgt)) { + p.sendMessage("§c" + tgt + + " 不是服务商"); + return true; + } + plugin.getDb() + .removeServiceProvider( + tgt); + p.sendMessage( + "§a已移除 " + tgt); + return true; + } + default: + return false; + } + } + + // ========== 私有 ========== + + private void settleTicket(int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) return; + String requester = + sv(t, "requester"); + String assigned = + sv(t, "assigned_to"); + int reward = nv(t, "reward_amount"); + int score = nv(t, "score"); + ConfigManager cfg = + plugin.getConfig2(); + if (reward > 0) { + plugin.getDb().addPoints( + requester, reward); + Player rp = Bukkit.getPlayer( + requester); + if (rp != null && rp.isOnline()) { + rp.sendMessage( + "§e§l[工单] §f获得 " + + reward + + " 积分奖励"); + } + } + if (!assigned.isEmpty()) { + int pts = (int) (score + * cfg.providerPointsPerScore); + if (pts > 0) { + plugin.getDb().addPoints( + assigned, pts); + } + double money = + cfg.baseEconomyReward + + score + * cfg.providerEconomyPerScore; + boolean ok = false; + try { + if (plugin.getEconomy() != null) { + OfflinePlayer op = + Bukkit.getOfflinePlayer( + assigned); + plugin.getEconomy() + .depositPlayer( + op, money); + ok = true; + } + } catch (Exception e) { + plugin.getLogger().warning( + "[Sdf1_login] 经济失败: " + + e.getMessage()); + } + Player pp = + Bukkit.getPlayer(assigned); + if (pp != null && pp.isOnline()) { + pp.sendMessage( + "§a§l[工单] §f工单 #" + + tid + + " 确认完结"); + if (pts > 0) + pp.sendMessage("§7积分: §e" + + pts); + if (ok) + pp.sendMessage("§7经济: §a$" + + String.format( + "%.0f", money)); + } + } + } + + private void notifyAdmins(String creator, + int tid) { + String tag = + plugin.getConfig2().adminTag; + for (Player op : + Bukkit.getOnlinePlayers()) { + if (op.getScoreboardTags() + .contains(tag)) { + op.sendMessage( + "§e§l[工单] §f" + creator + + " 提交了新工单 #" + + tid); + } + } + } + + private void playAssignedSound(Player p) { + for (int i = 0; i < 3; i++) { + final int d = i * 15; + Bukkit.getScheduler() + .runTaskLater(plugin, () -> { + if (p.isOnline()) { + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_CHIME, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_BASS, + 1.0f, 0.79f); + p.playSound(p.getLocation(), + Sound + .BLOCK_NOTE_BLOCK_PLING, + 1.0f, 0.79f); + } + }, d); + } + } + + private void runSync(Runnable task) { + Bukkit.getScheduler() + .runTask(plugin, task); + } + + private boolean adm(Player p) { + return p.getScoreboardTags().contains( + plugin.getConfig2().adminTag); + } + + private void fillBg(Inventory g) { + ItemStack glass = mk( + Material.GRAY_STAINED_GLASS_PANE, + " "); + for (int i = 0; i < g.getSize(); i++) { + if (g.getItem(i) == null + || g.getItem(i).getType() + == Material.AIR) { + g.setItem(i, glass); + } + } + } + + private void fillList(Inventory g, + List> list) { + if (list.isEmpty()) { + g.setItem(22, mk(Material.BARRIER, + "§7暂无工单")); + return; + } + int slot = 0; + for (Map t : list) { + if (slot >= 45) break; + g.setItem(slot, makeItem(t)); + slot++; + } + } + /** + * 执行删号流程 + */ + /** + * 执行删号流程 + */ + public void processDeleteAccount(int tid, + Map t) { + String requester = sv(t, "requester"); + plugin.getDb().updateTicketStatus( + tid, "resolved"); + plugin.getDb().updateTicketField(tid, + "admin_confirmed", 1); + Player target = + Bukkit.getPlayer(requester); + if (target != null && target.isOnline()) { + plugin.backupInventory(target); + target.sendMessage( + "§a§l[工单] §f您的删号请求已通过"); + target.sendMessage( + "§7物品已备份,下次登录可取回"); + target.kickPlayer( + "§a删号成功,物品已备份。\n" + + "请等待5分钟后重新注册取回物品。"); + } else { + // 离线:仅标记,不操作数据库 + plugin.getLogger().info( + "[Sdf1_login] " + requester + + " 删号工单已通过,等待上线执行"); + } + // 延迟执行删号 + final String name = requester; + Bukkit.getScheduler().runTaskLater( + plugin, () -> { + plugin.getDb().deleteUser(name); + plugin.getLogger().info( + "[Sdf1_login] " + name + + " 删号工单已执行"); + }, 100L); + } + + /** + * 管理员手动批准删号 + */ + public void approveDeleteTicket(Player p, + int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + if (!TYPE_DELETE.equals(sv(t, "type"))) { + p.sendMessage("§c这不是删号工单"); + return; + } + p.sendMessage("§a工单 #" + tid + + " 已批准,正在执行删号..."); + processDeleteAccount(tid, t); + } + + /** + * 管理员取消工单(服务商异常后) + */ + /** + * 管理员取消工单(解除服务商绑定,管理员可自行处理) + */ + public void cancelTicket(Player p, int tid) { + Map t = + plugin.getDb().getTicket(tid); + if (t == null) { + p.sendMessage("§c工单不存在"); + return; + } + // 解除服务商绑定,状态回退到pending + plugin.getDb().updateTicketField( + tid, "assigned_to", ""); + plugin.getDb().updateTicketStatus( + tid, "pending"); + p.sendMessage("§a工单 #" + tid + + " 已取消指派,回到待分配状态"); + // 通知原服务商 + String assigned = sv(t, "assigned_to"); + if (!assigned.isEmpty()) { + Player ap = Bukkit.getPlayer(assigned); + if (ap != null && ap.isOnline()) { + ap.sendMessage( + "§c§l[工单] §f工单 #" + + tid + + " 已被管理员取消指派"); + } + } + } + + + /** + * 服务商查看历史工单 + */ + public void openProviderHistory(Player p) { + Inventory g = Bukkit.createInventory( + null, 54, + "§6§l我的历史工单"); + fillBg(g); + List> all = + plugin.getDb() + .getTicketsByProviderAll( + p.getName()); + fillList(g, all); + g.setItem(49, mk(Material.ARROW, + "§7返回")); + p.openInventory(g); + } + + private ItemStack makeItem( + Map t) { + int id = nv(t, "id"); + String type = sv(t, "type"); + String status = sv(t, "status"); + String requester = + sv(t, "requester"); + String title = sv(t, "title"); + int reward = nv(t, "reward_amount"); + Material mat; + switch (status) { + case "processing": + mat = Material.YELLOW_DYE; + break; + case "replied": + mat = Material.CYAN_DYE; + break; + case "completed": + mat = Material.LIME_DYE; + break; + case "resolved": + mat = Material.LIME_DYE; + break; + case "closed": + mat = Material.GRAY_DYE; + break; + case "rejected": + mat = Material.RED_DYE; + break; + case "withdrawn": + mat = Material.GRAY_DYE; + break; + default: + mat = Material.ORANGE_DYE; + break; + } + ItemStack item = new ItemStack(mat); + ItemMeta im = item.getItemMeta(); + if (im != null) { + im.setDisplayName("§e#" + id + + " " + (title.isEmpty() + ? type : title)); + List lore = + new ArrayList<>(); + lore.add("§7类型: §f" + type); + lore.add("§7状态: " + + stxt(status)); + lore.add("§7提交者: §f" + + requester); + lore.add("§7奖励: §e" + reward + + " 积分"); + lore.add(""); + lore.add("§7点击查看详情"); + im.setLore(lore); + im.setCustomModelData(id); + item.setItemMeta(im); + } + return item; + } + + private int gt(ItemStack item) { + if (item == null) return -1; + ItemMeta im = item.getItemMeta(); + if (im == null) return -1; + if (im.hasCustomModelData()) + return im.getCustomModelData(); + return -1; + } + + private int gtFromDetail(Player p) { + ItemStack it = p.getOpenInventory() + .getTopInventory().getItem(10); + return gt(it); + } + + private String stxt(String status) { + switch (status) { + case "pending": + return "§e待处理"; + case "processing": + return "§b处理中"; + case "replied": + return "§9已回复"; + case "completed": + return "§a待确认"; + case "resolved": + return "§a已完结"; + case "closed": + return "§7已关闭"; + case "rejected": + return "§c已驳回"; + case "withdrawn": + return "§7已撤回"; + default: + return "§7" + status; + } + } + + private String sv(Map m, + String key) { + Object v = m.get(key); + return v != null + ? String.valueOf(v) : ""; + } + + private int nv(Map m, + String key) { + Object v = m.get(key); + return v instanceof Number + ? ((Number) v).intValue() : 0; + } + + private ItemStack mk(Material mat, + String name, String... lore) { + ItemStack it = new ItemStack(mat); + ItemMeta im = it.getItemMeta(); + if (im != null) { + im.setDisplayName(name); + if (lore.length > 0) { + List list = + new ArrayList<>(); + for (String l : lore) { + if (!l.isEmpty()) + list.add(l); + } + if (!list.isEmpty()) + im.setLore(list); + } + it.setItemMeta(im); + } + return it; + } +} diff --git a/txt/VerificationManager.txt b/txt/VerificationManager.txt new file mode 100644 index 0000000..6b42bdc --- /dev/null +++ b/txt/VerificationManager.txt @@ -0,0 +1,137 @@ +package Sdf1_login; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class VerificationManager { + + private final JavaPlugin plugin; + private final Logger logger; + + public VerificationManager(JavaPlugin plugin) { + this.plugin = plugin; + this.logger = plugin.getLogger(); + } + + public boolean isBedrockPlayer(Player player) { + UUID uuid = player.getUniqueId(); + if (uuid.toString().startsWith("00000000-0000-0000-0009-")) { + logger.info("[Sdf1_login] Geyser: UUID前缀匹配 → 基岩版"); + return true; + } + if (player.hasMetadata("isgeyserplayer")) { + logger.info("[Sdf1_login] Geyser: metadata匹配 → 基岩版"); + return true; + } + if (player.hasPermission("geyser.player")) { + logger.info("[Sdf1_login] Geyser: permission匹配 → 基岩版"); + return true; + } + try { + Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate"); + if (floodgate == null) floodgate = Bukkit.getPluginManager().getPlugin("Floodgate"); + if (floodgate != null && floodgate.isEnabled()) { + Class apiClass = Class.forName("org.geysermc.floodgate.api.FloodgateApi"); + Object api = apiClass.getMethod("getInstance").invoke(null); + boolean isBedrock = (boolean) apiClass.getMethod("isFloodgatePlayer", UUID.class).invoke(api, uuid); + if (isBedrock) { + logger.info("[Sdf1_login] Geyser: Floodgate API确认 → 基岩版"); + return true; + } + } + } catch (Exception ignored) {} + logger.info("[Sdf1_login] Geyser: 非基岩版"); + return false; + } + + public boolean isOnlineMode() { + return Bukkit.getOnlineMode(); + } + + public void verifyPremiumAsync(Player player, Consumer callback) { + String name = player.getName(); + UUID playerUUID = player.getUniqueId(); + logger.info("[Sdf1_login] Mojang验证: 查询 " + name + " ..."); + logger.info("[Sdf1_login] Mojang验证: 玩家UUID = " + playerUUID); + + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + UUID premiumUUID = queryMojangUUID(name); + + if (premiumUUID == null) { + logger.info("[Sdf1_login] Mojang验证: " + name + " 无记录 → 非正版"); + Bukkit.getScheduler().runTask(plugin, () -> callback.accept(false)); + return; + } + + logger.info("[Sdf1_login] Mojang验证: 正版UUID = " + premiumUUID); + boolean match = playerUUID.equals(premiumUUID); + + if (match) { + logger.info("[Sdf1_login] Mojang验证: ✓ UUID匹配 → 正版"); + } else { + logger.info("[Sdf1_login] Mojang验证: ✗ UUID不匹配 → 非正版"); + logger.info("[Sdf1_login] Mojang验证: 玩家: " + playerUUID); + logger.info("[Sdf1_login] Mojang验证: 正版: " + premiumUUID); + } + + final boolean finalResult = match; + Bukkit.getScheduler().runTask(plugin, () -> callback.accept(finalResult)); + }); + } + + private UUID queryMojangUUID(String name) { + try { + URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + name); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("User-Agent", "Sdf1_login"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + int code = conn.getResponseCode(); + if (code != 200) { + conn.disconnect(); + return null; + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8")); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) sb.append(line); + reader.close(); + conn.disconnect(); + + String json = sb.toString(); + logger.info("[Sdf1_login] Mojang响应: " + json); + + int idIndex = json.indexOf("\"id\""); + if (idIndex < 0) return null; + + int colonIndex = json.indexOf(":", idIndex); + int quoteStart = json.indexOf("\"", colonIndex + 1); + int quoteEnd = json.indexOf("\"", quoteStart + 1); + + String rawUUID = json.substring(quoteStart + 1, quoteEnd); + String formatted = rawUUID.replaceFirst( + "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", + "$1-$2-$3-$4-$5"); + + return UUID.fromString(formatted); + + } catch (Exception e) { + logger.log(Level.WARNING, "[Sdf1_login] Mojang查询异常: " + name, e); + return null; + } + } +} diff --git a/unnamed.patch b/unnamed.patch new file mode 100644 index 0000000..ee7e491 --- /dev/null +++ b/unnamed.patch @@ -0,0 +1,989 @@ +Index: .gitattributes +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/.gitattributes b/.gitattributes +--- a/.gitattributes (revision 145b392112786b20534254a7a38d716657c962ab) ++++ b/.gitattributes (revision 87de2e12c81f5a5e04825113e7835aca09e0fd09) +@@ -1,6 +1,5 @@ ++*.md text=auto encoding=UTF-8 + *.png binary + *.jpg binary + *.jpeg binary +-*.gif binary +-*.ico binary +-*.bmp binary +\ No newline at end of file ++*.gif binary +\ No newline at end of file +Index: src/main/java/Sdf1_login/ConfigManager.java +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/src/main/java/Sdf1_login/ConfigManager.java b/src/main/java/Sdf1_login/ConfigManager.java +--- a/src/main/java/Sdf1_login/ConfigManager.java (revision 145b392112786b20534254a7a38d716657c962ab) ++++ b/src/main/java/Sdf1_login/ConfigManager.java (revision 87de2e12c81f5a5e04825113e7835aca09e0fd09) +@@ -312,6 +312,14 @@ + requesterPointsReward = parseInt( + m.getOrDefault("报单人积分奖励", "10"), + 10); ++ garbageEnabled = Boolean.parseBoolean( ++ m.getOrDefault("垃圾站_启用", "true")); ++ garbageInterval = parseInt( ++ m.getOrDefault("垃圾站_清理间隔秒", ++ "300"), 300); ++ garbageMaxRounds = parseInt( ++ m.getOrDefault("垃圾站_保留轮数", ++ "1"), 1); + + + } +@@ -331,12 +339,10 @@ + L.add("挂机超时秒数=" + afkTimeout); + // 垃圾站设置 + L.add("# ---- 垃圾站 ----"); +- L.add("垃圾站_启用: " +- + garbageEnabled); +- L.add("垃圾站_清理间隔秒: " +- + garbageInterval); +- L.add("垃圾站_保留轮数: " +- + garbageMaxRounds); ++ L.add("垃圾站_启用=" + garbageEnabled); ++ L.add("垃圾站_清理间隔秒=" + garbageInterval); ++ L.add("垃圾站_保留轮数=" + garbageMaxRounds); ++ + + writeLines(file, L); + } +@@ -364,6 +370,11 @@ + L.add("基础经济奖励=" + baseEconomyReward); + L.add("经济奖励每评分=" + providerEconomyPerScore); + L.add("报单人积分奖励=" + requesterPointsReward); ++ L.add("# ---- 垃圾站 ----"); ++ L.add("垃圾站_启用=true"); ++ L.add("垃圾站_清理间隔秒=300"); ++ L.add("垃圾站_保留轮数=1"); ++ + + writeLines(f, L); + } +Index: src/main/java/Sdf1_login/DatabaseManager.java +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/src/main/java/Sdf1_login/DatabaseManager.java b/src/main/java/Sdf1_login/DatabaseManager.java +--- a/src/main/java/Sdf1_login/DatabaseManager.java (revision 145b392112786b20534254a7a38d716657c962ab) ++++ b/src/main/java/Sdf1_login/DatabaseManager.java (revision 87de2e12c81f5a5e04825113e7835aca09e0fd09) +@@ -201,7 +201,18 @@ + + "active INTEGER DEFAULT 1," + + "join_time INTEGER DEFAULT 0" + + ")"); +- ++ // 垃圾回收站物品表 ++ st.execute("CREATE TABLE IF NOT EXISTS " ++ + "items (" ++ + "id INTEGER PRIMARY KEY " ++ + "AUTOINCREMENT," ++ + "player_name TEXT NOT NULL," ++ + "item_data TEXT DEFAULT ''," ++ + "amount INTEGER DEFAULT 1," ++ + "create_time INTEGER DEFAULT 0" ++ + ")"); ++ safeAdd(st, "menu_snowball", ++ "INTEGER DEFAULT 1"); + st.close(); + logger.info("[Sdf1_login] 数据库初始化完成"); + } catch (Exception e) { +Index: src/main/java/Sdf1_login/GUIManager.java +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/src/main/java/Sdf1_login/GUIManager.java b/src/main/java/Sdf1_login/GUIManager.java +--- a/src/main/java/Sdf1_login/GUIManager.java (revision 145b392112786b20534254a7a38d716657c962ab) ++++ b/src/main/java/Sdf1_login/GUIManager.java (revision 87de2e12c81f5a5e04825113e7835aca09e0fd09) +@@ -489,6 +489,12 @@ + "§e设置密码")); + g.setItem(16, mkItem(Material.BARRIER, + "§c删除用户", "§7二次验证删除")); ++ // 雪球菜单检测(slot 14) ++ g.setItem(14, mkItem( ++ Material.SNOWBALL, ++ "§e§l雪球菜单", ++ "§7点击为该玩家发放菜单雪球")); ++ + g.setItem(22, mkItem(Material.ARROW, "§7返回")); + p.openInventory(g); + } +Index: src/main/java/Sdf1_login/GarbageManager.java +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/src/main/java/Sdf1_login/GarbageManager.java b/src/main/java/Sdf1_login/GarbageManager.java +--- a/src/main/java/Sdf1_login/GarbageManager.java (revision 145b392112786b20534254a7a38d716657c962ab) ++++ b/src/main/java/Sdf1_login/GarbageManager.java (revision 87de2e12c81f5a5e04825113e7835aca09e0fd09) +@@ -231,26 +231,26 @@ + + public void saveItem(ItemStack stack) { + { +- try { +- String data = +- stackToBase64(stack); +- if (data.isEmpty()) return; +- PreparedStatement ps = db.prepareStatement( +- "INSERT INTO items " +- + "(item_data, amount, " +- + "round, create_time) " +- + "VALUES (?,?,?,?)"); +- ps.setString(1, data); +- ps.setInt(2, stack.getAmount()); +- ps.setInt(3, currentRound); +- ps.setLong(4, +- System.currentTimeMillis()); +- ps.executeUpdate(); +- ps.close(); +- } catch (Exception e) { +- e.printStackTrace(); +- } +- }} ++ try { ++ String data = ++ stackToBase64(stack); ++ if (data.isEmpty()) return; ++ PreparedStatement ps = db.prepareStatement( ++ "INSERT INTO items " ++ + "(item_data, amount, " ++ + "round, create_time) " ++ + "VALUES (?,?,?,?)"); ++ ps.setString(1, data); ++ ps.setInt(2, stack.getAmount()); ++ ps.setInt(3, currentRound); ++ ps.setLong(4, ++ System.currentTimeMillis()); ++ ps.executeUpdate(); ++ ps.close(); ++ } catch (Exception e) { ++ e.printStackTrace(); ++ } ++ }} + + private int deleteOldItems(int beforeRound) { + try { +@@ -381,6 +381,38 @@ + .get("amount")).intValue(); + ItemStack stack = base64ToStack(data); + if (stack != null) { ++ try { ++ byte[] bytes = ++ stack.serializeAsBytes(); ++ String b64 = java.util.Base64 ++ .getEncoder() ++ .encodeToString(bytes); ++ int len = b64.length(); ++ String f = len > 15 ++ ? b64.substring(10, 15) : "短"; ++ String m = len >= 5 ++ ? b64.substring(len / 2, ++ Math.min(len / 2 + 5, len)) ++ : "短"; ++ String b = len > 10 ++ ? b64.substring(len - 10, len - 5) ++ : "短"; ++ plugin.getLogger().info( ++ "[垃圾站加载] ID#" + id ++ + " " + stack.getType() ++ + " x" + amount ++ + " base64[" + len + "]" ++ + " 前=" + f ++ + " 中=" + m ++ + " 后=" + b); ++ } catch (Exception ex) { ++ plugin.getLogger().warning( ++ "[垃圾站加载] 序列化异常: " ++ + ex.getMessage()); ++ } ++ } ++ ++ if (stack != null) { + stack.setAmount(amount); + ItemMeta im = + stack.getItemMeta(); +@@ -431,6 +463,10 @@ + g.setItem(51, mk(Material.ARROW, + "§7已是最后一页")); + } ++ // 52格:返回按钮 ++ g.setItem(52, mk(Material.ARROW, ++ "§7返回", ++ "§7返回主菜单")); + + p.openInventory(g); + } +@@ -459,6 +495,12 @@ + openRecyclePage(p, page + 1); + return true; + } ++ // 52:返回 ++ if (slot == 52) { ++ plugin.getGui().openMain(p); ++ return true; ++ } ++ + // 50/52/53:不处理 + if (slot >= 49 && slot <= 53) + return true; +Index: src/main/java/Sdf1_login/GiftManager.java +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/src/main/java/Sdf1_login/GiftManager.java b/src/main/java/Sdf1_login/GiftManager.java +--- a/src/main/java/Sdf1_login/GiftManager.java (revision 145b392112786b20534254a7a38d716657c962ab) ++++ b/src/main/java/Sdf1_login/GiftManager.java (revision 87de2e12c81f5a5e04825113e7835aca09e0fd09) +@@ -155,55 +155,18 @@ + } + + public boolean canClaim(Player p, int stage) { +- Map user = plugin.getDb() +- .getUser(p.getName()); +- if (user.isEmpty()) return false; +- int curStage = ((Number) user.getOrDefault( +- "gift_stage", 0)).intValue(); +- if (stage > curStage + 1) return false; +- if (stage < 1 || stage > stages.size()) +- return false; +- String claimed = (String) user.getOrDefault( +- "gift_claimed", ""); +- if (claimed.contains("[" + stage + "]")) +- return false; +- +- // debug 绕过:检查 tasks_completed +- String completed = (String) user.getOrDefault( +- "tasks_completed", ""); +- if (completed.contains("[" + stage + "]")) +- return true; +- +- // 正常条件检查 +- long dbTime = ((Number) user.getOrDefault( +- "total_online_time", 0L)).longValue(); +- long sessionTime = 0; +- Object lastObj = plugin.getDb().getField( +- p.getName(), "last_online_check"); +- if (lastObj != null && plugin.getLoggedIn() +- .contains(p.getName())) { +- long last = ((Number) lastObj).longValue(); +- if (last > 0) { +- sessionTime = +- System.currentTimeMillis() - last; +- } +- } +- long onlineTime = dbTime + sessionTime; +- int broken = ((Number) user.getOrDefault( +- "blocks_broken", 0)).intValue(); +- int placed = ((Number) user.getOrDefault( +- "blocks_placed", 0)).intValue(); ++ String name = p.getName(); ++ int curStage = ((Number) plugin.getDb() ++ .getField(name, "gift_stage")) ++ .intValue(); ++ String claimed = (String) plugin.getDb() ++ .getField(name, "gift_claimed"); ++ if (claimed == null) claimed = ""; ++ return curStage >= stage ++ && !claimed.contains( ++ String.valueOf(stage)); ++ } + +- switch (stage) { +- case 1: return true; +- case 2: return onlineTime >= 3600000; +- case 3: return broken >= 100 +- && onlineTime >= 86400000L; +- case 4: return (broken + placed) >= 300 +- && onlineTime >= 604800000L; +- default: return false; +- } +- } + + public void claimReward(Player p, int stage) { + if (!canClaim(p, stage)) return; +Index: src/main/java/Sdf1_login/Main.java +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/src/main/java/Sdf1_login/Main.java b/src/main/java/Sdf1_login/Main.java +--- a/src/main/java/Sdf1_login/Main.java (revision 145b392112786b20534254a7a38d716657c962ab) ++++ b/src/main/java/Sdf1_login/Main.java (revision 87de2e12c81f5a5e04825113e7835aca09e0fd09) +@@ -196,6 +196,8 @@ + getCommand("sdf1debug").setExecutor(this); + if (getCommand("oa") != null) + getCommand("oa").setExecutor(this); ++ if (getCommand("menu") != null) ++ getCommand("menu").setExecutor(this); + + + getServer().getPluginManager() +@@ -1099,6 +1101,9 @@ + return; + } + } ++ if (isFrozen(p)) { ++ p.setAllowFlight(true); ++ } + + if (!"manual".equals(config.approvalMode) + && !db.userExists(name)) { +@@ -1238,36 +1243,82 @@ + private static final String MENU_SNOWBALL_TAG = + "sdf1_menu"; + ++ // 雪球菜单 + public void giveMenuSnowball(Player p) { +- // 检查背包里是否已有菜单雪球 ++ // 检查开关 ++ Object val = db.getField( ++ p.getName(), "menu_snowball"); ++ if (val != null) { ++ int on = val instanceof Number ++ ? ((Number) val).intValue() ++ : 1; ++ if (on == 0) { ++ return; // 开关关了,不发放 ++ } ++ } ++ // 检查是否已有 + for (ItemStack it : p.getInventory() + .getContents()) { + if (isMenuSnowball(it)) { +- return; // 已有,不重复发 ++ return; + } + } +- // 没有则发放 ++ // 发放 + ItemStack snow = new ItemStack( + Material.SNOWBALL); + ItemMeta im = snow.getItemMeta(); + if (im != null) { + im.setDisplayName( + "§e§l[菜单] §f右键打开主菜单"); +- List lore = new ArrayList<>(); ++ List lore = ++ new ArrayList<>(); + lore.add("§7右键点击打开功能菜单"); + lore.add("§8" + MENU_SNOWBALL_TAG); + im.setLore(lore); + snow.setItemMeta(im); + } +- // 放到最后一个空格,避免覆盖 +- int emptySlot = +- p.getInventory() +- .firstEmpty(); +- if (emptySlot >= 0) { ++ int slot = p.getInventory() ++ .firstEmpty(); ++ if (slot >= 0) { ++ p.getInventory().setItem( ++ slot, snow); ++ } else { ++ p.getWorld().dropItemNaturally( ++ p.getLocation(), snow); ++ } ++ } ++ ++ /** ++ * 手动发放雪球菜单(绕过开关检查) ++ */ ++ public void forceGiveMenuSnowball( ++ Player p) { ++ // 只检查是否已有 ++ for (ItemStack it : p.getInventory() ++ .getContents()) { ++ if (isMenuSnowball(it)) { ++ return; ++ } ++ } ++ ItemStack snow = new ItemStack( ++ Material.SNOWBALL); ++ ItemMeta im = snow.getItemMeta(); ++ if (im != null) { ++ im.setDisplayName( ++ "§e§l[菜单] §f右键打开主菜单"); ++ List lore = ++ new ArrayList<>(); ++ lore.add("§7右键点击打开功能菜单"); ++ lore.add("§8" + MENU_SNOWBALL_TAG); ++ im.setLore(lore); ++ snow.setItemMeta(im); ++ } ++ int slot = p.getInventory() ++ .firstEmpty(); ++ if (slot >= 0) { + p.getInventory().setItem( +- emptySlot, snow); ++ slot, snow); + } else { +- // 背包满,丢在地上 + p.getWorld().dropItemNaturally( + p.getLocation(), snow); + } +@@ -1296,21 +1347,24 @@ + @EventHandler + public void onMove(PlayerMoveEvent e) { + Player p = e.getPlayer(); ++ if (!isFrozen(p)) return; ++ + Location from = e.getFrom(); + Location to = e.getTo(); +- if (isFrozen(p)) { +- if (to != null && (from.getX() != to.getX() +- || from.getY() != to.getY() +- || from.getZ() != to.getZ())) { +- e.setTo(e.getFrom()); +- } ++ if (to == null) return; ++ ++ // 允许转头(无位移) ++ if (from.getBlockX() == to.getBlockX() ++ && from.getBlockY() == to.getBlockY() ++ && from.getBlockZ() == to.getBlockZ()) { + return; + } +- if (to != null && (from.getX() != to.getX() +- || from.getY() != to.getY() +- || from.getZ() != to.getZ())) { +- afk.recordAction(p.getUniqueId()); +- } ++ ++ // 有位移 → 取消 ++ e.setCancelled(true); ++ ++ // 防止"服务器未启用飞行"踢出 ++ p.setAllowFlight(true); + } + + @EventHandler +@@ -1484,7 +1538,8 @@ + + + +- // ===== Inventory Click ===== ++ // ===== Inventory Click ===== ++ // ===== Inventory Click ===== + @EventHandler + public void onInvClick(InventoryClickEvent e) { + if (!(e.getWhoClicked() instanceof Player)) +@@ -1494,12 +1549,41 @@ + e.setCancelled(true); + return; + } +- String title = +- e.getView().getTitle(); ++ String title = e.getView().getTitle(); + int slot = e.getRawSlot(); + +- if (needsPasswordChange.contains(p.getName()) +- && !title.equals("§6§l任务面板")) { ++ // Shift+点击背包物品 → 存入垃圾箱 ++ if (e.isShiftClick() ++ && e.getClickedInventory() != null ++ && e.getClickedInventory() ++ == p.getInventory() ++ && e.getCurrentItem() != null ++ && e.getCurrentItem().getType() ++ != Material.AIR ++ && !isMenuSnowball( ++ e.getCurrentItem()) ++ && title.equals( ++ "§6§l垃圾回收站")) { ++ e.setCancelled(true); ++ ItemStack clicked = ++ e.getCurrentItem().clone(); ++ garbage.saveItem(clicked); ++ e.getInventory().setItem( ++ e.getSlot(), null); ++ Bukkit.getScheduler() ++ .runTaskLater(this, ++ () -> { ++ garbage.openRecyclePage(p, ++ garbage.getRecyclePage( ++ p.getName())); ++ }, 1L); ++ return; ++ } ++ ++ if (needsPasswordChange ++ .contains(p.getName()) ++ && !title.equals( ++ "§6§l任务面板")) { + e.setCancelled(true); + p.sendMessage("§c请先修改密码!"); + return; +@@ -1516,12 +1600,15 @@ + TicketManager.T_TICKET_ADMIN) + || title.equals( + TicketManager.T_TICKET_DETAIL) +- || title.equals("§d§l服务商面板") +- || title.equals("§6§l抢单大厅") ++ || title.equals( ++ "§d§l服务商面板") ++ || title.equals( ++ "§6§l抢单大厅") + || title.equals( + "§b§l我处理中的工单")) { + e.setCancelled(true); +- ticket.handleClick(p, title, slot); ++ ticket.handleClick( ++ p, title, slot); + return; + } + +@@ -1539,96 +1626,67 @@ + + ItemStack inSlot = + e.getInventory().getItem(slot); +- ItemStack onCursor = e.getCursor(); ++ ItemStack cursor = e.getCursor(); + +- // 1. 格子里是垃圾站物品 → 取出 +- if (isTrashDisplayItem(inSlot)) { +- e.setCancelled(true); +- try { +- List lore = +- inSlot.getItemMeta() +- .getLore(); +- String numStr = +- lore.get(0) +- .replace( +- "§7ID: #", "") +- .trim(); +- int dbId = Integer +- .parseInt(numStr); +- // 先从DB删除 +- if (garbage.removeItem( +- dbId)) { +- // 再给玩家(去掉ID lore) +- ItemStack give = +- inSlot.clone(); +- ItemMeta gm = +- give.getItemMeta(); +- if (gm != null) { +- List newLore = +- new ArrayList<>(); +- // 保留原始lore(跳过ID和数量行) +- if (lore.size() > 2) { +- for (int li = 2; +- li < lore +- .size(); +- li++) { +- newLore.add( +- lore.get( +- li)); +- } +- } +- gm.setLore( +- newLore.isEmpty() +- ? null +- : newLore); ++ // ===== 取出 ===== ++ if (inSlot != null ++ && inSlot.hasItemMeta() ++ && inSlot.getItemMeta().hasLore()) { ++ List lore = ++ inSlot.getItemMeta().getLore(); ++ if (lore != null && !lore.isEmpty() ++ && lore.get(0) ++ .startsWith("§7ID: #")) { ++ e.setCancelled(true); ++ try { ++ String numStr = lore.get(0) ++ .replace("§7ID: #", "") ++ .trim(); ++ int dbId = ++ Integer.parseInt(numStr); ++ if (garbage.removeItem(dbId)) { ++ ItemStack give = ++ inSlot.clone(); ++ ItemMeta gm = ++ give.getItemMeta(); ++ gm.setLore(null); + give.setItemMeta(gm); +- } +- p.getInventory() +- .addItem(give); +- // 清空格子 +- e.getInventory() +- .setItem(slot, +- null); +- } +- } catch (Exception ex) { +- p.sendMessage("§c取出失败"); +- } +- return; ++ p.getInventory() ++ .addItem(give); ++ e.getInventory() ++ .setItem(slot, null); ++ } ++ } catch (Exception ex) { ++ p.sendMessage("§c取出失败"); ++ } ++ return; ++ } + } + +- // 2. 格子空 + 手上有物品 → 放入 +- if ((inSlot == null +- || inSlot.getType() +- == Material.AIR) +- && onCursor != null +- && onCursor.getType() ++ // ===== 光标存入 ===== ++ if (cursor != null ++ && cursor.getType() + != Material.AIR +- && !isMenuSnowball( +- onCursor)) { +- // 不取消!让物品自然进入格子 +- Bukkit.getScheduler() +- .runTaskLater(this, +- () -> { +- ItemStack after = +- e.getInventory() +- .getItem( +- slot); +- if (after != null +- && after.getType() +- != Material.AIR) { +- garbage.saveItem( +- after +- .clone()); +- e.getInventory() +- .setItem( +- slot, +- null); +- } +- }, 1L); ++ && !isMenuSnowball(cursor)) { ++ ItemStack toSave = cursor.clone(); ++ getLogger().info( ++ "[垃圾站] 存入: " ++ + toSave.getType() ++ + " x" + toSave.getAmount()); ++ garbage.saveItem(toSave); ++ e.setCancelled(true); ++ Bukkit.getScheduler().runTaskLater( ++ this, () -> { ++ p.getOpenInventory() ++ .setCursor(null); ++ garbage.openRecyclePage(p, ++ garbage.getRecyclePage( ++ p.getName())); ++ }, 1L); + return; + } + +- // 3. 其他情况 ++ // 默认取消 + e.setCancelled(true); + return; + } +@@ -1806,6 +1864,7 @@ + p.closeInventory(); + p.sendMessage("§e请输入邀请码:"); + } ++ + return; + } + +@@ -1828,6 +1887,7 @@ + .replace("§e", "") + .replace("§a", "")); + } ++ + return; + } + +@@ -1856,7 +1916,18 @@ + p.closeInventory(); + p.sendMessage("§e输入" + tgt + + "的临时密码:"); +- } else if (slot == 16) { ++ } else if (slot == 14) { ++ Player tp = Bukkit.getPlayer(tgt); ++ if (tp != null && tp.isOnline()) { ++ forceGiveMenuSnowball(tp); ++ p.sendMessage("§a已发放雪球菜单给 " ++ + tgt); ++ } else { ++ p.sendMessage("§c玩家不在线"); ++ } ++ ++ ++ } else if (slot == 16) { + chatInput.getState(p).type = + ChatInputManager.InputType + .ADMIN_DELETE_CONFIRM; +@@ -1866,8 +1937,11 @@ + p.sendMessage(config.msg( + "admin_delete_confirm")); + } ++ + return; ++ + } ++ + + // 积分商城 + if (title.startsWith("§d§l积分商城")) { +@@ -1958,43 +2032,45 @@ + + // 回收站拖拽 + if (t.equals("§6§l垃圾回收站")) { +- for (int rawSlot : e.getRawSlots()) { +- if (rawSlot >= 49 +- && rawSlot <= 53) { ++ for (int rawSlot : ++ e.getRawSlots()) { ++ if (rawSlot >= 49) { + e.setCancelled(true); + return; + } + } +- // 0-48:不取消,延迟存DB ++ // 0-48允许拖入,不取消 ++ // 延迟2tick后扫描格子 ++ final Player fp = p; + Bukkit.getScheduler() +- .runTaskLater(this, () -> { +- for (int rawSlot : +- e.getRawSlots()) { +- if (rawSlot < 0 +- || rawSlot >= 49) +- continue; +- ItemStack item = +- e.getInventory() +- .getItem( +- rawSlot); +- if (item != null +- && item.getType() +- != Material.AIR +- && !isMenuSnowball( +- item)) { +- garbage.saveItem( +- item.clone()); +- e.getInventory() +- .setItem( +- rawSlot, +- null); +- } +- } +- }, 1L); ++ .runTaskLater(this, ++ () -> { ++ Inventory top = fp ++ .getOpenInventory() ++ .getTopInventory(); ++ for (int rawSlot : ++ e.getRawSlots()) { ++ if (rawSlot < 0 ++ || rawSlot >= 49) ++ continue; ++ ItemStack item = ++ top.getItem(rawSlot); ++ if (item != null ++ && item.getType() ++ != Material.AIR ++ && !isMenuSnowball( ++ item)) { ++ garbage.saveItem( ++ item.clone()); ++ top.setItem( ++ rawSlot, null); ++ } ++ } ++ }, 2L); + return; + } + +- ++ //服务商工单系统 + if (t.equals(TicketManager.T_TICKET_MAIN) + || t.equals( + TicketManager.T_TICKET_CREATE) +@@ -2050,6 +2126,38 @@ + + count + " 个掉落物"); + return true; + } ++ // /menu 开关雪球菜单 ++ if (cmdName.equals("menu")) { ++ if (!(sender instanceof Player)) { ++ sender.sendMessage( ++ "§c仅玩家可用"); ++ return true; ++ } ++ Player p2 = (Player) sender; ++ if (args.length == 0) { ++ p2.sendMessage( ++ "§e用法: /menu <开/关>"); ++ return true; ++ } ++ String arg = args[0]; ++ if (arg.equals("开")) { ++ db.setField(p2.getName(), ++ "menu_snowball", 1); ++ p2.sendMessage( ++ "§a雪球菜单已开启," ++ + "下次进入将发放"); ++ } else if (arg.equals("关")) { ++ db.setField(p2.getName(), ++ "menu_snowball", 0); ++ p2.sendMessage( ++ "§c雪球菜单已关闭," ++ + "下次进入不再发放"); ++ } else { ++ p2.sendMessage( ++ "§e用法: /menu <开/关>"); ++ } ++ return true; ++ } + + if (cmdName.equals("reg") + || cmdName.equals("注册")) { +@@ -2461,9 +2569,9 @@ + sender.sendMessage( + "§e§l================================"); + } +- } ++ } + +- // pw / password ++ // pw / password + if (sub.equals("pw") + || sub.equals("password")) { + if (!(sender instanceof Player)) { +@@ -2790,6 +2898,29 @@ + } + return true; + } ++ // give - 手动发放雪球菜单 ++ if (sub.equals("give")) { ++ if (args.length < 2) { ++ sender.sendMessage( ++ "§e用法: /sdf1_login give <玩家名>"); ++ return true; ++ } ++ Player tp = Bukkit.getPlayer( ++ args[1]); ++ if (tp == null || !tp.isOnline()) { ++ sender.sendMessage( ++ "§c玩家不在线: " ++ + args[1]); ++ return true; ++ } ++ forceGiveMenuSnowball(tp); ++ tp.sendMessage( ++ "§a管理员已为您发放菜单雪球"); ++ sender.sendMessage( ++ "§a已发放雪球菜单给 " ++ + args[1]); ++ return true; ++ } + + // shopadd + if (sub.equals("shopadd")) { +@@ -3052,4 +3183,4 @@ + return list; + } + +-} ++} +\ No newline at end of file +Index: src/main/java/Sdf1_login/TicketManager.java +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/src/main/java/Sdf1_login/TicketManager.java b/src/main/java/Sdf1_login/TicketManager.java +--- a/src/main/java/Sdf1_login/TicketManager.java (revision 145b392112786b20534254a7a38d716657c962ab) ++++ b/src/main/java/Sdf1_login/TicketManager.java (revision 87de2e12c81f5a5e04825113e7835aca09e0fd09) +@@ -1014,39 +1014,27 @@ + openDetail(p, id); + return true; + } +- // 管理员:标记完结(自己在处理) +- if (adm(p) && "processing".equals(st) ++ // 管理员:标记完结(已回复状态) ++ if (adm(p) && "replied".equals(st) + && asg.equals("admin")) { +- Map tk = +- plugin.getDb().getTicket(id); +- String tkType = sv(tk, "type"); +- if (TYPE_DELETE.equals(tkType)) { +- approveDeleteTicket(p, id); +- return true; +- } +- plugin.getDb().completeTicket(id); +- p.sendMessage("§a工单 #" + id +- + " 已标记完结"); +- openDetail(p, id); +- return true; +- } +- // 管理员:标记完结(已回复状态) +- if (adm(p) && "replied".equals(st) +- && asg.equals("admin")) { +- Map tk = +- plugin.getDb().getTicket(id); +- String tkType = sv(tk, "type"); +- if (TYPE_DELETE.equals(tkType)) { +- approveDeleteTicket(p, id); +- return true; +- } +- plugin.getDb().completeTicket(id); +- p.sendMessage("§a工单 #" + id +- + " 已标记完结"); +- openDetail(p, id); +- return true; ++ ++ // 管理员:标记完结(已回复状态) ++ if (adm(p) && "replied".equals(st) ++ && asg.equals("admin")) { ++ Map tk = ++ plugin.getDb().getTicket(id); ++ String tkType = sv(tk, "type"); ++ if (TYPE_DELETE.equals(tkType)) { ++ approveDeleteTicket(p, id); ++ return true; ++ } ++ plugin.getDb().completeTicket(id); ++ p.sendMessage("§a工单 #" + id ++ + " 已标记完结"); ++ openDetail(p, id); ++ return true; ++ } + } +- + // 服务商:抢单 + if (plugin.getDb() + .isServiceProvider(p.getName()) +Index: src/main/resources/plugin.yml +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml +--- a/src/main/resources/plugin.yml (revision 145b392112786b20534254a7a38d716657c962ab) ++++ b/src/main/resources/plugin.yml (revision 87de2e12c81f5a5e04825113e7835aca09e0fd09) +@@ -84,3 +84,6 @@ + 垃圾清理: + description: 手动清理掉落物 + usage: /垃圾清理 ++ menu: ++ description: 雪球菜单开关控制 ++ usage: /menu <开/关> +\ No newline at end of file diff --git a/工作簿1.xlsx b/工作簿1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..953a03ed634acee060c1a87f737349520d024f9b GIT binary patch literal 13611 zcmeHu1y>wf)^_7=!3pl}!8JI9#@z{xySqzp3GVLh5Zv9JAPEo%?k*p>cV_NP=6=86 zo$6IptE$g_Rvp<=dp{*F0|AKvfCj(<000tzFsJ{dH5dQ@2?YS417N|mL~N`bLDr6X z%5JtG2OUOND@)>BNO0~fq*#?*o%`1kC=AgOROM zV~WcoLL_8X4pwZ%N`%N342N37f^kl4;tS10w$?h*%nIMT;_9h?Ac51Gu~pgVr1!|{ zKOGaHk}Wr1>(XMr9gT32;&5@uK2%T<$}pk{It1RpJ-gz7Jg;o%^DnUdCijRlSz^xI z+5=%7f>F)Z0PC$sXqWg772tbtXP2s+*r(VJ@yJngTwKxVD}W>y1MJvJo-0I1Kg2^3 zd()-s2>iRURphmdTw$Wk#53f#&#!v+@&W;n{|_~7P+=y!d|i7VUjIdSRZ~5Ckfj3? zxQ?LVDs;ljw*ty zn>e%gKToFlEG8EIL=ybO{o!yX1#`&3&}6yNbI6k9>JCpu$&}ly+A!0ehty5a$g=A~ zG_4c;$&&$?KBP#>{^pi-Qeu!I`^rbNp8aez!>x}KuD5LHbl5+fsCfO=WBs?1l=~mK zq(J}xQ?ITK<#l9SEty@5s!Y%G5Hv2v9)>pXFcj;c8qp|9vTV_6D^m^kue6lFta zlodw9sa;DE;D`jlOA7Va?I&V-*@|DHr(sq~ZKm1u(~Xo=tQ!;gw^AH#fAH>lns9kF z(IM<)o(9*N=jNh|s7NN%MyRHR-W$Z4W|p#ptKl-Li&5@Fn9aG&+H`WSv3k3VE@glN zRM-?8NX+#X)7YsftbyvXwAJOfOf|j~8+)?ja1*KNs0vuEW;jv0hMFb({*%$&`Umf1 zrTS(X$rJX3xoD~Z_p-nPV3bHx|>gMa;@W2d`Xz);-84-m>y61Ij>tw0R8x<^ErZ;% zC0blGuk6j-LByyW8;)k!fy6_&gsSQJq(rpAk)*yYJsKX0cQ-oO`SQ3}m6e$Fa=SCR z;jNkYvy^c~wxMlg!BnLhsUmSwIZUsq&u5AT2s-5v-C#R>@kxOr9(fWDPx7ZbeWB+G zao7IlQH*R#yvlikn@<#}sxsnL?|v!XxZ7`>+y4>6H1V5z>TX|QqPi}ZGr?*d{w^70 zFM%Ma8S}2=^o@J2ru>dtAwt_*`k_;;RQ_pK_G=>92|dJsqdY`)UAx72%QULcp@P~k zIVuB<=qe)6+Oa51NvjjCB5bh%j^c$#6>}$E5_^;|14H%Ol=c2b9127PY_K|PVeA-g zF}__9_4*irny7nJrTGo-@I$Ybcjs_^p*+MUB*!!ls<(Tu@$$i@6nJ+G3 zS@6jwN)6RZILZ-N8SR+f-l?vQ|I>qzNtJWBW};gR2Ws#E=ECi?m-N}2>;21M;J!}x z&eE_lkrC_D+rQIeKY{q9i&y<^eRXzt09deBJ^s_*{kt~*)AxbBhNiF9;Q#hkt|%+h z_ZpfZJcls5q&Z`vEju!i9;zImzz)~btWuJ&d0%e4rES$wUyxyfunBfM86R}Md58QR z0{yC+t~l%sq&vFh2``k*_{BH`tl?1wp}zza`kTZ3gR&z8Wc3<4%ioP? z`T`eDiVw*RFugZNLU-FOb`5*p_Gm2YHeu17caOM(UnWCX`i|c-*pWD7NP@_RCeC3e zH|2}U$v4OM4<`1~t@7JC511NKh%2BIe9XZXO`7tnQTI6`xteYDIlT%-1J)EI9Z z{nxlq`-CEJGR~liOmL_ykDU z5%P+Q#lss*0iqQj*w7b>fUaoN>@Sg1k8bEkpOr7Y%w%iL&RMHEv1}GKp83j1$``>H zX+md*qesi7h4WBbx1jww8GZs5#`T60XPpX*Sh$?Y#sIj=<>T;_E(&LovGV!bF3 z@g4-s8`v6b8=O8_d8yCAiF9JWkIy22^+|x~xw-N~HhTChd8+0H@Z7+kIuKRZz9Y=~ zsFsmKzX4LD+BE^48T(a!Q!Y~>)$ZYT>NXUuX)8uYN_`<+Ej4kwXJ8biS>|!KMN<-X zCBdP9ug3JecVFi{=;*nUUi_pk!G&YjlJv1YY0I2x@Jcr-tsQ@>9lr^&$KmYGd-?6I zwN3hiOW$29=S1&1SF~XCER>E_KNP1wBV#ATmdq9rSB&EQwJyJ?6)7Y&7V>g&sWXkS)|iv=9hzK?=o+1ciJa|P3-!0BT5QH;~OWT%n(b8%<6B3O>aV!qUWnS<)7C?5j%A%!#{oR7?2Q%7qdKBEy&O^`CWoX+87b16s2eJsopguI@ zMVG-pfaB69mvxum1j)ID!zd&ef#qaF1ue?3^xXf1{WOi1Yk?)2Y@KYXoAoKG7j?E7 z#4Mmf5D@w)_m~T@Md29tLsn^2HzMpLLeMf(cvMB8g zb%_`YhZ0nEr$A7j0qkT#kA5B*-y(kS=~w0&PuAo}Zp4;>CZ0mMnY&+4i};Eh+x^5V zeVg!W2vG2Y<98JXg*qGCDPp&~rk{U40|UFj9rqml{if~${fXgP1C9TLC5ChV*C@z6 z@FI)+xkJxVad-|Id~!ba(G|%@NM>r}E5V2mg|448A+E1bMJ0#GYUIKXZsg(_V5i)YH;2WS;X8JAg0fqadcm zts_=E)xxST<6`tnZk&0;^}N}IS3F!*yy`q0;GWZ~UyNJHQVdN(^CKB5)_lD-TSvMX z+K^Dp!gkXuyU?&0Fo!|_ErMhh*$tJQ$BdBM%M1hm;l|JOr_BC!F@MQ=Ei9wtCa&CXkJT*2m6!=a7vj))Ze+70A&&9!xac>qKbeR9NJWT_ zY$GrIID^b5Q=zgm%;RC$4b#iNF?NiixsQxLC_h3 zV7>Z+;oH!?SnqOuk*V6-cJ)e5VCWNW**5osnxeq&=t!!G(kb{7oq<-t3NEQnWq86P zM%ab&Ty^Kii&ex%F;I0`&LSAUTzKyKW9<*Gq5Y(U25D7|yO95T4A0e5eDaZ=M^vCg! zXzWmPJo;-RW+(REi_o3`q0~kDDOa+b&EeWr53h49nM*KRZqgg5xVkR^q(YnN{s1U= za$!cOFkN?8e`r%*yrY~K8J#7`S;5@scw_QoTf-{Bsrsc1e#yBL{^#5Etu6GDRU+*H z#0+#28o5?o+tC|TsX|Zj&ti)n?6_(xgmLgxY-6tPTi3%RPXc&5AN#G^ib&s}L!O6z z?bTsYq+Kgh-N8t$Y3o;t>#F0%W>K}J!5oJ_@)?2g2R& z)^$FehE%CuVm~On^tm&~fX7XM9|^%Kn1enH(dt zLlRuz52G{5>7#o!aYVa58kD?3q80Zxty7y3J2@k$P_Bk+&2c}v$C-a=j}uVXXqVUc zY;}nX$OIbX<0jQkWKUnN8bmSp)yjk%1L=yFZ{waA8q2df|PUBr8X zgE+GhHG+ZG@PRAKMz2+=}$PjFV(DH_tIa-&^o6@-5AyOO~PXb+1Wjo)E zeS?C}H9*&C4d<4dt5gp^LuVWSObG>J%Eo9G7)2GKjg$64ZB3ALme-D%~z3wIU zohp0y_QQle>IRlk%0jnG)cT#7fFuw~R%P)EjAr|r?fyEG$t>@e$DewcKKDN_ihSO~ zIHHw&NT6tn{w$_(JSg~lz0vsq`E>A_05}V0T65JH=zP4cOY(VokShr(QBD5dueQK7`@tV&3`AXdsS#+0ZVqPCI zC{w1C=wje=yzm0_R2ls@%||&xq7|~(7K=l+zl!(NcZ~^qzfT4Yd~q$YJyhiGW}$og z2q+s8J2#>z1bvmHJ-zg2juK}hEs*))=X;iz?oA|O=+OEUQ(^d)27}+>Ctp{3QmlwK z$0yr#9`0Fk+K+_VhCe%$6YH!MSuZUv4AzV)8GqmDBcf zLRqaTJwYdhV|5yL4&%$7fnP?i-Q>N+udF1`!~`1BJDUlBS83&V(CV}D4BYw3PL6bu z!K)DWh0ju>knd4L-IyjVY{6<-By}VmyXwG=`c$zE>gx)$9JfK@+FRjD9#&bU!5OQ< zh;@Bn7K5pDL?BIkKtOA+#H%S#vbL66=|Pgm(YOMB_^gI0D4`DPeYR2JwXQY4rTWNm zHBs4C>r*hY9P~g`ZWZjBf{(5aY(!s_HFHo}aecWd7_V&cRGRvV!OjU4d4PG?j=?U5 z1hFGqGyjpp`+LTNfWydAs3wa&@%Tb?;9}}B4&HcX{BrWwLx@5&?fo+$7%Xd&>R4g_ zGCc*%3{kG2r<|@sLq?-YcM^pW3d^@C#&s3am3tg%a-+8~leI^{y3TDA%6N6mBX0<&n4VI^Q|o^jW=iEIBEos|-b5V>Qrptj z`cm9V)sQ+}_$UT5Zl+*a#i<{$3|$w6dq3|Xctp`os7hHUAtyYnH*M5c+xeWPMokN={Fhf zxtsgc$*nGPPSN~8o~o0r)QEw?5~!YsX?ikI9Jm!g>~Iy){=dEH#eBLq{}?8@x8 zdeu1AR`a`Tl6TA(yax9mb?ijS)A2jEJIk~z0l#gQK&3v4QQ#L1aF@dR579wqM>JU# z=Q5OaqMz@yu?sY4eI$Q;nqd5*yH|>T(Yw`#rw^AcJC49+Nw!yhZ7JNow3E9WxCUzi zI!th%$6A*xkJjMqF}9c%)6%Jwn{%qLc3(gq6}OtHuEEf@(n5=RfeN1C<{cZiXWCAY zNl;s{4NCNPZr6K=)b&E;fX3`x!%^99Xw@_0bZ?NZnl6UQ6W>I6_%#dT_H#E{mO5#f+o7`tLMJ_yLHa2_mDXUN}ocXhq@soz$l9H!#-+cg9H>AWIN)6CPg_TM5P9r<^$mEoVEuXG7~}NJ++t?VOZ~8aRj&wk8_Go9#4^wPTv?-=}lgX^=2H zve84lKutQ&E3}fc~u$gLx@|h_d?>kaHYVL*gGRqP6kPjaAyMxF<9X;t2rs8+1=Q9m_xB^-j z6uotb%4_P3!1~A?Hm%ia@lW~dai3=c6I4>p!A{?evKPR$R9Kq^Qvx-CU3Yfr^pm33 znMZH0C4rU0L+>zWi=rjWG)|>uMx440SVg9kZ}o3;sr9gGZlUK|S6!9cXDaQWF=RLg z^YX`}3+rR{)q6}c<+OKNi=sB6tT!8rZ*89(+vs@w5pCnsc6V3iveEu@b+j0A&2zKtQDnB@Llwnf z981_6u(^D5S|iJsJe3z(CM#;{{~bLI_AP0Vp;wL{cFz`Mj!lSP(AO^B*g6;VL#}Tn ze7Yxy)kV|?NbI)V_g(xB(@Migh$(;#jU9&MlRLLnpkl01FmRo2S z+7@m&jGYifz5@`z(V_2WVp2PGQME@twdYG!YNJL-htr;m*o%T3LfYe*abPVvz+a+W zQnZ2FJ&%QkWod|qiH_xwN+9HeoU^I-ImH1Tt~D-*2Oyxwfs%Tbpd?;Mr*yZEFcd`7Tf}(Q)Ng zGo3j-UWv>4+d7FFzTYjL@pLO{D*)%0B^ zD>A`qYDQ$s;Y%2dU*z$8ZbSbC&PLGQp zHz%EY**D9n#`1hhE&jfXU(mW7I?T|CH4MLh#}X+N=cTcIKQ0xvh}*g16>)j}M3%zX z!>Dz0pRAjtUHj?vM$3>CNmN>{AnfGwz4qKokkvV%zM7|bm?g}1A!UPvHQOV zP(8iVXNvfn;PfBld1B}-vTe>KsI^m`(P+kO=*AwST+grXHvgTEz9E6(Mt*(y*~JC` zF#cfyO%3cpMoNzMX4WQu7{YROEu~d)4DXWy{hYTB+ z&10hAb~FMpadFfieoEXWyxqX(j^hLqqOeMzW%%s zxm_wxZh1@0<*K05wL70!lR3nx>{BRd$QLHy+R=Qm!V-Bh&venc%J54WUI1*ykl*of z#}_wh7-RN06-;jAxGrSlxT&@!H$zMU6)Dy^D~6HwT~ypqyLFvZLxd`dVY`?_iTBpX z6RmHn3|Z#t!#;*#EF;(-AmvkI`H%s%*zDp8fDY4Zsn7T_LOwy35f#9XB5&^7{Zkfe zP#?ZEH`NkL$iGDkOTgJYOX!n=#>E{C^J{K+*VIlHN7^rXxcER-UUM=jE5m^OO0jdR! z9R5L~fhi~KnNEw0xhk(HDzjHO#p!Pagmw-wT7!Zwo!31_+%mHj$`3wjc~?b`21`|p_W%ALsv6$V)cw~yO=x40}VI8%3HHj9yI zWz;DTg-*=K!kC5JHN=E3@m234mNnw*5n;LC4{&XSXKXF3SU=eqsE8RYf$O}>3c23F zfk^R#2^#6Lt~l0>&!r57)>KIzV}7x4GN=#E6%O0nUf>BMWyJxh+_l9*^k@zl?C_Mq z6~I~mRg&)&1fT1~e7>QTS;6km$tS2M<{(p8&y=<&rl@U?z#LIxYG~x*&ar;n4)JLmT|s9V3;cwXj+!?kjlK36ZVyp)`6N*& zUUs+k>7_gKkRj)dn_Y;ckl}G_9-7WYgdR56ZSHifKo^VHcSNFDQ9Xz6-R!iX<6g4P z6pO<)2W9WTe5zE4UkDJbZ_#F0Bq(gHOs73sjxynch72kjF+3uOB}@bxhx1?^N!X{E1f}t)^R3otRK5`t*f!>3K{5>GVGAyvw}Qm5-7<>PE~2VvHNJ~X zO$nB_EN7sa5U}3K2FoAb5i~Ad4ieS=CJn91=torebR;!xtA6((zIN(Szkj zy&0m7S8bfqaN@Dkl<~u!9RmVLp)A&G_H?b5EDfR)b~Qednj@xYAf-jc&Exwq+6*LO z0B~+Lbk7h4u!G}8VhZW<}|)<5FE2%dJT^smrbT&iUdi)Vy_n zh!`bv(`jE;)BWjxbXa>tLGqBD;){jpkA$8A?i6?s2jkVwsByUNfwa|-*}U4rJ{|~8 zxv44@00R%5Ox&s|FkOIw3V46lbwTjGfR@~o>ZgNs+WRU5t21wwWAo7P(rfNdFmQtB z2$hktrpQh7m#+G0MCm&NA~{%IE>vN3700P`%SKjS`#I@+ixf)0*#1m(7}s ziSA!LhM8bJ3F?MI~-{bd?jysB@sb>N|9b?Ggxhf%Uwc0EN@k+^a|CQ(LkCBYy%NaL@mCDN z^xHKHqoJAgxjwAEyuPi$DR^A4zA3;6lP#YWgT)AZ*bw^BH#R8c2)XyI^X^l`hX}-U z#JXl#E=3$1TpV1oE{ERJc#vz7a1%zShCvMS8_=L;weP z&M?7j&nVbibs{L#aBSE*1kQHJ)Kow(<;(M81xHvg?X&3deSj)-u>J-rQ%pJ_03wDH zUR*?<9rBOE5F8jxHsywX60G8gzLCdkzy7A{bIJr{%C8d-dgXthz4AYdY(C1{+t@lV zeYCL${ezMGKibFZ&_%?_0(+S;0&gV0lAH_JdBbDSNl^&tD8Pl1qVkSkggeKkzK_@x&GNQrmpyr%6hd9{I=hH(yf@sSSbez;zpZ#MsrztzS0U& z|9vSM*xLSYCH_+1KezO_ZtK-wiP5I}};x&0alcwS?1cR&kX z&~k!5?|)*GL%n9L~@X zFmNG(bOBg1P3AeI9bkL}5I2Yr$G&ytV79IfV~cVhnA%S>A1JAt$P9?QSdJMhdPnqV z1BU%Rs%Z SK>z^p^+)--j89m8UHv~GU_-P3 literal 0 HcmV?d00001 diff --git a/新建 文本文档.txt b/新建 文本文档.txt new file mode 100644 index 0000000..d6089c3 --- /dev/null +++ b/新建 文本文档.txt @@ -0,0 +1 @@ +playSpecific无结果,位置:Main.java