Update: 添加RadioManager/RadioDownloadListener/UpdateChecker/WelcomeManager等新功能模块
This commit is contained in:
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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 命令触发
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+667
-112
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1641,4 +1641,4 @@ public class TicketManager {
|
||||
}
|
||||
return it;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,4 +86,5 @@ commands:
|
||||
usage: /垃圾清理
|
||||
menu:
|
||||
description: 雪球菜单开关控制
|
||||
usage: /menu <开/关>
|
||||
usage: /menu <开/关>
|
||||
commands:
|
||||
Reference in New Issue
Block a user