diff --git a/.gitignore b/.gitignore index 480bdf5..cc2bdba 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,10 @@ build/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +# 开发辅助文件(本地保留,不推送到仓库) +txt/ +*.xlsx +*.patch +新建*.txt \ No newline at end of file diff --git a/pom.xml b/pom.xml index e49b0db..2eac67e 100644 --- a/pom.xml +++ b/pom.xml @@ -62,5 +62,4 @@ - diff --git a/src/main/java/Sdf1_login/ChatFilterManager.java b/src/main/java/Sdf1_login/ChatFilterManager.java index 013b7f6..91dabc3 100644 --- a/src/main/java/Sdf1_login/ChatFilterManager.java +++ b/src/main/java/Sdf1_login/ChatFilterManager.java @@ -1,564 +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 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 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/src/main/java/Sdf1_login/ChatInputManager.java b/src/main/java/Sdf1_login/ChatInputManager.java index e564b28..233546a 100644 --- a/src/main/java/Sdf1_login/ChatInputManager.java +++ b/src/main/java/Sdf1_login/ChatInputManager.java @@ -1,823 +1,824 @@ -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); - } - } -} +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; + } + } +} diff --git a/src/main/java/Sdf1_login/ConfigManager.java b/src/main/java/Sdf1_login/ConfigManager.java index f91a00d..5bb0609 100644 --- a/src/main/java/Sdf1_login/ConfigManager.java +++ b/src/main/java/Sdf1_login/ConfigManager.java @@ -25,7 +25,10 @@ public class ConfigManager { public long smtpVerifyExpire = 0; public long smtpLastSendTime = 0; public String smtpConfiguringPlayer = ""; - public String updatePlatform = "gh"; + public String broadcastServerUrl = ""; + public int httpPort = 8080; + public String garbageTag= "admin"; + public ConfigManager(File dataFolder) { @@ -321,11 +324,8 @@ public class ConfigManager { garbageMaxRounds = parseInt( m.getOrDefault("垃圾站_保留轮数", "1"), 1); - - + // loadSettings() 末尾添加: } - - public void saveSettings() { File file = new File(dataFolder, "插件设置.txt"); List L = new ArrayList<>(); @@ -344,8 +344,7 @@ public class ConfigManager { L.add("垃圾站_清理间隔秒=" + garbageInterval); L.add("垃圾站_保留轮数=" + garbageMaxRounds); - - writeLines(file, L); + writeLines(file, L); } public double providerPointsPerScore = 2.0; public int baseEconomyReward = 100; @@ -375,6 +374,9 @@ public class ConfigManager { L.add("垃圾站_启用=true"); L.add("垃圾站_清理间隔秒=300"); L.add("垃圾站_保留轮数=1"); + L.add("# ---- 资源包(HTTP) ----"); + L.add("resource-pack-url="); + L.add("http-port=8080"); writeLines(f, L); diff --git a/src/main/java/Sdf1_login/DatabaseManager.java b/src/main/java/Sdf1_login/DatabaseManager.java index d3816a3..95102f5 100644 --- a/src/main/java/Sdf1_login/DatabaseManager.java +++ b/src/main/java/Sdf1_login/DatabaseManager.java @@ -213,18 +213,6 @@ 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) { @@ -873,8 +861,9 @@ 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 6c60ac6..8651270 100644 --- a/src/main/java/Sdf1_login/GUIManager.java +++ b/src/main/java/Sdf1_login/GUIManager.java @@ -5,6 +5,7 @@ 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.*; @@ -41,7 +42,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、求助、举报")); @@ -105,6 +106,32 @@ public class GUIManager { p.openInventory(g); } + // 在 openGiftStages 方法之前添加: + + /** + * 获取任务列表(带真实完成状态) + * 使用 QuestTracker 解析 .md 文件 + */ + public List> getQuestList( + String category) { + QuestTracker qt = plugin.getQuestTracker(); + if (qt == null) return new ArrayList<>(); + List quests = + qt.getQuests(category); + List> list = + new ArrayList<>(); + for (QuestTracker.QuestFile qf : quests) { + Map map = + new LinkedHashMap<>(); + map.put("name", qf.displayName); + map.put("conditions", qf.conditions); + map.put("rewards", qf.rewards); + map.put("questFile", qf); + list.add(map); + } + return list; + } + /** * 打开账号删除选择面板 * 展示该玩家IP下所有账号,供选择删除 @@ -370,7 +397,6 @@ public class GUIManager { "§7双击管理")); slot++; } - g.setItem(53, mkItem(Material.ARROW, "§7返回")); p.openInventory(g); } @@ -379,279 +405,154 @@ 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[" - + newInfo + "]", - newLore.toArray(new String[0]))); - - // 主线任务 - List mainQuests = - qt.getQuests("主线任务"); + "§e§l新人任务", + "§7完成新手阶段获取奖励")); g.setItem(12, mkItem(Material.COMPASS, - "§b§l主线任务 §7[" - + qt.getCompletedStageCount( - player, mainQuests) - + "/" + mainQuests.size() - + "]", + "§b§l主线任务", "§7查看主线任务列表")); - - // 支线任务 - List sideQuests = - qt.getQuests("支线任务"); g.setItem(14, mkItem(Material.PAPER, - "§a§l支线任务 §7[" - + qt.getCompletedStageCount( - player, sideQuests) - + "/" + sideQuests.size() - + "]", + "§a§l支线任务", "§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返回")); + g.setItem(22, mkItem(Material.ARROW, "§7返回")); p.openInventory(g); } - - public void openGiftStages(Player p) { Inventory g = Bukkit.createInventory( - null, 54, T_GIFT_STAGES); + null, 27, T_GIFT_STAGES); fillBg(g); - QuestTracker qt = - plugin.getQuestTracker(); + QuestTracker qt = plugin.getQuestTracker(); + if (qt == null) { + g.setItem(22, mkItem(Material.BARRIER, + "§7任务系统未加载")); + p.openInventory(g); + return; + } + List quests = qt.getQuests("新人任务"); - int total = quests.size(); - int completed = - qt.getCompletedStageCount( - p.getName(), quests); - QuestTracker.QuestFile current = - qt.getCurrentStage( - p.getName(), quests); + String playerName = p.getName(); - 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); + int max = quests.size(); + for (int i = 0; i < max && i < 9; i++) { + QuestTracker.QuestFile qf = quests.get(i); + boolean completed = + qt.isStageCompleted(playerName, qf); + boolean claimed = + qt.hasClaimed(playerName, qf); Material mat; - List lore = new ArrayList<>(); - - if (done) { + String status; + if (completed && claimed) { mat = Material.LIME_WOOL; - } else if (isCurrent) { + status = "§a已领取"; + } else if (completed) { mat = Material.YELLOW_WOOL; + status = "§e可领取"; } else { mat = Material.RED_WOOL; + status = "§c未达标"; } - 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已领取奖励"); + // 显示进度 + List lore = new ArrayList<>(); + lore.add("§7" + status); + for (String cond : qf.conditions) { + String progress = + qt.getProgress(playerName, cond); + lore.add("§7" + progress); } g.setItem(10 + i, mkItem(mat, "§e" + qf.displayName, lore.toArray(new String[0]))); } - // 返回按钮 - g.setItem(53, mkItem( - Material.ARROW, "§7返回")); + g.setItem(26, 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, + public void openTaskList(Player p, String folderName) { + Inventory g = Bukkit.createInventory(null, 54, "§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; + // [FIX] 使用 QuestTracker 获取真实任务数据 + QuestTracker qt = plugin.getQuestTracker(); + if (qt == null) { + g.setItem(22, mkItem(Material.BARRIER, + "§7任务系统未加载")); + p.openInventory(g); + return; + } + + List quests = + qt.getQuests(folderName); + String playerName = p.getName(); + + if (quests.isEmpty()) { + g.setItem(22, mkItem(Material.BARRIER, + "§7暂无任务")); + p.openInventory(g); + return; + } + + int slot = 0; + for (QuestTracker.QuestFile qf : quests) { + if (slot >= 45) break; + + boolean completed = + qt.isStageCompleted(playerName, qf); + boolean claimed = + qt.hasClaimed(playerName, qf); + + Material mat; + String status; + if (completed && claimed) { + mat = Material.LIME_WOOL; + status = "§a✓ 已完成并领取"; + } else if (completed) { + mat = Material.YELLOW_WOOL; + status = "§e✓ 已完成 - 可领取奖励"; + } else { + mat = Material.RED_WOOL; + status = "§c未完成"; + } List lore = new ArrayList<>(); + lore.add(status); + lore.add("§7───── 条件 ─────"); - // 解锁条件 - 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)); - } + // 显示每个条件及完成状态 + for (String cond : qf.conditions) { + String progress = + qt.getProgress(playerName, cond); + if (qt.isOptional(cond)) { + lore.add("§7[可选] " + progress); + } else { + lore.add("§e" + progress); } } - // 任务详情 - 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──── 任务奖励 ────"); + lore.add("§7───── 奖励 ─────"); for (String r : qf.rewards) { lore.add("§a" + r); } } - g.setItem(i, mkItem(mat, - (done ? "§a✓ " : "§e") - + qf.displayName, + g.setItem(slot, mkItem(mat, + "§e" + qf.displayName, lore.toArray(new String[0]))); + slot++; } - if (tasks.isEmpty()) { - g.setItem(22, mkItem( - Material.BARRIER, - "§7暂无任务")); - } - g.setItem(size - 1, mkItem( - Material.ARROW, "§7返回")); + g.setItem(53, mkItem(Material.ARROW, "§7返回")); p.openInventory(g); } @@ -683,81 +584,11 @@ 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 67c52a0..23f80d2 100644 --- a/src/main/java/Sdf1_login/InviteManager.java +++ b/src/main/java/Sdf1_login/InviteManager.java @@ -1,89 +1,96 @@ -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; - } -} +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; + } +} diff --git a/src/main/java/Sdf1_login/LoginManager.java b/src/main/java/Sdf1_login/LoginManager.java index a32263a..5b3db8c 100644 --- a/src/main/java/Sdf1_login/LoginManager.java +++ b/src/main/java/Sdf1_login/LoginManager.java @@ -1,302 +1,308 @@ -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); + p.setAllowFlight(false); + p.setFlying(false); + 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().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); + p.setAllowFlight(false); + p.setFlying(false); + 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 e206751..95b80e5 100644 --- a/src/main/java/Sdf1_login/Main.java +++ b/src/main/java/Sdf1_login/Main.java @@ -1,5 +1,6 @@ package Sdf1_login; +import java.io.File; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; @@ -9,28 +10,16 @@ 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 org.bukkit.SoundCategory; 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; @@ -46,6 +35,7 @@ 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; @@ -58,6 +48,13 @@ import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; import org.bukkit.scheduler.BukkitRunnable; +// 在现有import区域最后添加: +import java.util.HashSet; +import java.util.Collections; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.inventory.CraftItemEvent; +import org.bukkit.event.player.PlayerFishEvent; + public class Main extends JavaPlugin implements CommandExecutor, Listener, @@ -81,6 +78,19 @@ public class Main extends JavaPlugin private TicketManager ticket; private ChatFilterManager chatFilter; private GarbageManager garbage; + public RadioManager radio; + // 在 lastActivity 字段后面添加: + // [ADDED] 任务追踪系统 + private QuestTracker questTracker; + // [FIX] 垃圾箱防抖锁(记录上次操作时间戳) + private final Map garbageBusy = + new ConcurrentHashMap<>(); + // [ADDED] 群系检测节流 + private final Map lastBiomeCheck = + new ConcurrentHashMap<>(); + // 在 private WelcomeManager welcome; 字段(若不存在)添加: + private WelcomeManager welcome; + private final Set loggedIn = new TreeSet<>( @@ -88,9 +98,6 @@ 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 = @@ -101,20 +108,17 @@ 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 CommissionManager commission; + public CommissionManager getCommission() { + return commission; + } private static class PwdRollback { final String hash; final String salt; final long time; + PwdRollback(String h, String s, long t) { hash = h; salt = s; @@ -137,34 +141,87 @@ public class Main extends JavaPlugin 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; + + 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 QuestTracker getQuestTracker() { + return questTracker; + } + + public ChatFilterManager getChatFilter() { + return chatFilter; + } + + public GarbageManager getGarbage() { + return garbage; + } + + public Set getLoggedIn() { + return loggedIn; + } + + public Set getNeedsPasswordChange() { + return needsPasswordChange; + } + // public RadioManager radio; // ===== Enable/Disable ===== @Override @@ -205,13 +262,20 @@ public class Main extends JavaPlugin garbage.init(); garbage.loadConfig(); garbage.startAutoCleanup(); + // 先初始化 commission + commission = new CommissionManager(this); + // 再初始化 questTracker(依赖commission) + questTracker = new QuestTracker(this); chatFilter = new ChatFilterManager(this); chatFilter.loadConfig(); + // [ADDED] 重载radio和任务 + if (radio != null) radio.reload(); + if (questTracker != null) { + questTracker.shutdown(); + questTracker = new QuestTracker(this); + } + // [ADDED] 初始化欢迎仪式 welcome = new WelcomeManager(this); - updateChecker = new UpdateChecker(this); - questTracker = new QuestTracker(this); - commission = new CommissionManager(this); - setupEconomy(); @@ -243,8 +307,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); @@ -253,33 +317,34 @@ public class Main extends JavaPlugin startTimeoutCheck(); startPasswordReminder(); startTicketAutoProcess(); - startPlayTimeTracker(); - // 启动后异步检查更新 - updateChecker.checkOnEnable(); + // [ADDED] 启动全服广播 + radio.startMainRadio(); + // [ADDED] 在线时间追踪 + new BukkitRunnable() { + public void run() { + if (questTracker == null) return; + for (Player p : Bukkit.getOnlinePlayers()) { + if (!isFrozen(p)) + questTracker.onPlayTime(p.getName(), 30000L); + } + } + }.runTaskTimer(this, 600L, 600L); getLogger().info( "Sdf1_login v1.0 | 就绪"); - for (Player online - : Bukkit.getOnlinePlayers()) { - radio.sendResourcePack(online); + for (Player online : Bukkit.getOnlinePlayers()) { } } - @Override + @Override public void onDisable() { - if (questTracker != null) { - questTracker.shutdown(); - } - if (radio != null) { - radio.stopAllLoginRadio(); - radio.stopMainRadio(); - } - - if (questTracker != null) { - } + if (db != null) db.close(); + if (garbage != null) garbage.close(); + if (questTracker != null) + questTracker.shutdown(); } -// 没替换,只是粗略看了下,有几个问题 + private void setupEconomy() { if (getServer().getPluginManager() .getPlugin("Vault") == null) return; @@ -290,15 +355,6 @@ 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() { @@ -501,36 +557,6 @@ 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); } @@ -661,8 +687,6 @@ public class Main extends JavaPlugin req.id))); return; } - radio.stopLoginRadio(p); - } String salt = PasswordUtils.generateSalt(); @@ -679,7 +703,10 @@ public class Main extends JavaPlugin } loggedIn.add(name); - removeFlightOnce.add(name); + p.setAllowFlight(false); + p.setFlying(false); + + db.setLoggedIn(name, true); db.setField(name, "last_login_time", @@ -691,99 +718,16 @@ public class Main extends JavaPlugin 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您已自动登录!"); + if (welcome != null) welcome.onLogin(p); 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) { @@ -868,6 +812,35 @@ 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( @@ -1037,6 +1010,7 @@ public class Main extends JavaPlugin sb.append("§c§l============================"); return sb.toString(); } + /** * 读取备份,返回物品名预览 */ @@ -1227,20 +1201,7 @@ public class Main extends JavaPlugin } } - @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); - } - + // ===== Events ===== @EventHandler public void onJoin(PlayerJoinEvent e) { Player p = e.getPlayer(); @@ -1250,6 +1211,7 @@ 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)) @@ -1271,10 +1233,6 @@ 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) @@ -1291,6 +1249,16 @@ public class Main extends JavaPlugin return; } } + // [ADDED] 资源包(延迟发送,不强制) + if (radio != null) { + final Player jp = p; + Bukkit.getScheduler().runTaskLater( + this, () -> { + if (jp.isOnline()) { + radio.sendResourcePack(jp); + } + }, 60L); + } if (isOnlineMode) { autoLogin(p, "premium"); @@ -1307,6 +1275,18 @@ public class Main extends JavaPlugin afk.remove(p.getUniqueId()); afk.recordAction(p.getUniqueId()); hideInventoryDelayed(p, 0); + // [ADDED] 发送资源包 + if (radio != null) radio.sendResourcePack(p); + // [ADDED] 发送资源包(延迟5秒确保客户端就绪) + if (radio != null) { + final Player jp = p; + Bukkit.getScheduler().runTaskLater(this, + () -> { + if (jp.isOnline()) { + radio.sendResourcePack(jp); + } + }, 100L); + } verification.verifyPremiumAsync(p, @@ -1331,43 +1311,23 @@ 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()) { @@ -1401,7 +1361,8 @@ public class Main extends JavaPlugin double exp = p.getExp(); debugPrintBase64( - "退出保存", name, contents); + "退出保存", name, + contents); db.saveInventoryBackup(name, contents, armor, extra, level, exp); @@ -1414,7 +1375,8 @@ public class Main extends JavaPlugin } else { getLogger().info( "[BACKUP_DEBUG] 退出跳过 " - + name + ": 背包为空" + + name + + ": 背包为空" + "(插件已清空)"); } @@ -1424,12 +1386,10 @@ 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 = @@ -1535,63 +1495,53 @@ public class Main extends JavaPlugin 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()) { + // 冻结处理(未登录玩家不能移动) + if (isFrozen(p)) { + if (from.getBlockX() == to.getBlockX() + && from.getBlockY() == to.getBlockY() + && from.getBlockZ() == to.getBlockZ()) { + return; + } + e.setCancelled(true); 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) + if (isFrozen(p)) { + e.setCancelled(true); return; - if (!isMenuSnowball(item)) return; - if (isFrozen(p)) return; - if (!loggedIn.contains(p.getName())) return; - e.setCancelled(true); - gui.openMain(p); + } + + // 同时检查主手和副手 + ItemStack hand = p.getInventory() + .getItemInMainHand(); + ItemStack offhand = p.getInventory() + .getItemInOffHand(); + + if (isMenuSnowball(hand) + || isMenuSnowball(offhand)) { + e.setCancelled(true); + gui.openMain(p); + } } + @EventHandler public void onBlockBreak(BlockBreakEvent e) { Player p = e.getPlayer(); @@ -1600,6 +1550,11 @@ public class Main extends JavaPlugin return; } afk.recordAction(p.getUniqueId()); + // [ADDED] 任务计数 + if (questTracker != null) + questTracker.onBlockMined(p.getName(), + e.getBlock().getType().name()); + } @EventHandler @@ -1614,7 +1569,6 @@ public class Main extends JavaPlugin e.setCancelled(true); return; } - // 禁止丢弃菜单雪球 ItemStack drop = e.getItemDrop().getItemStack(); if (isMenuSnowball(drop)) { @@ -1622,6 +1576,7 @@ public class Main extends JavaPlugin } } + @EventHandler public void onPickup(PlayerPickupItemEvent e) { if (isFrozen(e.getPlayer())) @@ -1649,6 +1604,39 @@ public class Main extends JavaPlugin e.setCancelled(true); } + // [ADDED] 击杀追踪 + @EventHandler + public void onEntityDeath(EntityDeathEvent e) { + if (questTracker == null) return; + Player killer = e.getEntity().getKiller(); + if (killer == null || isFrozen(killer)) return; + if (e.getEntity() instanceof Player) + questTracker.onPlayerKill(killer.getName()); + else + questTracker.onMobKill(killer.getName(), + e.getEntityType().name()); + } + + // [ADDED] 合成追踪 + @EventHandler + public void onCraft(CraftItemEvent e) { + if (e.getWhoClicked() instanceof Player) { + Player p = (Player) e.getWhoClicked(); + if (!isFrozen(p) && questTracker != null) + questTracker.onItemCrafted(p.getName()); + } + } + + // [ADDED] 钓鱼追踪 + @EventHandler + public void onFish(PlayerFishEvent e) { + Player p = e.getPlayer(); + if (p == null || isFrozen(p)) return; + if (questTracker != null + && e.getState() == PlayerFishEvent.State.CAUGHT_FISH) + questTracker.onFishCaught(p.getName()); + } + @EventHandler(priority = EventPriority.LOWEST) public void onChat(AsyncPlayerChatEvent e) { Player p = e.getPlayer(); @@ -1672,35 +1660,13 @@ 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)); - } + // [ADDED] 任务聊天计数 + if (questTracker != null) + questTracker.onPlayerChat(p.getName()); 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( @@ -1711,13 +1677,13 @@ public class Main extends JavaPlugin List urls = chatFilter.extractUrls(msg); if (urls.isEmpty()) return; - for (String u : urls) { - if (chatFilter.isPlayerWhitelisted(u)) - continue; - if (!chatFilter.isLikelyDomain(u)) + if (chatFilter.isWhitelisted(u)) continue; + // [FIX] 增强域名检测:支持中文域名 + if (!isLikelyUrl(u)) continue; + e.setCancelled(true); chatFilter.incrementViolation( p.getName()); @@ -1752,19 +1718,64 @@ public class Main extends JavaPlugin } } } - if (chatFilter.isNotifyAll()) { - Bukkit.broadcastMessage(chatFilter - .msg("chat_url_broadcast") - .replace("{player}", - p.getName())); + Bukkit.broadcastMessage( + chatFilter + .msg("chat_url_broadcast") + .replace("{player}", + p.getName())); } } } + /** + * 增强URL检测:支持中文域名+新后缀 + * 匹配 http(s)://xxx 或裸域名 xxx.xxx + */ + private boolean isLikelyUrl(String text) { + if (text.toLowerCase().startsWith("http://") + || text.toLowerCase() + .startsWith("https://")) { + return true; + } + boolean hasChinese = false; + for (int i = 0; i < text.length(); i++) { + int code = text.codePointAt(i); + if (code >= 0x4E00 && code <= 0x9FFF) { + hasChinese = true; + break; + } + if (code >= 0x3400 && code <= 0x4DBF) { + hasChinese = true; + break; + } + } + String[] commonTlds = { + ".com", ".net", ".org", ".cn", + ".top", ".xyz", ".cc", ".icu", + ".online", ".site", ".club", + ".vip", ".shop", ".live", + ".app", ".dev", ".me", ".io", + ".co", ".tk", ".ml" + }; + String lower = text.toLowerCase().trim(); + for (String tld : commonTlds) { + if (lower.contains(tld)) return true; + } + if (hasChinese + && lower.matches( + ".*\\.[a-z]{2,}.*")) { + return true; + } + if (lower.matches( + "[\\w\\u4e00-\\u9fff" + + "\\u3400-\\u4dbf" + + "\\.-]+\\.[a-z]{2,}")) { + return true; + } + return false; + } - - // ===== Inventory Click ===== // ===== Inventory Click ===== @EventHandler public void onInvClick(InventoryClickEvent e) { @@ -1778,127 +1789,16 @@ public class Main extends JavaPlugin 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任务面板")) { + && !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) + if (title.equals(TicketManager.T_TICKET_MAIN) || title.equals( TicketManager.T_TICKET_CREATE) || title.equals( @@ -1907,35 +1807,29 @@ public class Main extends JavaPlugin TicketManager.T_TICKET_ADMIN) || title.equals( TicketManager.T_TICKET_DETAIL) - || title.equals( - "§d§l服务商面板") - || title.equals( - "§6§l抢单大厅") - || title.equals( - "§b§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; } // 垃圾回收站 if (title.equals("§6§l垃圾回收站")) { if (slot < 0 || slot >= 54) return; + e.setCancelled(true); // 功能格 49-53 if (slot >= 49 && slot <= 53) { - e.setCancelled(true); - garbage.handleClick( - p, title, slot); + garbage.handleClick(p, title, slot); return; } ItemStack inSlot = e.getInventory().getItem(slot); - ItemStack cursor = e.getCursor(); - // ===== 取出 ===== + // ===== 取出物品(点击垃圾箱格子) ===== if (inSlot != null && inSlot.hasItemMeta() && inSlot.getItemMeta().hasLore()) { @@ -1944,7 +1838,6 @@ public class Main extends JavaPlugin if (lore != null && !lore.isEmpty() && lore.get(0) .startsWith("§7ID: #")) { - e.setCancelled(true); try { String numStr = lore.get(0) .replace("§7ID: #", "") @@ -1970,34 +1863,42 @@ public class Main extends JavaPlugin } } - // ===== 光标存入 ===== - if (cursor != null - && cursor.getType() + // ===== Shift+Click 存入(仅在垃圾箱界面生效) ===== + if (e.isShiftClick() + && e.getClickedInventory() != null + && e.getClickedInventory() + == p.getInventory() + && e.getCurrentItem() != null + && e.getCurrentItem().getType() != Material.AIR - && !isMenuSnowball(cursor)) { - ItemStack toSave = cursor.clone(); - getLogger().info( - "[垃圾站] 存入: " - + toSave.getType() - + " x" + toSave.getAmount()); + && !isMenuSnowball( + e.getCurrentItem())) { + long now = System.currentTimeMillis(); + Long last = garbageBusy + .get(p.getUniqueId()); + if (last != null + && now - last < 200L) { + return; + } + garbageBusy.put(p.getUniqueId(), now); + + ItemStack toSave = + e.getCurrentItem().clone(); + p.getInventory() + .setItem(e.getSlot(), null); garbage.saveItem(toSave); - e.setCancelled(true); - Bukkit.getScheduler().runTaskLater( - this, () -> { - p.getOpenInventory() - .setCursor(null); - garbage.openRecyclePage(p, + Bukkit.getScheduler() + .runTaskLater(this, () -> { + garbage.openRecyclePage( + p, garbage.getRecyclePage( p.getName())); }, 1L); - return; } - - // 默认取消 - e.setCancelled(true); return; } + // 主菜单 if (title.equals(GUIManager.T_MAIN)) { e.setCancelled(true); @@ -2014,9 +1915,7 @@ public class Main extends JavaPlugin .contains(config.adminTag)) { garbage.openRecycle(p); } - } - - else if (slot == 22 && isAdmin(p)) { + } else if (slot == 22 && isAdmin(p)) { p.closeInventory(); chatInput.getState(p).type = ChatInputManager.InputType @@ -2025,6 +1924,71 @@ public class Main extends JavaPlugin } return; } + // 新人礼包(QuestTracker版) + if (title.equals(GUIManager.T_GIFT_STAGES)) { + e.setCancelled(true); + if (slot == 26) { + gui.openTaskCenter(p); + } else if (slot >= 10 && slot <= 18) { + QuestTracker qt = getQuestTracker(); + if (qt == null) return; + List quests = + qt.getQuests("新人任务"); + int idx = slot - 10; + if (idx >= 0 && idx < quests.size()) { + QuestTracker.QuestFile qf = + quests.get(idx); + String pn = p.getName(); + if (qt.hasClaimed(pn, qf)) { + p.sendMessage( + "§c该阶段奖励已领取"); + } else if (!qt.isStageCompleted( + pn, qf)) { + p.sendMessage( + "§c条件未满足"); + } else { + qt.claimRewards(p, qf); + qt.markClaimed(pn, qf); + gui.openGiftStages(p); + playSuccessSound(p); + } + } + } + return; + } + + // 主线/支线任务 + if (title.equals("§d§l主线任务") + || title.equals("§d§l支线任务")) { + e.setCancelled(true); + if (slot == e.getInventory().getSize() - 1) { + gui.openTaskCenter(p); + } else if (questTracker != null) { + // 点击任务项 → 尝试领取奖励 + String cat = title.contains("主线") + ? "主线任务" : "支线任务"; + List quests = + questTracker.getQuests(cat); + if (slot < quests.size()) { + QuestTracker.QuestFile qf = + quests.get(slot); + if (!questTracker.isStageCompleted( + p.getName(), qf)) { + p.sendMessage("§c条件未满足"); + } else if (questTracker.hasClaimed( + p.getName(), qf)) { + p.sendMessage("§c已领取过"); + } else { + questTracker.claimRewards(p, qf); + questTracker.markClaimed( + p.getName(), qf); + gui.openTaskList(p, cat); + playSuccessSound(p); + } + } + } + return; + } // 我的信息 @@ -2137,51 +2101,24 @@ public class Main extends JavaPlugin return; } - // 新人任务 + // 新人礼包 if (title.equals( GUIManager.T_GIFT_STAGES)) { e.setCancelled(true); - if (slot == 53) { + if (slot == 26) 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该阶段尚未完成"); - } - } + 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")); } return; } - - // 邀请 if (title.equals(GUIManager.T_INVITE)) { e.setCancelled(true); @@ -2250,8 +2187,6 @@ 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()) { @@ -2261,7 +2196,7 @@ public class Main extends JavaPlugin } else { p.sendMessage("§c玩家不在线"); } - } else if (slot == 16) { + } else if (slot == 16) { chatInput.getState(p).type = @@ -2312,6 +2247,21 @@ 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); @@ -2339,171 +2289,6 @@ 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) { @@ -2511,7 +2296,6 @@ public class Main extends JavaPlugin return; Player p = (Player) e.getWhoClicked(); if (isFrozen(p)) { - frozenFlightPlayers.add(p.getName()); e.setCancelled(true); return; } @@ -2519,44 +2303,50 @@ public class Main extends JavaPlugin // 回收站拖拽 if (t.equals("§6§l垃圾回收站")) { - for (int rawSlot : - e.getRawSlots()) { + for (int rawSlot : e.getRawSlots()) { if (rawSlot >= 49) { e.setCancelled(true); return; } } - // 0-48允许拖入,不取消 - // 延迟2tick后扫描格子 + // 取消所有拖拽,改用Shift+Click + e.setCancelled(true); + 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); + // 取出光标上的物品 + final ItemStack curItem = + fp.getOpenInventory() + .getCursor(); + if (curItem != null + && curItem.getType() + != Material.AIR + && !isMenuSnowball(curItem)) { + // 1. 保存到数据库 + garbage.saveItem(curItem.clone()); + // 2. 清空光标 + fp.getOpenInventory() + .setCursor(null); + // 3. 从背包移除 + fp.getInventory() + .removeItem( + curItem.clone()); + // 4. 刷新GUI + Bukkit.getScheduler() + .runTaskLater(this, + () -> { + garbage.openRecyclePage( + fp, + garbage + .getRecyclePage( + fp + .getName())); + }, 2L); + } return; } + + //服务商工单系统 if (t.equals(TicketManager.T_TICKET_MAIN) || t.equals( @@ -2601,16 +2391,6 @@ 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("垃圾清理")) { @@ -2655,7 +2435,7 @@ public class Main extends JavaPlugin } return true; } -// /reg + if (cmdName.equals("reg") || cmdName.equals("注册")) { if (!(sender instanceof Player)) { @@ -2671,6 +2451,12 @@ public class Main extends JavaPlugin } try { loginMgr.handleRegister(p, args); + // [ADDED] 欢迎仪式 + if (welcome != null + && loggedIn.contains(p.getName())) { + welcome.onRegister(p); + } + } catch (Exception e) { getLogger().severe( "[Sdf1_login] reg命令异常: " @@ -2678,24 +2464,10 @@ 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("登录")) { @@ -2706,15 +2478,24 @@ 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; } try { loginMgr.handleLogin(p, args); + // [FIX] 手动登录后关闭飞行(仅一次) + if (loggedIn.contains(p.getName()) + && p.getAllowFlight()) { + p.setAllowFlight(false); + p.setFlying(false); + } + + // [ADDED] 欢迎仪式 + if (welcome != null + && loggedIn.contains(p.getName())) { + welcome.onLogin(p); + } + } catch (Exception e) { getLogger().severe( "[Sdf1_login] login命令异常: " @@ -2722,17 +2503,6 @@ 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; } @@ -2896,19 +2666,6 @@ 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")) { @@ -2966,6 +2723,7 @@ public class Main extends JavaPlugin config.loadSmtp(); config.loadSettings(); chatFilter.loadConfig(); + if (radio != null) radio.reload(); sender.sendMessage("§a配置已重载!"); return true; } @@ -3108,556 +2866,619 @@ public class Main extends JavaPlugin "§e§l================================"); } } - // radio - 播放/停止广播 + // radio子命令(控制台+玩家) + // sdf1_login radio → 停止全服广播 + // sdf1_login radio reload → 重载 + // sdf1_login radio <歌名> → 全服播放 + // sdf1_login radio <歌名> <玩家名> → 指定玩家播放 + // sdf1_login radio <歌名> login → 推送给登录中玩家 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") - || sub.equals("password")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); + if (args.length < 2) { + // 无参数 = 停止 + radio.stopMainRadio(); + sender.sendMessage("§c全服广播已停止"); return true; } - Player p2 = (Player) sender; + String arg1 = args[1]; + + // reload + if (arg1.equalsIgnoreCase("reload")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; + } + radio.reload(); + sender.sendMessage("§aRadio已重载"); + return true; + } + + // radio <歌名> → 全服播放 + // radio <歌名> <玩家名> → 指定玩家 + // radio <歌名> login → 登录阶段播放 + String songName = arg1; 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; - } + String target = args[2]; - 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请输入登录密码以确认修改:"); + if (target.equalsIgnoreCase("login")) { + if (radio.hasSong(songName)) { + radio.playToLoginPlayers(songName); + sender.sendMessage( + "§a已推送 §e" + songName + + " §a给登录中玩家"); + } else { + sender.sendMessage( + "§c未找到歌曲: " + songName); + } } 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发送失败"); - } - }); - }); + Player targetP = + Bukkit.getPlayer(target); + if (targetP != null + && targetP.isOnline()) { + if (radio.hasSong(songName)) { + radio.playToPlayer( + targetP, songName); + sender.sendMessage( + "§a已向 §e" + target + + " §a播放 §e" + + songName); + targetP.sendMessage( + "§6§l[广播] §f正在播放: §e" + + songName); + } else { + sender.sendMessage( + "§c未找到歌曲: " + + songName); + } + } else { + sender.sendMessage( + "§c玩家不在线: " + + target); + } + return true; } } else { + if (radio.hasSong(songName)) { + radio.playToAll(songName); + sender.sendMessage( + "§a全服播放: §e" + songName); + } else { + sender.sendMessage( + "§c未找到歌曲: " + songName); + } + return true; + } + + + // 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()); + } + 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 - .SET_EMAIL; - p2.sendMessage("§e请输入邮箱:"); - } - return true; - } - - - // sign - if (sub.equals("sign")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); + .CHANGE_PWD_STEP1; + p2.sendMessage( + "§e请输入当前密码:"); 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仅玩家可用"); + // 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; } - loginMgr.handleReset( - (Player) sender); - return true; - } - // undo - if (sub.equals("undo")) { - if (!(sender instanceof Player)) { - sender.sendMessage("§c仅玩家可用"); + + // 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; } - Player p2 = (Player) sender; - PwdRollback rb = - pwdRollback.get(p2.getName()); - if (rb == null) { - p2.sendMessage("§c无可撤销记录"); + + // reset + if (sub.equals("reset")) { + if (!(sender instanceof Player)) { + sender.sendMessage("§c仅玩家可用"); + return true; + } + loginMgr.handleReset( + (Player) sender); return true; } - if (System.currentTimeMillis() - rb.time - > 600000L) { + + // 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("§c撤销已过期"); + p2.sendMessage("§a密码已撤销!"); 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权限不足"); + // 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; } - 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); + // 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 <玩家名>"); + 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 body = "玩家 " + tgt + + ":\n您的临时密码已由管理员设置。\n" + + "临时密码将在5分钟后过期,请尽快登录并修改密码。"; + final String to = emailAddr; + final String finalBody = body; + Bukkit.getScheduler() + .runTaskAsynchronously(this, + () -> email + .sendTempPassword( + to, tgt, + finalBody)); + } return true; } - Player tp = Bukkit.getPlayer( - args[1]); - if (tp == null || !tp.isOnline()) { + + // 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( - "§c玩家不在线: " + "§a已发放雪球菜单给 " + 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) { - } + // shopadd + if (sub.equals("shopadd")) { + if (!isAdmin(sender)) { + sender.sendMessage("§c权限不足"); + return true; } - 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"); + 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; } - - // 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) */ @@ -3692,6 +3513,7 @@ public class Main extends JavaPlugin p.sendMessage( "§e§l=================================="); } + private int findEmptySlot(Inventory inv) { for (int i = 0; i < 49; i++) { ItemStack item = inv.getItem(i); @@ -3719,7 +3541,7 @@ public class Main extends JavaPlugin "get", "del", "set", "take", "add", "kick", "ticket", "oa", "stop", - "shopadd", "shopdel","back","update")); + "shopadd", "shopdel", "back", "radio", "back")); } else { list.addAll(Arrays.asList( "pw", "email", "sign", @@ -3737,5 +3559,4 @@ public class Main extends JavaPlugin } return list; } - -} \ No newline at end of file +} diff --git a/src/main/java/Sdf1_login/QuestTracker.java b/src/main/java/Sdf1_login/QuestTracker.java index 27132c5..103284c 100644 --- a/src/main/java/Sdf1_login/QuestTracker.java +++ b/src/main/java/Sdf1_login/QuestTracker.java @@ -23,7 +23,7 @@ import java.util.regex.Pattern; public class QuestTracker { private final Plugin plugin; - private final PlayerStorage storage; + private final QuestStorage storage; private final Map playerBiome = new HashMap<>(); private final Map> @@ -76,9 +76,9 @@ public class QuestTracker { public QuestTracker(Plugin plugin) { this.plugin = plugin; - this.storage = new PlayerStorage( - plugin.getDataFolder(), - plugin.getLogger()); + this.storage = new QuestStorage( + plugin.getDataFolder()); + loadAllQuests(); } @@ -1490,30 +1490,22 @@ public class QuestTracker { } return unmet; } - public String getProgress( - String player, - String rawCond) { - if (player == null - || rawCond == null) + String player, String rawCond) { + if (player == null || rawCond == null) return "§7未知"; - String key = stripOptional(rawCond); - // 打卡群系进度 - if (key.contains("打卡") - && (key.contains("群系") - || key.contains("风景点"))) { + 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, @@ -1522,16 +1514,12 @@ public class QuestTracker { + "群系: §e" + catCount + "/1"; } - int total = getCounter( player, "biome_ALL"); return "§7群系打卡: §e" + total + "/1"; } - - - // 阶段依赖 if (key.contains("完成") && key.contains("阶段")) { boolean done = checkStageDependency( @@ -1541,9 +1529,6 @@ public class QuestTracker { : "§c✗ " + key; } - - // 注册登录(检查主数据库是否已注册) - // 注册登录(检查主数据库是否已注册) if ("完成注册登录".equals(key)) { boolean done = getDb() != null && getDb().userExists( @@ -1553,7 +1538,6 @@ public class QuestTracker { : "§c✗ 完成注册登录"; } - // 绑定邮箱 if (key.contains("绑定Email") || key.contains("绑定邮箱")) { Object val = @@ -1566,7 +1550,6 @@ public class QuestTracker { : "§c✗ 绑定Email"; } - // 在线时间 if (key.contains("在线") && key.contains("小时")) { int needed = parseNumberFromText(key); @@ -1578,17 +1561,16 @@ public class QuestTracker { + needed + "小时"; } - // 打招呼/发消息/聊天 if (key.contains("打招呼") || key.contains("打个招呼") || key.contains("发消息") || key.contains("聊天")) { int cur = getCounter( player, "CHAT"); - return "§7聊天: §e" + cur + "/1"; + return "§7聊天: §e" + + cur + "/1"; } - // 破坏/放置方块 if ((key.contains("破坏") || key.contains("放置")) && key.contains("方块")) { @@ -1606,20 +1588,17 @@ public class QuestTracker { && 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"); - } + Object val = + getDb().getField( + player, + "checkin_streak"); + int streak = val instanceof Number + ? ((Number) val).intValue() + : 0; return "§7连续登录: §e" - + totalDays + "/" + needed + "天"; + + streak + "/" + needed + "天"; } - - // 挖矿 if (key.startsWith("MINE:")) { String[] parts = key.split(":"); if (parts.length >= 3) { @@ -1634,7 +1613,6 @@ public class QuestTracker { } } - // 击杀生物 if (key.startsWith("KILL:")) { String[] parts = key.split(":"); if (parts.length >= 3) { @@ -1649,7 +1627,6 @@ public class QuestTracker { } } - // 击杀玩家 if (key.startsWith("KILL_PLAYER")) { String[] parts = key.split(":"); int needed = parts.length >= 2 @@ -1660,7 +1637,6 @@ public class QuestTracker { + cur + "/" + needed; } - // 合成 if (key.startsWith("CRAFT")) { String[] parts = key.split(":"); int needed = parts.length >= 2 @@ -1671,7 +1647,6 @@ public class QuestTracker { + cur + "/" + needed; } - // 钓鱼 if (key.startsWith("FISH")) { String[] parts = key.split(":"); int needed = parts.length >= 2 @@ -1682,7 +1657,6 @@ public class QuestTracker { + cur + "/" + needed; } - // 在线时长(毫秒) if (key.startsWith("PLAYTIME")) { String[] parts = key.split(":"); long needed = parts.length >= 2 @@ -1694,7 +1668,7 @@ public class QuestTracker { + formatMs(cur) + "/" + formatMs(needed); } - // 上报村庄 + if (key.contains("上报") && key.contains("村庄")) { int needed = parseNumberFromText(key); @@ -1705,7 +1679,6 @@ public class QuestTracker { + cur + "/" + needed; } - // 探索村庄 if (key.contains("探索") && key.contains("村庄")) { int cur = getCounter( @@ -1713,40 +1686,28 @@ public class QuestTracker { 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( + int streak = getIntField( player, - "total_checkin_days"); - if (totalDays == 0) { - totalDays = getIntField( - player, - "checkin_streak"); - } - if (totalDays == 0) { - totalDays = getIntField( + "checkin_streak"); + if (streak == 0) { + streak = getIntField( player, "streak"); } - - plugin.getLogger().info( - "[QuestTracker] 登录检测: " - + player - + " 需要=" + needed - + " 实际(累计天数)=" + totalDays); + if (streak == 0) { + streak = getIntField( + player, + "total_checkin_days"); + } return "§7近7天登录: §e" - + totalDays + "/" + + streak + "/" + needed + "天"; - - } - - // 完成新手任务全部 if (key.contains("新手任务") && key.contains("全部")) { List quests = @@ -1763,7 +1724,6 @@ public class QuestTracker { + quests.size(); } - // 完成主线任务全部 if (key.contains("主线任务") && key.contains("全部")) { List quests = @@ -1782,23 +1742,7 @@ public class QuestTracker { 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, @@ -2051,32 +1995,42 @@ public class QuestTracker { return false; String key = stripOptional(rawCond); - // 打卡群系 + + // 无条件类文本直接通过 + if (key.equals("无前级条件") + || key.equals("无条件") + || key.equals("无") + || key.equals("none") + || key.equals("无需条件") + || key.equals("无解锁条件")) { + return true; + } + 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 catCount >= 1; } - return getCounter( player, "biome_ALL") >= 1; } - // 注册登录 + if (key.contains("完成") + && key.contains("阶段")) { + return checkStageDependency( + player, key); + } if ("完成注册登录".equals(key)) { return getDb() != null @@ -2084,8 +2038,6 @@ public class QuestTracker { player); } - - // 绑定邮箱 if (key.contains("绑定Email") || key.contains("绑定邮箱")) { Object val = @@ -2095,7 +2047,6 @@ public class QuestTracker { && !val.toString().isEmpty(); } - // 在线时间 if (key.contains("在线") && key.contains("小时")) { int needed = parseNumberFromText(key); @@ -2105,7 +2056,6 @@ public class QuestTracker { return cur >= needed * 3600000L; } - // 打招呼/发消息/聊天 if (key.contains("打招呼") || key.contains("打个招呼") || key.contains("发消息") @@ -2114,7 +2064,6 @@ public class QuestTracker { player, "CHAT") >= 1; } - // 破坏/放置方块 if ((key.contains("破坏") || key.contains("放置")) && key.contains("方块")) { @@ -2127,25 +2076,20 @@ public class QuestTracker { 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; + 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) { @@ -2155,7 +2099,6 @@ public class QuestTracker { } } - // 击杀生物 if (key.startsWith("KILL:")) { String[] p = key.split(":"); if (p.length >= 3) { @@ -2165,7 +2108,6 @@ public class QuestTracker { } } - // 击杀玩家 if (key.startsWith("KILL_PLAYER")) { String[] p = key.split(":"); int n = p.length >= 2 @@ -2174,7 +2116,6 @@ public class QuestTracker { "KILL_PLAYER") >= n; } - // 合成 if (key.startsWith("CRAFT")) { String[] p = key.split(":"); int n = p.length >= 2 @@ -2183,7 +2124,6 @@ public class QuestTracker { player, "CRAFT") >= n; } - // 钓鱼 if (key.startsWith("FISH")) { String[] p = key.split(":"); int n = p.length >= 2 @@ -2192,7 +2132,6 @@ public class QuestTracker { player, "FISH") >= n; } - // 在线时长 if (key.startsWith("PLAYTIME")) { String[] p = key.split(":"); long n = p.length >= 2 @@ -2200,69 +2139,27 @@ public class QuestTracker { 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; + return getCounter( + player, "REPORT_VILLAGE") + >= 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"); @@ -2275,17 +2172,9 @@ public class QuestTracker { player, "total_checkin_days"); } - - plugin.getLogger().info( - "[QuestTracker] 登录检测: " - + player - + " 需要=" + needed - + " 实际=" + streak); - return streak >= needed; } - // 完成新手任务全部内容 if (key.contains("新手任务") && key.contains("全部")) { List quests = @@ -2299,7 +2188,6 @@ public class QuestTracker { return true; } - // 完成主线任务全部内容 if (key.contains("主线任务") && key.contains("全部")) { List quests = @@ -2365,6 +2253,23 @@ public class QuestTracker { } return 0; } + 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; + } private long getCounterLong( String player, String key) { diff --git a/src/main/java/Sdf1_login/RadioDownloadListener.java b/src/main/java/Sdf1_login/RadioDownloadListener.java index a9c4d0c..75dfb32 100644 --- a/src/main/java/Sdf1_login/RadioDownloadListener.java +++ b/src/main/java/Sdf1_login/RadioDownloadListener.java @@ -14,15 +14,20 @@ import java.util.logging.Logger; public class RadioDownloadListener implements Listener { - private final Logger log; private final Main plugin; + private final Logger log; private final Map retryCount = new ConcurrentHashMap<>(); - private static final int MAX_RETRY = 2; + private final Map totalAttempts = + new ConcurrentHashMap<>(); + private static final int MAX_RETRY = 3; + // 每位玩家最多总共尝试5次,防死循环 + private static final int MAX_TOTAL = 5; public RadioDownloadListener(Main plugin) { this.plugin = plugin; this.log = plugin.getLogger(); + } @EventHandler @@ -33,86 +38,81 @@ public class RadioDownloadListener String status = e.getStatus().name(); log.info("[Radio] " + p.getName() - + " status: " + status); + + " 资源包: " + status); switch (status) { - case "ACCEPTED": - break; - - case "DOWNLOADED": - break; - case "SUCCESS": log.info("[Radio] " + p.getName() + " 资源包加载成功"); retryCount.remove(uuid); + totalAttempts.remove(uuid); break; case "FAILED_DOWNLOAD": - handleRetry(p, uuid); + int retry = retryCount + .getOrDefault(uuid, 0) + 1; + int total = totalAttempts + .getOrDefault(uuid, 0) + 1; + retryCount.put(uuid, retry); + totalAttempts.put(uuid, total); + + log.warning("[Radio] " + p.getName() + + " 下载失败 (重试" + + retry + "/" + MAX_RETRY + + ", 总计" + total + + "/" + MAX_TOTAL + ")"); + + if (total >= MAX_TOTAL) { + p.sendMessage( + "§7[Radio] §f资源包加载失败,跳过"); + retryCount.remove(uuid); + totalAttempts.remove(uuid); + break; + } + + if (retry > MAX_RETRY) { + retryCount.put(uuid, 0); + Bukkit.getScheduler() + .runTaskLater(plugin, + () -> { + if (p.isOnline() + && plugin + .radio + != null) { + plugin.radio + .sendResourcePack( + p); + } + }, 100L); + break; + } + + p.sendMessage( + "§e[Radio] §f下载失败,重试中..."); + Bukkit.getScheduler() + .runTaskLater(plugin, + () -> { + if (p.isOnline() + && plugin.radio + != null) { + plugin.radio + .sendResourcePack( + p); + } + }, 60L); 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); + totalAttempts.remove(uuid); break; default: - log.info("[Radio] " + p.getName() - + " 未知: " + status); + retryCount.remove(uuid); + totalAttempts.remove(uuid); 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); - } - } -} +} \ No newline at end of file diff --git a/src/main/java/Sdf1_login/RadioManager.java b/src/main/java/Sdf1_login/RadioManager.java index 7e79c0e..631317f 100644 --- a/src/main/java/Sdf1_login/RadioManager.java +++ b/src/main/java/Sdf1_login/RadioManager.java @@ -1,20 +1,20 @@ 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.concurrent.TimeUnit; import java.util.zip.*; +import java.util.Collections; +import org.bukkit.NamespacedKey; +import java.util.Set; + public class RadioManager { @@ -22,441 +22,381 @@ public class RadioManager { 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 = + private String externalUrl = ""; + private int httpPort = 443; + private final Set pendingPack = Collections.newSetFromMap( new ConcurrentHashMap<>()); - private String adminTag = "admin"; + private String lastPlayedSound = ""; + + private int radioIndex = 0; + + // ========== 登录阶段推送(管理员手动触发) ========== + public void startLoginRadio(Player p) { + loginPlayers.put(p.getName(), true); + } + + public void stopLoginRadio(Player p) { + loginPlayers.remove(p.getName()); + } + + public void stopAllLoginRadio() { + loginPlayers.clear(); + } + + private final List mainFiles = new ArrayList<>(); + private final List loginFiles = new ArrayList<>(); public RadioManager(Main plugin) { this.plugin = plugin; - } - - // ========== init ========== - public void init() { - radioDir = new File( + this.radioDir = new File( plugin.getDataFolder(), "radio"); - oggDir = new File( - plugin.getDataFolder(), - "radio/ogg"); - packDir = new File( - plugin.getDataFolder(), - "radiopack"); + this.oggDir = new File( + plugin.getDataFolder(), "radio/ogg"); + this.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 ========== + // ========== 读取 radio_config.txt ========== private void loadRadioConfig() { - configFile = new File( + File cfg = new File( plugin.getDataFolder(), "radio_config.txt"); - if (!configFile.exists()) { - writeDefaultConfig(); + + // 不存在则创建默认配置 + if (!cfg.exists()) { + createRadioConfig(cfg); + plugin.getLogger().info( + "[Radio] 已生成 radio_config.txt"); } - try (BufferedReader br = - new BufferedReader( - new FileReader( - configFile))) { + + // 读取 + try (BufferedReader r = new BufferedReader( + new InputStreamReader( + new FileInputStream(cfg), + java.nio.charset + .StandardCharsets + .UTF_8))) { String line; - while ((line = br.readLine()) != null) { - line = line.trim(); - if (line.isEmpty() - || line.startsWith("#")) + while ((line = r.readLine()) != null) { + if (line.startsWith("#") + || line.trim().isEmpty()) continue; int eq = line.indexOf('='); - if (eq <= 0) continue; + 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")) { + if (key.equals("resource-pack-url")) { + externalUrl = 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(); + httpPort = Integer.parseInt( + val); + } catch (NumberFormatException ignored) { } } } } catch (Exception e) { - // ignore + plugin.getLogger().warning( + "[Radio] 读取radio_config.txt失败: " + + e.getMessage()); } + + // 443是HTTPS端口,内嵌HTTP不能用 + if (httpPort == 443) httpPort = 8080; + plugin.getLogger().info( - "[Radio] admin tag: " + adminTag); + "[Radio] 配置: externalUrl=[" + + externalUrl + "] httpPort=" + + httpPort); } - /** - * 检查玩家是否有管理标签 - * 控制台不受限制 - */ - 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")); + private void createRadioConfig(File f) { + try (PrintWriter pw = new PrintWriter( + new OutputStreamWriter( + new FileOutputStream(f), + java.nio.charset + .StandardCharsets + .UTF_8))) { + pw.println("# ============ Sdf1 Radio Config ============"); + pw.println("#"); + pw.println("# resource-pack-url"); + pw.println("# 填外部地址(HTTP或HTTPS均可)"); + pw.println("# 例: https://example.com/radiopack.zip"); + pw.println("# 留空 = 使用内置HTTP服务"); + pw.println("#"); + pw.println("# http-port"); + pw.println("# 内置HTTP端口(仅resource-pack-url为空时生效)"); + pw.println("#"); + pw.println("# ============ Settings ==============="); + pw.println(); + pw.println("resource-pack-url="); + pw.println("http-port=8080"); } catch (Exception e) { plugin.getLogger().warning( - "[Radio] config create fail"); + "[Radio] 创建radio_config.txt失败: " + + e.getMessage()); } } - // ========== 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); + // ========== 初始化 ========== + public void init() { + loadRadioConfig(); + convertNonOgg(); + loadAllOggs(); + buildResourcePack(); + startHttpServer(); // 启动HTTP + plugin.getLogger().info( + "[Radio] 初始化完成: 全服=" + + mainFiles.size() + + " 登录=" + loginFiles.size()); + } + + + // ========== 转换非ogg文件 ========== + private void convertNonOgg() { + convertDir(radioDir); + convertDir(new File(radioDir, "login")); + convertDir(new File(radioDir, "reg")); + } + + private void convertDir(File src) { + if (!src.exists() || !src.isDirectory()) return; + File[] files = src.listFiles((d, n) -> { + String l = n.toLowerCase(); + return l.endsWith(".mp3") + || l.endsWith(".wav") + || l.endsWith(".flac"); + }); + if (files == null) return; + + for (File f : files) { + String oggName = f.getName() + .replaceAll("\\.(mp3|wav|flac)$", + ".ogg"); + File oggFile = new File(src, oggName); + if (oggFile.exists() + && oggFile.lastModified() + >= f.lastModified()) { + continue; + } + // 调用ffmpeg转换 + if (convertWithFfmpeg(f, oggFile)) { plugin.getLogger().info( - "[Radio] copy: " - + src.getName()); - } else if (runFfmpeg(src, dest)) { - plugin.getLogger().info( - "[Radio] convert: " - + src.getName()); + "[Radio] 转换成功: " + + f.getName()); } else { plugin.getLogger().warning( - "[Radio] skip(need ffmpeg): " - + src.getName()); + "[Radio] 转换失败: " + + f.getName() + + " (需要安装ffmpeg)"); } } } - 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) { + private boolean convertWithFfmpeg( + File input, File output) { 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(), + "ffmpeg", "-y", + "-i", input.getAbsolutePath(), "-c:a", "libvorbis", "-q:a", "5", - dest.getAbsolutePath()); + output.getAbsolutePath()); pb.redirectErrorStream(true); Process p = pb.start(); - p.waitFor(); - return dest.exists(); + boolean finished = + p.waitFor(30, TimeUnit.SECONDS); + if (!finished) { + p.destroyForcibly(); + return false; + } + return output.exists() + && output.length() > 0; } catch (Exception e) { return false; } } - // ========== pack ========== - private void buildPack() { + // ========== 加载ogg文件列表 ========== + private void loadAllOggs() { + mainFiles.clear(); + loginFiles.clear(); + loadOggsFlat(radioDir, mainFiles); + loadOggsFlat( + new File(radioDir, "login"), + loginFiles); + loadOggsFlat( + new File(radioDir, "reg"), + loginFiles); + } + + private void loadOggsFlat( + File dir, List list) { + if (!dir.exists() || !dir.isDirectory()) + return; + File[] files = dir.listFiles( + (d, n) -> n.toLowerCase() + .endsWith(".ogg")); + if (files == null) return; + Arrays.sort(files); + for (File f : files) list.add(f); + } + + // ========== 资源包构建 ========== + private void buildResourcePack() { deleteDir(packDir); packDir.mkdirs(); + List allOggs = new ArrayList<>(); - scanOggs(oggDir, allOggs); + collectOggs(radioDir, allOggs); + if (allOggs.isEmpty()) { plugin.getLogger().warning( - "[Radio] no ogg found"); + "[Radio] radio/ 目录下" + + "无 .ogg 文件,跳过资源包"); return; } + // 正确路径:assets/minecraft/sounds/custom/ 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); - } - + // 生成 sounds.json 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(); + for (int i = 0; i < allOggs.size(); i++) { + File ogg = allOggs.get(i); + String simpleName = + ogg.getName() + .replace(".ogg", ""); File dest = new File(soundsDir, - name + ".ogg"); + simpleName + ".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; + } catch (Exception e) { + // ignore } + // 音频事件名: custom.xxx + // 文件路径: custom/xxx json.append(" \"custom.") - .append(name).append("\": {\n"); - json.append( - " \"replace\": false,\n"); - json.append( - " \"sounds\": [{\n"); - json.append( - " \"name\": \"custom/") - .append(name).append("\",\n"); + .append(simpleName) + .append("\": {\n"); + json.append(" \"sounds\": [{\n"); + json.append(" \"name\":" + + " \"custom/") + .append(simpleName) + .append("\",\n"); json.append( " \"stream\": true\n"); json.append(" }]\n"); json.append(" }"); - idx++; - if (idx < total) json.append(","); + if (i < allOggs.size() - 1) + json.append(","); json.append("\n"); } - json.append("}\n"); + json.append("}"); + // sounds.json 放在 assets/minecraft/ 下 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"); + "[Radio] sounds.json写入失败"); } + // pack.mcmeta(pack_format=54) 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) {} + ("{\n \"pack\": {\n" + + " \"pack_format\": 54,\n" + + " \"description\":" + + " \"Sdf1 Radio\"\n }\n}") + .getBytes("UTF-8")); + } catch (Exception e) { + // ignore + } - 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(); + File zipFile = new File( + plugin.getDataFolder(), + "radiopack.zip"); + packZip(packDir, zipFile); plugin.getLogger().info( - "[Radio] zip: " - + zipFile.length() - + " bytes, SHA1=" - + packHash); + "[Radio] 资源包生成: " + + allOggs.size() + " 个音频"); } catch (Exception e) { plugin.getLogger().warning( - "[Radio] zip fail: " + "[Radio] zip打包失败: " + 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); + if (f.isDirectory()) deleteDir(f); else f.delete(); } } dir.delete(); } - private void zipPack(File root, File zipFile) - throws Exception { + 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 void packZip(File root, + File zipFile) throws Exception { try (ZipOutputStream zos = new ZipOutputStream( new FileOutputStream( zipFile))) { - addZipEntries(root, root, zos); + zipDir(root, root, zos); } } - private void addZipEntries(File root, File cur, - ZipOutputStream zos) + private void zipDir(File root, + File current, + ZipOutputStream zos) throws Exception { - File[] files = cur.listFiles(); + File[] files = current.listFiles(); if (files == null) return; for (File f : files) { if (f.isDirectory()) { - addZipEntries(root, f, zos); + zipDir(root, f, zos); } else { String path = root.toURI() .relativize(f.toURI()) .getPath(); - zos.putNextEntry( - new ZipEntry(path)); + zos.putNextEntry(new ZipEntry(path)); zos.write(Files.readAllBytes( f.toPath())); zos.closeEntry(); @@ -464,326 +404,320 @@ public class RadioManager { } } - // ========== HTTP server ========== - private void startHttpServer() { - if (packBytes == null) return; - if (!resourcePackUrl.isEmpty()) { + // ========== 资源包发送 ========== + + /** + * 发送资源包(可选,不强制) + * 优先用HTTP URL发送,兜底用byte[] + */ + public void sendResourcePack(Player p) { + UUID uuid = p.getUniqueId(); + if (pendingPack.contains(uuid)) return; + pendingPack.add(uuid); + Bukkit.getScheduler().runTaskLater( + plugin, () -> { + pendingPack.remove(uuid); + }, 200L); + + String url = getResourcePackUrl(); + if (url == null || url.isEmpty()) { + pendingPack.remove(uuid); + return; + } + + File zip = new File( + plugin.getDataFolder(), + "radiopack.zip"); + if (!zip.exists()) { + pendingPack.remove(uuid); + return; + } + + // 异步:下载远程zip → 算hash → 比对本地hash + final Player fp = p; + final String finalUrl = url + "?t=" + + System.currentTimeMillis(); + + Bukkit.getScheduler() + .runTaskAsynchronously(plugin, () -> { + try { + // 1. 本地hash + byte[] localData = + Files.readAllBytes( + zip.toPath()); + String localHash = + sha1(localData); + + // 2. 远程hash + byte[] remoteData = + downloadBytes(finalUrl); + String remoteHash = + sha1(remoteData); + + // 3. 比对 + plugin.getLogger().info( + "[Radio] 本地hash: " + + localHash); + plugin.getLogger().info( + "[Radio] 远程hash: " + + remoteHash); + + if (!localHash.equals(remoteHash)) { + plugin.getLogger().warning( + "[Radio] hash不一致," + + "跳过发送资源包"); + plugin.getLogger().warning( + "[Radio] 请更新远程" + + "radiopack.zip"); + Bukkit.getScheduler() + .runTask(plugin, () -> { + fp.sendMessage( + "§7[Radio] §f资源包" + + "版本不一致," + + "跳过安装"); + }); + return; + } + + // 4. hash一致,发送给客户端 + Bukkit.getScheduler() + .runTask(plugin, () -> { + sendPackWithHash( + fp, finalUrl, + remoteHash); + }); + + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] 校验失败: " + + e.getMessage()); + } + }); + } + + private void sendPackWithHash( + Player p, String url, String hash) { + UUID uuid = UUID.randomUUID(); + + // Paper 1.21: setResourcePack(UUID, String, String, Component, boolean) + try { + Object component = net.kyori.adventure + .text.Component.text("Sdf1 Radio"); + p.getClass().getMethod( + "setResourcePack", + UUID.class, + String.class, + String.class, + net.kyori.adventure.text + .Component.class, + boolean.class) + .invoke(p, uuid, url, + hash, component, false); plugin.getLogger().info( - "[Radio] skip HTTP " - + "(external URL)"); + "[Radio] 发送成功: " + + p.getName()); + return; + } catch (Exception ignored) { + } + + // 降级:旧版 String 签名 + try { + p.getClass().getMethod( + "setResourcePack", + UUID.class, + String.class, + String.class, + String.class, + boolean.class) + .invoke(p, uuid, url, + hash, "Sdf1 Radio", false); + plugin.getLogger().info( + "[Radio] 旧版签名成功: " + + p.getName()); + return; + } catch (Exception ignored) { + } + + // 降级3参 + try { + p.getClass().getMethod( + "setResourcePack", + UUID.class, + String.class, + String.class) + .invoke(p, uuid, url, hash); + plugin.getLogger().info( + "[Radio] 3参签名成功: " + + p.getName()); + return; + } catch (Exception ignored) { + } + + plugin.getLogger().severe( + "[Radio] 所有签名失败: " + + p.getName()); + } + + + private void sendPackFromLocal( + Player p, String url) { + File zip = new File( + plugin.getDataFolder(), + "radiopack.zip"); + if (!zip.exists()) return; + try { + byte[] data = + Files.readAllBytes( + zip.toPath()); + String hash = sha1(data); + sendPackWithHash(p, url, hash); + } catch (Exception e) { + plugin.getLogger().warning( + "[Radio] 降级发送失败"); + } + } + + // 从URL下载字节 + private byte[] downloadBytes(String urlStr) + throws Exception { + java.net.URL url = + new java.net.URL(urlStr); + java.net.HttpURLConnection conn = + (java.net.HttpURLConnection) + url.openConnection(); + conn.setConnectTimeout(10000); + conn.setReadTimeout(30000); + try (java.io.InputStream is = + conn.getInputStream()) { + return is.readAllBytes(); + } + } + + // SHA-1计算 + private String sha1(byte[] data) + throws Exception { + java.security.MessageDigest md = + java.security.MessageDigest + .getInstance("SHA-1"); + md.update(data); + byte[] hash = md.digest(); + StringBuilder sb = + new StringBuilder(); + for (byte b : hash) + sb.append(String.format( + "%02x", b)); + return sb.toString(); + } + + public void clearPending(UUID uuid) { + pendingPack.remove(uuid); + } + + + + // ========== 内嵌HTTP服务 ========== + private com.sun.net.httpserver.HttpServer + httpServer; + + private void startHttpServer() { + // 如果配置了外部地址则不启动 + if (plugin.getConfig2() != null + && !plugin.getConfig2() + .broadcastServerUrl + .isEmpty()) { return; } try { - httpServer = HttpServer.create( - new InetSocketAddress( - "0.0.0.0", - httpPort), 0); + int port = 8080; + if (plugin.getConfig2() != null) { + port = plugin.getConfig2().httpPort; + } + httpServer = + com.sun.net.httpserver + .HttpServer.create( + new java.net + .InetSocketAddress( + port), 0); httpServer.createContext( "/radiopack.zip", exchange -> { - if (packBytes != null) { - exchange - .getResponseHeaders() + File zip = new File( + plugin.getDataFolder(), + "radiopack.zip"); + if (zip.exists()) { + byte[] data = + Files.readAllBytes( + zip.toPath()); + exchange.getResponseHeaders() .set("Content-Type", "application/zip"); - exchange - .sendResponseHeaders( - 200, - packBytes.length); - exchange - .getResponseBody() - .write(packBytes); - exchange - .getResponseBody() + exchange.getResponseHeaders() + .set("Access-Control" + + "-Allow-Origin", + "*"); + exchange.sendResponseHeaders( + 200, data.length); + exchange.getResponseBody() + .write(data); + exchange.getResponseBody() .close(); } else { - exchange - .sendResponseHeaders( - 404, 0); - exchange.close(); + byte[] empty = + "not found".getBytes(); + exchange.sendResponseHeaders( + 404, empty.length); + exchange.getResponseBody() + .write(empty); + exchange.getResponseBody() + .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"); + "[Radio] HTTP服务: http://0.0.0.0:" + + port + "/radiopack.zip"); } catch (Exception e) { - plugin.getLogger().warning( - "[Radio] HTTP fail: " + plugin.getLogger().severe( + "[Radio] HTTP启动失败: " + e.getMessage()); } } - // ========== send resource pack ========== - public void sendResourcePack(Player p) { - if (packBytes == null) return; - UUID pid = p.getUniqueId(); + private void stopHttpServer() { + if (httpServer != null) { + httpServer.stop(0); + httpServer = null; + } + } - if (sending.contains(pid)) { + public String getResourcePackUrl() { + if (externalUrl != null + && !externalUrl.isEmpty()) { plugin.getLogger().info( - "[Radio] skip duplicate: " - + p.getName()); - return; + "[Radio] 使用外部地址: " + + externalUrl); + return externalUrl; } - 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); + plugin.getLogger().warning( + "[Radio] 未配置外部地址," + + "跳过资源包"); + return null; } - // ========== 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(); + if (mainFiles.isEmpty()) { + plugin.getLogger().warning( + "[Radio] 无 .ogg 文件"); + return; + } + plugin.getLogger().info( + "[Radio] 启动全服广播 (" + + mainFiles.size() + " 个文件)"); mainRadioTask = new BukkitRunnable() { + @Override public void run() { playNext(); } @@ -791,84 +725,136 @@ public class RadioManager { } private void playNext() { - if (!oggFiles.isEmpty()) { - File f = oggFiles.get( - radioIndex % oggFiles.size()); - playCustom(f.getName() - .replace(".ogg", "")); - radioIndex++; + if (mainFiles.isEmpty()) return; + File file = mainFiles.get( + radioIndex % mainFiles.size()); + String name = file.getName() + .replace(".ogg", ""); + lastPlayedSound = "custom." + name; + String key = "minecraft:" + lastPlayedSound; + 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 playToAll(String name) { + String key = findSongKey(name); + if (key == null) return; + lastPlayedSound = "custom." + name; + for (Player p : Bukkit.getOnlinePlayers()) { + p.playSound(p.getLocation(), key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + Bukkit.broadcastMessage( + "§6§l[广播] §f正在播放: §e" + name); + } + + + public void playToPlayer( + Player p, String name) { + String key = findSongKey(name); + if (key == null) { + p.sendMessage("§c未找到: " + name); + return; + } + p.playSound(p.getLocation(), key, + SoundCategory.RECORDS, + 2.0f, 1.0f); + } + + public void playToLoginPlayers(String name) { + String key = findSongKey(name); + if (key == null) return; + for (String pName + : loginPlayers.keySet()) { + Player lp = Bukkit.getPlayer(pName); + if (lp != null && lp.isOnline()) { + lp.playSound(lp.getLocation(), + key, + SoundCategory.RECORDS, + 1.5f, 1.0f); + } } } + + private String findSongKey(String name) { + List all = new ArrayList<>(); + collectOggs(radioDir, all); + for (File f : all) { + String fn = f.getName() + .replace(".ogg", ""); + if (fn.equalsIgnoreCase(name)) { + return "minecraft:custom." + name; + } + } + plugin.getLogger().warning( + "[Radio] 未找到歌曲: " + name); + return null; + } + + + public boolean hasSong(String name) { + return findSongKey(name) != null; + } + + // ========== 手动播放 ========== + public boolean playSpecific( + Player sender, String name) { + List all = new ArrayList<>(); + collectOggs(radioDir, 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; + } + } + if (sender != null) + sender.sendMessage("§c未找到: " + name); + return false; + } 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); - } - } + if (lastPlayedSound != null + && !lastPlayedSound.isEmpty()) { + for (Player p : Bukkit.getOnlinePlayers()) { + p.stopSound(lastPlayedSound, + SoundCategory.RECORDS); } - }.runTaskTimer(plugin, 0L, 60L); - } - - public void stopLoginRadio(Player p) { - loginPlayers.remove(p.getName()); - if (loginPlayers.isEmpty() - && loginRadioTask != null) { - loginRadioTask.cancel(); - loginRadioTask = null; + lastPlayedSound = ""; } } - public void stopAllLoginRadio() { - loginPlayers.clear(); - if (loginRadioTask != null) { - loginRadioTask.cancel(); - loginRadioTask = null; - } - } - - public void stopAll() { + // ========== 重载 ========== + public void reload() { stopMainRadio(); stopAllLoginRadio(); - if (httpServer != null) - httpServer.stop(0); + stopHttpServer(); + loadAllOggs(); + buildResourcePack(); + startHttpServer(); + loadRadioConfig(); } - - private void broadcast(String msg) { - Bukkit.broadcastMessage(msg); - } -} +} \ No newline at end of file diff --git a/src/main/java/Sdf1_login/TicketManager.java b/src/main/java/Sdf1_login/TicketManager.java index ff05ee7..c21edc2 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/resources/plugin.yml b/src/main/resources/plugin.yml index 814b24e..32ad554 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -86,5 +86,4 @@ commands: usage: /垃圾清理 menu: description: 雪球菜单开关控制 - usage: /menu <开/关> - commands: \ No newline at end of file + usage: /menu <开/关> \ No newline at end of file