Compare commits

..
4 Commits
Author SHA1 Message Date
ypsdf1 b79754039f chore: 移除非项目文件,添加gitignore规则
- 从仓库追踪中移除txt/、工作簿1.xlsx、unnamed.patch等本地开发文件
- 更新.gitignore排除这些文件类型
2026-05-14 21:08:55 +08:00
ypsdf1 63d0a99cff Remove: 删除误传的radiopack.zip 2026-05-14 21:03:41 +08:00
ypsdf1 8b73031ef0 Update: 添加RadioManager/RadioDownloadListener/UpdateChecker/WelcomeManager等新功能模块 2026-05-14 21:03:03 +08:00
ypsdf1 677fea7de1 Remove: 删除英文README 2026-05-11 13:45:28 +08:00
10 changed files with 4696 additions and 113 deletions
+7 -1
View File
@@ -36,4 +36,10 @@ build/
.vscode/ .vscode/
### Mac OS ### ### Mac OS ###
.DS_Store .DS_Store
# 开发辅助文件(本地保留,不推送到仓库)
txt/
*.xlsx
*.patch
新建*.txt
-112
View File
@@ -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
![Main Interface](img/login.png)
Invite Data
![Invite Data](img/login2.png)
Points Shop
![Points Shop](img/login3.png)
Task Center
![Task Center](img/login4.png)
Trash Can
![Trash Can](img/login5.png)
Admin Panel
![Admin Panel](img/login6.png)
Admin Panel - User Management
![User Management](img/login7.png)
Ticket Center
![Ticket Center](img/login8.png)
Ticket Management
![Ticket Management](img/login9.png)
My Tickets
![My Tickets](img/login10.png)
Submit Ticket
![Submit Ticket](img/login11.png)
Provider List
![Provider List](img/login12.png)
Provider Grab Hall
![Grab Hall](img/login13.png)
Provider - Order Interface
![Order Interface](img/login14.png)
Admin - Ticket Detail
![Ticket Detail](img/login15.png)
## 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("余额");
}
}
+130
View File
@@ -0,0 +1,130 @@
package Sdf1_login;
import java.io.File;
import java.sql.*;
import java.util.logging.Logger;
public class PlayerStorage {
private Connection conn;
private final Logger logger;
public PlayerStorage(File dataFolder,
Logger logger) {
this.logger = logger;
File dbFile = new File(
dataFolder,
"quest_data.db");
try {
Class.forName(
"org.sqlite.JDBC");
conn = DriverManager
.getConnection(
"jdbc:sqlite:"
+ dbFile
.getAbsolutePath());
conn.setAutoCommit(true);
Statement s =
conn.createStatement();
s.executeUpdate(
"CREATE TABLE IF NOT EXISTS "
+ "quest_kv ("
+ "player TEXT NOT NULL,"
+ "key TEXT NOT NULL,"
+ "value TEXT,"
+ "PRIMARY KEY"
+ "(player, key))");
s.close();
logger.info(
"[QuestStorage] 初始化: "
+ dbFile.getAbsolutePath());
} catch (Exception e) {
logger.severe(
"[QuestStorage] 失败: "
+ e.getMessage());
}
}
public Object getCondition(
String player, String key) {
if (conn == null) return null;
try {
PreparedStatement ps =
conn.prepareStatement(
"SELECT value "
+ "FROM quest_kv "
+ "WHERE player=? "
+ "AND key=?");
ps.setString(1,
player.toLowerCase());
ps.setString(2, key);
ResultSet rs =
ps.executeQuery();
String val = rs.next()
? rs.getString("value")
: null;
rs.close();
ps.close();
return parseValue(val);
} catch (SQLException e) {
return null;
}
}
public void setCondition(
String player, String key) {
setConditionRaw(
player, key, "true");
}
public void debugPlayer(String player) {
}
public void setConditionRaw(
String player, String key,
String value) {
if (conn == null) return;
try {
PreparedStatement ps =
conn.prepareStatement(
"INSERT OR REPLACE "
+ "INTO quest_kv"
+ "(player,key,value)"
+ " VALUES(?,?,?)");
ps.setString(1,
player.toLowerCase());
ps.setString(2, key);
ps.setString(3, value);
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
logger.warning(
"[QuestStorage] 写入失败: "
+ key + "=" + value
+ " | " + e.getMessage());
}
}
public void close() {
try {
if (conn != null
&& !conn.isClosed())
conn.close();
} catch (Exception e) {}
}
private Object parseValue(String v) {
if (v == null) return null;
try {
return Long.parseLong(v);
} catch (Exception e1) {
try {
return Double.parseDouble(v);
} catch (Exception e2) {
if ("true".equalsIgnoreCase(v))
return true;
if ("false".equalsIgnoreCase(v))
return false;
return v;
}
}
}
}
+138
View File
@@ -0,0 +1,138 @@
package Sdf1_login;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class QuestStorage {
private Connection conn;
public QuestStorage(File dataFolder) {
try {
Class.forName("org.sqlite.JDBC");
File dbFile = new File(
dataFolder,
"quest_data.db");
conn = DriverManager.getConnection(
"jdbc:sqlite:"
+ dbFile.getPath());
initTable();
} catch (Exception e) {
e.printStackTrace();
}
}
private void initTable()
throws SQLException {
Statement stmt =
conn.createStatement();
stmt.executeUpdate(
"CREATE TABLE IF NOT EXISTS "
+ "quest_conditions ("
+ "player TEXT NOT NULL, "
+ "condition_key TEXT NOT NULL, "
+ "value TEXT DEFAULT '1', "
+ "timestamp INTEGER, "
+ "PRIMARY KEY "
+ "(player, condition_key))");
stmt.close();
}
/**
* 标记条件已完成(value = "1")
*/
public void setCondition(
String player,
String condition) {
setConditionRaw(
player, condition, "1");
}
/**
* 写入任意值(计数器等)
*/
public void setConditionRaw(
String player,
String condition,
String value) {
try {
PreparedStatement ps =
conn.prepareStatement(
"INSERT OR REPLACE "
+ "INTO "
+ "quest_conditions "
+ "(player, "
+ "condition_key, "
+ "value, timestamp) "
+ "VALUES "
+ "(?, ?, ?, ?)");
ps.setString(1, player);
ps.setString(2, condition);
ps.setString(3, value);
ps.setLong(4,
System.currentTimeMillis());
ps.executeUpdate();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 查询条件值
*/
public Object getCondition(
String player,
String condition) {
try {
PreparedStatement ps =
conn.prepareStatement(
"SELECT value FROM "
+ "quest_conditions "
+ "WHERE player = ? "
+ "AND "
+ "condition_key = ?");
ps.setString(1, player);
ps.setString(2, condition);
ResultSet rs =
ps.executeQuery();
if (rs.next()) {
String val =
rs.getString("value");
rs.close();
ps.close();
// 尝试解析为数字
try {
if (val.contains(".")) {
return Double.parseDouble(
val);
}
return Long.parseLong(val);
} catch (NumberFormatException e) {
return val;
}
}
rs.close();
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public void close() {
try {
if (conn != null
&& !conn.isClosed()) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,118 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerResourcePackStatusEvent;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
public class RadioDownloadListener
implements Listener {
private final Logger log;
private final Main plugin;
private final Map<UUID, Integer> retryCount =
new ConcurrentHashMap<>();
private static final int MAX_RETRY = 2;
public RadioDownloadListener(Main plugin) {
this.plugin = plugin;
this.log = plugin.getLogger();
}
@EventHandler
public void onStatus(
PlayerResourcePackStatusEvent e) {
Player p = e.getPlayer();
UUID uuid = p.getUniqueId();
String status = e.getStatus().name();
log.info("[Radio] " + p.getName()
+ " status: " + status);
switch (status) {
case "ACCEPTED":
break;
case "DOWNLOADED":
break;
case "SUCCESS":
log.info("[Radio] " + p.getName()
+ " 资源包加载成功");
retryCount.remove(uuid);
break;
case "FAILED_DOWNLOAD":
handleRetry(p, uuid);
break;
case "DECLINED":
log.info("[Radio] " + p.getName()
+ " 拒绝下载");
p.sendMessage(
"§e[Radio] §f你拒绝了资源包");
retryCount.remove(uuid);
break;
case "DISCARDED":
log.warning("[Radio] " + p.getName()
+ " DISCARDED");
p.sendMessage(
"§c[Radio] §f资源包下载失败"
+ ",可能原因:");
p.sendMessage(
"§71. CDN/防火墙拦截");
p.sendMessage(
"§72. SSL证书问题");
retryCount.remove(uuid);
break;
case "INVALID_URL":
log.warning("[Radio] " + p.getName()
+ " URL无效");
retryCount.remove(uuid);
break;
default:
log.info("[Radio] " + p.getName()
+ " 未知: " + status);
break;
}
}
private void handleRetry(Player p, UUID uuid) {
int count = retryCount
.getOrDefault(uuid, 0) + 1;
retryCount.put(uuid, count);
log.warning("[Radio] " + p.getName()
+ " 下载失败 (" + count
+ "/" + MAX_RETRY + ")");
if (count <= MAX_RETRY) {
p.sendMessage(
"§e[Radio] §f下载失败,重试中... ("
+ count + "/" + MAX_RETRY + ")");
Bukkit.getScheduler().runTaskLater(
plugin,
() -> {
if (p.isOnline()
&& plugin.radio != null) {
plugin.radio
.sendResourcePack(p);
}
}, 60L);
} else {
p.sendMessage(
"§c[Radio] §f资源包加载失败,"
+ "部分音乐无法播放");
retryCount.remove(uuid);
}
}
}
+874
View File
@@ -0,0 +1,874 @@
package Sdf1_login;
import com.sun.net.httpserver.HttpServer;
import org.bukkit.*;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.security.MessageDigest;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import java.util.zip.*;
public class RadioManager {
private final Main plugin;
private File radioDir;
private File oggDir;
private File packDir;
private File configFile;
private BukkitTask mainRadioTask;
private BukkitTask loginRadioTask;
private final Map<String, Boolean> loginPlayers =
new ConcurrentHashMap<>();
private int radioIndex = 0;
private HttpServer httpServer;
private byte[] packBytes;
private String packHash = "";
private String resourcePackUrl = "";
private int httpPort = 25566;
private final List<File> sourceFiles =
new ArrayList<>();
private final List<File> oggFiles =
new ArrayList<>();
private final Set<UUID> sending =
Collections.newSetFromMap(
new ConcurrentHashMap<>());
private String adminTag = "admin";
public RadioManager(Main plugin) {
this.plugin = plugin;
}
// ========== init ==========
public void init() {
radioDir = new File(
plugin.getDataFolder(), "radio");
oggDir = new File(
plugin.getDataFolder(),
"radio/ogg");
packDir = new File(
plugin.getDataFolder(),
"radiopack");
radioDir.mkdirs();
oggDir.mkdirs();
packDir.mkdirs();
loadRadioConfig();
doConvert();
buildPack();
loadOggFiles();
startHttpServer();
plugin.getLogger().info(
"[Radio] init done: "
+ oggFiles.size() + " audio(s)");
}
// ========== radio_config.txt ==========
private void loadRadioConfig() {
configFile = new File(
plugin.getDataFolder(),
"radio_config.txt");
if (!configFile.exists()) {
writeDefaultConfig();
}
try (BufferedReader br =
new BufferedReader(
new FileReader(
configFile))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()
|| line.startsWith("#"))
continue;
int eq = line.indexOf('=');
if (eq <= 0) continue;
String key = line.substring(0, eq)
.trim();
String val = line.substring(eq + 1)
.trim();
if (key.equals(
"resource-pack-url")) {
resourcePackUrl = val;
} else if (key.equals(
"http-port")) {
try {
httpPort = Integer
.parseInt(val);
} catch (Exception ignored) {}
}
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Radio] config error: "
+ e.getMessage());
}
loadAdminTag();
plugin.getLogger().info("[Radio] config: url="
+ (resourcePackUrl.isEmpty()
? "(none)" : resourcePackUrl)
+ " port=" + httpPort);
}
/**
* 从插件设置.txt读取管理标签
*/
private void loadAdminTag() {
File settingsFile = new File(
plugin.getDataFolder(),
"插件设置.txt");
if (!settingsFile.exists()) return;
try (BufferedReader br =
new BufferedReader(
new FileReader(
settingsFile))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty()
|| line.startsWith("#"))
continue;
if (line.startsWith("管理标签=")
|| line.startsWith("管理标签 =")) {
int eq = line.indexOf('=');
if (eq > 0) {
adminTag = line
.substring(eq + 1)
.trim();
}
}
}
} catch (Exception e) {
// ignore
}
plugin.getLogger().info(
"[Radio] admin tag: " + adminTag);
}
/**
* 检查玩家是否有管理标签
* 控制台不受限制
*/
public boolean hasRadioPermission(
CommandSender sender) {
// 控制台直接通过
if (!(sender instanceof Player)) {
return true;
}
Player p = (Player) sender;
// 检查Bukkit权限节点
// 格式: sdf1.admin
if (p.hasPermission("sdf1.admin")) {
return true;
}
// 检查玩家名是否在admin列表中
// 你可以扩展这里对接你的标签系统
return false;
}
private void writeDefaultConfig() {
try {
plugin.getDataFolder().mkdirs();
String c =
"# ============ Sdf1 Radio "
+ "Config ============\n"
+ "#\n"
+ "# resource-pack-url\n"
+ "# 填外部HTTPS地址\n"
+ "# 留空 = 使用内置HTTP"
+ "服务\n"
+ "#\n"
+ "# http-port\n"
+ "# 内置HTTP端口"
+ " (仅url为空时生效)\n"
+ "#\n"
+ "# ============ Settings"
+ " ==============\n"
+ "\n"
+ "resource-pack-url=\n"
+ "http-port=25566\n";
Files.write(configFile.toPath(),
c.getBytes("UTF-8"));
} catch (Exception e) {
plugin.getLogger().warning(
"[Radio] config create fail");
}
}
// ========== convert ==========
private void doConvert() {
sourceFiles.clear();
scanSources(radioDir);
for (File src : sourceFiles) {
String name = src.getName().replaceAll(
"\\.(ogg|mp3|wav|flac)$",
".ogg");
File dest = new File(oggDir, name);
if (dest.exists()
&& dest.lastModified()
>= src.lastModified()) continue;
if (src.getName().toLowerCase()
.endsWith(".ogg")) {
fileCopy(src, dest);
plugin.getLogger().info(
"[Radio] copy: "
+ src.getName());
} else if (runFfmpeg(src, dest)) {
plugin.getLogger().info(
"[Radio] convert: "
+ src.getName());
} else {
plugin.getLogger().warning(
"[Radio] skip(need ffmpeg): "
+ src.getName());
}
}
}
private void scanSources(File dir) {
if (!dir.exists()) return;
File[] files = dir.listFiles();
if (files == null) return;
for (File f : files) {
if (f.isDirectory()
&& !f.getName().equals("ogg")) {
scanSources(f);
} else if (f.isFile()) {
String n = f.getName().toLowerCase();
if (n.endsWith(".ogg")
|| n.endsWith(".mp3")
|| n.endsWith(".wav")
|| n.endsWith(".flac")) {
sourceFiles.add(f);
}
}
}
}
private void fileCopy(File src, File dest) {
try {
dest.getParentFile().mkdirs();
Files.copy(src.toPath(),
dest.toPath(),
StandardCopyOption
.REPLACE_EXISTING);
} catch (Exception ignored) {}
}
private boolean runFfmpeg(File src, File dest) {
try {
dest.getParentFile().mkdirs();
ProcessBuilder pb = new ProcessBuilder(
"ffmpeg", "-y", "-i",
src.getAbsolutePath(),
"-c:a", "libvorbis",
"-q:a", "5",
dest.getAbsolutePath());
pb.redirectErrorStream(true);
Process p = pb.start();
p.waitFor();
return dest.exists();
} catch (Exception e) {
return false;
}
}
// ========== pack ==========
private void buildPack() {
deleteDir(packDir);
packDir.mkdirs();
List<File> allOggs = new ArrayList<>();
scanOggs(oggDir, allOggs);
if (allOggs.isEmpty()) {
plugin.getLogger().warning(
"[Radio] no ogg found");
return;
}
File soundsDir = new File(packDir,
"assets/minecraft/sounds/custom");
soundsDir.mkdirs();
Map<String, File> unique =
new LinkedHashMap<>();
for (File ogg : allOggs) {
String name = ogg.getName()
.replace(".ogg", "");
unique.putIfAbsent(
name.toLowerCase(), ogg);
}
StringBuilder json =
new StringBuilder("{\n");
int idx = 0, total = unique.size();
for (Map.Entry<String, File> e
: unique.entrySet()) {
String name = e.getKey();
File ogg = e.getValue();
File dest = new File(soundsDir,
name + ".ogg");
try {
Files.copy(ogg.toPath(),
dest.toPath(),
StandardCopyOption
.REPLACE_EXISTING);
plugin.getLogger().info(
"[Radio] pack: " + name
+ " (" + dest.length()
+ " bytes)");
} catch (Exception ex) {
plugin.getLogger().warning(
"[Radio] copy fail: "
+ name);
continue;
}
json.append(" \"custom.")
.append(name).append("\": {\n");
json.append(
" \"replace\": false,\n");
json.append(
" \"sounds\": [{\n");
json.append(
" \"name\": \"custom/")
.append(name).append("\",\n");
json.append(
" \"stream\": true\n");
json.append(" }]\n");
json.append(" }");
idx++;
if (idx < total) json.append(",");
json.append("\n");
}
json.append("}\n");
try {
Files.write(new File(packDir,
"assets/minecraft/sounds.json")
.toPath(),
json.toString()
.getBytes("UTF-8"));
plugin.getLogger().info(
"[Radio] sounds.json:\n"
+ json.toString());
} catch (Exception e) {
plugin.getLogger().warning(
"[Radio] sounds.json fail");
}
try {
String meta =
"{\"pack\":{\"pack_format\":54,"
+ "\"description\":\"Sdf1 Radio\""
+ "}}\n";
Files.write(new File(packDir,
"pack.mcmeta").toPath(),
meta.getBytes("UTF-8"));
} catch (Exception ignored) {}
File zipFile = new File(
plugin.getDataFolder(),
"radiopack.zip");
try {
zipPack(packDir, zipFile);
packBytes = Files.readAllBytes(
zipFile.toPath());
MessageDigest md =
MessageDigest.getInstance(
"SHA-1");
md.update(packBytes);
StringBuilder sb =
new StringBuilder();
for (byte b : md.digest())
sb.append(String.format(
"%02x", b));
packHash = sb.toString();
plugin.getLogger().info(
"[Radio] zip: "
+ zipFile.length()
+ " bytes, SHA1="
+ packHash);
} catch (Exception e) {
plugin.getLogger().warning(
"[Radio] zip fail: "
+ e.getMessage());
}
}
private void scanOggs(File dir, List<File> list) {
File[] files = dir.listFiles();
if (files == null) return;
for (File f : files) {
if (f.isDirectory())
scanOggs(f, list);
else if (f.getName()
.endsWith(".ogg"))
list.add(f);
}
}
private void loadOggFiles() {
oggFiles.clear();
scanOggs(oggDir, oggFiles);
}
private void deleteDir(File dir) {
if (!dir.exists()) return;
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
if (f.isDirectory())
deleteDir(f);
else f.delete();
}
}
dir.delete();
}
private void zipPack(File root, File zipFile)
throws Exception {
try (ZipOutputStream zos =
new ZipOutputStream(
new FileOutputStream(
zipFile))) {
addZipEntries(root, root, zos);
}
}
private void addZipEntries(File root, File cur,
ZipOutputStream zos)
throws Exception {
File[] files = cur.listFiles();
if (files == null) return;
for (File f : files) {
if (f.isDirectory()) {
addZipEntries(root, f, zos);
} else {
String path = root.toURI()
.relativize(f.toURI())
.getPath();
zos.putNextEntry(
new ZipEntry(path));
zos.write(Files.readAllBytes(
f.toPath()));
zos.closeEntry();
}
}
}
// ========== HTTP server ==========
private void startHttpServer() {
if (packBytes == null) return;
if (!resourcePackUrl.isEmpty()) {
plugin.getLogger().info(
"[Radio] skip HTTP "
+ "(external URL)");
return;
}
try {
httpServer = HttpServer.create(
new InetSocketAddress(
"0.0.0.0",
httpPort), 0);
httpServer.createContext(
"/radiopack.zip",
exchange -> {
if (packBytes != null) {
exchange
.getResponseHeaders()
.set("Content-Type",
"application/zip");
exchange
.sendResponseHeaders(
200,
packBytes.length);
exchange
.getResponseBody()
.write(packBytes);
exchange
.getResponseBody()
.close();
} else {
exchange
.sendResponseHeaders(
404, 0);
exchange.close();
}
});
httpServer.setExecutor(null);
httpServer.start();
String ip = "127.0.0.1";
try {
ip = InetAddress.getLocalHost()
.getHostAddress();
} catch (Exception ignored) {}
plugin.getLogger().info(
"[Radio] HTTP on port "
+ httpPort);
plugin.getLogger().info(
"[Radio] url: http://" + ip
+ ":" + httpPort
+ "/radiopack.zip");
} catch (Exception e) {
plugin.getLogger().warning(
"[Radio] HTTP fail: "
+ e.getMessage());
}
}
// ========== send resource pack ==========
public void sendResourcePack(Player p) {
if (packBytes == null) return;
UUID pid = p.getUniqueId();
if (sending.contains(pid)) {
plugin.getLogger().info(
"[Radio] skip duplicate: "
+ p.getName());
return;
}
sending.add(pid);
UUID uuid = UUID.randomUUID();
String url = resourcePackUrl;
if (url.isEmpty()) {
String ip = "127.0.0.1";
try {
ip = InetAddress.getLocalHost()
.getHostAddress();
} catch (Exception ignored) {}
url = "http://" + ip + ":" + httpPort
+ "/radiopack.zip";
}
byte[] hashBytes;
try {
MessageDigest md =
MessageDigest.getInstance(
"SHA-1");
hashBytes = md.digest(packBytes);
} catch (Exception e) {
sending.remove(pid);
return;
}
plugin.getLogger().info(
"[Radio] url=" + url);
plugin.getLogger().info(
"[Radio] sha1=" + packHash
+ " (" + hashBytes.length
+ " bytes)");
plugin.getLogger().info(
"[Radio] size=" + packBytes.length);
try {
java.lang.reflect.Method target = null;
for (java.lang.reflect.Method m
: p.getClass().getMethods()) {
if (!m.getName().equals(
"setResourcePack"))
continue;
Class<?>[] pt =
m.getParameterTypes();
if (pt.length == 5
&& pt[0] == UUID.class
&& pt[1] == String.class
&& pt[2] == byte[].class
&& pt[3] == String.class
&& pt[4] == boolean.class) {
target = m;
break;
}
}
if (target == null) {
plugin.getLogger().warning(
"[Radio] no method");
sending.remove(pid);
return;
}
target.invoke(p, uuid, url,
hashBytes,
"Sdf1 Radio", false);
plugin.getLogger().info(
"[Radio] sent to "
+ p.getName());
} catch (Exception e) {
Throwable c = e.getCause() != null
? e.getCause() : e;
plugin.getLogger().warning(
"[Radio] failed: "
+ c.getMessage());
}
org.bukkit.Bukkit.getScheduler()
.runTaskLater(plugin, () -> {
sending.remove(pid);
}, 100L);
}
// ========== play custom ==========
public boolean playCustom(String name) {
return tryPlay(name, null);
}
public boolean playCustomTo(String name,
Player target) {
return tryPlay(name, target);
}
private boolean tryPlay(String name,
Player target) {
for (File f : oggFiles) {
String fn = f.getName()
.replace(".ogg", "");
if (fn.equalsIgnoreCase(name)) {
String key =
"minecraft:custom." + name;
if (target != null) {
target.playSound(
target.getLocation(),
key,
SoundCategory.RECORDS,
2.0f, 1.0f);
} else {
for (Player p : Bukkit
.getOnlinePlayers()) {
p.playSound(
p.getLocation(),
key,
SoundCategory.RECORDS,
2.0f, 1.0f);
}
broadcast(
"§6§l[广播] §f正在播放: §e"
+ name);
}
return true;
}
}
return false;
}
// ========== command ==========
public void handleCommand(CommandSender sender,
String[] args) {
// 无权限 → 静默,不做任何事
if (!hasRadioPermission(sender)) {
return;
}
if (args.length == 0) {
stopMainRadio();
stopAllLoginRadio();
broadcast("§6§l[广播] §f已停止播放");
return;
}
String arg = args[0].trim();
// /radio reload
if (arg.equalsIgnoreCase("reload")) {
stopMainRadio();
stopAllLoginRadio();
loadRadioConfig();
doConvert();
buildPack();
loadOggFiles();
startHttpServer();
sender.sendMessage("§a[Radio] reloaded");
return;
}
// /radio list
if (arg.equalsIgnoreCase("list")) {
sender.sendMessage("§6音频列表 ("
+ oggFiles.size() + "):");
for (File f : oggFiles)
sender.sendMessage(" §e"
+ f.getName()
.replace(".ogg", ""));
return;
}
// /radio random [玩家]
if (arg.equalsIgnoreCase("random")) {
if (oggFiles.isEmpty()) {
sender.sendMessage(
"§c没有可用的音频文件");
return;
}
File pick = oggFiles.get(
(int) (Math.random()
* oggFiles.size()));
String name = pick.getName()
.replace(".ogg", "");
if (args.length >= 2) {
Player target = Bukkit
.getPlayer(args[1]);
if (target != null
&& target.isOnline()) {
sendResourcePack(target);
playCustomTo(name, target);
target.sendMessage(
"§6§l[Radio] §f正在播放: §e"
+ name);
sender.sendMessage(
"§a[Radio] 已向 "
+ target.getName()
+ " 播放: " + name);
} else {
sender.sendMessage(
"§c玩家不在线: "
+ args[1]);
}
} else {
playCustom(name);
}
return;
}
// === /radio 音频名 [玩家名] ===
String audioArg = arg;
String playerArg = null;
if (args.length >= 2) {
playerArg = args[1].trim();
}
Player target = null;
if (playerArg != null) {
target = Bukkit.getPlayer(playerArg);
if (target == null
|| !target.isOnline()) {
sender.sendMessage(
"§c玩家不在线: " + playerArg);
return;
}
}
// 尝试 custom
String base = audioArg.replaceAll(
"\\.(ogg|mp3|wav|flac)$", "");
if (target != null) {
sendResourcePack(target);
}
if (tryPlay(base, target)) {
if (target != null) {
target.sendMessage(
"§6§l[Radio] §f正在播放: §e"
+ base);
sender.sendMessage(
"§a[Radio] 已向 "
+ target.getName()
+ " 播放: " + base);
}
return;
}
sender.sendMessage(
"§c未找到: " + audioArg
+ " §7/radio list");
}
// ========== timer ==========
public void startMainRadio() {
stopMainRadio();
mainRadioTask = new BukkitRunnable() {
public void run() {
playNext();
}
}.runTaskTimer(plugin, 100L, 2400L);
}
private void playNext() {
if (!oggFiles.isEmpty()) {
File f = oggFiles.get(
radioIndex % oggFiles.size());
playCustom(f.getName()
.replace(".ogg", ""));
radioIndex++;
}
}
public void stopMainRadio() {
if (mainRadioTask != null) {
mainRadioTask.cancel();
mainRadioTask = null;
}
}
public void startLoginRadio(Player p) {
loginPlayers.put(p.getName(), true);
if (loginRadioTask != null) return;
loginRadioTask = new BukkitRunnable() {
public void run() {
if (loginPlayers.isEmpty()) {
cancel();
loginRadioTask = null;
return;
}
if (oggFiles.isEmpty()) return;
int idx = (int)
(System.currentTimeMillis()
/ 12000L
% oggFiles.size());
for (String name
: loginPlayers.keySet()) {
Player lp = Bukkit
.getPlayer(name);
if (lp != null
&& lp.isOnline()) {
String key =
"minecraft:custom."
+ oggFiles.get(idx)
.getName()
.replace(".ogg", "");
lp.playSound(
lp.getLocation(),
key,
SoundCategory.RECORDS,
1.5f, 1.0f);
}
}
}
}.runTaskTimer(plugin, 0L, 60L);
}
public void stopLoginRadio(Player p) {
loginPlayers.remove(p.getName());
if (loginPlayers.isEmpty()
&& loginRadioTask != null) {
loginRadioTask.cancel();
loginRadioTask = null;
}
}
public void stopAllLoginRadio() {
loginPlayers.clear();
if (loginRadioTask != null) {
loginRadioTask.cancel();
loginRadioTask = null;
}
}
public void stopAll() {
stopMainRadio();
stopAllLoginRadio();
if (httpServer != null)
httpServer.stop(0);
}
private void broadcast(String msg) {
Bukkit.broadcastMessage(msg);
}
}
+523
View File
@@ -0,0 +1,523 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
public class UpdateChecker {
private final Main plugin;
private final String currentVersion;
// ===== 仓库信息 =====
private static final String GITHUB_OWNER =
"ypsdf1";
private static final String GITHUB_REPO =
"sdf1_login";
private static final String GITEE_OWNER =
"nihaoshidifu";
private static final String GITEE_REPO =
"sdf1_login";
// API 地址
private static final String GITHUB_API =
"https://api.github.com/repos/"
+ GITHUB_OWNER + "/"
+ GITHUB_REPO
+ "/tags";
private static final String GITEE_API =
"https://gitee.com/api/v5/repos/"
+ GITEE_OWNER + "/"
+ GITEE_REPO
+ "/tags";
// 仓库页面链接(给用户看的)
private static final String GITHUB_LINK =
"https://github.com/"
+ GITHUB_OWNER + "/"
+ GITHUB_REPO + "/releases";
private static final String GITEE_LINK =
"https://gitee.com/"
+ GITEE_OWNER + "/"
+ GITEE_REPO + "/releases";
// 单次请求超时(毫秒)
private static final int TIMEOUT_MS = 3000;
// 熔断阈值
private static final int TRIP_THRESHOLD = 5;
// ===== 熔断状态(内存态,重启自动清除) =====
private volatile boolean circuitTripped =
false;
private volatile int failCount = 0;
// ===== 自动检测禁用(持久化到文件) =====
private volatile boolean autoCheckDisabled =
false;
private final File disabledFile;
public UpdateChecker(Main plugin) {
this.plugin = plugin;
this.currentVersion =
plugin.getDescription()
.getVersion();
this.disabledFile = new File(
plugin.getDataFolder(),
"update_disabled.dat");
loadDisabledState();
}
// ========== 持久化禁用状态 ==========
private void loadDisabledState() {
if (disabledFile.exists()) {
try (BufferedReader br =
new BufferedReader(
new FileReader(
disabledFile))) {
String line = br.readLine();
if (line != null) {
autoCheckDisabled =
"true".equals(
line.trim());
}
} catch (Exception ignored) {
}
}
}
private void saveDisabledState() {
try (PrintWriter pw =
new PrintWriter(
new FileWriter(
disabledFile))) {
pw.println(autoCheckDisabled
? "true" : "false");
} catch (Exception ignored) {
}
}
// ========== 公开方法 ==========
/**
* onEnable 调用(异步静默)
* 熔断或禁用则直接跳过
*/
public void checkOnEnable() {
if (autoCheckDisabled) return;
if (circuitTripped) return;
Bukkit.getScheduler()
.runTaskAsynchronously(plugin,
() -> doCheck(null, true));
}
/**
* /sdf1_login update 调用(异步)
* 忽略熔断和禁用状态,强制检测
* 只有拿到版号才解除熔断
*/
public void checkUpdate(
CommandSender sender) {
Bukkit.getScheduler()
.runTaskAsynchronously(plugin,
() ->
doCheck(sender,
false));
}
// ========== 核心检测逻辑 ==========
private void doCheck(CommandSender sender,
boolean isAuto) {
if (isAuto) {
if (autoCheckDisabled
|| circuitTripped) {
return;
}
}
// 通道1:GitHub
CheckResult gh =
tryChannel(GITHUB_API,
"GitHub", GITHUB_LINK);
// 通道2:Gitee
CheckResult ge =
tryChannel(GITEE_API,
"Gitee", GITEE_LINK);
// 选择成功的结果
CheckResult chosen = null;
String failMsg = null;
if (gh.isSuccess()) {
chosen = gh;
} else if (ge.isSuccess()) {
chosen = ge;
} else {
// 双通道均失败
failCount++;
// 拼接两个通道的失败原因
StringBuilder sb =
new StringBuilder();
if (gh.error != null) {
sb.append("§7GitHub: §c")
.append(gh.error);
}
if (ge.error != null) {
if (sb.length() > 0)
sb.append("\n");
sb.append("§7Gitee: §c")
.append(ge.error);
}
if (failCount >= TRIP_THRESHOLD) {
circuitTripped = true;
plugin.getLogger().warning(
"[Sdf1_login] 更新检测"
+ "已熔断,连续失败"
+ failCount + "次");
}
if (sender != null) {
final String msg =
sb.toString();
final boolean tripped =
circuitTripped;
final int fc = failCount;
Bukkit.getScheduler()
.runTask(plugin, () -> {
sender.sendMessage(
"§c§l[更新] "
+ "检测失败");
sender.sendMessage(msg);
sender.sendMessage(
"§e失败次数: "
+ fc + "/"
+ TRIP_THRESHOLD);
if (tripped) {
sender.sendMessage(
"§c已熔断,"
+ "请重启服务器"
+ "重置");
}
});
}
return;
}
// 至少一个通道成功
failCount = 0;
// 检查禁用信号
if (isDisableSignal(chosen.version)) {
autoCheckDisabled = true;
saveDisabledState();
plugin.getLogger().info(
"[Sdf1_login] 远程版本信号"
+ " \"" + chosen.version
+ "\",自动更新检测已禁用");
if (sender != null) {
Bukkit.getScheduler()
.runTask(plugin, () ->
sender.sendMessage(
"§e[更新] 远程已"
+ "禁用自动"
+ "更新检测"));
}
return;
}
// 手动调用:成功才解除熔断和禁用
if (!isAuto) {
if (circuitTripped) {
circuitTripped = false;
}
if (autoCheckDisabled) {
autoCheckDisabled = false;
saveDisabledState();
}
}
final String src = chosen.source;
final String lnk = chosen.link;
final String rv = chosen.version;
Bukkit.getScheduler()
.runTask(plugin, () -> {
if (sender == null) return;
sender.sendMessage(
"§e[更新] 检测成功"
+ " via: §a" + src);
sender.sendMessage(
"§e[更新] 本地版本:"
+ " §f"
+ currentVersion);
sender.sendMessage(
"§e[更新] 远程版本:"
+ " §f" + rv);
if (isNewer(rv,
currentVersion)) {
sender.sendMessage(
"§c[更新] 发现新"
+ "版本: " + rv);
sender.sendMessage(
"§a请前往仓库下载"
+ "更新:");
sender.sendMessage(
"§a" + lnk);
} else {
sender.sendMessage(
"§a[更新] 已是"
+ "最新版本");
}
// 显示另一通道的失败原因
// (仅信息,不影响结果)
CheckResult other =
(src.equals("GitHub"))
? ge : gh;
if (!other.isSuccess()
&& other.error != null) {
sender.sendMessage(
"§7[更新] "
+ other.source
+ "不可用: "
+ other.error);
}
});
}
// ========== 禁用信号判断 ==========
private boolean isDisableSignal(
String version) {
if (version == null) return false;
String v = version.trim().toLowerCase();
return "999".equals(v)
|| "关闭".equals(v)
|| "false".equals(v)
|| "禁用".equals(v);
}
// ========== HTTP 请求(带超时) ==========
// ========== 检测结果 ==========
private static class CheckResult {
final String version;
final String source;
final String link;
final String error;
CheckResult(String version,
String source,
String link,
String error) {
this.version = version;
this.source = source;
this.link = link;
this.error = error;
}
boolean isSuccess() {
return version != null;
}
}
// ========== HTTP 请求 ==========
private CheckResult tryChannel(
String apiUrl,
String sourceName,
String link) {
HttpURLConnection conn = null;
try {
URL url = new URL(apiUrl);
conn = (HttpURLConnection)
url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; "
+ "Win64; x64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) "
+ "Chrome/120.0.0.0 Safari/537.36");
conn.setRequestProperty(
"Accept",
"*/*");
conn.setRequestProperty(
"Accept-Language",
"zh-CN,zh;q=0.9");
conn.setConnectTimeout(TIMEOUT_MS);
conn.setReadTimeout(TIMEOUT_MS);
conn.setInstanceFollowRedirects(true);
int code = conn.getResponseCode();
if (code == 404) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": 未找到 (HTTP 404)");
}
if (code == 403) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": 被限流 (HTTP 403)");
}
if (code != 200) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": HTTP " + code);
}
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
conn
.getInputStream()));
StringBuilder sb =
new StringBuilder();
String line;
while ((line = reader.readLine())
!= null) {
sb.append(line);
}
reader.close();
String tag =
parseTagName(sb.toString());
if (tag == null) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": 无法解析版本号");
}
return new CheckResult(tag,
sourceName, link, null);
} catch (java.net
.SocketTimeoutException e) {
return new CheckResult(null,
sourceName, link,
sourceName + ": 连接超时");
} catch (java.net.ConnectException e) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": 连接被拒绝");
} catch (java.net
.UnknownHostException e) {
return new CheckResult(null,
sourceName, link,
sourceName
+ ": DNS解析失败");
} catch (Exception e) {
return new CheckResult(null,
sourceName, link,
sourceName + ": "
+ e.getMessage());
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
/**
* 从 JSON 中提取 tag_name
*/
private String parseTagName(String json) {
// tags API 返回数组: [{"name":"v1.1",...}]
// releases API 返回对象: {"tag_name":"v1.1"}
// 尝试 tag_name(releases格式)
int idx =
json.indexOf("\"tag_name\"");
if (idx >= 0) {
int colon =
json.indexOf(":", idx);
int q1 =
json.indexOf("\"", colon + 1);
int q2 =
json.indexOf("\"", q1 + 1);
if (q2 > q1) {
return cleanTag(
json.substring(
q1 + 1, q2));
}
}
// 尝试 "name"(tags格式)
idx = json.indexOf("\"name\"");
if (idx >= 0) {
int colon =
json.indexOf(":", idx);
int q1 =
json.indexOf("\"", colon + 1);
int q2 =
json.indexOf("\"", q1 + 1);
if (q2 > q1) {
return cleanTag(
json.substring(
q1 + 1, q2));
}
}
return null;
}
private String cleanTag(String tag) {
if (tag.startsWith("v")
|| tag.startsWith("V")) {
tag = tag.substring(1);
}
return tag.isEmpty() ? null : tag;
}
// ========== 版本比较 ==========
private boolean isNewer(String remote,
String local) {
String[] r = remote.split("\\.");
String[] l = local.split("\\.");
int len = Math.max(r.length, l.length);
for (int i = 0; i < len; i++) {
int ri = i < r.length
? parseNum(r[i]) : 0;
int li = i < l.length
? parseNum(l[i]) : 0;
if (ri > li) return true;
if (ri < li) return false;
}
return false;
}
private int parseNum(String s) {
try {
return Integer.parseInt(
s.replaceAll("[^0-9]", ""));
} catch (Exception e) {
return 0;
}
}
}
@@ -0,0 +1,163 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class WelcomeManager {
private final Main plugin;
private final Random random = new Random();
private final List<String> registerMsgs =
new ArrayList<>();
private final List<String> loginMsgs =
new ArrayList<>();
public WelcomeManager(Main plugin) {
this.plugin = plugin;
loadMessages();
}
public void loadMessages() {
registerMsgs.clear();
loginMsgs.clear();
File f = new File(
plugin.getDataFolder(),
"欢迎仪式.txt");
if (!f.exists()) {
writeDefault(f);
}
try (BufferedReader r =
new BufferedReader(
new InputStreamReader(
new FileInputStream(f),
StandardCharsets
.UTF_8))) {
String section = "";
String line;
while ((line = r.readLine()) != null) {
if (line.startsWith("\uFEFF"))
line = line.substring(1);
String t = line.trim();
if (t.isEmpty()
|| t.startsWith("#"))
continue;
if (t.equals("注册")
|| t.equals("注册:")) {
section = "register";
continue;
}
if (t.equals("登录")
|| t.equals("登录:")) {
section = "login";
continue;
}
// 支持 = 和 : 两种分隔
if (section.isEmpty()) {
if (t.startsWith("注册")
&& (t.contains("=")
|| t.contains(":"))) {
int idx = t.indexOf('=');
int idx2 = t.indexOf(':');
if (idx < 0) idx = idx2;
else if (idx2 >= 0)
idx = Math.min(idx, idx2);
String v = t.substring(idx + 1)
.trim();
if (!v.isEmpty())
registerMsgs.add(
colorize(v));
continue;
}
if (t.startsWith("登录")
&& (t.contains("=")
|| t.contains(":"))) {
int idx = t.indexOf('=');
int idx2 = t.indexOf(':');
if (idx < 0) idx = idx2;
else if (idx2 >= 0)
idx = Math.min(idx, idx2);
String v = t.substring(idx + 1)
.trim();
if (!v.isEmpty())
loginMsgs.add(
colorize(v));
continue;
}
continue;
}
if (!t.isEmpty()) {
if (section.equals("register"))
registerMsgs.add(
colorize(t));
else
loginMsgs.add(
colorize(t));
}
}
} catch (Exception e) {
plugin.getLogger().severe(
"[欢迎] 加载失败: "
+ e.getMessage());
}
plugin.getLogger().info(
"[欢迎] 加载完成: 注册="
+ registerMsgs.size()
+ " 登录=" + loginMsgs.size());
}
public void onRegister(Player p) {
sendRandom(p, registerMsgs, "注册");
}
public void onLogin(Player p) {
sendRandom(p, loginMsgs, "登录");
}
private void sendRandom(Player p,
List<String> msgs, String tag) {
if (msgs.isEmpty()) return;
String msg = msgs.get(
random.nextInt(msgs.size()));
msg = msg.replace("{player}",
p.getName());
final String finalMsg = msg;
Bukkit.getScheduler()
.runTaskLater(plugin,
() -> Bukkit
.broadcastMessage(
finalMsg), 10L);
}
private String colorize(String msg) {
return msg.replace("&", "\u00a7");
}
private void writeDefault(File f) {
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets
.UTF_8))) {
pw.println("# 欢迎仪式配置");
pw.println("# &为颜色代码");
pw.println("# {player} = 玩家名");
pw.println();
pw.println("注册:");
pw.println("&a&l欢迎 &f{player} &a加入服务器!&7[注册]");
pw.println("&6&l{player} &e已完成注册,欢迎入驻!&7[注册]");
pw.println("&b&l新成员 &f{player} &b已就位!&7[注册]");
pw.println();
pw.println("登录:");
pw.println("&a&l欢迎回来 &f{player}&a!&7[登录]");
pw.println("&e&l{player} &e已登录,继续冒险吧!&7[登录]");
pw.println("&b&l{player} &b回来了!&7[登录]");
} catch (IOException ignored) {
}
}
}