Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b79754039f | ||
|
|
63d0a99cff | ||
|
|
8b73031ef0 | ||
|
|
677fea7de1 |
+7
-1
@@ -36,4 +36,10 @@ build/
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# 开发辅助文件(本地保留,不推送到仓库)
|
||||||
|
txt/
|
||||||
|
*.xlsx
|
||||||
|
*.patch
|
||||||
|
新建*.txt
|
||||||
-112
@@ -1,112 +0,0 @@
|
|||||||
# Sdf1_login Plugin User Guide
|
|
||||||
|
|
||||||
## 1. Features
|
|
||||||
|
|
||||||
| Name | Description/Function | Status |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| Login | User login/registration | Completed |
|
|
||||||
| World Trash Can | Clear server drops | In Progress |
|
|
||||||
| Ticket System | User ticket submission | Completed |
|
|
||||||
| Check-in System | Check-in points for rewards | Completed |
|
|
||||||
|
|
||||||
## 2. Installation
|
|
||||||
|
|
||||||
| Prerequisite | Optional |
|
|
||||||
| :--- | :--- |
|
|
||||||
| CY_beibao | Optional |
|
|
||||||
| EssentialsX | Optional |
|
|
||||||
|
|
||||||
### 2.1 Download
|
|
||||||
Download the plugin from GitHub or Gitee, put it into the server plugin directory, restart the server.
|
|
||||||
|
|
||||||
### 2.2 Configuration Files
|
|
||||||
|
|
||||||
#### 2.2.1 messages.txt
|
|
||||||
Controls all plugin prompt messages. Execute /sdf1_login reload after modifying.
|
|
||||||
|
|
||||||
#### 2.2.2 afk_whitelist.txt
|
|
||||||
Players in this config will not be affected by AFK kick.
|
|
||||||
|
|
||||||
#### 2.2.3 SMTP Settings.txt
|
|
||||||
Controls SMTP mail server config, used for password recovery emails.
|
|
||||||
|
|
||||||
#### 2.2.4 Plugin Settings.txt
|
|
||||||
Controls the entire plugin including account limits, admin password, etc.
|
|
||||||
|
|
||||||
#### 2.2.5 chat.txt
|
|
||||||
Controls chat filter for handling illegal links.
|
|
||||||
|
|
||||||
## 3. Commands
|
|
||||||
|
|
||||||
- Sdf1_login main command
|
|
||||||
- Sdf1_login reload reload config
|
|
||||||
- reg register account
|
|
||||||
- login login to account
|
|
||||||
- l login to account
|
|
||||||
|
|
||||||
## 4. Screenshots
|
|
||||||
|
|
||||||
Main Interface
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Invite Data
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Points Shop
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Task Center
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Trash Can
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Admin Panel
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Admin Panel - User Management
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Ticket Center
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Ticket Management
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
My Tickets
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Submit Ticket
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Provider List
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Provider Grab Hall
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Provider - Order Interface
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Admin - Ticket Detail
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 5. Download Links
|
|
||||||
|
|
||||||
- GitHub: https://github.com/ypsdf1/sdf1_login
|
|
||||||
- Gitee: https://gitee.com/nihaoshidifu/sdf1_login
|
|
||||||
@@ -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("余额");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user