Update: 添加RadioManager/RadioDownloadListener/UpdateChecker/WelcomeManager等新功能模块

This commit is contained in:
ypsdf1
2026-05-14 21:03:03 +08:00
parent 677fea7de1
commit 8b73031ef0
46 changed files with 23884 additions and 2001 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,227 @@
package Sdf1_login;
import org.bukkit.entity.Player;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.logging.Logger;
public class CommissionManager {
private final Main main;
private final Logger logger;
private final List<double[]> rules =
new ArrayList<>();
public CommissionManager(Main main) {
this.main = main;
this.logger = main.getLogger();
loadRules();
}
public void loadRules() {
rules.clear();
File f = new File(
main.getDataFolder(),
"提成规则.txt");
if (!f.exists()) {
writeDefault(f);
}
try (BufferedReader br =
new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets
.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()
|| line.startsWith("#"))
continue;
// 格式: 邀请人数:提成百分比
// 如 "5:15" = 邀请5人提成15%
String[] parts =
line.split(":");
if (parts.length >= 2) {
try {
int count = Integer.parseInt(
parts[0].trim());
double rate = Double
.parseDouble(
parts[1]
.trim()) / 100.0;
rules.add(new double[]{
count, rate});
} catch (Exception ignored) {
}
}
}
// 按人数排序(从大到小)
rules.sort((a, b) ->
Double.compare(b[0], a[0]));
logger.info(
"[Commission] 加载 "
+ rules.size() + " 条提成规则");
} catch (Exception e) {
logger.warning(
"[Commission] 加载失败: "
+ e.getMessage());
}
}
private void writeDefault(File f) {
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets.UTF_8))) {
pw.println("# 邀请提成规则");
pw.println("# 格式: 邀请人数:提成百分比");
pw.println("# 邀请10人提成20%");
pw.println("10:20");
pw.println("# 邀请5人提成15%");
pw.println("5:15");
pw.println("# 邀请1人提成10%");
pw.println("1:10");
} catch (IOException ignored) {
}
}
/**
* 获取邀请人的提成比例
* 根据已邀请人数匹配最高规则
*/
public double getCommissionRate(
String inviter) {
int inviteCount = getInviteCount(
inviter);
for (double[] rule : rules) {
if (inviteCount >= (int) rule[0]) {
return rule[1];
}
}
return 0;
}
/**
* 获取邀请人数
*/
public int getInviteCount(
String inviter) {
try {
Object val =
main.getDb().getField(
inviter,
"monthly_invite_count");
if (val instanceof Number) {
return ((Number) val).intValue();
}
Object total =
main.getDb().getField(
inviter,
"total_invite_count");
if (total instanceof Number) {
return ((Number) total).intValue();
}
} catch (Exception ignored) {
}
return 0;
}
/**
* 发放提成
* 被邀请人获得奖励时,邀请人获得提成
*/
public void payCommission(
String invitee,
double originalAmount) {
// 查找邀请人
String inviter = getInviter(invitee);
if (inviter == null
|| inviter.isEmpty()) return;
double rate = getCommissionRate(
inviter);
if (rate <= 0) return;
double commission =
originalAmount * rate;
if (commission <= 0) return;
// 通过 Vault 发放
var rsp = main.getServer()
.getServicesManager()
.getRegistration(
net.milkbowl.vault
.economy.Economy.class);
if (rsp == null) return;
net.milkbowl.vault.economy.Economy econ =
rsp.getProvider();
if (econ == null) return;
econ.depositPlayer(inviter, commission);
// 通知邀请人
Player inviterP =
main.getServer()
.getPlayer(inviter);
if (inviterP != null
&& inviterP.isOnline()) {
inviterP.sendMessage(
"§a§l[邀请提成] §f"
+ invitee + " 完成任务,"
+ "你获得 §e"
+ String.format("%.2f",
commission)
+ " §f游戏币提成 ("
+ (int)(rate * 100) + "%)");
}
logger.info(
"[Commission] 提成: "
+ inviter + " ← "
+ invitee + " | "
+ String.format("%.2f",
originalAmount)
+ " × "
+ (int)(rate * 100) + "% = "
+ String.format("%.2f",
commission));
}
/**
* 获取邀请人
*/
private String getInviter(String player) {
try {
Object val =
main.getDb().getField(
player,
"invited_by");
if (val != null
&& !val.toString()
.isEmpty()) {
return val.toString();
}
} catch (Exception ignored) {
}
return null;
}
/**
* 判断金额是否包含经济内容
*/
public boolean hasEconomyContent(
String text) {
String t = text.toLowerCase();
return t.contains("游戏币")
|| t.contains("金币")
|| t.contains("积分")
|| t.contains("块钱")
|| t.contains("元")
|| t.contains("$")
|| t.contains("经济")
|| t.contains("余额");
}
}
@@ -25,6 +25,7 @@ public class ConfigManager {
public long smtpVerifyExpire = 0;
public long smtpLastSendTime = 0;
public String smtpConfiguringPlayer = "";
public String updatePlatform = "gh";
public ConfigManager(File dataFolder) {
+14 -3
View File
@@ -213,6 +213,18 @@ public class DatabaseManager {
+ ")");
safeAdd(st, "menu_snowball",
"INTEGER DEFAULT 1");
// ===== 独立任务进度表 =====
st.executeUpdate(
"CREATE TABLE IF NOT EXISTS "
+ "quest_progress ("
+ "player_name TEXT NOT NULL, "
+ "condition_key TEXT NOT NULL, "
+ "progress_value INTEGER DEFAULT 0, "
+ "completed INTEGER DEFAULT 0, "
+ "PRIMARY KEY (player_name, "
+ "condition_key)"
+ ")");
st.close();
logger.info("[Sdf1_login] 数据库初始化完成");
} catch (Exception e) {
@@ -861,9 +873,8 @@ public class DatabaseManager {
return list;
}
/**
* 获取指定ID的备份数据
/*
获取指定ID的备份数据
*/
public String[] getInventoryBackupById(
int backupId) {
+327 -68
View File
@@ -5,7 +5,6 @@ import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.*;
import org.bukkit.inventory.meta.ItemMeta;
import java.io.File;
import java.util.*;
@@ -42,7 +41,7 @@ public class GUIManager {
g.setItem(14, mkItem(Material.EMERALD,
"§d§l积分商城"));
g.setItem(16, mkItem(Material.CHEST,
"§e§l新人礼包"));
"§e§l任务中心"));
g.setItem(18, mkItem(Material.BOOK,
"§6工单系统",
"§7提交bug、求助、举报"));
@@ -371,6 +370,7 @@ public class GUIManager {
"§7双击管理"));
slot++;
}
g.setItem(53, mkItem(Material.ARROW, "§7返回"));
p.openInventory(g);
}
@@ -379,90 +379,279 @@ public class GUIManager {
Inventory g = Bukkit.createInventory(
null, 27, T_TASK_CENTER);
fillBg(g);
QuestTracker qt =
plugin.getQuestTracker();
String player = p.getName();
// 新人任务进度
List<QuestTracker.QuestFile> 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<String> newLore = new ArrayList<>();
newLore.add("§7进度: §e" + newInfo);
if (newCurrent != null) {
newLore.add("§7当前: §f"
+ newCurrent.displayName);
if (newCurrent.hasConditions()) {
List<String> unmet =
qt.getUnmetConditions(
player,
newCurrent.conditions);
if (!unmet.isEmpty()) {
for (String c : unmet) {
newLore.add("§7 "
+ qt.getProgress(
player, c));
}
} else {
newLore.add("§a✓ 条件已满足");
}
}
} else {
newLore.add("§a§l全部完成!");
}
g.setItem(10, mkItem(Material.BOOK,
"§e§l新人任务",
"§7完成新手阶段获取奖励"));
"§e§l新人任务 §7["
+ newInfo + "]",
newLore.toArray(new String[0])));
// 主线任务
List<QuestTracker.QuestFile> mainQuests =
qt.getQuests("主线任务");
g.setItem(12, mkItem(Material.COMPASS,
"§b§l主线任务",
"§b§l主线任务 §7["
+ qt.getCompletedStageCount(
player, mainQuests)
+ "/" + mainQuests.size()
+ "]",
"§7查看主线任务列表"));
// 支线任务
List<QuestTracker.QuestFile> sideQuests =
qt.getQuests("支线任务");
g.setItem(14, mkItem(Material.PAPER,
"§a§l支线任务",
"§a§l支线任务 §7["
+ qt.getCompletedStageCount(
player, sideQuests)
+ "/" + sideQuests.size()
+ "]",
"§7查看支线任务列表"));
g.setItem(16, mkItem(Material.EMERALD,
"§d§l每日签到",
"§7签到获取积分"));
g.setItem(22, mkItem(Material.ARROW, "§7返回"));
p.openInventory(g);
}
public void openGiftStages(Player p) {
Inventory g = Bukkit.createInventory(
null, 27, T_GIFT_STAGES);
fillBg(g);
int max = plugin.getGift().getMaxStage();
for (int i = 0; i < max && i < 9; i++) {
int stage = i + 1;
boolean claimed = plugin.getDb()
.getField(p.getName(),
"gift_claimed") != null
&& ((String) plugin.getDb()
.getField(p.getName(),
"gift_claimed"))
.contains("[" + stage + "]");
boolean can = plugin.getGift()
.canClaim(p, stage);
Material mat = claimed ? Material.LIME_WOOL
: (can ? Material.YELLOW_WOOL
: Material.RED_WOOL);
String status = claimed ? "§a已领取"
: (can ? "§e可领取" : "§c未达标");
g.setItem(10 + i, mkItem(mat,
"§e第" + stage + "阶段",
"§7" + status));
}
g.setItem(26, mkItem(Material.ARROW, "§7返回"));
// 打卡上报按钮(slot 21)
g.setItem(21, mkItem(
Material.EMERALD,
"§a§l打卡上报",
"§7验证当前阶段任务完成情况",
"§7满足条件将自动标记完成"));
// 返回按钮(slot 26)
g.setItem(26, mkItem(
Material.ARROW, "§7返回"));
p.openInventory(g);
}
public void openTaskList(Player p, String folderName) {
File folder = new File(
plugin.getDataFolder(), folderName);
List<Map<String, Object>> tasks =
plugin.getGift()
.loadTasksFromFolder(folder);
int size = Math.max(54,
((tasks.size() / 9) + 1) * 9);
if (size > 54) size = 54;
Inventory g = Bukkit.createInventory(null, size,
"§d§l" + folderName);
public void openGiftStages(Player p) {
Inventory g = Bukkit.createInventory(
null, 54, T_GIFT_STAGES);
fillBg(g);
for (int i = 0; i < tasks.size()
&& i < 45; i++) {
Map<String, Object> task = tasks.get(i);
String name = (String) task.get("name");
List<String> conds =
(List<String>) task.get("conditions");
List<String> rewards =
(List<String>) task.get("rewards");
QuestTracker qt =
plugin.getQuestTracker();
List<QuestTracker.QuestFile> quests =
qt.getQuests("新人任务");
int total = quests.size();
int completed =
qt.getCompletedStageCount(
p.getName(), quests);
QuestTracker.QuestFile current =
qt.getCurrentStage(
p.getName(), quests);
g.setItem(4, mkItem(Material.BOOK,
"§e§l新人任务进度",
"§7已完成: §e" + completed
+ "/" + total,
current != null
? "§7当前: §f"
+ current.displayName
: "§a§l全部完成!"));
for (int i = 0;
i < quests.size() && i < 44; i++) {
QuestTracker.QuestFile qf =
quests.get(i);
boolean done =
qt.isStageCompleted(
p.getName(), qf);
boolean isCurrent =
(current != null
&& current == qf);
Material mat;
List<String> lore = new ArrayList<>();
lore.add("§7───── 条件 ─────");
if (conds != null)
for (String c : conds)
lore.add("§e" + c);
lore.add("");
lore.add("§7───── 奖励 ─────");
if (rewards != null)
for (String r2 : rewards)
lore.add("§a" + r2);
g.setItem(i, mkItem(Material.BOOK,
"§e" + name,
if (done) {
mat = Material.LIME_WOOL;
} else if (isCurrent) {
mat = Material.YELLOW_WOOL;
} else {
mat = Material.RED_WOOL;
}
if (!qf.details.isEmpty()) {
lore.add("§7---- 任务详情 ----");
for (String d : qf.details) {
lore.add("§f" + d);
}
}
if (qf.hasConditions()
&& (done || isCurrent)) {
lore.add("");
for (String c : qf.conditions) {
if (qt.isOptional(c))
continue;
if (qt.isConditionMet(
p.getName(), c)) {
lore.add("§7 §a✓ "
+ qt.getProgress(
p.getName(), c));
} else {
lore.add("§7 §c✗ "
+ qt.getProgress(
p.getName(), c));
}
}
}
if (!qf.rewards.isEmpty()) {
lore.add("");
lore.add("§7---- 奖励 ----");
for (String r : qf.rewards) {
lore.add("§a" + r);
}
}
if (done
&& !qf.rewards.isEmpty()
&& !qt.hasClaimed(
p.getName(), qf)) {
lore.add("");
lore.add("§e§l点击领取奖励");
}
if (done && qt.hasClaimed(
p.getName(), qf)) {
lore.add("");
lore.add("§7已领取奖励");
}
g.setItem(10 + i, mkItem(mat,
"§e" + qf.displayName,
lore.toArray(new String[0])));
}
// 返回按钮
g.setItem(53, mkItem(
Material.ARROW, "§7返回"));
p.openInventory(g);
}
public void openTaskList(Player p,
String folderName) {
QuestTracker qt =
plugin.getQuestTracker();
List<QuestTracker.QuestFile> tasks =
qt.getQuests(folderName);
int size = Math.min(54,
Math.max(54,
((tasks.size() / 9) + 1) * 9));
Inventory g = Bukkit.createInventory(
null, size,
"§d§l" + folderName);
fillBg(g);
for (int i = 0;
i < tasks.size()
&& i < 45; i++) {
QuestTracker.QuestFile qf =
tasks.get(i);
boolean done =
qt.isStageCompleted(
p.getName(), qf);
Material mat = done
? Material.LIME_WOOL
: Material.BOOK;
List<String> lore = new ArrayList<>();
// 解锁条件
if (qf.hasConditions()) {
lore.add("§7──── 解锁条件 ────");
for (String c : qf.conditions) {
if (qt.isOptional(c))
continue;
if (qt.isConditionMet(
p.getName(), c)) {
lore.add("§a ✓ "
+ qt.getProgress(
p.getName(), c));
} else {
lore.add("§c ✗ "
+ qt.getProgress(
p.getName(), c));
}
}
}
// 任务详情
if (!qf.details.isEmpty()) {
lore.add("");
lore.add("§7──── 任务详情 ────");
for (String d : qf.details) {
lore.add("§f" + d);
}
}
// 任务奖励
if (!qf.rewards.isEmpty()) {
lore.add("");
lore.add("§7──── 任务奖励 ────");
for (String r : qf.rewards) {
lore.add("§a" + r);
}
}
g.setItem(i, mkItem(mat,
(done ? "§a✓ " : "§e")
+ qf.displayName,
lore.toArray(new String[0])));
}
if (tasks.isEmpty()) {
g.setItem(22, mkItem(Material.BARRIER,
g.setItem(22, mkItem(
Material.BARRIER,
"§7暂无任务"));
}
g.setItem(size - 1,
mkItem(Material.ARROW, "§7返回"));
g.setItem(size - 1, mkItem(
Material.ARROW, "§7返回"));
p.openInventory(g);
}
@@ -494,11 +683,81 @@ public class GUIManager {
Material.SNOWBALL,
"§e§l雪球菜单",
"§7点击为该玩家发放菜单雪球"));
g.setItem(16, mkItem(
Material.ENDER_EYE,
"§b§l打卡记录",
"§7查看该玩家的群系打卡记录"));
g.setItem(22, mkItem(Material.ARROW, "§7返回"));
p.openInventory(g);
}
public void openCheckInRecords(
Player p, String target) {
Inventory g = Bukkit.createInventory(
null, 54,
"§b§l打卡记录: " + target);
fillBg(g);
QuestTracker qt =
plugin.getQuestTracker();
List<String> 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 命令触发
+89 -96
View File
@@ -1,96 +1,89 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.text.SimpleDateFormat;
import java.util.*;
public class InviteManager {
private final Main plugin;
public InviteManager(Main plugin) {
this.plugin = plugin;
}
public String generateCode(Player p) {
String code = PasswordUtils.generateInviteCode();
plugin.getDb().setField(
p.getName(), "invite_code", code);
return code;
}
public void onInviteeCheckIn(String inviteeName) {
String invitedBy = (String) plugin.getDb()
.getField(inviteeName, "invited_by");
if (invitedBy == null || invitedBy.isEmpty())
return;
String month = new SimpleDateFormat("yyyy-MM")
.format(new Date());
String lastMonth = (String) plugin.getDb()
.getField(invitedBy, "invite_month");
if (!month.equals(lastMonth)) {
plugin.getDb().setField(
invitedBy, "invite_month", month);
plugin.getDb().setField(
invitedBy, "monthly_invite_count", 0);
}
int count = ((Number) plugin.getDb()
.getField(invitedBy,
"monthly_invite_count"))
.intValue();
plugin.getDb().setField(
invitedBy, "monthly_invite_count",
count + 1);
double rate = 0.10;
if (count + 1 > 15) {
rate = 0.60;
} else if (count + 1 > 10) {
rate = 0.35;
} else if (count + 1 > 5) {
rate = 0.15;
}
int bonus = (int) (1 * rate);
if (bonus > 0) {
plugin.getDb().addPoints(invitedBy, bonus);
Player inviterPlayer = Bukkit
.getPlayerExact(invitedBy);
if (inviterPlayer != null
&& inviterPlayer.isOnline()) {
inviterPlayer.sendMessage(
plugin.getConfig2()
.msg("invite_referral_bonus")
.replace("{points}",
String.valueOf(bonus)));
}
}
}
public List<Map<String, Object>> getAllInviteData() {
List<Map<String, Object>> result =
new ArrayList<>();
for (Map<String, Object> user
: plugin.getDb().getAllUsers()) {
String code = (String) user.getOrDefault(
"invite_code", "");
if (!code.isEmpty()) {
Map<String, Object> row =
new LinkedHashMap<>();
row.put("player",
user.get("player_name"));
row.put("code", code);
row.put("monthly",
user.getOrDefault(
"monthly_invite_count", 0));
result.add(row);
}
}
return result;
}
}
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.text.SimpleDateFormat;
import java.util.*;
public class InviteManager {
private final Main plugin;
public InviteManager(Main plugin) {
this.plugin = plugin;
}
public String generateCode(Player p) {
String code = PasswordUtils.generateInviteCode();
plugin.getDb().setField(
p.getName(), "invite_code", code);
return code;
}
public void onInviteeCheckIn(String inviteeName) {
String invitedBy = (String) plugin.getDb()
.getField(inviteeName, "invited_by");
if (invitedBy == null || invitedBy.isEmpty())
return;
String month = new SimpleDateFormat("yyyy-MM")
.format(new Date());
String lastMonth = (String) plugin.getDb()
.getField(invitedBy, "invite_month");
if (!month.equals(lastMonth)) {
plugin.getDb().setField(
invitedBy, "invite_month", month);
plugin.getDb().setField(
invitedBy, "monthly_invite_count", 0);
}
int count = ((Number) plugin.getDb()
.getField(invitedBy,
"monthly_invite_count"))
.intValue();
plugin.getDb().setField(
invitedBy, "monthly_invite_count",
count + 1);
double rate = plugin.getCommission()
.getCommissionRate(invitedBy);
int bonus = (int) (1 * rate);
if (bonus > 0) {
plugin.getDb().addPoints(invitedBy, bonus);
Player inviterPlayer = Bukkit
.getPlayerExact(invitedBy);
if (inviterPlayer != null
&& inviterPlayer.isOnline()) {
inviterPlayer.sendMessage(
plugin.getConfig2()
.msg("invite_referral_bonus")
.replace("{points}",
String.valueOf(bonus)));
}
}
}
public List<Map<String, Object>> getAllInviteData() {
List<Map<String, Object>> result =
new ArrayList<>();
for (Map<String, Object> user
: plugin.getDb().getAllUsers()) {
String code = (String) user.getOrDefault(
"invite_code", "");
if (!code.isEmpty()) {
Map<String, Object> 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;
}
}
+302 -302
View File
@@ -1,302 +1,302 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
public class LoginManager {
private final Main plugin;
public LoginManager(Main plugin) {
this.plugin = plugin;
}
public boolean handleRegister(Player p,
String[] args) {
String name = p.getName();
// 已注册玩家不能再注册
if (plugin.getDb().userExists(name)) {
p.sendMessage(plugin.getConfig2()
.msg("already_registered"));
return true;
}
// IP注册限制检查
String ip = plugin.getPlayerIP(p);
if (ip != null && plugin.getConfig2()
.maxAccountsPerIP > 0) {
if (!plugin.getIPGroup()
.canRegister(ip)) {
int count = plugin.getIPGroup()
.getAccountCount(ip);
int max = plugin.getConfig2()
.maxAccountsPerIP;
p.sendMessage("§c该IP注册已达上限("
+ count + "/" + max + ")");
return true;
}
}
if (args.length < 1) {
p.sendMessage("§c用法: /reg <密码>");
p.sendMessage("§7密码要求: 6位以上,"
+ "包含大小写字母和数字");
return true;
}
String pwd = args[0];
if (pwd.length() < 6) {
p.sendMessage("§c密码长度不足6位");
return true;
}
if (!pwd.matches(".*[a-z].*")
|| !pwd.matches(".*[A-Z].*")
|| !pwd.matches(".*[0-9].*")) {
p.sendMessage("§c密码必须包含大写字母、"
+ "小写字母和数字");
return true;
}
String salt =
PasswordUtils.generateSalt();
String hash =
PasswordUtils.hash(pwd, salt);
plugin.getDb().createUser(
name, hash, salt);
plugin.getLoggedIn().add(name);
plugin.getDb().setLoggedIn(name, true);
plugin.getDb().setField(name,
"last_login_time",
System.currentTimeMillis());
plugin.getDb().setField(name,
"last_online_check",
System.currentTimeMillis());
plugin.getDb().recordIP(name, ip);
plugin.getDb().setField(name,
"register_time",
System.currentTimeMillis());
plugin.restoreInventory(p);
plugin.recordIPLogin(p);
plugin.giveMenuSnowball(p);
p.sendMessage(plugin.getConfig2()
.msg("reg_success"));
playRegisterSound(p);
try {
org.bukkit.plugin.Plugin cy =
Bukkit.getPluginManager()
.getPlugin("CY_beibao");
if (cy != null && cy.isEnabled()) {
cy.getClass().getMethod(
"onSdf1Activation",
String.class,
int.class, int.class)
.invoke(cy, name, 9, 0);
}
} catch (Exception ignored) {
}
return true;
}
private void playRegisterSound(Player p) {
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_CHIME,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_BASS,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_PLING,
1.0f, 0.79f);
}
public boolean handleLogin(Player p,
String[] args) {
String name = p.getName();
try {
if (plugin.getLoggedIn().contains(name)) {
p.sendMessage(plugin.getConfig2()
.msg("already_logged_in"));
return true;
}
if (!plugin.getDb().userExists(name)) {
p.sendMessage(plugin.getConfig2()
.msg("not_registered"));
return true;
}
if (args.length < 1) {
p.sendMessage("§c用法: /login <密码>");
return true;
}
String pwd = args[0];
String salt = (String) plugin.getDb()
.getField(name, "password_salt");
if (salt == null) {
p.sendMessage(
"§c数据异常,请联系管理员");
plugin.getLogger().severe(
"[Sdf1_login] 玩家 " + name
+ " 盐值为null");
return true;
}
String hash =
PasswordUtils.hash(pwd, salt);
boolean matchMain =
plugin.getDb().checkPassword(
name, hash);
if (matchMain) {
plugin.getLoggedIn().add(name);
plugin.getDb().setLoggedIn(
name, true);
plugin.getDb().setField(name,
"last_login_time",
System.currentTimeMillis());
plugin.getDb().setField(name,
"last_online_check",
System.currentTimeMillis());
// 只在有有效备份时恢复
plugin.restoreInventory(p);
plugin.recordIPLogin(p);
plugin.giveMenuSnowball(p);
p.sendMessage(plugin.getConfig2()
.msg("login_success"));
activateCY(p, name);
return true;
}
boolean matchTemp =
plugin.getDb()
.checkPasswordOrTemp(
name, hash);
if (matchTemp) {
plugin.getLoggedIn().add(name);
plugin.getDb().setLoggedIn(
name, true);
plugin.getDb().setField(name,
"last_login_time",
System.currentTimeMillis());
plugin.getDb().setField(name,
"last_online_check",
System.currentTimeMillis());
plugin.restoreInventory(p);
plugin.recordIPLogin(p);
plugin.giveMenuSnowball(p);
plugin.getNeedsPasswordChange()
.add(name);
p.sendMessage(plugin.getConfig2()
.msg("login_success"));
p.sendMessage(
"§c§l[警告] §f您使用的是临时密码,请尽快修改密码!");
p.sendMessage(
"§7用法: /sdf1_login pw");
activateCY(p, name);
return true;
}
p.sendMessage(plugin.getConfig2()
.msg("login_failed"));
} catch (Exception e) {
plugin.getLogger().severe(
"[Sdf1_login] handleLogin异常: "
+ e.getMessage());
e.printStackTrace();
p.sendMessage(
"§c登录过程出错,请联系管理员");
}
return true;
}
public void handleReset(Player p) {
String name = p.getName();
String emailAddr = (String)
plugin.getDb().getField(
name, "email");
if (emailAddr == null
|| emailAddr.isEmpty()) {
p.sendMessage(
"§c未设置邮箱,无法重置密码");
return;
}
String tempPwd = PasswordUtils
.generateTempPassword();
String salt = (String)
plugin.getDb().getField(
name, "password_salt");
String hash = PasswordUtils.hash(
tempPwd, salt);
plugin.getDb().setField(name,
"temp_password", hash);
plugin.getDb().setField(name,
"temp_pw_expire",
System.currentTimeMillis()
+ 300000L);
plugin.getDb().setField(name,
"temp_pw_used", 0);
p.sendMessage("§e正在发送临时密码...");
final String to = emailAddr;
final String fName = name;
final String pwd = tempPwd;
final Main self = plugin;
Bukkit.getScheduler()
.runTaskAsynchronously(self,
() -> {
final boolean[] sent = {false};
Thread t = new Thread(() -> {
sent[0] = self.getEmail()
.sendTempPassword(
to, fName, pwd);
}, "Sdf1_login-reset");
t.setDaemon(true);
t.start();
try {
t.join(15000);
} catch (
InterruptedException ignored) {
}
if (t.isAlive()) {
t.interrupt();
Bukkit.getScheduler().runTask(
self, () ->
p.sendMessage(
"§c邮件发送超时"));
return;
}
Bukkit.getScheduler().runTask(
self, () -> {
if (sent[0]) {
p.sendMessage(
"§a临时密码已发送到 "
+ to);
} else {
p.sendMessage(
"§c邮件发送失败");
}
});
});
}
private void activateCY(Player p,
String name) {
try {
org.bukkit.plugin.Plugin cy =
Bukkit.getPluginManager()
.getPlugin("CY_beibao");
if (cy != null && cy.isEnabled()) {
cy.getClass().getMethod(
"onSdf1Activation",
String.class,
int.class, int.class)
.invoke(cy, name, 0, 0);
}
} catch (Exception ignored) {
}
}
}
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.bukkit.entity.Player;
public class LoginManager {
private final Main plugin;
public LoginManager(Main plugin) {
this.plugin = plugin;
}
public boolean handleRegister(Player p,
String[] args) {
String name = p.getName();
// 已注册玩家不能再注册
if (plugin.getDb().userExists(name)) {
p.sendMessage(plugin.getConfig2()
.msg("already_registered"));
return true;
}
// IP注册限制检查
String ip = plugin.getPlayerIP(p);
if (ip != null && plugin.getConfig2()
.maxAccountsPerIP > 0) {
if (!plugin.getIPGroup()
.canRegister(ip)) {
int count = plugin.getIPGroup()
.getAccountCount(ip);
int max = plugin.getConfig2()
.maxAccountsPerIP;
p.sendMessage("§c该IP注册已达上限("
+ count + "/" + max + ")");
return true;
}
}
if (args.length < 1) {
p.sendMessage("§c用法: /reg <密码>");
p.sendMessage("§7密码要求: 6位以上,"
+ "包含大小写字母和数字");
return true;
}
String pwd = args[0];
if (pwd.length() < 6) {
p.sendMessage("§c密码长度不足6位");
return true;
}
if (!pwd.matches(".*[a-z].*")
|| !pwd.matches(".*[A-Z].*")
|| !pwd.matches(".*[0-9].*")) {
p.sendMessage("§c密码必须包含大写字母、"
+ "小写字母和数字");
return true;
}
String salt =
PasswordUtils.generateSalt();
String hash =
PasswordUtils.hash(pwd, salt);
plugin.getDb().createUser(
name, hash, salt);
plugin.getLoggedIn().add(name);
plugin.getDb().setLoggedIn(name, true);
plugin.getDb().setField(name,
"last_login_time",
System.currentTimeMillis());
plugin.getDb().setField(name,
"last_online_check",
System.currentTimeMillis());
plugin.getDb().recordIP(name, ip);
plugin.getDb().setField(name,
"register_time",
System.currentTimeMillis());
plugin.restoreInventory(p);
plugin.recordIPLogin(p);
plugin.giveMenuSnowball(p);
p.sendMessage(plugin.getConfig2()
.msg("reg_success"));
playRegisterSound(p);
try {
org.bukkit.plugin.Plugin cy =
Bukkit.getPluginManager()
.getPlugin("CY_beibao");
if (cy != null && cy.isEnabled()) {
cy.getClass().getMethod(
"onSdf1Activation",
String.class,
int.class, int.class)
.invoke(cy, name, 9, 0);
}
} catch (Exception ignored) {
}
return true;
}
private void playRegisterSound(Player p) {
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_CHIME,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_BASS,
1.0f, 0.79f);
p.playSound(p.getLocation(),
Sound.BLOCK_NOTE_BLOCK_PLING,
1.0f, 0.79f);
}
public boolean handleLogin(Player p,
String[] args) {
String name = p.getName();
try {
if (plugin.getLoggedIn().contains(name)) {
p.sendMessage(plugin.getConfig2()
.msg("already_logged_in"));
return true;
}
if (!plugin.getDb().userExists(name)) {
p.sendMessage(plugin.getConfig2()
.msg("not_registered"));
return true;
}
if (args.length < 1) {
p.sendMessage("§c用法: /login <密码>");
return true;
}
String pwd = args[0];
String salt = (String) plugin.getDb()
.getField(name, "password_salt");
if (salt == null) {
p.sendMessage(
"§c数据异常,请联系管理员");
plugin.getLogger().severe(
"[Sdf1_login] 玩家 " + name
+ " 盐值为null");
return true;
}
String hash =
PasswordUtils.hash(pwd, salt);
boolean matchMain =
plugin.getDb().checkPassword(
name, hash);
if (matchMain) {
plugin.getLoggedIn().add(name);
plugin.getDb().setLoggedIn(
name, true);
plugin.getDb().setField(name,
"last_login_time",
System.currentTimeMillis());
plugin.getDb().setField(name,
"last_online_check",
System.currentTimeMillis());
// 只在有有效备份时恢复
plugin.restoreInventory(p);
plugin.recordIPLogin(p);
plugin.giveMenuSnowball(p);
p.sendMessage(plugin.getConfig2()
.msg("login_success"));
activateCY(p, name);
return true;
}
boolean matchTemp =
plugin.getDb()
.checkPasswordOrTemp(
name, hash);
if (matchTemp) {
plugin.getLoggedIn().add(name);
plugin.getDb().setLoggedIn(
name, true);
plugin.getDb().setField(name,
"last_login_time",
System.currentTimeMillis());
plugin.getDb().setField(name,
"last_online_check",
System.currentTimeMillis());
plugin.restoreInventory(p);
plugin.recordIPLogin(p);
plugin.giveMenuSnowball(p);
plugin.getNeedsPasswordChange()
.add(name);
p.sendMessage(plugin.getConfig2()
.msg("login_success"));
p.sendMessage(
"§c§l[警告] §f您使用的是临时密码,请尽快修改密码!");
p.sendMessage(
"§7用法: /sdf1_login pw");
activateCY(p, name);
return true;
}
p.sendMessage(plugin.getConfig2()
.msg("login_failed"));
} catch (Exception e) {
plugin.getLogger().severe(
"[Sdf1_login] handleLogin异常: "
+ e.getMessage());
e.printStackTrace();
p.sendMessage(
"§c登录过程出错,请联系管理员");
}
return true;
}
public void handleReset(Player p) {
String name = p.getName();
String emailAddr = (String)
plugin.getDb().getField(
name, "email");
if (emailAddr == null
|| emailAddr.isEmpty()) {
p.sendMessage(
"§c未设置邮箱,无法重置密码");
return;
}
String tempPwd = PasswordUtils
.generateTempPassword();
String salt = (String)
plugin.getDb().getField(
name, "password_salt");
String hash = PasswordUtils.hash(
tempPwd, salt);
plugin.getDb().setField(name,
"temp_password", hash);
plugin.getDb().setField(name,
"temp_pw_expire",
System.currentTimeMillis()
+ 300000L);
plugin.getDb().setField(name,
"temp_pw_used", 0);
p.sendMessage("§e正在发送临时密码...");
final String to = emailAddr;
final String fName = name;
final String pwd = tempPwd;
final Main self = plugin;
Bukkit.getScheduler()
.runTaskAsynchronously(self,
() -> {
final boolean[] sent = {false};
Thread t = new Thread(() -> {
sent[0] = self.getEmail()
.sendTempPassword(
to, fName, pwd);
}, "Sdf1_login-reset");
t.setDaemon(true);
t.start();
try {
t.join(15000);
} catch (
InterruptedException ignored) {
}
if (t.isAlive()) {
t.interrupt();
Bukkit.getScheduler().runTask(
self, () ->
p.sendMessage(
"§c邮件发送超时"));
return;
}
Bukkit.getScheduler().runTask(
self, () -> {
if (sent[0]) {
p.sendMessage(
"§a临时密码已发送到 "
+ to);
} else {
p.sendMessage(
"§c邮件发送失败");
}
});
});
}
private void activateCY(Player p,
String name) {
try {
org.bukkit.plugin.Plugin cy =
Bukkit.getPluginManager()
.getPlugin("CY_beibao");
if (cy != null && cy.isEnabled()) {
cy.getClass().getMethod(
"onSdf1Activation",
String.class,
int.class, int.class)
.invoke(cy, name, 0, 0);
}
} catch (Exception ignored) {
}
}
}
File diff suppressed because it is too large Load Diff
+130
View File
@@ -0,0 +1,130 @@
package Sdf1_login;
import java.io.File;
import java.sql.*;
import java.util.logging.Logger;
public class PlayerStorage {
private Connection conn;
private final Logger logger;
public PlayerStorage(File dataFolder,
Logger logger) {
this.logger = logger;
File dbFile = new File(
dataFolder,
"quest_data.db");
try {
Class.forName(
"org.sqlite.JDBC");
conn = DriverManager
.getConnection(
"jdbc:sqlite:"
+ dbFile
.getAbsolutePath());
conn.setAutoCommit(true);
Statement s =
conn.createStatement();
s.executeUpdate(
"CREATE TABLE IF NOT EXISTS "
+ "quest_kv ("
+ "player TEXT NOT NULL,"
+ "key TEXT NOT NULL,"
+ "value TEXT,"
+ "PRIMARY KEY"
+ "(player, key))");
s.close();
logger.info(
"[QuestStorage] 初始化: "
+ dbFile.getAbsolutePath());
} catch (Exception e) {
logger.severe(
"[QuestStorage] 失败: "
+ e.getMessage());
}
}
public Object getCondition(
String player, String key) {
if (conn == null) return null;
try {
PreparedStatement ps =
conn.prepareStatement(
"SELECT value "
+ "FROM quest_kv "
+ "WHERE player=? "
+ "AND key=?");
ps.setString(1,
player.toLowerCase());
ps.setString(2, key);
ResultSet rs =
ps.executeQuery();
String val = rs.next()
? rs.getString("value")
: null;
rs.close();
ps.close();
return parseValue(val);
} catch (SQLException e) {
return null;
}
}
public void setCondition(
String player, String key) {
setConditionRaw(
player, key, "true");
}
public void debugPlayer(String player) {
}
public void setConditionRaw(
String player, String key,
String value) {
if (conn == null) return;
try {
PreparedStatement ps =
conn.prepareStatement(
"INSERT OR REPLACE "
+ "INTO quest_kv"
+ "(player,key,value)"
+ " VALUES(?,?,?)");
ps.setString(1,
player.toLowerCase());
ps.setString(2, key);
ps.setString(3, value);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
logger.warning(
"[QuestStorage] 写入失败: "
+ key + "=" + value
+ " | " + e.getMessage());
}
}
public void close() {
try {
if (conn != null
&& !conn.isClosed())
conn.close();
} catch (Exception e) {}
}
private Object parseValue(String v) {
if (v == null) return null;
try {
return Long.parseLong(v);
} catch (Exception e1) {
try {
return Double.parseDouble(v);
} catch (Exception e2) {
if ("true".equalsIgnoreCase(v))
return true;
if ("false".equalsIgnoreCase(v))
return false;
return v;
}
}
}
}
+138
View File
@@ -0,0 +1,138 @@
package Sdf1_login;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class QuestStorage {
private Connection conn;
public QuestStorage(File dataFolder) {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(
dataFolder,
"quest_data.db");
conn = DriverManager.getConnection(
"jdbc:sqlite:"
+ dbFile.getPath());
initTable();
} catch (Exception e) {
e.printStackTrace();
}
}
private void initTable()
throws SQLException {
Statement stmt =
conn.createStatement();
stmt.executeUpdate(
"CREATE TABLE IF NOT EXISTS "
+ "quest_conditions ("
+ "player TEXT NOT NULL, "
+ "condition_key TEXT NOT NULL, "
+ "value TEXT DEFAULT '1', "
+ "timestamp INTEGER, "
+ "PRIMARY KEY "
+ "(player, condition_key))");
stmt.close();
}
/**
* 标记条件已完成(value = "1")
*/
public void setCondition(
String player,
String condition) {
setConditionRaw(
player, condition, "1");
}
/**
* 写入任意值(计数器等)
*/
public void setConditionRaw(
String player,
String condition,
String value) {
try {
PreparedStatement ps =
conn.prepareStatement(
"INSERT OR REPLACE "
+ "INTO "
+ "quest_conditions "
+ "(player, "
+ "condition_key, "
+ "value, timestamp) "
+ "VALUES "
+ "(?, ?, ?, ?)");
ps.setString(1, player);
ps.setString(2, condition);
ps.setString(3, value);
ps.setLong(4,
System.currentTimeMillis());
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 查询条件值
*/
public Object getCondition(
String player,
String condition) {
try {
PreparedStatement ps =
conn.prepareStatement(
"SELECT value FROM "
+ "quest_conditions "
+ "WHERE player = ? "
+ "AND "
+ "condition_key = ?");
ps.setString(1, player);
ps.setString(2, condition);
ResultSet rs =
ps.executeQuery();
if (rs.next()) {
String val =
rs.getString("value");
rs.close();
ps.close();
// 尝试解析为数字
try {
if (val.contains(".")) {
return Double.parseDouble(
val);
}
return Long.parseLong(val);
} catch (NumberFormatException e) {
return val;
}
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public void close() {
try {
if (conn != null
&& !conn.isClosed()) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,118 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerResourcePackStatusEvent;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
public class RadioDownloadListener
implements Listener {
private final Logger log;
private final Main plugin;
private final Map<UUID, Integer> retryCount =
new ConcurrentHashMap<>();
private static final int MAX_RETRY = 2;
public RadioDownloadListener(Main plugin) {
this.plugin = plugin;
this.log = plugin.getLogger();
}
@EventHandler
public void onStatus(
PlayerResourcePackStatusEvent e) {
Player p = e.getPlayer();
UUID uuid = p.getUniqueId();
String status = e.getStatus().name();
log.info("[Radio] " + p.getName()
+ " status: " + status);
switch (status) {
case "ACCEPTED":
break;
case "DOWNLOADED":
break;
case "SUCCESS":
log.info("[Radio] " + p.getName()
+ " 资源包加载成功");
retryCount.remove(uuid);
break;
case "FAILED_DOWNLOAD":
handleRetry(p, uuid);
break;
case "DECLINED":
log.info("[Radio] " + p.getName()
+ " 拒绝下载");
p.sendMessage(
"§e[Radio] §f你拒绝了资源包");
retryCount.remove(uuid);
break;
case "DISCARDED":
log.warning("[Radio] " + p.getName()
+ " DISCARDED");
p.sendMessage(
"§c[Radio] §f资源包下载失败"
+ ",可能原因:");
p.sendMessage(
"§71. CDN/防火墙拦截");
p.sendMessage(
"§72. SSL证书问题");
retryCount.remove(uuid);
break;
case "INVALID_URL":
log.warning("[Radio] " + p.getName()
+ " URL无效");
retryCount.remove(uuid);
break;
default:
log.info("[Radio] " + p.getName()
+ " 未知: " + status);
break;
}
}
private void handleRetry(Player p, UUID uuid) {
int count = retryCount
.getOrDefault(uuid, 0) + 1;
retryCount.put(uuid, count);
log.warning("[Radio] " + p.getName()
+ " 下载失败 (" + count
+ "/" + MAX_RETRY + ")");
if (count <= MAX_RETRY) {
p.sendMessage(
"§e[Radio] §f下载失败,重试中... ("
+ count + "/" + MAX_RETRY + ")");
Bukkit.getScheduler().runTaskLater(
plugin,
() -> {
if (p.isOnline()
&& plugin.radio != null) {
plugin.radio
.sendResourcePack(p);
}
}, 60L);
} else {
p.sendMessage(
"§c[Radio] §f资源包加载失败,"
+ "部分音乐无法播放");
retryCount.remove(uuid);
}
}
}
+874
View File
@@ -0,0 +1,874 @@
package Sdf1_login;
import com.sun.net.httpserver.HttpServer;
import org.bukkit.*;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.security.MessageDigest;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import java.util.zip.*;
public class RadioManager {
private final Main plugin;
private File radioDir;
private File oggDir;
private File packDir;
private File configFile;
private BukkitTask mainRadioTask;
private BukkitTask loginRadioTask;
private final Map<String, Boolean> 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<File> sourceFiles =
new ArrayList<>();
private final List<File> oggFiles =
new ArrayList<>();
private final Set<UUID> sending =
Collections.newSetFromMap(
new ConcurrentHashMap<>());
private String adminTag = "admin";
public RadioManager(Main plugin) {
this.plugin = plugin;
}
// ========== init ==========
public void init() {
radioDir = new File(
plugin.getDataFolder(), "radio");
oggDir = new File(
plugin.getDataFolder(),
"radio/ogg");
packDir = new File(
plugin.getDataFolder(),
"radiopack");
radioDir.mkdirs();
oggDir.mkdirs();
packDir.mkdirs();
loadRadioConfig();
doConvert();
buildPack();
loadOggFiles();
startHttpServer();
plugin.getLogger().info(
"[Radio] init done: "
+ oggFiles.size() + " audio(s)");
}
// ========== radio_config.txt ==========
private void loadRadioConfig() {
configFile = new File(
plugin.getDataFolder(),
"radio_config.txt");
if (!configFile.exists()) {
writeDefaultConfig();
}
try (BufferedReader br =
new BufferedReader(
new FileReader(
configFile))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()
|| line.startsWith("#"))
continue;
int eq = line.indexOf('=');
if (eq <= 0) continue;
String key = line.substring(0, eq)
.trim();
String val = line.substring(eq + 1)
.trim();
if (key.equals(
"resource-pack-url")) {
resourcePackUrl = val;
} else if (key.equals(
"http-port")) {
try {
httpPort = Integer
.parseInt(val);
} catch (Exception ignored) {}
}
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Radio] config error: "
+ e.getMessage());
}
loadAdminTag();
plugin.getLogger().info("[Radio] config: url="
+ (resourcePackUrl.isEmpty()
? "(none)" : resourcePackUrl)
+ " port=" + httpPort);
}
/**
* 从插件设置.txt读取管理标签
*/
private void loadAdminTag() {
File settingsFile = new File(
plugin.getDataFolder(),
"插件设置.txt");
if (!settingsFile.exists()) return;
try (BufferedReader br =
new BufferedReader(
new FileReader(
settingsFile))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()
|| line.startsWith("#"))
continue;
if (line.startsWith("管理标签=")
|| line.startsWith("管理标签 =")) {
int eq = line.indexOf('=');
if (eq > 0) {
adminTag = line
.substring(eq + 1)
.trim();
}
}
}
} catch (Exception e) {
// ignore
}
plugin.getLogger().info(
"[Radio] admin tag: " + adminTag);
}
/**
* 检查玩家是否有管理标签
* 控制台不受限制
*/
public boolean hasRadioPermission(
CommandSender sender) {
// 控制台直接通过
if (!(sender instanceof Player)) {
return true;
}
Player p = (Player) sender;
// 检查Bukkit权限节点
// 格式: sdf1.admin
if (p.hasPermission("sdf1.admin")) {
return true;
}
// 检查玩家名是否在admin列表中
// 你可以扩展这里对接你的标签系统
return false;
}
private void writeDefaultConfig() {
try {
plugin.getDataFolder().mkdirs();
String c =
"# ============ Sdf1 Radio "
+ "Config ============\n"
+ "#\n"
+ "# resource-pack-url\n"
+ "# 填外部HTTPS地址\n"
+ "# 留空 = 使用内置HTTP"
+ "服务\n"
+ "#\n"
+ "# http-port\n"
+ "# 内置HTTP端口"
+ " (仅url为空时生效)\n"
+ "#\n"
+ "# ============ Settings"
+ " ==============\n"
+ "\n"
+ "resource-pack-url=\n"
+ "http-port=25566\n";
Files.write(configFile.toPath(),
c.getBytes("UTF-8"));
} catch (Exception e) {
plugin.getLogger().warning(
"[Radio] config create fail");
}
}
// ========== convert ==========
private void doConvert() {
sourceFiles.clear();
scanSources(radioDir);
for (File src : sourceFiles) {
String name = src.getName().replaceAll(
"\\.(ogg|mp3|wav|flac)$",
".ogg");
File dest = new File(oggDir, name);
if (dest.exists()
&& dest.lastModified()
>= src.lastModified()) continue;
if (src.getName().toLowerCase()
.endsWith(".ogg")) {
fileCopy(src, dest);
plugin.getLogger().info(
"[Radio] copy: "
+ src.getName());
} else if (runFfmpeg(src, dest)) {
plugin.getLogger().info(
"[Radio] convert: "
+ src.getName());
} else {
plugin.getLogger().warning(
"[Radio] skip(need ffmpeg): "
+ src.getName());
}
}
}
private void scanSources(File dir) {
if (!dir.exists()) return;
File[] files = dir.listFiles();
if (files == null) return;
for (File f : files) {
if (f.isDirectory()
&& !f.getName().equals("ogg")) {
scanSources(f);
} else if (f.isFile()) {
String n = f.getName().toLowerCase();
if (n.endsWith(".ogg")
|| n.endsWith(".mp3")
|| n.endsWith(".wav")
|| n.endsWith(".flac")) {
sourceFiles.add(f);
}
}
}
}
private void fileCopy(File src, File dest) {
try {
dest.getParentFile().mkdirs();
Files.copy(src.toPath(),
dest.toPath(),
StandardCopyOption
.REPLACE_EXISTING);
} catch (Exception ignored) {}
}
private boolean runFfmpeg(File src, File dest) {
try {
dest.getParentFile().mkdirs();
ProcessBuilder pb = new ProcessBuilder(
"ffmpeg", "-y", "-i",
src.getAbsolutePath(),
"-c:a", "libvorbis",
"-q:a", "5",
dest.getAbsolutePath());
pb.redirectErrorStream(true);
Process p = pb.start();
p.waitFor();
return dest.exists();
} catch (Exception e) {
return false;
}
}
// ========== pack ==========
private void buildPack() {
deleteDir(packDir);
packDir.mkdirs();
List<File> allOggs = new ArrayList<>();
scanOggs(oggDir, allOggs);
if (allOggs.isEmpty()) {
plugin.getLogger().warning(
"[Radio] no ogg found");
return;
}
File soundsDir = new File(packDir,
"assets/minecraft/sounds/custom");
soundsDir.mkdirs();
Map<String, File> unique =
new LinkedHashMap<>();
for (File ogg : allOggs) {
String name = ogg.getName()
.replace(".ogg", "");
unique.putIfAbsent(
name.toLowerCase(), ogg);
}
StringBuilder json =
new StringBuilder("{\n");
int idx = 0, total = unique.size();
for (Map.Entry<String, File> e
: unique.entrySet()) {
String name = e.getKey();
File ogg = e.getValue();
File dest = new File(soundsDir,
name + ".ogg");
try {
Files.copy(ogg.toPath(),
dest.toPath(),
StandardCopyOption
.REPLACE_EXISTING);
plugin.getLogger().info(
"[Radio] pack: " + name
+ " (" + dest.length()
+ " bytes)");
} catch (Exception ex) {
plugin.getLogger().warning(
"[Radio] copy fail: "
+ name);
continue;
}
json.append(" \"custom.")
.append(name).append("\": {\n");
json.append(
" \"replace\": false,\n");
json.append(
" \"sounds\": [{\n");
json.append(
" \"name\": \"custom/")
.append(name).append("\",\n");
json.append(
" \"stream\": true\n");
json.append(" }]\n");
json.append(" }");
idx++;
if (idx < total) json.append(",");
json.append("\n");
}
json.append("}\n");
try {
Files.write(new File(packDir,
"assets/minecraft/sounds.json")
.toPath(),
json.toString()
.getBytes("UTF-8"));
plugin.getLogger().info(
"[Radio] sounds.json:\n"
+ json.toString());
} catch (Exception e) {
plugin.getLogger().warning(
"[Radio] sounds.json fail");
}
try {
String meta =
"{\"pack\":{\"pack_format\":54,"
+ "\"description\":\"Sdf1 Radio\""
+ "}}\n";
Files.write(new File(packDir,
"pack.mcmeta").toPath(),
meta.getBytes("UTF-8"));
} catch (Exception ignored) {}
File zipFile = new File(
plugin.getDataFolder(),
"radiopack.zip");
try {
zipPack(packDir, zipFile);
packBytes = Files.readAllBytes(
zipFile.toPath());
MessageDigest md =
MessageDigest.getInstance(
"SHA-1");
md.update(packBytes);
StringBuilder sb =
new StringBuilder();
for (byte b : md.digest())
sb.append(String.format(
"%02x", b));
packHash = sb.toString();
plugin.getLogger().info(
"[Radio] zip: "
+ zipFile.length()
+ " bytes, SHA1="
+ packHash);
} catch (Exception e) {
plugin.getLogger().warning(
"[Radio] zip fail: "
+ e.getMessage());
}
}
private void scanOggs(File dir, List<File> list) {
File[] files = dir.listFiles();
if (files == null) return;
for (File f : files) {
if (f.isDirectory())
scanOggs(f, list);
else if (f.getName()
.endsWith(".ogg"))
list.add(f);
}
}
private void loadOggFiles() {
oggFiles.clear();
scanOggs(oggDir, oggFiles);
}
private void deleteDir(File dir) {
if (!dir.exists()) return;
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
if (f.isDirectory())
deleteDir(f);
else f.delete();
}
}
dir.delete();
}
private void zipPack(File root, File zipFile)
throws Exception {
try (ZipOutputStream zos =
new ZipOutputStream(
new FileOutputStream(
zipFile))) {
addZipEntries(root, root, zos);
}
}
private void addZipEntries(File root, File cur,
ZipOutputStream zos)
throws Exception {
File[] files = cur.listFiles();
if (files == null) return;
for (File f : files) {
if (f.isDirectory()) {
addZipEntries(root, f, zos);
} else {
String path = root.toURI()
.relativize(f.toURI())
.getPath();
zos.putNextEntry(
new ZipEntry(path));
zos.write(Files.readAllBytes(
f.toPath()));
zos.closeEntry();
}
}
}
// ========== HTTP server ==========
private void startHttpServer() {
if (packBytes == null) return;
if (!resourcePackUrl.isEmpty()) {
plugin.getLogger().info(
"[Radio] skip HTTP "
+ "(external URL)");
return;
}
try {
httpServer = HttpServer.create(
new InetSocketAddress(
"0.0.0.0",
httpPort), 0);
httpServer.createContext(
"/radiopack.zip",
exchange -> {
if (packBytes != null) {
exchange
.getResponseHeaders()
.set("Content-Type",
"application/zip");
exchange
.sendResponseHeaders(
200,
packBytes.length);
exchange
.getResponseBody()
.write(packBytes);
exchange
.getResponseBody()
.close();
} else {
exchange
.sendResponseHeaders(
404, 0);
exchange.close();
}
});
httpServer.setExecutor(null);
httpServer.start();
String ip = "127.0.0.1";
try {
ip = InetAddress.getLocalHost()
.getHostAddress();
} catch (Exception ignored) {}
plugin.getLogger().info(
"[Radio] HTTP on port "
+ httpPort);
plugin.getLogger().info(
"[Radio] url: http://" + ip
+ ":" + httpPort
+ "/radiopack.zip");
} catch (Exception e) {
plugin.getLogger().warning(
"[Radio] HTTP fail: "
+ e.getMessage());
}
}
// ========== send resource pack ==========
public void sendResourcePack(Player p) {
if (packBytes == null) return;
UUID pid = p.getUniqueId();
if (sending.contains(pid)) {
plugin.getLogger().info(
"[Radio] skip duplicate: "
+ p.getName());
return;
}
sending.add(pid);
UUID uuid = UUID.randomUUID();
String url = resourcePackUrl;
if (url.isEmpty()) {
String ip = "127.0.0.1";
try {
ip = InetAddress.getLocalHost()
.getHostAddress();
} catch (Exception ignored) {}
url = "http://" + ip + ":" + httpPort
+ "/radiopack.zip";
}
byte[] hashBytes;
try {
MessageDigest md =
MessageDigest.getInstance(
"SHA-1");
hashBytes = md.digest(packBytes);
} catch (Exception e) {
sending.remove(pid);
return;
}
plugin.getLogger().info(
"[Radio] url=" + url);
plugin.getLogger().info(
"[Radio] sha1=" + packHash
+ " (" + hashBytes.length
+ " bytes)");
plugin.getLogger().info(
"[Radio] size=" + packBytes.length);
try {
java.lang.reflect.Method target = null;
for (java.lang.reflect.Method m
: p.getClass().getMethods()) {
if (!m.getName().equals(
"setResourcePack"))
continue;
Class<?>[] pt =
m.getParameterTypes();
if (pt.length == 5
&& pt[0] == UUID.class
&& pt[1] == String.class
&& pt[2] == byte[].class
&& pt[3] == String.class
&& pt[4] == boolean.class) {
target = m;
break;
}
}
if (target == null) {
plugin.getLogger().warning(
"[Radio] no method");
sending.remove(pid);
return;
}
target.invoke(p, uuid, url,
hashBytes,
"Sdf1 Radio", false);
plugin.getLogger().info(
"[Radio] sent to "
+ p.getName());
} catch (Exception e) {
Throwable c = e.getCause() != null
? e.getCause() : e;
plugin.getLogger().warning(
"[Radio] failed: "
+ c.getMessage());
}
org.bukkit.Bukkit.getScheduler()
.runTaskLater(plugin, () -> {
sending.remove(pid);
}, 100L);
}
// ========== play custom ==========
public boolean playCustom(String name) {
return tryPlay(name, null);
}
public boolean playCustomTo(String name,
Player target) {
return tryPlay(name, target);
}
private boolean tryPlay(String name,
Player target) {
for (File f : oggFiles) {
String fn = f.getName()
.replace(".ogg", "");
if (fn.equalsIgnoreCase(name)) {
String key =
"minecraft:custom." + name;
if (target != null) {
target.playSound(
target.getLocation(),
key,
SoundCategory.RECORDS,
2.0f, 1.0f);
} else {
for (Player p : Bukkit
.getOnlinePlayers()) {
p.playSound(
p.getLocation(),
key,
SoundCategory.RECORDS,
2.0f, 1.0f);
}
broadcast(
"§6§l[广播] §f正在播放: §e"
+ name);
}
return true;
}
}
return false;
}
// ========== command ==========
public void handleCommand(CommandSender sender,
String[] args) {
// 无权限 → 静默,不做任何事
if (!hasRadioPermission(sender)) {
return;
}
if (args.length == 0) {
stopMainRadio();
stopAllLoginRadio();
broadcast("§6§l[广播] §f已停止播放");
return;
}
String arg = args[0].trim();
// /radio reload
if (arg.equalsIgnoreCase("reload")) {
stopMainRadio();
stopAllLoginRadio();
loadRadioConfig();
doConvert();
buildPack();
loadOggFiles();
startHttpServer();
sender.sendMessage("§a[Radio] reloaded");
return;
}
// /radio list
if (arg.equalsIgnoreCase("list")) {
sender.sendMessage("§6音频列表 ("
+ oggFiles.size() + "):");
for (File f : oggFiles)
sender.sendMessage(" §e"
+ f.getName()
.replace(".ogg", ""));
return;
}
// /radio random [玩家]
if (arg.equalsIgnoreCase("random")) {
if (oggFiles.isEmpty()) {
sender.sendMessage(
"§c没有可用的音频文件");
return;
}
File pick = oggFiles.get(
(int) (Math.random()
* oggFiles.size()));
String name = pick.getName()
.replace(".ogg", "");
if (args.length >= 2) {
Player target = Bukkit
.getPlayer(args[1]);
if (target != null
&& target.isOnline()) {
sendResourcePack(target);
playCustomTo(name, target);
target.sendMessage(
"§6§l[Radio] §f正在播放: §e"
+ name);
sender.sendMessage(
"§a[Radio] 已向 "
+ target.getName()
+ " 播放: " + name);
} else {
sender.sendMessage(
"§c玩家不在线: "
+ args[1]);
}
} else {
playCustom(name);
}
return;
}
// === /radio 音频名 [玩家名] ===
String audioArg = arg;
String playerArg = null;
if (args.length >= 2) {
playerArg = args[1].trim();
}
Player target = null;
if (playerArg != null) {
target = Bukkit.getPlayer(playerArg);
if (target == null
|| !target.isOnline()) {
sender.sendMessage(
"§c玩家不在线: " + playerArg);
return;
}
}
// 尝试 custom
String base = audioArg.replaceAll(
"\\.(ogg|mp3|wav|flac)$", "");
if (target != null) {
sendResourcePack(target);
}
if (tryPlay(base, target)) {
if (target != null) {
target.sendMessage(
"§6§l[Radio] §f正在播放: §e"
+ base);
sender.sendMessage(
"§a[Radio] 已向 "
+ target.getName()
+ " 播放: " + base);
}
return;
}
sender.sendMessage(
"§c未找到: " + audioArg
+ " §7/radio list");
}
// ========== timer ==========
public void startMainRadio() {
stopMainRadio();
mainRadioTask = new BukkitRunnable() {
public void run() {
playNext();
}
}.runTaskTimer(plugin, 100L, 2400L);
}
private void playNext() {
if (!oggFiles.isEmpty()) {
File f = oggFiles.get(
radioIndex % oggFiles.size());
playCustom(f.getName()
.replace(".ogg", ""));
radioIndex++;
}
}
public void stopMainRadio() {
if (mainRadioTask != null) {
mainRadioTask.cancel();
mainRadioTask = null;
}
}
public void startLoginRadio(Player p) {
loginPlayers.put(p.getName(), true);
if (loginRadioTask != null) return;
loginRadioTask = new BukkitRunnable() {
public void run() {
if (loginPlayers.isEmpty()) {
cancel();
loginRadioTask = null;
return;
}
if (oggFiles.isEmpty()) return;
int idx = (int)
(System.currentTimeMillis()
/ 12000L
% oggFiles.size());
for (String name
: loginPlayers.keySet()) {
Player lp = Bukkit
.getPlayer(name);
if (lp != null
&& lp.isOnline()) {
String key =
"minecraft:custom."
+ oggFiles.get(idx)
.getName()
.replace(".ogg", "");
lp.playSound(
lp.getLocation(),
key,
SoundCategory.RECORDS,
1.5f, 1.0f);
}
}
}
}.runTaskTimer(plugin, 0L, 60L);
}
public void stopLoginRadio(Player p) {
loginPlayers.remove(p.getName());
if (loginPlayers.isEmpty()
&& loginRadioTask != null) {
loginRadioTask.cancel();
loginRadioTask = null;
}
}
public void stopAllLoginRadio() {
loginPlayers.clear();
if (loginRadioTask != null) {
loginRadioTask.cancel();
loginRadioTask = null;
}
}
public void stopAll() {
stopMainRadio();
stopAllLoginRadio();
if (httpServer != null)
httpServer.stop(0);
}
private void broadcast(String msg) {
Bukkit.broadcastMessage(msg);
}
}
+1 -1
View File
@@ -1641,4 +1641,4 @@ public class TicketManager {
}
return it;
}
}
}
+523
View File
@@ -0,0 +1,523 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
public class UpdateChecker {
private final Main plugin;
private final String currentVersion;
// ===== 仓库信息 =====
private static final String GITHUB_OWNER =
"ypsdf1";
private static final String GITHUB_REPO =
"sdf1_login";
private static final String GITEE_OWNER =
"nihaoshidifu";
private static final String GITEE_REPO =
"sdf1_login";
// API 地址
private static final String GITHUB_API =
"https://api.github.com/repos/"
+ GITHUB_OWNER + "/"
+ GITHUB_REPO
+ "/tags";
private static final String GITEE_API =
"https://gitee.com/api/v5/repos/"
+ GITEE_OWNER + "/"
+ GITEE_REPO
+ "/tags";
// 仓库页面链接(给用户看的)
private static final String GITHUB_LINK =
"https://github.com/"
+ GITHUB_OWNER + "/"
+ GITHUB_REPO + "/releases";
private static final String GITEE_LINK =
"https://gitee.com/"
+ GITEE_OWNER + "/"
+ GITEE_REPO + "/releases";
// 单次请求超时(毫秒)
private static final int TIMEOUT_MS = 3000;
// 熔断阈值
private static final int TRIP_THRESHOLD = 5;
// ===== 熔断状态(内存态,重启自动清除) =====
private volatile boolean circuitTripped =
false;
private volatile int failCount = 0;
// ===== 自动检测禁用(持久化到文件) =====
private volatile boolean autoCheckDisabled =
false;
private final File disabledFile;
public UpdateChecker(Main plugin) {
this.plugin = plugin;
this.currentVersion =
plugin.getDescription()
.getVersion();
this.disabledFile = new File(
plugin.getDataFolder(),
"update_disabled.dat");
loadDisabledState();
}
// ========== 持久化禁用状态 ==========
private void loadDisabledState() {
if (disabledFile.exists()) {
try (BufferedReader br =
new BufferedReader(
new FileReader(
disabledFile))) {
String line = br.readLine();
if (line != null) {
autoCheckDisabled =
"true".equals(
line.trim());
}
} catch (Exception ignored) {
}
}
}
private void saveDisabledState() {
try (PrintWriter pw =
new PrintWriter(
new FileWriter(
disabledFile))) {
pw.println(autoCheckDisabled
? "true" : "false");
} catch (Exception ignored) {
}
}
// ========== 公开方法 ==========
/**
* onEnable 调用(异步静默)
* 熔断或禁用则直接跳过
*/
public void checkOnEnable() {
if (autoCheckDisabled) return;
if (circuitTripped) return;
Bukkit.getScheduler()
.runTaskAsynchronously(plugin,
() -> doCheck(null, true));
}
/**
* /sdf1_login update 调用(异步)
* 忽略熔断和禁用状态,强制检测
* 只有拿到版号才解除熔断
*/
public void checkUpdate(
CommandSender sender) {
Bukkit.getScheduler()
.runTaskAsynchronously(plugin,
() ->
doCheck(sender,
false));
}
// ========== 核心检测逻辑 ==========
private void doCheck(CommandSender sender,
boolean isAuto) {
if (isAuto) {
if (autoCheckDisabled
|| circuitTripped) {
return;
}
}
// 通道1:GitHub
CheckResult gh =
tryChannel(GITHUB_API,
"GitHub", GITHUB_LINK);
// 通道2:Gitee
CheckResult ge =
tryChannel(GITEE_API,
"Gitee", GITEE_LINK);
// 选择成功的结果
CheckResult chosen = null;
String failMsg = null;
if (gh.isSuccess()) {
chosen = gh;
} else if (ge.isSuccess()) {
chosen = ge;
} else {
// 双通道均失败
failCount++;
// 拼接两个通道的失败原因
StringBuilder sb =
new StringBuilder();
if (gh.error != null) {
sb.append("§7GitHub: §c")
.append(gh.error);
}
if (ge.error != null) {
if (sb.length() > 0)
sb.append("\n");
sb.append("§7Gitee: §c")
.append(ge.error);
}
if (failCount >= TRIP_THRESHOLD) {
circuitTripped = true;
plugin.getLogger().warning(
"[Sdf1_login] 更新检测"
+ "已熔断,连续失败"
+ failCount + "次");
}
if (sender != null) {
final String msg =
sb.toString();
final boolean tripped =
circuitTripped;
final int fc = failCount;
Bukkit.getScheduler()
.runTask(plugin, () -> {
sender.sendMessage(
"§c§l[更新] "
+ "检测失败");
sender.sendMessage(msg);
sender.sendMessage(
"§e失败次数: "
+ fc + "/"
+ TRIP_THRESHOLD);
if (tripped) {
sender.sendMessage(
"§c已熔断,"
+ "请重启服务器"
+ "重置");
}
});
}
return;
}
// 至少一个通道成功
failCount = 0;
// 检查禁用信号
if (isDisableSignal(chosen.version)) {
autoCheckDisabled = true;
saveDisabledState();
plugin.getLogger().info(
"[Sdf1_login] 远程版本信号"
+ " \"" + chosen.version
+ "\",自动更新检测已禁用");
if (sender != null) {
Bukkit.getScheduler()
.runTask(plugin, () ->
sender.sendMessage(
"§e[更新] 远程已"
+ "禁用自动"
+ "更新检测"));
}
return;
}
// 手动调用:成功才解除熔断和禁用
if (!isAuto) {
if (circuitTripped) {
circuitTripped = false;
}
if (autoCheckDisabled) {
autoCheckDisabled = false;
saveDisabledState();
}
}
final String src = chosen.source;
final String lnk = chosen.link;
final String rv = chosen.version;
Bukkit.getScheduler()
.runTask(plugin, () -> {
if (sender == null) return;
sender.sendMessage(
"§e[更新] 检测成功"
+ " via: §a" + src);
sender.sendMessage(
"§e[更新] 本地版本:"
+ " §f"
+ currentVersion);
sender.sendMessage(
"§e[更新] 远程版本:"
+ " §f" + rv);
if (isNewer(rv,
currentVersion)) {
sender.sendMessage(
"§c[更新] 发现新"
+ "版本: " + rv);
sender.sendMessage(
"§a请前往仓库下载"
+ "更新:");
sender.sendMessage(
"§a" + lnk);
} else {
sender.sendMessage(
"§a[更新] 已是"
+ "最新版本");
}
// 显示另一通道的失败原因
// (仅信息,不影响结果)
CheckResult other =
(src.equals("GitHub"))
? ge : gh;
if (!other.isSuccess()
&& other.error != null) {
sender.sendMessage(
"§7[更新] "
+ other.source
+ "不可用: "
+ other.error);
}
});
}
// ========== 禁用信号判断 ==========
private boolean isDisableSignal(
String version) {
if (version == null) return false;
String v = version.trim().toLowerCase();
return "999".equals(v)
|| "关闭".equals(v)
|| "false".equals(v)
|| "禁用".equals(v);
}
// ========== HTTP 请求(带超时) ==========
// ========== 检测结果 ==========
private static class CheckResult {
final String version;
final String source;
final String link;
final String error;
CheckResult(String version,
String source,
String link,
String error) {
this.version = version;
this.source = source;
this.link = link;
this.error = error;
}
boolean isSuccess() {
return version != null;
}
}
// ========== HTTP 请求 ==========
private CheckResult tryChannel(
String apiUrl,
String sourceName,
String link) {
HttpURLConnection conn = null;
try {
URL url = new URL(apiUrl);
conn = (HttpURLConnection)
url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; "
+ "Win64; x64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) "
+ "Chrome/120.0.0.0 Safari/537.36");
conn.setRequestProperty(
"Accept",
"*/*");
conn.setRequestProperty(
"Accept-Language",
"zh-CN,zh;q=0.9");
conn.setConnectTimeout(TIMEOUT_MS);
conn.setReadTimeout(TIMEOUT_MS);
conn.setInstanceFollowRedirects(true);
int code = conn.getResponseCode();
if (code == 404) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": 未找到 (HTTP 404)");
}
if (code == 403) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": 被限流 (HTTP 403)");
}
if (code != 200) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": HTTP " + code);
}
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
conn
.getInputStream()));
StringBuilder sb =
new StringBuilder();
String line;
while ((line = reader.readLine())
!= null) {
sb.append(line);
}
reader.close();
String tag =
parseTagName(sb.toString());
if (tag == null) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": 无法解析版本号");
}
return new CheckResult(tag,
sourceName, link, null);
} catch (java.net
.SocketTimeoutException e) {
return new CheckResult(null,
sourceName, link,
sourceName + ": 连接超时");
} catch (java.net.ConnectException e) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": 连接被拒绝");
} catch (java.net
.UnknownHostException e) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": DNS解析失败");
} catch (Exception e) {
return new CheckResult(null,
sourceName, link,
sourceName + ": "
+ e.getMessage());
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
/**
* 从 JSON 中提取 tag_name
*/
private String parseTagName(String json) {
// tags API 返回数组: [{"name":"v1.1",...}]
// releases API 返回对象: {"tag_name":"v1.1"}
// 尝试 tag_name(releases格式)
int idx =
json.indexOf("\"tag_name\"");
if (idx >= 0) {
int colon =
json.indexOf(":", idx);
int q1 =
json.indexOf("\"", colon + 1);
int q2 =
json.indexOf("\"", q1 + 1);
if (q2 > q1) {
return cleanTag(
json.substring(
q1 + 1, q2));
}
}
// 尝试 "name"(tags格式)
idx = json.indexOf("\"name\"");
if (idx >= 0) {
int colon =
json.indexOf(":", idx);
int q1 =
json.indexOf("\"", colon + 1);
int q2 =
json.indexOf("\"", q1 + 1);
if (q2 > q1) {
return cleanTag(
json.substring(
q1 + 1, q2));
}
}
return null;
}
private String cleanTag(String tag) {
if (tag.startsWith("v")
|| tag.startsWith("V")) {
tag = tag.substring(1);
}
return tag.isEmpty() ? null : tag;
}
// ========== 版本比较 ==========
private boolean isNewer(String remote,
String local) {
String[] r = remote.split("\\.");
String[] l = local.split("\\.");
int len = Math.max(r.length, l.length);
for (int i = 0; i < len; i++) {
int ri = i < r.length
? parseNum(r[i]) : 0;
int li = i < l.length
? parseNum(l[i]) : 0;
if (ri > li) return true;
if (ri < li) return false;
}
return false;
}
private int parseNum(String s) {
try {
return Integer.parseInt(
s.replaceAll("[^0-9]", ""));
} catch (Exception e) {
return 0;
}
}
}
@@ -0,0 +1,163 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class WelcomeManager {
private final Main plugin;
private final Random random = new Random();
private final List<String> registerMsgs =
new ArrayList<>();
private final List<String> loginMsgs =
new ArrayList<>();
public WelcomeManager(Main plugin) {
this.plugin = plugin;
loadMessages();
}
public void loadMessages() {
registerMsgs.clear();
loginMsgs.clear();
File f = new File(
plugin.getDataFolder(),
"欢迎仪式.txt");
if (!f.exists()) {
writeDefault(f);
}
try (BufferedReader r =
new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets
.UTF_8))) {
String section = "";
String line;
while ((line = r.readLine()) != null) {
if (line.startsWith("\uFEFF"))
line = line.substring(1);
String t = line.trim();
if (t.isEmpty()
|| t.startsWith("#"))
continue;
if (t.equals("注册")
|| t.equals("注册:")) {
section = "register";
continue;
}
if (t.equals("登录")
|| t.equals("登录:")) {
section = "login";
continue;
}
// 支持 = 和 : 两种分隔
if (section.isEmpty()) {
if (t.startsWith("注册")
&& (t.contains("=")
|| t.contains(":"))) {
int idx = t.indexOf('=');
int idx2 = t.indexOf(':');
if (idx < 0) idx = idx2;
else if (idx2 >= 0)
idx = Math.min(idx, idx2);
String v = t.substring(idx + 1)
.trim();
if (!v.isEmpty())
registerMsgs.add(
colorize(v));
continue;
}
if (t.startsWith("登录")
&& (t.contains("=")
|| t.contains(":"))) {
int idx = t.indexOf('=');
int idx2 = t.indexOf(':');
if (idx < 0) idx = idx2;
else if (idx2 >= 0)
idx = Math.min(idx, idx2);
String v = t.substring(idx + 1)
.trim();
if (!v.isEmpty())
loginMsgs.add(
colorize(v));
continue;
}
continue;
}
if (!t.isEmpty()) {
if (section.equals("register"))
registerMsgs.add(
colorize(t));
else
loginMsgs.add(
colorize(t));
}
}
} catch (Exception e) {
plugin.getLogger().severe(
"[欢迎] 加载失败: "
+ e.getMessage());
}
plugin.getLogger().info(
"[欢迎] 加载完成: 注册="
+ registerMsgs.size()
+ " 登录=" + loginMsgs.size());
}
public void onRegister(Player p) {
sendRandom(p, registerMsgs, "注册");
}
public void onLogin(Player p) {
sendRandom(p, loginMsgs, "登录");
}
private void sendRandom(Player p,
List<String> msgs, String tag) {
if (msgs.isEmpty()) return;
String msg = msgs.get(
random.nextInt(msgs.size()));
msg = msg.replace("{player}",
p.getName());
final String finalMsg = msg;
Bukkit.getScheduler()
.runTaskLater(plugin,
() -> Bukkit
.broadcastMessage(
finalMsg), 10L);
}
private String colorize(String msg) {
return msg.replace("&", "\u00a7");
}
private void writeDefault(File f) {
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets
.UTF_8))) {
pw.println("# 欢迎仪式配置");
pw.println("# &为颜色代码");
pw.println("# {player} = 玩家名");
pw.println();
pw.println("注册:");
pw.println("&a&l欢迎 &f{player} &a加入服务器!&7[注册]");
pw.println("&6&l{player} &e已完成注册,欢迎入驻!&7[注册]");
pw.println("&b&l新成员 &f{player} &b已就位!&7[注册]");
pw.println();
pw.println("登录:");
pw.println("&a&l欢迎回来 &f{player}&a!&7[登录]");
pw.println("&e&l{player} &e已登录,继续冒险吧!&7[登录]");
pw.println("&b&l{player} &b回来了!&7[登录]");
} catch (IOException ignored) {
}
}
}
+2 -1
View File
@@ -86,4 +86,5 @@ commands:
usage: /垃圾清理
menu:
description: 雪球菜单开关控制
usage: /menu <开/关>
usage: /menu <开/关>
commands: