修复签到功能任意参数均视为成功的问题,加入签到给经济功能
This commit is contained in:
@@ -43,3 +43,4 @@ txt/
|
||||
*.xlsx
|
||||
*.patch
|
||||
新建*.txt
|
||||
群戏归类.txt
|
||||
@@ -37,6 +37,16 @@ public class ChatFilterManager {
|
||||
private boolean notifyAdmin = true;
|
||||
private boolean notifyAll = false;
|
||||
private int muteDuration = 300;
|
||||
private boolean enabled = true;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean v) {
|
||||
enabled = v;
|
||||
}
|
||||
|
||||
|
||||
private static final Pattern URL_PATTERN =
|
||||
Pattern.compile(
|
||||
@@ -106,6 +116,7 @@ public class ChatFilterManager {
|
||||
if (line.startsWith("\uFEFF"))
|
||||
line = line.substring(1);
|
||||
String t = line.trim();
|
||||
|
||||
if (t.isEmpty()
|
||||
|| t.startsWith("#"))
|
||||
continue;
|
||||
@@ -128,6 +139,12 @@ public class ChatFilterManager {
|
||||
String[] kv = t.split(":", 2);
|
||||
String k = kv[0].trim();
|
||||
String v = kv[1].trim();
|
||||
if (equals("启用过滤")
|
||||
|| equals("enabled")) {
|
||||
enabled = parseBool(v);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (k.equals("通知管理员")) {
|
||||
notifyAdmin = parseBool(v);
|
||||
continue;
|
||||
@@ -238,6 +255,7 @@ public class ChatFilterManager {
|
||||
new FileOutputStream(f),
|
||||
StandardCharsets.UTF_8))) {
|
||||
pw.println("# Sdf1_chat 聊天过滤配置");
|
||||
pw.println("启用过滤: " + enabled);
|
||||
pw.println("通知管理员: true");
|
||||
pw.println("全服通报: false");
|
||||
pw.println("禁言时长: 300");
|
||||
|
||||
@@ -36,7 +36,8 @@ public class ChatInputManager {
|
||||
TICKET_ADMIN_TYPE,
|
||||
TICKET_ADMIN_TITLE,
|
||||
TICKET_ADMIN_DESC,
|
||||
TICKET_ADMIN_PROVIDER
|
||||
TICKET_ADMIN_PROVIDER,
|
||||
BACK_CHECK_ECONOMY_CONFIRM,
|
||||
}
|
||||
|
||||
public static class PlayerState {
|
||||
@@ -63,6 +64,11 @@ public class ChatInputManager {
|
||||
public void reset(Player p) {
|
||||
states.remove(p.getName());
|
||||
}
|
||||
public boolean isInFlow(Player p) {
|
||||
PlayerState state = states.get(p.getName());
|
||||
return state != null
|
||||
&& state.type != InputType.NONE;
|
||||
}
|
||||
|
||||
public void handleInput(Player p, String msg,
|
||||
Main mainPlugin) {
|
||||
@@ -542,24 +548,33 @@ public class ChatInputManager {
|
||||
}
|
||||
|
||||
case BACK_CHECKIN: {
|
||||
int cost = 20;
|
||||
if (!mainPlugin.getDb()
|
||||
.deductPoints(
|
||||
p.getName(), cost)) {
|
||||
p.sendMessage(
|
||||
"§c积分不足,补签需 "
|
||||
+ cost + " 积分");
|
||||
reset(p);
|
||||
String result =
|
||||
mainPlugin.getCheckIn()
|
||||
.backCheckIn(p, msg);
|
||||
if (result == null) {
|
||||
// 等待选择(NEED_CONFIRM)
|
||||
return;
|
||||
}
|
||||
p.sendMessage(
|
||||
"§a补签成功!扣除 "
|
||||
+ cost + " 积分。"
|
||||
+ "日期: " + msg);
|
||||
if (result.startsWith(
|
||||
"NEED_CONFIRM:")) {
|
||||
String[] parts =
|
||||
result.split(":");
|
||||
// 存储到Main的Map
|
||||
mainPlugin.pendingBackCheck.put(
|
||||
p.getName(),
|
||||
parts[1] + ":" + parts[2]);
|
||||
return;
|
||||
}
|
||||
if (result.startsWith("§c")) {
|
||||
p.sendMessage(result);
|
||||
} else {
|
||||
p.sendMessage(result);
|
||||
}
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
case INVITE_INPUT_CODE: {
|
||||
String code = msg.trim();
|
||||
String myName = p.getName();
|
||||
|
||||
@@ -3,6 +3,7 @@ package Sdf1_login;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
public class CheckInManager {
|
||||
@@ -13,14 +14,19 @@ public class CheckInManager {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public boolean isCheckedInToday(String name) {
|
||||
// ===== 今日是否已签到 =====
|
||||
public boolean isCheckedInToday(
|
||||
String name) {
|
||||
String last = (String) plugin.getDb()
|
||||
.getField(name, "last_checkin_date");
|
||||
String today = new SimpleDateFormat("yyyy-MM-dd")
|
||||
.getField(name,
|
||||
"last_checkin_date");
|
||||
String today = new SimpleDateFormat(
|
||||
"yyyy-MM-dd")
|
||||
.format(new Date());
|
||||
return today.equals(last);
|
||||
}
|
||||
|
||||
// ===== 签到 =====
|
||||
public String checkIn(Player p) {
|
||||
String name = p.getName();
|
||||
if (isCheckedInToday(name)) {
|
||||
@@ -29,25 +35,36 @@ public class CheckInManager {
|
||||
}
|
||||
|
||||
Object streakObj = plugin.getDb()
|
||||
.getField(name, "checkin_streak");
|
||||
.getField(name,
|
||||
"checkin_streak");
|
||||
int streak = streakObj != null
|
||||
? ((Number) streakObj).intValue() : 0;
|
||||
? ((Number) streakObj)
|
||||
.intValue() : 0;
|
||||
|
||||
String lastDate = (String) plugin.getDb()
|
||||
.getField(name, "last_checkin_date");
|
||||
String today = new SimpleDateFormat("yyyy-MM-dd")
|
||||
String lastDate = (String) plugin
|
||||
.getDb().getField(name,
|
||||
"last_checkin_date");
|
||||
String today = new SimpleDateFormat(
|
||||
"yyyy-MM-dd")
|
||||
.format(new Date());
|
||||
|
||||
if (lastDate != null && !lastDate.isEmpty()) {
|
||||
if (lastDate != null
|
||||
&& !lastDate.isEmpty()) {
|
||||
try {
|
||||
java.util.Date lastD = new SimpleDateFormat(
|
||||
"yyyy-MM-dd").parse(lastDate);
|
||||
java.util.Date todayD = new SimpleDateFormat(
|
||||
"yyyy-MM-dd").parse(today);
|
||||
long diff = todayD.getTime() - lastD.getTime();
|
||||
java.util.Date lastD =
|
||||
new SimpleDateFormat(
|
||||
"yyyy-MM-dd")
|
||||
.parse(lastDate);
|
||||
java.util.Date todayD =
|
||||
new SimpleDateFormat(
|
||||
"yyyy-MM-dd")
|
||||
.parse(today);
|
||||
long diff = todayD.getTime()
|
||||
- lastD.getTime();
|
||||
if (diff > 86400000L * 2) {
|
||||
streak = 0;
|
||||
} else if (diff > 86400000L) {
|
||||
} else if (diff
|
||||
> 86400000L) {
|
||||
streak++;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
@@ -62,18 +79,30 @@ public class CheckInManager {
|
||||
else if (streak >= 14) multi = 1.3;
|
||||
else if (streak >= 7) multi = 1.1;
|
||||
|
||||
int points = (int) (1 * multi);
|
||||
plugin.getDb().addPoints(name, points);
|
||||
int basePoints = plugin.getConfig2()
|
||||
.checkinPoints;
|
||||
int points =
|
||||
(int) (basePoints * multi);
|
||||
|
||||
plugin.getDb().addPoints(
|
||||
name, points);
|
||||
plugin.getDb().setField(
|
||||
name, "checkin_streak", streak);
|
||||
name, "checkin_streak",
|
||||
streak);
|
||||
plugin.getDb().setField(
|
||||
name, "last_checkin_date", today);
|
||||
name,
|
||||
"last_checkin_date", today);
|
||||
|
||||
Object totalObj = plugin.getDb()
|
||||
.getField(name, "total_checkin_days");
|
||||
.getField(name,
|
||||
"total_checkin_days");
|
||||
int total = totalObj != null
|
||||
? ((Number) totalObj).intValue() : 0;
|
||||
? ((Number) totalObj)
|
||||
.intValue() : 0;
|
||||
plugin.getDb().setField(
|
||||
name, "total_checkin_days", total + 1);
|
||||
name,
|
||||
"total_checkin_days",
|
||||
total + 1);
|
||||
|
||||
return plugin.getConfig2()
|
||||
.msg("checkin_success")
|
||||
@@ -85,30 +114,173 @@ public class CheckInManager {
|
||||
String.valueOf(multi));
|
||||
}
|
||||
|
||||
public String backCheckIn(Player p, String dateStr) {
|
||||
// ===== 获取连续天数 =====
|
||||
public int getStreak(String name) {
|
||||
Object val = plugin.getDb()
|
||||
.getField(name,
|
||||
"checkin_streak");
|
||||
if (val instanceof Number) {
|
||||
return ((Number) val)
|
||||
.intValue();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ===== 补签入口 =====
|
||||
public String backCheckIn(
|
||||
Player p, String dateStr) {
|
||||
String name = p.getName();
|
||||
if (!plugin.getDb().deductPoints(name, 10)) {
|
||||
return plugin.getConfig2()
|
||||
.msg("checkin_already");
|
||||
}
|
||||
String normalized =
|
||||
normalizeDate(dateStr);
|
||||
|
||||
// 严格日期验证
|
||||
java.text.SimpleDateFormat sdf =
|
||||
new java.text.SimpleDateFormat(
|
||||
"yyyy-MM-dd");
|
||||
sdf.setLenient(false);
|
||||
java.util.Date target;
|
||||
try {
|
||||
java.util.Date target = new SimpleDateFormat(
|
||||
"yyyy-MM-dd").parse(dateStr);
|
||||
java.util.Date now = new SimpleDateFormat(
|
||||
"yyyy-MM-dd").parse(
|
||||
new SimpleDateFormat("yyyy-MM-dd")
|
||||
.format(new Date()));
|
||||
long diff = now.getTime() - target.getTime();
|
||||
if (diff < 0 || diff > 3 * 86400000L) {
|
||||
plugin.getDb().addPoints(name, 10);
|
||||
return plugin.getConfig2()
|
||||
.msg("checkin_already");
|
||||
}
|
||||
plugin.getDb().addPoints(name, 1);
|
||||
return "§a补签成功!日期: " + dateStr;
|
||||
} catch (Exception e) {
|
||||
plugin.getDb().addPoints(name, 10);
|
||||
return "§c日期格式错误,请用 yyyy-MM-dd";
|
||||
target = sdf.parse(normalized);
|
||||
} catch (Exception ex) {
|
||||
return "§c无效日期: " + dateStr;
|
||||
}
|
||||
String reformat =
|
||||
sdf.format(target);
|
||||
if (!reformat.equals(normalized)) {
|
||||
return "§c无效日期: " + dateStr;
|
||||
}
|
||||
|
||||
String today = sdf.format(new Date());
|
||||
|
||||
if (normalized.compareTo(today) >= 0) {
|
||||
return "§c今天不需要补签";
|
||||
}
|
||||
|
||||
String threeDaysAgo = getPastDate(3);
|
||||
if (normalized.compareTo(
|
||||
threeDaysAgo) < 0) {
|
||||
return "§c只能补签3天内"
|
||||
+ "(最多到 "
|
||||
+ threeDaysAgo + ")";
|
||||
}
|
||||
|
||||
Object val = plugin.getDb()
|
||||
.getField(name,
|
||||
"backcheck_dates");
|
||||
String existing = val != null
|
||||
? val.toString() : "";
|
||||
boolean alreadyBacked = false;
|
||||
if (!existing.isEmpty()) {
|
||||
for (String d
|
||||
: existing.split(",")) {
|
||||
if (normalizeDate(d.trim())
|
||||
.equals(normalized)) {
|
||||
alreadyBacked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (alreadyBacked) {
|
||||
return "§c该日期已补签: "
|
||||
+ normalized;
|
||||
}
|
||||
|
||||
Object normalCheck =
|
||||
plugin.getDb().getField(
|
||||
name,
|
||||
"last_checkin_date");
|
||||
String lastCheckin = normalCheck
|
||||
!= null
|
||||
? normalCheck.toString()
|
||||
: "";
|
||||
if (lastCheckin.equals(normalized)) {
|
||||
return "§c该日期已签到: "
|
||||
+ normalized;
|
||||
}
|
||||
|
||||
if (!plugin.getDb()
|
||||
.deductPoints(name, 10)) {
|
||||
return "§c积分不足,补签需 10 积分";
|
||||
}
|
||||
|
||||
net.milkbowl.vault.economy
|
||||
.Economy econ =
|
||||
plugin.getEconomy();
|
||||
if (econ != null) {
|
||||
double bal = econ.getBalance(p);
|
||||
if (bal < 10.0) {
|
||||
plugin.getDb()
|
||||
.addPoints(name, 10);
|
||||
int mult = plugin.getConfig2()
|
||||
.backCheckPointMultiplier;
|
||||
int needed = 10 * mult;
|
||||
p.sendMessage("§e§l[补签] "
|
||||
+ "§f经济不足($"
|
||||
+ String.format(
|
||||
"%.2f", bal)
|
||||
+ " < $10)");
|
||||
p.sendMessage("§7是否用 §a§l"
|
||||
+ needed + "积分 "
|
||||
+ "§7代替 10经济?");
|
||||
p.sendMessage("§7输入 §a1 "
|
||||
+ "§7同意,"
|
||||
+ "§c其他 §7取消");
|
||||
return "NEED_CONFIRM:"
|
||||
+ normalized
|
||||
+ ":" + needed;
|
||||
}
|
||||
econ.withdrawPlayer(p, 10);
|
||||
}
|
||||
|
||||
doBackCheckIn(p, normalized);
|
||||
return "§a补签成功!日期: "
|
||||
+ normalized
|
||||
+ " §7(扣除10积分+10经济)";
|
||||
}
|
||||
|
||||
|
||||
// ===== 执行补签 =====
|
||||
public void doBackCheckIn(
|
||||
Player p, String dateStr) {
|
||||
String name = p.getName();
|
||||
Object val = plugin.getDb()
|
||||
.getField(name,
|
||||
"backcheck_dates");
|
||||
String existing = val != null
|
||||
? val.toString() : "";
|
||||
String updated = existing.isEmpty()
|
||||
? dateStr
|
||||
: existing + "," + dateStr;
|
||||
plugin.getDb().setField(name,
|
||||
"backcheck_dates", updated);
|
||||
int streak = getStreak(name);
|
||||
plugin.getDb().setField(name,
|
||||
"checkin_streak",
|
||||
streak + 1);
|
||||
}
|
||||
|
||||
// ===== 标准化日期 =====
|
||||
private String normalizeDate(
|
||||
String input) {
|
||||
String s = input.trim()
|
||||
.replace("/", "-");
|
||||
if (s.matches("\\d{8}")) {
|
||||
return s.substring(0, 4) + "-"
|
||||
+ s.substring(4, 6) + "-"
|
||||
+ s.substring(6, 8);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// ===== 获取N天前日期 =====
|
||||
private String getPastDate(
|
||||
int daysAgo) {
|
||||
Calendar cal =
|
||||
Calendar.getInstance();
|
||||
cal.add(Calendar.DAY_OF_MONTH,
|
||||
-daysAgo);
|
||||
return new SimpleDateFormat(
|
||||
"yyyy-MM-dd")
|
||||
.format(cal.getTime());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@ import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
import static java.lang.Double.parseDouble;
|
||||
|
||||
public class ConfigManager {
|
||||
|
||||
private final File dataFolder;
|
||||
private final Map<String, String> messages = new LinkedHashMap<>();
|
||||
private final Map<String, String> smtpSettings = new LinkedHashMap<>();
|
||||
private final Map<String, String> messages =
|
||||
new LinkedHashMap<>();
|
||||
private final Map<String, String> smtpSettings =
|
||||
new LinkedHashMap<>();
|
||||
|
||||
// ===== 公共字段 =====
|
||||
public String adminTag = "admin";
|
||||
public String adminPassword = "qweasd";
|
||||
public int maxAccountsPerIP = 3;
|
||||
@@ -27,9 +28,26 @@ public class ConfigManager {
|
||||
public String smtpConfiguringPlayer = "";
|
||||
public String broadcastServerUrl = "";
|
||||
public int httpPort = 8080;
|
||||
public String garbageTag= "admin";
|
||||
public String garbageTag = "admin";
|
||||
|
||||
// 签到经济奖励
|
||||
public double checkinRewardFixed = 0;
|
||||
public double checkinRewardMin = 0;
|
||||
public double checkinRewardMax = 0;
|
||||
public boolean checkinGivePoints = true;
|
||||
public int checkinPoints = 10;
|
||||
public String checkinRewardType = "none";
|
||||
public int backCheckPointMultiplier = 5;
|
||||
// 提成/工单
|
||||
public double providerPointsPerScore = 2.0;
|
||||
public int baseEconomyReward = 100;
|
||||
public double providerEconomyPerScore = 50.0;
|
||||
public int requesterPointsReward = 10;
|
||||
|
||||
// 垃圾站
|
||||
public boolean garbageEnabled = true;
|
||||
public int garbageInterval = 300;
|
||||
public int garbageMaxRounds = 1;
|
||||
|
||||
public ConfigManager(File dataFolder) {
|
||||
this.dataFolder = dataFolder;
|
||||
@@ -39,232 +57,132 @@ public class ConfigManager {
|
||||
|
||||
public void loadMessages() {
|
||||
messages.clear();
|
||||
File file = new File(dataFolder, "消息.txt");
|
||||
if (!file.exists()) createDefaultMessages(file);
|
||||
File file =
|
||||
new File(dataFolder, "消息.txt");
|
||||
if (!file.exists())
|
||||
createDefaultMessages(file);
|
||||
loadMap(file, messages);
|
||||
}
|
||||
|
||||
public String msg(String key) {
|
||||
String val = messages.getOrDefault(key, key);
|
||||
String val =
|
||||
messages.getOrDefault(key, key);
|
||||
val = val.replace("<b>", "&l");
|
||||
val = val.replace("</b>", "&r");
|
||||
val = val.replace("<br>", "\n");
|
||||
val = val.replace("\\n", "\n");
|
||||
// &颜色码 转换为 §颜色码
|
||||
val = val.replaceAll(
|
||||
"&([0-9a-fk-orA-FK-OR])", "§$1");
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
private void createDefaultMessages(File f) {
|
||||
List<String> L = new ArrayList<>();
|
||||
L.add("# ==========================================");
|
||||
L.add("# Sdf1_login 消息配置文件");
|
||||
L.add("# ==========================================");
|
||||
L.add("# 颜色代码使用 § 符号");
|
||||
L.add("# §0黑色 §1深蓝 §2深绿 §3深青");
|
||||
L.add("# §4深红 §5深紫 §6金色 §7灰色");
|
||||
L.add("# §8深灰 §9蓝色 §a绿色 §b浅蓝");
|
||||
L.add("# §c红色 §d粉色 §e黄色 §f白色");
|
||||
L.add("# §l粗体 §n下划线 §o斜体 §r重置");
|
||||
L.add("#");
|
||||
L.add("# 占位符用 {xxx} 表示,运行时自动替换");
|
||||
L.add("# 支持 <b>粗体</b> 标签");
|
||||
L.add("# 支持 \\n 或 <br> 换行");
|
||||
L.add("# 修改后执行 /sdf1_login reload 重载");
|
||||
L.add("# ==========================================");
|
||||
L.add("# ===== Sdf1_login 消息配置 =====");
|
||||
L.add("# 占位符用 {xxx} 表示");
|
||||
L.add("# 修改后执行 /sdf1_login reload");
|
||||
L.add("");
|
||||
|
||||
// --- 登录/注册 ---
|
||||
L.add("# ---- 登录注册相关 ----");
|
||||
L.add("# login_timeout: 登录超时被踢出时的提示");
|
||||
L.add("# ---- 登录注册 ----");
|
||||
L.add("login_timeout=§c登录超时,请重新登录");
|
||||
L.add("# not_registered: 未注册玩家收到的提示");
|
||||
L.add("not_registered=§e您尚未注册,请使用 /reg <密码>");
|
||||
L.add("# not_logged_in: 已注册但未登录的提示");
|
||||
L.add("not_logged_in=§e您尚未登录,请使用 /login <密码>");
|
||||
L.add("# already_registered: 已注册玩家再次注册时的提示");
|
||||
L.add("already_registered=§c您已注册过账号");
|
||||
L.add("# already_logged_in: 已登录玩家再次登录时的提示");
|
||||
L.add("already_logged_in=§c您已登录");
|
||||
L.add("# reg_success: 注册成功提示");
|
||||
L.add("reg_success=§a注册成功!欢迎来到服务器");
|
||||
L.add("# login_success: 登录成功提示");
|
||||
L.add("login_success=§a登录成功!");
|
||||
L.add("# login_failed: 密码错误提示");
|
||||
L.add("login_failed=§c密码错误,请重试");
|
||||
L.add("");
|
||||
|
||||
// --- 密码 ---
|
||||
L.add("# ---- 密码相关 ----");
|
||||
L.add("# password_wrong: 旧密码验证失败");
|
||||
L.add("# ---- 密码 ----");
|
||||
L.add("password_wrong=§c密码错误");
|
||||
L.add("# password_changed: 密码修改成功");
|
||||
L.add("password_changed=§a密码修改成功!");
|
||||
L.add("# password_format_error: 密码格式不符合要求");
|
||||
L.add("password_format_error=§c密码需6位以上,含大小写字母和数字");
|
||||
L.add("# password_same: 新密码与旧密码相同");
|
||||
L.add("password_same=§c新密码不能与旧密码相同");
|
||||
L.add("# reset_no_email: 找回密码时未绑定邮箱");
|
||||
L.add("reset_no_email=§c您未绑定邮箱,无法找回密码");
|
||||
L.add("# reset_sent: 临时密码已发送到邮箱");
|
||||
L.add("reset_sent=§a临时密码已发送到您的邮箱,请查收");
|
||||
L.add("reset_sent=§a临时密码已发送到您的邮箱");
|
||||
L.add("");
|
||||
|
||||
// --- 签到积分 ---
|
||||
L.add("# ---- 签到积分相关 ----");
|
||||
L.add("# checkin_already: 今日已签到");
|
||||
L.add("# ---- 签到积分 ----");
|
||||
L.add("checkin_already=§c您今日已签到");
|
||||
L.add("# checkin_success: 签到成功,{points}=获得积分 {streak}=连续天数 {multi}=倍率");
|
||||
L.add("checkin_success=§a签到成功!获得 {points} 积分,连续 {streak} 天,倍率 x{multi}");
|
||||
L.add("# points_insufficient: 积分不足");
|
||||
L.add("checkin_success=§a签到成功!获得 {points} 积分");
|
||||
L.add("points_insufficient=§c积分不足,当前: {points}");
|
||||
L.add("# points_purchase_success: 积分购买成功");
|
||||
L.add("points_purchase_success=§a购买成功!消耗 {count} 积分");
|
||||
L.add("");
|
||||
|
||||
// --- 挂机 ---
|
||||
L.add("# ---- 挂机检测相关 ----");
|
||||
L.add("# afk_set_enabled: 挂机检测开启");
|
||||
L.add("# ---- 挂机检测 ----");
|
||||
L.add("afk_set_enabled=§a挂机检测已开启");
|
||||
L.add("# afk_set_disabled: 挂机检测关闭");
|
||||
L.add("afk_set_disabled=§c挂机检测已关闭");
|
||||
L.add("# afk_kick: 因挂机被踢出");
|
||||
L.add("afk_kick=§c因挂机过久被踢出");
|
||||
L.add("");
|
||||
|
||||
// --- 邮箱 ---
|
||||
L.add("# ---- 邮箱相关 ----");
|
||||
L.add("# email_set: 邮箱设置成功");
|
||||
L.add("# ---- 邮箱 ----");
|
||||
L.add("email_set=§a邮箱设置成功:{email}");
|
||||
L.add("# email_format_error: 邮箱格式错误");
|
||||
L.add("email_format_error=§c邮箱格式不正确");
|
||||
L.add("");
|
||||
|
||||
// --- 邀请 ---
|
||||
L.add("# ---- 邀请相关 ----");
|
||||
L.add("# invite_code_generated: 邀请码生成");
|
||||
L.add("# ---- 邀请 ----");
|
||||
L.add("invite_code_generated=§a邀请码:{code}");
|
||||
L.add("# invite_used: 使用邀请码成功");
|
||||
L.add("invite_used=§a邀请成功,邀请人:{player}");
|
||||
L.add("# invite_self: 使用自己的邀请码");
|
||||
L.add("invite_self=§c不能使用自己的邀请码");
|
||||
L.add("# invite_not_found: 邀请码不存在");
|
||||
L.add("invite_not_found=§c邀请码不存在");
|
||||
L.add("# invite_referral_bonus: 被邀请人签到后邀请人获得奖励");
|
||||
L.add("invite_referral_bonus=§a邀请奖励!获得 {points} 积分");
|
||||
L.add("");
|
||||
|
||||
// --- 礼包 ---
|
||||
L.add("# ---- 新人礼包相关 ----");
|
||||
L.add("# gift_not_ready: 礼包条件未满足");
|
||||
L.add("# ---- 礼包 ----");
|
||||
L.add("gift_not_ready=§c礼包条件未满足");
|
||||
L.add("# gift_claimed: 礼包领取成功");
|
||||
L.add("gift_claimed=§a礼包领取成功!");
|
||||
L.add("");
|
||||
|
||||
// --- 审批/IP ---
|
||||
L.add("# ---- 审批与IP限制 ----");
|
||||
L.add("# need_approval: 注册需审批");
|
||||
L.add("# ---- 审批与IP ----");
|
||||
L.add("need_approval=§e注册需审批 (工单#{id})");
|
||||
L.add("# ip_max_accounts: IP注册已达上限");
|
||||
L.add("ip_max_accounts=§cIP注册已达上限");
|
||||
L.add("");
|
||||
|
||||
// --- 管理员 ---
|
||||
L.add("# ---- 管理员操作 ----");
|
||||
L.add("# admin_delete_confirm: 管理员删除账号确认提示");
|
||||
L.add("# ---- 管理员 ----");
|
||||
L.add("admin_delete_confirm=§c请在聊天中输入玩家名确认删除");
|
||||
L.add("");
|
||||
|
||||
// --- 聊天过滤 ---
|
||||
L.add("# ---- 聊天过滤 ----");
|
||||
L.add("# chat_muted: 禁言玩家尝试聊天");
|
||||
L.add("chat_muted=§c§l[Sdf1_chat] §f你已被禁言,无法聊天");
|
||||
L.add("# chat_url_blocked: 发送违规链接被拦截");
|
||||
L.add("# {url}=检测到的链接地址");
|
||||
L.add("chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规第三方链接: §e{url}");
|
||||
L.add("# chat_url_violation: 当前违规次数");
|
||||
L.add("# {count}=当前违规次数");
|
||||
L.add("chat_muted=§c§l[Sdf1_chat] §f你已被禁言");
|
||||
L.add("chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规链接: §e{url}");
|
||||
L.add("chat_url_violation=§7当前违规次数: §e{count}");
|
||||
L.add("# chat_url_admin_notify: 通知管理员,{player}=玩家名 {url}=链接 {count}=次数");
|
||||
L.add("chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url} (第{count}次)");
|
||||
L.add("# chat_url_broadcast: 全服通报,{player}=玩家名");
|
||||
L.add("chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url}");
|
||||
L.add("chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚");
|
||||
L.add("");
|
||||
|
||||
// --- 通用 ---
|
||||
L.add("# ---- 通用 ----");
|
||||
L.add("# 未知参数: 未知命令或权限不足时的提示");
|
||||
L.add("未知参数=§c未知参数或权限不足");
|
||||
L.add("# only_player: 仅玩家可用的命令");
|
||||
L.add("# only_player=§c此命令仅玩家可用");
|
||||
|
||||
writeLines(f, L);
|
||||
}
|
||||
|
||||
|
||||
// ==================== SMTP ====================
|
||||
|
||||
public void loadSmtp() {
|
||||
smtpSettings.clear();
|
||||
File file = new File(dataFolder, "SMTP设置.txt");
|
||||
if (!file.exists()) createDefaultSmtp(file);
|
||||
File file =
|
||||
new File(dataFolder, "SMTP设置.txt");
|
||||
if (!file.exists())
|
||||
createDefaultSmtp(file);
|
||||
loadMap(file, smtpSettings);
|
||||
}
|
||||
private double parseDouble(String s, double def) {
|
||||
try { return Double.parseDouble(s.trim()); }
|
||||
catch (Exception e) { return def; }
|
||||
}
|
||||
|
||||
public String getSmtp(String key) {
|
||||
return smtpSettings.getOrDefault(key, "");
|
||||
}
|
||||
/** 获取挂机白名单列表 */
|
||||
public List<String> getAfkWhitelist() {
|
||||
List<String> list = new ArrayList<>();
|
||||
File file = new File(dataFolder, "挂机白名单.txt");
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
file.createNewFile();
|
||||
try (BufferedWriter w = new BufferedWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(file),
|
||||
StandardCharsets.UTF_8))) {
|
||||
w.write("# 每行一个玩家名,前面加 # 为注释");
|
||||
w.newLine();
|
||||
w.write("# 以下玩家不会被挂机踢出");
|
||||
w.newLine();
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
try (BufferedReader r = new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(file),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty() && !line.startsWith("#")) {
|
||||
list.add(line);
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 设置SMTP单项(聊天输入用) */
|
||||
public void setSmtp(String key, String value) {
|
||||
smtpSettings.put(key, value);
|
||||
}
|
||||
|
||||
/** 将当前SMTP设置写回文件 */
|
||||
public void saveSmtp() {
|
||||
File file = new File(dataFolder, "SMTP设置.txt");
|
||||
File file =
|
||||
new File(dataFolder, "SMTP设置.txt");
|
||||
List<String> L = new ArrayList<>();
|
||||
L.add("# ===== SMTP 邮件服务器配置 =====");
|
||||
for (Map.Entry<String, String> e : smtpSettings.entrySet()) {
|
||||
for (Map.Entry<String, String> e
|
||||
: smtpSettings.entrySet()) {
|
||||
L.add(e.getKey() + "=" + e.getValue());
|
||||
}
|
||||
writeLines(file, L);
|
||||
@@ -285,17 +203,25 @@ public class ConfigManager {
|
||||
// ==================== 插件设置 ====================
|
||||
|
||||
public void loadSettings() {
|
||||
File file = new File(dataFolder, "插件设置.txt");
|
||||
if (!file.exists()) createDefaultSettings(file);
|
||||
Map<String, String> m = new LinkedHashMap<>();
|
||||
File file =
|
||||
new File(dataFolder, "插件设置.txt");
|
||||
if (!file.exists())
|
||||
createDefaultSettings(file);
|
||||
Map<String, String> m =
|
||||
new LinkedHashMap<>();
|
||||
loadMap(file, m);
|
||||
adminTag = m.getOrDefault("管理标签", adminTag);
|
||||
adminPassword = m.getOrDefault("管理密码", adminPassword);
|
||||
|
||||
adminTag = m.getOrDefault(
|
||||
"管理标签", adminTag);
|
||||
adminPassword = m.getOrDefault(
|
||||
"管理密码", adminPassword);
|
||||
maxAccountsPerIP = parseInt(
|
||||
m.getOrDefault("每IP最大账号数", "3"), 3);
|
||||
approvalMode = m.getOrDefault("审批模式", approvalMode);
|
||||
approvalMode = m.getOrDefault(
|
||||
"审批模式", approvalMode);
|
||||
autoApproveDelayMinutes = parseInt(
|
||||
m.getOrDefault("自动审批延迟分钟", "30"), 30);
|
||||
m.getOrDefault("自动审批延迟分钟", "30"),
|
||||
30);
|
||||
loginTimeout = parseInt(
|
||||
m.getOrDefault("登录超时秒数", "60"), 60);
|
||||
afkEnabled = Boolean.parseBoolean(
|
||||
@@ -304,56 +230,106 @@ public class ConfigManager {
|
||||
m.getOrDefault("挂机超时秒数", "300"), 300);
|
||||
smtpSsl = Boolean.parseBoolean(
|
||||
m.getOrDefault("smtp加密", "true"));
|
||||
httpPort = parseInt(
|
||||
m.getOrDefault("http-port", "8080"),
|
||||
8080);
|
||||
broadcastServerUrl = m.getOrDefault(
|
||||
"resource-pack-url", "");
|
||||
|
||||
// 提成
|
||||
providerPointsPerScore = parseDouble(
|
||||
m.getOrDefault("服务商积分倍率", "2.0"),
|
||||
2.0);
|
||||
m.getOrDefault(
|
||||
"服务商积分倍率", "2.0"), 2.0);
|
||||
baseEconomyReward = parseInt(
|
||||
m.getOrDefault("基础经济奖励", "100"),
|
||||
100);
|
||||
m.getOrDefault(
|
||||
"基础经济奖励", "100"), 100);
|
||||
providerEconomyPerScore = parseDouble(
|
||||
m.getOrDefault("经济奖励每评分", "50.0"),
|
||||
50.0);
|
||||
m.getOrDefault(
|
||||
"经济奖励每评分", "50.0"), 50.0);
|
||||
requesterPointsReward = parseInt(
|
||||
m.getOrDefault("报单人积分奖励", "10"),
|
||||
10);
|
||||
m.getOrDefault(
|
||||
"报单人积分奖励", "10"), 10);
|
||||
|
||||
// 垃圾站
|
||||
garbageEnabled = Boolean.parseBoolean(
|
||||
m.getOrDefault("垃圾站_启用", "true"));
|
||||
m.getOrDefault(
|
||||
"垃圾站_启用", "true"));
|
||||
garbageInterval = parseInt(
|
||||
m.getOrDefault("垃圾站_清理间隔秒",
|
||||
"300"), 300);
|
||||
m.getOrDefault(
|
||||
"垃圾站_清理间隔秒", "300"), 300);
|
||||
garbageMaxRounds = parseInt(
|
||||
m.getOrDefault("垃圾站_保留轮数",
|
||||
"1"), 1);
|
||||
// loadSettings() 末尾添加:
|
||||
m.getOrDefault(
|
||||
"垃圾站_保留轮数", "1"), 1);
|
||||
|
||||
// 签到
|
||||
checkinRewardFixed = parseDouble(
|
||||
m.getOrDefault(
|
||||
"签到固定金额", "0"), 0);
|
||||
checkinRewardMin = parseDouble(
|
||||
m.getOrDefault(
|
||||
"签到最小金额", "0"), 0);
|
||||
checkinRewardMax = parseDouble(
|
||||
m.getOrDefault(
|
||||
"签到最大金额", "0"), 0);
|
||||
checkinGivePoints = Boolean.parseBoolean(
|
||||
m.getOrDefault(
|
||||
"签到给积分", "true"));
|
||||
checkinPoints = parseInt(
|
||||
m.getOrDefault(
|
||||
"签到积分数量", "10"), 10);
|
||||
backCheckPointMultiplier = parseInt(
|
||||
m.getOrDefault(
|
||||
"补签积分倍率", "5"), 5);
|
||||
|
||||
|
||||
// 自动判断模式
|
||||
if (checkinRewardFixed > 0) {
|
||||
checkinRewardType = "fixed";
|
||||
} else if (checkinRewardMin > 0
|
||||
&& checkinRewardMax > 0
|
||||
&& checkinRewardMax
|
||||
> checkinRewardMin) {
|
||||
checkinRewardType = "range";
|
||||
} else {
|
||||
checkinRewardType = "none";
|
||||
}
|
||||
}
|
||||
|
||||
public void saveSettings() {
|
||||
File file = new File(dataFolder, "插件设置.txt");
|
||||
File file =
|
||||
new File(dataFolder, "插件设置.txt");
|
||||
List<String> L = new ArrayList<>();
|
||||
L.add("# ===== Sdf1_login 插件设置 =====");
|
||||
L.add("管理标签=" + adminTag);
|
||||
L.add("管理密码=" + adminPassword);
|
||||
L.add("每IP最大账号数=" + maxAccountsPerIP);
|
||||
L.add("审批模式=" + approvalMode);
|
||||
L.add("自动审批延迟分钟=" + autoApproveDelayMinutes);
|
||||
L.add("自动审批延迟分钟="
|
||||
+ autoApproveDelayMinutes);
|
||||
L.add("登录超时秒数=" + loginTimeout);
|
||||
L.add("挂机踢出=" + afkEnabled);
|
||||
L.add("挂机超时秒数=" + afkTimeout);
|
||||
// 垃圾站设置
|
||||
L.add("# ---- 资源包 ----");
|
||||
L.add("resource-pack-url="
|
||||
+ broadcastServerUrl);
|
||||
L.add("http-port=" + httpPort);
|
||||
L.add("# ---- 垃圾站 ----");
|
||||
L.add("垃圾站_启用=" + garbageEnabled);
|
||||
L.add("垃圾站_清理间隔秒=" + garbageInterval);
|
||||
L.add("垃圾站_清理间隔秒="
|
||||
+ garbageInterval);
|
||||
L.add("垃圾站_保留轮数=" + garbageMaxRounds);
|
||||
L.add("# ---- 签到盲盒奖励 ----");
|
||||
L.add("签到固定金额=" + checkinRewardFixed);
|
||||
L.add("签到最小金额=" + checkinRewardMin);
|
||||
L.add("签到最大金额=" + checkinRewardMax);
|
||||
L.add("签到给积分=" + checkinGivePoints);
|
||||
L.add("签到积分数量=" + checkinPoints);
|
||||
L.add("补签积分倍率="
|
||||
+ backCheckPointMultiplier);
|
||||
|
||||
writeLines(file, L);
|
||||
|
||||
writeLines(file, L);
|
||||
}
|
||||
public double providerPointsPerScore = 2.0;
|
||||
public int baseEconomyReward = 100;
|
||||
public double providerEconomyPerScore = 50.0;
|
||||
public int requesterPointsReward = 10;
|
||||
public boolean garbageEnabled = true;
|
||||
public int garbageInterval = 300;
|
||||
public int garbageMaxRounds = 1;
|
||||
|
||||
|
||||
private void createDefaultSettings(File f) {
|
||||
List<String> L = new ArrayList<>();
|
||||
@@ -366,100 +342,177 @@ public class ConfigManager {
|
||||
L.add("登录超时秒数=60");
|
||||
L.add("挂机踢出=false");
|
||||
L.add("挂机超时秒数=300");
|
||||
L.add("服务商积分倍率=" + providerPointsPerScore);
|
||||
L.add("基础经济奖励=" + baseEconomyReward);
|
||||
L.add("经济奖励每评分=" + providerEconomyPerScore);
|
||||
L.add("报单人积分奖励=" + requesterPointsReward);
|
||||
L.add("# ---- 资源包 ----");
|
||||
L.add("resource-pack-url=");
|
||||
L.add("http-port=8080");
|
||||
L.add("# ---- 垃圾站 ----");
|
||||
L.add("垃圾站_启用=true");
|
||||
L.add("垃圾站_清理间隔秒=300");
|
||||
L.add("垃圾站_保留轮数=1");
|
||||
L.add("# ---- 资源包(HTTP) ----");
|
||||
L.add("resource-pack-url=");
|
||||
L.add("http-port=8080");
|
||||
L.add("# ---- 签到盲盒奖励 ----");
|
||||
L.add("签到固定金额=0");
|
||||
L.add("签到最小金额=50");
|
||||
L.add("签到最大金额=500");
|
||||
L.add("签到给积分=true");
|
||||
L.add("签到积分数量=10");
|
||||
L.add("补签积分倍率=5");
|
||||
|
||||
|
||||
writeLines(f, L);
|
||||
}
|
||||
|
||||
// ==================== 通用读写 ====================
|
||||
// ==================== 通用工具 ====================
|
||||
|
||||
private void loadMap(File file, Map<String, String> map) {
|
||||
try (BufferedReader r = new BufferedReader(
|
||||
new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||
public List<String> getAfkWhitelist() {
|
||||
List<String> list = new ArrayList<>();
|
||||
File file = new File(
|
||||
dataFolder, "挂机白名单.txt");
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
file.createNewFile();
|
||||
try (BufferedWriter w =
|
||||
new BufferedWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(
|
||||
file),
|
||||
StandardCharsets
|
||||
.UTF_8))) {
|
||||
w.write("# 每行一个玩家名");
|
||||
w.newLine();
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
try (BufferedReader r =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(
|
||||
file),
|
||||
StandardCharsets
|
||||
.UTF_8))) {
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
int eq = line.indexOf('=');
|
||||
if (eq > 0) map.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim());
|
||||
if (!line.isEmpty()
|
||||
&& !line.startsWith("#")) {
|
||||
list.add(line);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("[Sdf1_login] 读取失败: " + file.getName());
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
return list;
|
||||
}
|
||||
|
||||
private void writeLines(File file, List<String> lines) {
|
||||
try (BufferedWriter w = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
|
||||
for (String l : lines) { w.write(l); w.newLine(); }
|
||||
} catch (IOException e) {
|
||||
System.err.println("[Sdf1_login] 写入失败: " + file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private int parseInt(String s, int def) {
|
||||
try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; }
|
||||
}
|
||||
/**
|
||||
* 生成6位验证码(字母+数字)
|
||||
*/
|
||||
public String generateSmtpVerifyCode() {
|
||||
String chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
+ "abcdefghijklmnopqrstuvwxyz"
|
||||
+ "0123456789";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
java.util.Random r = new java.util.Random();
|
||||
java.util.Random r =
|
||||
new java.util.Random();
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sb.append(chars.charAt(r.nextInt(
|
||||
chars.length())));
|
||||
sb.append(chars.charAt(
|
||||
r.nextInt(chars.length())));
|
||||
}
|
||||
smtpVerifyCode = sb.toString();
|
||||
smtpVerifyExpire =
|
||||
System.currentTimeMillis() + 300000L;
|
||||
System.currentTimeMillis()
|
||||
+ 300000L;
|
||||
return smtpVerifyCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查验证码是否有效
|
||||
*/
|
||||
public boolean checkSmtpVerifyCode(String code) {
|
||||
if (System.currentTimeMillis() > smtpVerifyExpire) {
|
||||
public boolean checkSmtpVerifyCode(
|
||||
String code) {
|
||||
if (System.currentTimeMillis()
|
||||
> smtpVerifyExpire) {
|
||||
smtpVerifyCode = "";
|
||||
return false;
|
||||
}
|
||||
return smtpVerifyCode.equalsIgnoreCase(code);
|
||||
return smtpVerifyCode
|
||||
.equalsIgnoreCase(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除验证码
|
||||
*/
|
||||
public void clearSmtpVerifyCode() {
|
||||
smtpVerifyCode = "";
|
||||
smtpVerifyExpire = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查发送间隔是否>=1分钟
|
||||
*/
|
||||
public boolean canSendSmtpVerify() {
|
||||
return System.currentTimeMillis()
|
||||
- smtpLastSendTime >= 60000L;
|
||||
}
|
||||
|
||||
public void recordSmtpSendTime() {
|
||||
smtpLastSendTime = System.currentTimeMillis();
|
||||
smtpLastSendTime =
|
||||
System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// ==================== 文件读写 ====================
|
||||
|
||||
private void loadMap(File file,
|
||||
Map<String, String> map) {
|
||||
try (BufferedReader r =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(
|
||||
file),
|
||||
StandardCharsets
|
||||
.UTF_8))) {
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()
|
||||
|| line.startsWith("#"))
|
||||
continue;
|
||||
int eq = line.indexOf('=');
|
||||
if (eq > 0) {
|
||||
map.put(
|
||||
line.substring(0, eq)
|
||||
.trim(),
|
||||
line.substring(eq + 1)
|
||||
.trim());
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println(
|
||||
"[Sdf1_login] 读取失败: "
|
||||
+ file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLines(File file,
|
||||
List<String> lines) {
|
||||
try (BufferedWriter w =
|
||||
new BufferedWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(
|
||||
file),
|
||||
StandardCharsets
|
||||
.UTF_8))) {
|
||||
for (String l : lines) {
|
||||
w.write(l);
|
||||
w.newLine();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println(
|
||||
"[Sdf1_login] 写入失败: "
|
||||
+ file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private double parseDouble(
|
||||
String s, double def) {
|
||||
try {
|
||||
return Double.parseDouble(s.trim());
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
private int parseInt(String s, int def) {
|
||||
try {
|
||||
return Integer.parseInt(s.trim());
|
||||
} catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ public class GUIManager {
|
||||
public GUIManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void openMain(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 27, T_MAIN);
|
||||
@@ -42,17 +41,21 @@ public class GUIManager {
|
||||
g.setItem(14, mkItem(Material.EMERALD,
|
||||
"§d§l积分商城"));
|
||||
g.setItem(16, mkItem(Material.CHEST,
|
||||
"§e§l新人礼包"));
|
||||
g.setItem(18, mkItem(Material.BOOK,
|
||||
"§e§l任务中心"));
|
||||
g.setItem(18, mkItem(Material.EMERALD_BLOCK,
|
||||
"§a§l每日签到",
|
||||
"§7签到获取积分和惊喜奖励"));
|
||||
g.setItem(20, mkItem(Material.BOOK,
|
||||
"§6工单系统",
|
||||
"§7提交bug、求助、举报"));
|
||||
if (p.isOp() || isAdmin(p)) {
|
||||
g.setItem(20, mkItem(Material.CHEST,
|
||||
g.setItem(22, mkItem(Material.CHEST,
|
||||
"§6垃圾回收站",
|
||||
"§7查看和取回清理的物品"));
|
||||
}
|
||||
if (isAdmin(p)) {
|
||||
g.setItem(22, mkItem(Material.REDSTONE_BLOCK,
|
||||
g.setItem(24, mkItem(
|
||||
Material.REDSTONE_BLOCK,
|
||||
"§c§l管理员面板"));
|
||||
}
|
||||
p.openInventory(g);
|
||||
@@ -107,6 +110,70 @@ public class GUIManager {
|
||||
}
|
||||
|
||||
// 在 openGiftStages 方法之前添加:
|
||||
public void openGiftStages(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 27, T_GIFT_STAGES);
|
||||
fillBg(g);
|
||||
|
||||
QuestTracker qt =
|
||||
plugin.getQuestTracker();
|
||||
if (qt == null) {
|
||||
g.setItem(22, mkItem(
|
||||
Material.BARRIER,
|
||||
"§7任务系统未加载"));
|
||||
p.openInventory(g);
|
||||
return;
|
||||
}
|
||||
|
||||
List<QuestTracker.QuestFile> quests =
|
||||
qt.getQuests("新人任务");
|
||||
String playerName = p.getName();
|
||||
|
||||
int max = quests.size();
|
||||
for (int i = 0;
|
||||
i < max && i < 9; i++) {
|
||||
QuestTracker.QuestFile qf =
|
||||
quests.get(i);
|
||||
boolean completed =
|
||||
qt.isStageCompleted(
|
||||
playerName, qf);
|
||||
boolean claimed =
|
||||
qt.hasClaimed(
|
||||
playerName, qf);
|
||||
|
||||
Material mat;
|
||||
String status;
|
||||
if (completed && claimed) {
|
||||
mat = Material.LIME_WOOL;
|
||||
status = "§a已领取";
|
||||
} else if (completed) {
|
||||
mat = Material.YELLOW_WOOL;
|
||||
status = "§e可领取";
|
||||
} else {
|
||||
mat = Material.RED_WOOL;
|
||||
status = "§c未达标";
|
||||
}
|
||||
|
||||
List<String> lore =
|
||||
new ArrayList<>();
|
||||
lore.add("§7" + status);
|
||||
for (String cond :
|
||||
qf.conditions) {
|
||||
String progress =
|
||||
qt.getProgress(
|
||||
playerName, cond);
|
||||
lore.add("§7" + progress);
|
||||
}
|
||||
|
||||
g.setItem(10 + i, mkItem(mat,
|
||||
"§e" + qf.displayName,
|
||||
lore.toArray(
|
||||
new String[0])));
|
||||
}
|
||||
g.setItem(26, mkItem(Material.ARROW,
|
||||
"§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务列表(带真实完成状态)
|
||||
@@ -408,72 +475,27 @@ public class GUIManager {
|
||||
g.setItem(10, mkItem(Material.BOOK,
|
||||
"§e§l新人任务",
|
||||
"§7完成新手阶段获取奖励"));
|
||||
g.setItem(12, mkItem(Material.COMPASS,
|
||||
g.setItem(12, mkItem(Material.MAP,
|
||||
"§a§l群系打卡",
|
||||
"§7在不同群系打卡完成任务",
|
||||
"§7打卡范围: 32格去重"));
|
||||
g.setItem(14, mkItem(Material.COMPASS,
|
||||
"§b§l主线任务",
|
||||
"§7查看主线任务列表"));
|
||||
g.setItem(14, mkItem(Material.PAPER,
|
||||
g.setItem(16, mkItem(Material.PAPER,
|
||||
"§a§l支线任务",
|
||||
"§7查看支线任务列表"));
|
||||
g.setItem(16, mkItem(Material.EMERALD,
|
||||
"§d§l每日签到",
|
||||
"§7签到获取积分"));
|
||||
g.setItem(22, mkItem(Material.ARROW, "§7返回"));
|
||||
g.setItem(18, mkItem(Material.BOOK,
|
||||
"§6工单系统",
|
||||
"§7提交bug、求助、举报"));
|
||||
g.setItem(20, mkItem(Material.EMERALD,
|
||||
"§a§l每日签到",
|
||||
"§7签到获取积分和奖励"));
|
||||
g.setItem(22, mkItem(Material.ARROW,
|
||||
"§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
public void openGiftStages(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 27, T_GIFT_STAGES);
|
||||
fillBg(g);
|
||||
|
||||
QuestTracker qt = plugin.getQuestTracker();
|
||||
if (qt == null) {
|
||||
g.setItem(22, mkItem(Material.BARRIER,
|
||||
"§7任务系统未加载"));
|
||||
p.openInventory(g);
|
||||
return;
|
||||
}
|
||||
|
||||
List<QuestTracker.QuestFile> quests =
|
||||
qt.getQuests("新人任务");
|
||||
String playerName = p.getName();
|
||||
|
||||
int max = quests.size();
|
||||
for (int i = 0; i < max && i < 9; i++) {
|
||||
QuestTracker.QuestFile qf = quests.get(i);
|
||||
boolean completed =
|
||||
qt.isStageCompleted(playerName, qf);
|
||||
boolean claimed =
|
||||
qt.hasClaimed(playerName, qf);
|
||||
|
||||
Material mat;
|
||||
String status;
|
||||
if (completed && claimed) {
|
||||
mat = Material.LIME_WOOL;
|
||||
status = "§a已领取";
|
||||
} else if (completed) {
|
||||
mat = Material.YELLOW_WOOL;
|
||||
status = "§e可领取";
|
||||
} else {
|
||||
mat = Material.RED_WOOL;
|
||||
status = "§c未达标";
|
||||
}
|
||||
|
||||
// 显示进度
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("§7" + status);
|
||||
for (String cond : qf.conditions) {
|
||||
String progress =
|
||||
qt.getProgress(playerName, cond);
|
||||
lore.add("§7" + progress);
|
||||
}
|
||||
|
||||
g.setItem(10 + i, mkItem(mat,
|
||||
"§e" + qf.displayName,
|
||||
lore.toArray(new String[0])));
|
||||
}
|
||||
g.setItem(26, mkItem(Material.ARROW, "§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
|
||||
public void openTaskList(Player p, String folderName) {
|
||||
@@ -552,6 +574,7 @@ public class GUIManager {
|
||||
slot++;
|
||||
}
|
||||
|
||||
|
||||
g.setItem(53, mkItem(Material.ARROW, "§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
@@ -230,27 +230,26 @@ public class GarbageManager {
|
||||
}
|
||||
|
||||
public void saveItem(ItemStack stack) {
|
||||
{
|
||||
try {
|
||||
String data =
|
||||
stackToBase64(stack);
|
||||
if (data.isEmpty()) return;
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO items "
|
||||
+ "(item_data, amount, "
|
||||
+ "round, create_time) "
|
||||
+ "VALUES (?,?,?,?)");
|
||||
ps.setString(1, data);
|
||||
ps.setInt(2, stack.getAmount());
|
||||
ps.setInt(3, currentRound);
|
||||
ps.setLong(4,
|
||||
System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}}
|
||||
try {
|
||||
String data = stackToBase64(stack);
|
||||
if (data.isEmpty()) return;
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO items "
|
||||
+ "(item_data, amount, "
|
||||
+ "round, create_time) "
|
||||
+ "VALUES (?,?,?,?)");
|
||||
ps.setString(1, data);
|
||||
ps.setInt(2, stack.getAmount());
|
||||
ps.setInt(3, currentRound);
|
||||
ps.setLong(4,
|
||||
System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int deleteOldItems(int beforeRound) {
|
||||
try {
|
||||
@@ -344,11 +343,9 @@ public class GarbageManager {
|
||||
}
|
||||
|
||||
public void openRecyclePage(Player p, int page) {
|
||||
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 54, "§6§l垃圾回收站");
|
||||
|
||||
// 获取全部存储物品
|
||||
List<Map<String, Object>> allItems =
|
||||
getStoredItems(999);
|
||||
int total = allItems.size();
|
||||
@@ -381,38 +378,6 @@ public class GarbageManager {
|
||||
.get("amount")).intValue();
|
||||
ItemStack stack = base64ToStack(data);
|
||||
if (stack != null) {
|
||||
try {
|
||||
byte[] bytes =
|
||||
stack.serializeAsBytes();
|
||||
String b64 = java.util.Base64
|
||||
.getEncoder()
|
||||
.encodeToString(bytes);
|
||||
int len = b64.length();
|
||||
String f = len > 15
|
||||
? b64.substring(10, 15) : "短";
|
||||
String m = len >= 5
|
||||
? b64.substring(len / 2,
|
||||
Math.min(len / 2 + 5, len))
|
||||
: "短";
|
||||
String b = len > 10
|
||||
? b64.substring(len - 10, len - 5)
|
||||
: "短";
|
||||
plugin.getLogger().info(
|
||||
"[垃圾站加载] ID#" + id
|
||||
+ " " + stack.getType()
|
||||
+ " x" + amount
|
||||
+ " base64[" + len + "]"
|
||||
+ " 前=" + f
|
||||
+ " 中=" + m
|
||||
+ " 后=" + b);
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().warning(
|
||||
"[垃圾站加载] 序列化异常: "
|
||||
+ ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (stack != null) {
|
||||
stack.setAmount(amount);
|
||||
ItemMeta im =
|
||||
stack.getItemMeta();
|
||||
@@ -429,10 +394,7 @@ public class GarbageManager {
|
||||
slot++;
|
||||
}
|
||||
|
||||
// 45-48 格:额外投放区(空位)
|
||||
// 52-53 格:额外投放区(空位)
|
||||
|
||||
// 49 格:翻页-上一页
|
||||
// 49 格:上一页
|
||||
if (page > 0) {
|
||||
g.setItem(49, mk(Material.ARROW,
|
||||
"§e上一页 §7("
|
||||
@@ -451,9 +413,8 @@ public class GarbageManager {
|
||||
"§7当前页: §f" + (page + 1)
|
||||
+ "/" + totalPages));
|
||||
|
||||
// 51 格:翻页-下一页
|
||||
boolean canNext = total >= pageSize
|
||||
&& page < totalPages - 1;
|
||||
// 51 格:下一页
|
||||
boolean canNext = page < totalPages - 1;
|
||||
if (canNext) {
|
||||
g.setItem(51, mk(Material.ARROW,
|
||||
"§e下一页 §7("
|
||||
@@ -463,13 +424,15 @@ public class GarbageManager {
|
||||
g.setItem(51, mk(Material.ARROW,
|
||||
"§7已是最后一页"));
|
||||
}
|
||||
// 52格:返回按钮
|
||||
|
||||
// 52 格:返回
|
||||
g.setItem(52, mk(Material.ARROW,
|
||||
"§7返回",
|
||||
"§7返回主菜单"));
|
||||
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
public int getRecyclePage(String name) {
|
||||
return recyclePages
|
||||
.getOrDefault(name, 0);
|
||||
|
||||
@@ -22,7 +22,7 @@ public class GiftManager {
|
||||
public GiftManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.giftFolder = new File(
|
||||
plugin.getDataFolder(), "新人礼包");
|
||||
plugin.getDataFolder(), "任务面板");
|
||||
}
|
||||
|
||||
public void loadStages() {
|
||||
|
||||
+984
-368
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
@@ -178,20 +179,111 @@ public class QuestTracker {
|
||||
BIOME_CATEGORY.put("minecraft:warped_forest", "下界");
|
||||
BIOME_CATEGORY.put("minecraft:soul_sand_valley", "下界");
|
||||
BIOME_CATEGORY.put("minecraft:basalt_deltas", "下界");
|
||||
// 岩岸/沙滩
|
||||
BIOME_CATEGORY.put("minecraft:stony_shore",
|
||||
"石岸");
|
||||
// 海岸
|
||||
BIOME_CATEGORY.put("minecraft:mushroom_fields",
|
||||
"蘑菇岛");
|
||||
// 苍白之园
|
||||
BIOME_CATEGORY.put(
|
||||
"minecraft:pale_garden", "森林");
|
||||
// 末地小型岛屿
|
||||
BIOME_CATEGORY.put(
|
||||
"minecraft:small_end_islands", "末地");
|
||||
// 主世界结构归类
|
||||
BIOME_CATEGORY.put("minecraft:village", "平原");
|
||||
BIOME_CATEGORY.put(
|
||||
"minecraft:desert_pyramid", "沙漠");
|
||||
BIOME_CATEGORY.put(
|
||||
"minecraft:jungle_pyramid", "森林");
|
||||
BIOME_CATEGORY.put(
|
||||
"minecraft:swamp_hut", "沼泽");
|
||||
|
||||
// 末地群系
|
||||
BIOME_CATEGORY.put("minecraft:the_end", "末地");
|
||||
BIOME_CATEGORY.put("minecraft:the_void", "末地");
|
||||
BIOME_CATEGORY.put("minecraft:end_highlands", "末地");
|
||||
BIOME_CATEGORY.put("minecraft:end_midlands", "末地");
|
||||
BIOME_CATEGORY.put("minecraft:end_barrens", "末地");
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 所有群系大类列表
|
||||
private static final String[] ALL_CATEGORIES =
|
||||
{"海洋", "平原", "森林", "沙漠",
|
||||
"恶地", "雪原", "沙砾", "沼泽",
|
||||
"河流", "蘑菇岛", "地下",
|
||||
"下界", "末地"};
|
||||
private static final String[]
|
||||
ALL_CATEGORIES = {
|
||||
"海洋", "平原", "森林", "沙漠",
|
||||
"恶地", "雪原", "沙砾", "沼泽",
|
||||
"河流", "石岸", "蘑菇岛", "地下",
|
||||
"下界", "末地"
|
||||
};
|
||||
|
||||
// ===== 结构映射 =====
|
||||
private static final Map<String, String>
|
||||
STRUCTURE_CATEGORY =
|
||||
new HashMap<>();
|
||||
static {
|
||||
// 主世界结构
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:village", "村庄");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:desert_pyramid",
|
||||
"沙漠");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:jungle_pyramid",
|
||||
"森林");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:swamp_hut", "沼泽");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:igloo", "雪原");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:mansion", "森林");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:pillager_outpost",
|
||||
"平原");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:mineshaft", "地下");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:stronghold", "地下");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:ancient_city",
|
||||
"地下");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:trail_ruins",
|
||||
"平原");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:trial_chambers",
|
||||
"地下");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:ocean_ruin", "海洋");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:shipwreck", "海洋");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:buried_treasure",
|
||||
"海洋");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:ruined_portal",
|
||||
"平原");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:fossil", "平原");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:ancient_city",
|
||||
"地下");
|
||||
// 下界结构
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:nether_fortress",
|
||||
"下界");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:bastion_remnant",
|
||||
"下界");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:nether_fossil",
|
||||
"下界");
|
||||
STRUCTURE_CATEGORY.put(
|
||||
"minecraft:basalt_pillars",
|
||||
"下界");
|
||||
}
|
||||
|
||||
// 所有群系分类的条件关键词映射
|
||||
private static final Map<String, String>
|
||||
@@ -214,6 +306,10 @@ public class QuestTracker {
|
||||
CATEGORY_KEYWORDS.put("地狱", "下界");
|
||||
CATEGORY_KEYWORDS.put("下届", "下界");
|
||||
CATEGORY_KEYWORDS.put("主岛", "末地");
|
||||
CATEGORY_KEYWORDS.put("石岸", "石岸");
|
||||
CATEGORY_KEYWORDS.put("沙滩", "石岸");
|
||||
CATEGORY_KEYWORDS.put("海岸", "石岸");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,25 +320,53 @@ public class QuestTracker {
|
||||
*/
|
||||
public String mapBiomeCategory(
|
||||
String rawKey, String worldName) {
|
||||
// 下界维度优先用世界名
|
||||
// 优先用世界名判断维度
|
||||
if (worldName != null) {
|
||||
String w = worldName.toLowerCase();
|
||||
String w =
|
||||
worldName.toLowerCase();
|
||||
if (w.contains("nether"))
|
||||
return "下界";
|
||||
if (w.contains("end"))
|
||||
return "末地";
|
||||
}
|
||||
|
||||
// 用群系映射表
|
||||
if (rawKey != null) {
|
||||
String lower =
|
||||
rawKey.toLowerCase();
|
||||
// 带 minecraft: 前缀的直接查
|
||||
String cat =
|
||||
BIOME_CATEGORY.get(
|
||||
rawKey.toLowerCase());
|
||||
if (cat != null) return cat;
|
||||
BIOME_CATEGORY.get(lower);
|
||||
if (cat != null)
|
||||
return cat;
|
||||
|
||||
// 不带前缀的尝试补全
|
||||
String prefixed =
|
||||
"minecraft:" + lower;
|
||||
cat = BIOME_CATEGORY.get(prefixed);
|
||||
if (cat != null)
|
||||
return cat;
|
||||
|
||||
// 模糊匹配群系名
|
||||
for (Map.Entry<String, String> entry
|
||||
: BIOME_CATEGORY.entrySet()) {
|
||||
if (entry.getKey()
|
||||
.contains(lower)
|
||||
|| lower.contains(
|
||||
entry.getKey()
|
||||
.replace(
|
||||
"minecraft:",
|
||||
""))) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "主世界";
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ========== 解析为区块 ==========
|
||||
|
||||
private List<Section> parseIntoSections(
|
||||
@@ -1611,22 +1735,68 @@ public class QuestTracker {
|
||||
+ ": §e" + cur
|
||||
+ "/" + needed;
|
||||
}
|
||||
if (key.contains("村民")
|
||||
&& key.contains("交易")) {
|
||||
int needed = parseNumberFromText(key);
|
||||
if (needed <= 0) needed = 1;
|
||||
int cur = getCounter(
|
||||
player, "VILLAGER_TRADE");
|
||||
return "§7村民交易: §e"
|
||||
+ cur + "/" + needed;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ===== 击杀泛解析(支持中文列表) =====
|
||||
// 匹配 "击杀10只骷髅、僵尸、苦力怕" 等
|
||||
if (key.contains("击杀") && key.contains("只")) {
|
||||
int needed = parseNumberFromText(key);
|
||||
if (needed <= 0) needed = 1;
|
||||
|
||||
String mobPart = key;
|
||||
java.util.regex.Matcher killM =
|
||||
java.util.regex.Pattern
|
||||
.compile("击杀\\d+只(.+)")
|
||||
.matcher(key);
|
||||
if (killM.find()) {
|
||||
mobPart = killM.group(1).trim();
|
||||
}
|
||||
|
||||
String[] mobNames = mobPart
|
||||
.split("[、,,,]+");
|
||||
int totalKills = 0;
|
||||
StringBuilder names = new StringBuilder();
|
||||
for (String name : mobNames) {
|
||||
String trimmed = name.trim();
|
||||
if (trimmed.isEmpty()) continue;
|
||||
String entityType =
|
||||
chineseMobToEntity(trimmed);
|
||||
if (!entityType.isEmpty()) {
|
||||
totalKills += getCounter(
|
||||
player,
|
||||
"KILL:" + entityType);
|
||||
if (names.length() > 0)
|
||||
names.append("/");
|
||||
names.append(trimmed);
|
||||
}
|
||||
}
|
||||
return "§7击杀" + names + ": §e"
|
||||
+ totalKills + "/" + needed;
|
||||
}
|
||||
|
||||
// 单一KILL:xxx:N 格式(原有逻辑保留)
|
||||
if (key.startsWith("KILL:")) {
|
||||
String[] parts = key.split(":");
|
||||
if (parts.length >= 3) {
|
||||
int needed =
|
||||
parseInt(parts[2]);
|
||||
int cur = getCounter(
|
||||
player,
|
||||
"KILL:" + parts[1]);
|
||||
return "§7杀 " + parts[1]
|
||||
+ ": §e" + cur
|
||||
+ "/" + needed;
|
||||
String[] p = key.split(":");
|
||||
if (p.length >= 3) {
|
||||
int needed = parseInt(p[2]);
|
||||
int cur = getCounter(player,
|
||||
"KILL:" + p[1]);
|
||||
return "§7击杀" + p[1] + ": §e"
|
||||
+ cur + "/" + needed;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (key.startsWith("KILL_PLAYER")) {
|
||||
String[] parts = key.split(":");
|
||||
int needed = parts.length >= 2
|
||||
@@ -1739,10 +1909,219 @@ public class QuestTracker {
|
||||
+ done + "/"
|
||||
+ quests.size();
|
||||
}
|
||||
// ===== 泛解析:击杀类进度 =====
|
||||
if (key.contains("击杀") || key.contains("猎杀")
|
||||
|| key.contains("消灭")) {
|
||||
int needed = parseNumberFromText(key);
|
||||
if (needed <= 0) needed = 1;
|
||||
List<String> mobTypes = extractMobTypes(key);
|
||||
if (!mobTypes.isEmpty()) {
|
||||
int total = 0;
|
||||
StringBuilder names =
|
||||
new StringBuilder();
|
||||
for (int i = 0;
|
||||
i < mobTypes.size(); i++) {
|
||||
total += getCounter(player,
|
||||
"KILL:" + mobTypes.get(i));
|
||||
if (i > 0) names.append("/");
|
||||
names.append(reverseMobName(
|
||||
mobTypes.get(i)));
|
||||
}
|
||||
return "§7击杀" + names + ": §e"
|
||||
+ total + "/" + needed;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 泛解析:村民交易进度 =====
|
||||
if (key.contains("交易")
|
||||
&& (key.contains("村民")
|
||||
|| key.contains("与村民"))) {
|
||||
int needed = parseNumberFromText(key);
|
||||
if (needed <= 0) needed = 1;
|
||||
int cur = getCounter(player, "TRADE");
|
||||
return "§7村民交易: §e"
|
||||
+ cur + "/" + needed;
|
||||
}
|
||||
// ===== 泛解析:采集/挖矿进度 =====
|
||||
if (key.contains("采集")
|
||||
|| key.contains("挖")) {
|
||||
int needed =
|
||||
parseNumberFromText(key);
|
||||
if (needed <= 0) needed = 1;
|
||||
List<String> matTypes =
|
||||
extractMaterialTypes(key);
|
||||
if (!matTypes.isEmpty()) {
|
||||
int total = 0;
|
||||
StringBuilder names =
|
||||
new StringBuilder();
|
||||
for (int i = 0;
|
||||
i < matTypes.size(); i++) {
|
||||
total += getCounter(
|
||||
player,
|
||||
"MINE:" + matTypes
|
||||
.get(i));
|
||||
if (i > 0)
|
||||
names.append("/");
|
||||
names.append(
|
||||
translateMaterialName(
|
||||
matTypes
|
||||
.get(i)));
|
||||
}
|
||||
return "§7采集" + names
|
||||
+ ": §e" + total
|
||||
+ "/" + needed;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 泛解析:合成进度 =====
|
||||
if (key.contains("合成") || key.contains("制作")) {
|
||||
int needed = parseNumberFromText(key);
|
||||
if (needed <= 0) needed = 1;
|
||||
int cur = getCounter(player, "CRAFT");
|
||||
return "§7合成: §e" + cur + "/" + needed;
|
||||
}
|
||||
|
||||
// ===== 泛解析:钓鱼进度 =====
|
||||
if (key.contains("钓鱼") || key.contains("钓")) {
|
||||
int needed = parseNumberFromText(key);
|
||||
if (needed <= 0) needed = 1;
|
||||
int cur = getCounter(player, "FISH");
|
||||
return "§7钓鱼: §e" + cur + "/" + needed;
|
||||
}
|
||||
|
||||
return "§7" + key;
|
||||
}
|
||||
private String translateMaterialName(
|
||||
String name) {
|
||||
switch (name) {
|
||||
case "COAL_ORE":
|
||||
return "煤矿石";
|
||||
case "DEEPSLATE_COAL_ORE":
|
||||
return "深层煤矿石";
|
||||
case "IRON_ORE":
|
||||
return "铁矿石";
|
||||
case "DEEPSLATE_IRON_ORE":
|
||||
return "深层铁矿石";
|
||||
case "COPPER_ORE":
|
||||
return "铜矿石";
|
||||
case "DEEPSLATE_COPPER_ORE":
|
||||
return "深层铜矿石";
|
||||
case "GOLD_ORE":
|
||||
return "金矿石";
|
||||
case "DEEPSLATE_GOLD_ORE":
|
||||
return "深层金矿石";
|
||||
case "DIAMOND_ORE":
|
||||
return "钻石矿石";
|
||||
case "DEEPSLATE_DIAMOND_ORE":
|
||||
return "深层钻石矿石";
|
||||
case "EMERALD_ORE":
|
||||
return "绿宝石矿石";
|
||||
case "DEEPSLATE_EMERALD_ORE":
|
||||
return "深层绿宝石矿石";
|
||||
case "LAPIS_ORE":
|
||||
return "青金石矿石";
|
||||
case "DEEPSLATE_LAPIS_ORE":
|
||||
return "深层青金石矿石";
|
||||
case "REDSTONE_ORE":
|
||||
return "红石矿石";
|
||||
case "DEEPSLATE_REDSTONE_ORE":
|
||||
return "深层红石矿石";
|
||||
case "NETHER_GOLD_ORE":
|
||||
return "下界金矿石";
|
||||
case "NETHER_QUARTZ_ORE":
|
||||
return "下界石英矿石";
|
||||
case "ANCIENT_DEBRIS":
|
||||
return "远古残骸";
|
||||
case "COAL":
|
||||
return "煤炭";
|
||||
case "RAW_IRON":
|
||||
return "粗铁";
|
||||
case "RAW_GOLD":
|
||||
return "粗金";
|
||||
case "RAW_COPPER":
|
||||
return "粗铜";
|
||||
case "DIAMOND":
|
||||
return "钻石";
|
||||
case "EMERALD":
|
||||
return "绿宝石";
|
||||
case "LAPIS_LAZULI":
|
||||
return "青金石";
|
||||
case "REDSTONE":
|
||||
return "红石";
|
||||
case "QUARTZ":
|
||||
return "石英";
|
||||
default:
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文物种名 → Bukkit EntityType 名
|
||||
* 支持击杀泛解析的模糊匹配
|
||||
*/
|
||||
private String chineseMobToEntity(String cn) {
|
||||
String n = cn.toLowerCase().trim();
|
||||
|
||||
// 直接别名表
|
||||
Map<String, String> mobMap = new HashMap<>();
|
||||
mobMap.put("骷髅", "SKELETON");
|
||||
mobMap.put("僵尸", "ZOMBIE");
|
||||
mobMap.put("苦力怕", "CREEPER");
|
||||
mobMap.put("蜘蛛", "SPIDER");
|
||||
mobMap.put("洞穴蜘蛛", "CAVE_SPIDER");
|
||||
mobMap.put("小白", "SKELETON");
|
||||
mobMap.put("末影人", "ENDERMAN");
|
||||
mobMap.put("enderman", "ENDERMAN");
|
||||
mobMap.put("猪灵", "PIGLIN");
|
||||
mobMap.put("猪灵蛮兵", "PIGLIN_BRUTE");
|
||||
mobMap.put("恶魂", "GHAST");
|
||||
mobMap.put("凋灵骷髅", "WITHER_SKELETON");
|
||||
mobMap.put("烈焰人", "BLAZE");
|
||||
mobMap.put("史莱姆", "SLIME");
|
||||
mobMap.put("岩浆怪", "MAGMA_CUBE");
|
||||
mobMap.put("女巫", "WITCH");
|
||||
mobMap.put("蠹虫", "SILVERFISH");
|
||||
mobMap.put("潜影贝", "SHULKER");
|
||||
mobMap.put("守卫者", "GUARDIAN");
|
||||
mobMap.put("远古守卫者", "ELDER_GUARDIAN");
|
||||
mobMap.put("溺尸", "DROWNED");
|
||||
mobMap.put("僵尸村民", "ZOMBIE_VILLAGER");
|
||||
mobMap.put("尸壳", "HUSK");
|
||||
mobMap.put("流浪者", "STRAY");
|
||||
mobMap.put("幻翼", "PHANTOM");
|
||||
mobMap.put("掠夺者", "PILLAGER");
|
||||
mobMap.put("卫道士", "VINDICATOR");
|
||||
mobMap.put("唤魔者", "EVOKER");
|
||||
mobMap.put("恼鬼", "VEX");
|
||||
mobMap.put("劫掠兽", "RAVAGER");
|
||||
mobMap.put("蜘蛛骑士", "SPIDER_JOCKEY");
|
||||
mobMap.put("凋灵", "WITHER");
|
||||
mobMap.put("末影龙", "ENDER_DRAGON");
|
||||
|
||||
// 精确匹配
|
||||
if (mobMap.containsKey(n)) {
|
||||
return mobMap.get(n);
|
||||
}
|
||||
|
||||
// 模糊匹配(包含关系)
|
||||
for (Map.Entry<String, String> en
|
||||
: mobMap.entrySet()) {
|
||||
if (n.contains(en.getKey())
|
||||
|| en.getKey().contains(n)) {
|
||||
return en.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试直接用英文名
|
||||
try {
|
||||
return org.bukkit.entity.EntityType
|
||||
.valueOf(n.toUpperCase()
|
||||
.replace(" ", "_"))
|
||||
.name();
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> checkInBiome(
|
||||
String player,
|
||||
@@ -1776,6 +2155,23 @@ public class QuestTracker {
|
||||
String category =
|
||||
mapBiomeCategory(
|
||||
rawBiomeKey, worldName);
|
||||
// 结构检测(覆盖纯生物群系分类)
|
||||
// 地下检测:Y低于表面则归类为地下
|
||||
// 覆盖废弃矿井/试炼密室/要塞/远古城市等
|
||||
try {
|
||||
org.bukkit.World uw =
|
||||
Bukkit.getWorld(worldName);
|
||||
if (uw != null) {
|
||||
int surfaceY = uw
|
||||
.getHighestBlockYAt(
|
||||
(int) Math.floor(x),
|
||||
(int) Math.floor(z));
|
||||
if (y < surfaceY - 8) {
|
||||
category = "地下";
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
|
||||
// 记录总打卡
|
||||
int allCur = getCounter(
|
||||
@@ -1851,6 +2247,344 @@ public class QuestTracker {
|
||||
return matched;
|
||||
}
|
||||
|
||||
/*
|
||||
* 打卡入口:报告坐标 → 32格去重 → 匹配条件 → 记录
|
||||
*/
|
||||
public String checkInAtPosition(
|
||||
String player, String worldName,
|
||||
double x, double y, double z) {
|
||||
|
||||
List<double[]> history =
|
||||
loadCheckInPositions(player);
|
||||
for (double[] pos : history) {
|
||||
double dist = Math.sqrt(
|
||||
Math.pow(x - pos[0], 2)
|
||||
+ Math.pow(y - pos[1], 2)
|
||||
+ Math.pow(z - pos[2], 2));
|
||||
if (dist <= 32) {
|
||||
return "§c距离上次打卡点太近("
|
||||
+ String.format(
|
||||
"%.0f", dist)
|
||||
+ "格)";
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 获取群系和分类 =====
|
||||
String biome = "minecraft:plains";
|
||||
String category = "平原";
|
||||
try {
|
||||
org.bukkit.World w =
|
||||
Bukkit.getWorld(worldName);
|
||||
if (w != null) {
|
||||
org.bukkit.Location loc =
|
||||
new org.bukkit.Location(
|
||||
w,
|
||||
(int) Math.floor(x),
|
||||
(int) Math.floor(y),
|
||||
(int) Math.floor(z));
|
||||
biome = loc.getBlock()
|
||||
.getBiome().getKey()
|
||||
.toString();
|
||||
category = mapBiomeCategory(
|
||||
biome, worldName);
|
||||
|
||||
int surfaceY = w
|
||||
.getHighestBlockYAt(
|
||||
(int) Math.floor(x),
|
||||
(int) Math.floor(z));
|
||||
if (y < surfaceY - 8) {
|
||||
category = "地下";
|
||||
}
|
||||
|
||||
if ("下界".equals(category)
|
||||
|| w.getEnvironment()
|
||||
== org.bukkit.World
|
||||
.Environment.NETHER) {
|
||||
category = "下界";
|
||||
} else if ("末地".equals(category)
|
||||
|| w.getEnvironment()
|
||||
== org.bukkit.World
|
||||
.Environment.THE_END) {
|
||||
category = "末地";
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().warning(
|
||||
"[QuestTracker] 群系读取失败: "
|
||||
+ ex.getMessage());
|
||||
}
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[QuestTracker] 打卡: "
|
||||
+ player
|
||||
+ " world=" + worldName
|
||||
+ " biome=" + biome
|
||||
+ " cat=" + category
|
||||
+ " pos="
|
||||
+ String.format(
|
||||
"%.0f,%.0f,%.0f",
|
||||
x, y, z));
|
||||
|
||||
saveCheckInPosition(
|
||||
player, x, y, z);
|
||||
recordCheckIn(
|
||||
player, biome, category,
|
||||
worldName, x, y, z);
|
||||
List<String> matched =
|
||||
checkInBiome(
|
||||
player, biome,
|
||||
worldName, x, y, z);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("§a打卡成功!群系: §e")
|
||||
.append(category);
|
||||
if (!matched.isEmpty()) {
|
||||
sb.append("\n§a完成任务条件:");
|
||||
for (String m : matched) {
|
||||
String qn =
|
||||
findQuestName(m);
|
||||
sb.append("\n§7• ");
|
||||
if (qn != null) {
|
||||
sb.append("§e[")
|
||||
.append(qn)
|
||||
.append("§7] §f");
|
||||
}
|
||||
sb.append(m);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据条件文本查找所属任务名
|
||||
*/
|
||||
private String findQuestName(
|
||||
String condition) {
|
||||
for (String cat : Arrays.asList(
|
||||
"新人任务",
|
||||
"主线任务",
|
||||
"支线任务")) {
|
||||
for (QuestFile qf :
|
||||
getQuests(cat)) {
|
||||
if (qf.conditions.contains(
|
||||
condition)) {
|
||||
return qf.displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 加载玩家所有历史打卡坐标
|
||||
* 格式: x1,y1,z1;x2,y2,z2;...
|
||||
*/
|
||||
private List<double[]> loadCheckInPositions(
|
||||
String player) {
|
||||
List<double[]> list = new ArrayList<>();
|
||||
Object val = storage.getCondition(
|
||||
player, "checkin_all_pos");
|
||||
if (val == null) return list;
|
||||
String data = val.toString();
|
||||
if (data.isEmpty()) return list;
|
||||
String[] records = data.split(";");
|
||||
for (String r : records) {
|
||||
String[] parts = r.split(",");
|
||||
if (parts.length >= 3) {
|
||||
try {
|
||||
list.add(new double[]{
|
||||
Double.parseDouble(parts[0]),
|
||||
Double.parseDouble(parts[1]),
|
||||
Double.parseDouble(parts[2])});
|
||||
} catch (NumberFormatException e) {
|
||||
// 跳过损坏数据
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存打卡坐标(追加到已有记录)
|
||||
*/
|
||||
private void saveCheckInPosition(
|
||||
String player, double x,
|
||||
double y, double z) {
|
||||
List<double[]> list =
|
||||
loadCheckInPositions(player);
|
||||
list.add(new double[]{x, y, z});
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (double[] pos : list) {
|
||||
if (sb.length() > 0) sb.append(";");
|
||||
sb.append(pos[0]).append(",")
|
||||
.append(pos[1]).append(",")
|
||||
.append(pos[2]);
|
||||
}
|
||||
storage.setConditionRaw(
|
||||
player,
|
||||
"checkin_all_pos",
|
||||
sb.toString());
|
||||
}
|
||||
// ========== 中文生物名 → EntityType 映射 ==========
|
||||
private static final Map<String, String>
|
||||
MOB_NAME_MAP = new HashMap<>();
|
||||
static {
|
||||
MOB_NAME_MAP.put("洞穴蜘蛛", "CAVE_SPIDER");
|
||||
MOB_NAME_MAP.put("骷髅", "SKELETON");
|
||||
MOB_NAME_MAP.put("僵尸", "ZOMBIE");
|
||||
MOB_NAME_MAP.put("苦力怕", "CREEPER");
|
||||
MOB_NAME_MAP.put("蜘蛛", "SPIDER");
|
||||
MOB_NAME_MAP.put("末影人", "ENDERMAN");
|
||||
MOB_NAME_MAP.put("女巫", "WITCH");
|
||||
MOB_NAME_MAP.put("烈焰人", "BLAZE");
|
||||
MOB_NAME_MAP.put("僵尸猪灵",
|
||||
"ZOMBIFIED_PIGLIN");
|
||||
MOB_NAME_MAP.put("猪灵", "PIGLIN");
|
||||
MOB_NAME_MAP.put("恶魂", "GHAST");
|
||||
MOB_NAME_MAP.put("凋灵骷髅",
|
||||
"WITHER_SKELETON");
|
||||
MOB_NAME_MAP.put("史莱姆", "SLIME");
|
||||
MOB_NAME_MAP.put("岩浆怪", "MAGMA_CUBE");
|
||||
MOB_NAME_MAP.put("溺尸", "DROWNED");
|
||||
MOB_NAME_MAP.put("尸壳", "HUSK");
|
||||
MOB_NAME_MAP.put("流髑", "STRAY");
|
||||
MOB_NAME_MAP.put("幻翼", "PHANTOM");
|
||||
MOB_NAME_MAP.put("守卫者", "GUARDIAN");
|
||||
MOB_NAME_MAP.put("远古守卫者",
|
||||
"ELDER_GUARDIAN");
|
||||
MOB_NAME_MAP.put("掠夺者", "PILLAGER");
|
||||
MOB_NAME_MAP.put("卫道士", "VINDICATOR");
|
||||
MOB_NAME_MAP.put("唤魔者", "EVOKER");
|
||||
MOB_NAME_MAP.put("恼鬼", "VEX");
|
||||
MOB_NAME_MAP.put("劫掠兽", "RAVAGER");
|
||||
MOB_NAME_MAP.put("潜影贝", "SHULKER");
|
||||
MOB_NAME_MAP.put("末影螨", "ENDERMITE");
|
||||
MOB_NAME_MAP.put("凋灵", "WITHER");
|
||||
MOB_NAME_MAP.put("末影龙", "ENDER_DRAGON");
|
||||
MOB_NAME_MAP.put("监守者", "WARDEN");
|
||||
MOB_NAME_MAP.put("嗅探兽", "SNIFFER");
|
||||
MOB_NAME_MAP.put("僵尸村民",
|
||||
"ZOMBIE_VILLAGER");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从条件文本中提取所有匹配的生物类型
|
||||
* 按名称长度降序匹配,避免短名误匹配
|
||||
*/
|
||||
private List<String> extractMobTypes(
|
||||
String text) {
|
||||
List<String> types = new ArrayList<>();
|
||||
List<Map.Entry<String, String>> sorted =
|
||||
new ArrayList<>(
|
||||
MOB_NAME_MAP.entrySet());
|
||||
sorted.sort((a, b) ->
|
||||
b.getKey().length()
|
||||
- a.getKey().length());
|
||||
String remaining = text;
|
||||
for (Map.Entry<String, String> entry
|
||||
: sorted) {
|
||||
if (remaining.contains(
|
||||
entry.getKey())) {
|
||||
if (!types.contains(
|
||||
entry.getValue())) {
|
||||
types.add(entry.getValue());
|
||||
}
|
||||
remaining = remaining.replace(
|
||||
entry.getKey(), "");
|
||||
}
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
/**
|
||||
* EntityType → 中文名
|
||||
*/
|
||||
private String reverseMobName(
|
||||
String entityType) {
|
||||
for (Map.Entry<String, String> entry
|
||||
: MOB_NAME_MAP.entrySet()) {
|
||||
if (entry.getValue()
|
||||
.equals(entityType)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return entityType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从条件文本中提取矿物名
|
||||
* "采集任意矿物(煤炭除外)1组"
|
||||
* → [COAL_ORE, IRON_ORE, GOLD_ORE, ...]
|
||||
*/
|
||||
private List<String> extractMaterialTypes(
|
||||
String text) {
|
||||
List<String> types = new ArrayList<>();
|
||||
// 排除列表
|
||||
List<String> excludes =
|
||||
new ArrayList<>();
|
||||
if (text.contains("煤炭除外")) {
|
||||
excludes.add("COAL");
|
||||
excludes.add("COAL_ORE");
|
||||
excludes.add("DEEPSLATE_COAL_ORE");
|
||||
}
|
||||
// 通配符:"矿物" "矿石"
|
||||
if (text.contains("矿物")
|
||||
|| text.contains("矿石")) {
|
||||
String[] ores = {
|
||||
"COAL_ORE",
|
||||
"DEEPSLATE_COAL_ORE",
|
||||
"IRON_ORE",
|
||||
"DEEPSLATE_IRON_ORE",
|
||||
"COPPER_ORE",
|
||||
"DEEPSLATE_COPPER_ORE",
|
||||
"GOLD_ORE",
|
||||
"DEEPSLATE_GOLD_ORE",
|
||||
"DIAMOND_ORE",
|
||||
"DEEPSLATE_DIAMOND_ORE",
|
||||
"EMERALD_ORE",
|
||||
"DEEPSLATE_EMERALD_ORE",
|
||||
"LAPIS_ORE",
|
||||
"DEEPSLATE_LAPIS_ORE",
|
||||
"REDSTONE_ORE",
|
||||
"DEEPSLATE_REDSTONE_ORE",
|
||||
"NETHER_GOLD_ORE",
|
||||
"NETHER_QUARTZ_ORE",
|
||||
"ANCIENT_DEBRIS"
|
||||
};
|
||||
for (String ore : ores) {
|
||||
if (!excludes.contains(ore)) {
|
||||
types.add(ore);
|
||||
}
|
||||
}
|
||||
return types;
|
||||
}
|
||||
// 单个矿物名
|
||||
if (text.contains("煤炭"))
|
||||
types.add("COAL");
|
||||
if (text.contains("铁矿")
|
||||
|| text.contains("铁锭"))
|
||||
types.add("RAW_IRON");
|
||||
if (text.contains("金矿")
|
||||
|| text.contains("金锭"))
|
||||
types.add("RAW_GOLD");
|
||||
if (text.contains("钻石"))
|
||||
types.add("DIAMOND");
|
||||
if (text.contains("绿宝石"))
|
||||
types.add("EMERALD");
|
||||
if (text.contains("青金石"))
|
||||
types.add("LAPIS_LAZULI");
|
||||
if (text.contains("红石"))
|
||||
types.add("REDSTONE");
|
||||
if (text.contains("铜"))
|
||||
types.add("RAW_COPPER");
|
||||
if (text.contains("下界石英")
|
||||
|| text.contains("石英"))
|
||||
types.add("QUARTZ");
|
||||
return types;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从条件文本中提取目标群系分类
|
||||
@@ -2108,6 +2842,16 @@ public class QuestTracker {
|
||||
}
|
||||
}
|
||||
|
||||
if (key.contains("村民")
|
||||
&& key.contains("交易")) {
|
||||
int needed = parseNumberFromText(key);
|
||||
if (needed <= 0) needed = 1;
|
||||
return getCounter(
|
||||
player, "VILLAGER_TRADE")
|
||||
>= needed;
|
||||
}
|
||||
|
||||
|
||||
if (key.startsWith("KILL_PLAYER")) {
|
||||
String[] p = key.split(":");
|
||||
int n = p.length >= 2
|
||||
@@ -2200,6 +2944,63 @@ public class QuestTracker {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// ===== 泛解析:击杀类中文条件 =====
|
||||
if (key.contains("击杀") || key.contains("猎杀")
|
||||
|| key.contains("消灭")) {
|
||||
int needed = parseNumberFromText(key);
|
||||
if (needed <= 0) needed = 1;
|
||||
List<String> mobTypes = extractMobTypes(key);
|
||||
if (!mobTypes.isEmpty()) {
|
||||
int total = 0;
|
||||
for (String mobType : mobTypes) {
|
||||
total += getCounter(player,
|
||||
"KILL:" + mobType);
|
||||
}
|
||||
return total >= needed;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 泛解析:村民交易条件 =====
|
||||
if (key.contains("交易")
|
||||
&& (key.contains("村民")
|
||||
|| key.contains("与村民"))) {
|
||||
int needed = parseNumberFromText(key);
|
||||
if (needed <= 0) needed = 1;
|
||||
return getCounter(player, "TRADE")
|
||||
>= needed;
|
||||
}
|
||||
|
||||
// ===== 泛解析:采集/挖矿类条件 =====
|
||||
if (key.contains("采集") || key.contains("挖")) {
|
||||
int needed = parseNumberFromText(key);
|
||||
if (needed <= 0) needed = 1;
|
||||
List<String> matTypes =
|
||||
extractMaterialTypes(key);
|
||||
if (!matTypes.isEmpty()) {
|
||||
int total = 0;
|
||||
for (String mat : matTypes) {
|
||||
total += getCounter(player,
|
||||
"MINE:" + mat);
|
||||
}
|
||||
return total >= needed;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 泛解析:合成类条件 =====
|
||||
if (key.contains("合成") || key.contains("制作")) {
|
||||
int needed = parseNumberFromText(key);
|
||||
if (needed <= 0) needed = 1;
|
||||
return getCounter(player, "CRAFT")
|
||||
>= needed;
|
||||
}
|
||||
|
||||
// ===== 泛解析:钓鱼类条件 =====
|
||||
if (key.contains("钓鱼") || key.contains("钓")) {
|
||||
int needed = parseNumberFromText(key);
|
||||
if (needed <= 0) needed = 1;
|
||||
return getCounter(player, "FISH")
|
||||
>= needed;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -2271,6 +3072,12 @@ public class QuestTracker {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void onVillagerTrade(String player) {
|
||||
String key = "VILLAGER_TRADE";
|
||||
int cur = getCounter(player, key);
|
||||
setCounter(player, key, cur + 1);
|
||||
}
|
||||
|
||||
private long getCounterLong(
|
||||
String player, String key) {
|
||||
Object val = storage.getCondition(
|
||||
|
||||
@@ -4,30 +4,34 @@ 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 org.bukkit.event.player
|
||||
.PlayerResourcePackStatusEvent;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.concurrent
|
||||
.ConcurrentHashMap;
|
||||
|
||||
public class RadioDownloadListener
|
||||
implements Listener {
|
||||
|
||||
private final Main plugin;
|
||||
private final Logger log;
|
||||
private final Map<UUID, Integer> retryCount =
|
||||
new ConcurrentHashMap<>();
|
||||
private final Map<UUID, Integer> totalAttempts =
|
||||
new ConcurrentHashMap<>();
|
||||
private static final int MAX_RETRY = 3;
|
||||
// 每位玩家最多总共尝试5次,防死循环
|
||||
private static final int MAX_TOTAL = 5;
|
||||
private final java.util.logging.Logger log;
|
||||
|
||||
private final ConcurrentHashMap<UUID, Integer>
|
||||
failCount = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<UUID, Boolean>
|
||||
blocked = new ConcurrentHashMap<>();
|
||||
|
||||
private static final int MAX_FAILED = 3;
|
||||
|
||||
public RadioDownloadListener(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.log = plugin.getLogger();
|
||||
}
|
||||
|
||||
public void onPlayerQuit(UUID uuid) {
|
||||
failCount.remove(uuid);
|
||||
blocked.remove(uuid);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@@ -35,84 +39,142 @@ public class RadioDownloadListener
|
||||
PlayerResourcePackStatusEvent e) {
|
||||
Player p = e.getPlayer();
|
||||
UUID uuid = p.getUniqueId();
|
||||
String status = e.getStatus().name();
|
||||
String status =
|
||||
e.getStatus().name();
|
||||
|
||||
log.info("[Radio] " + p.getName()
|
||||
+ " 资源包: " + status);
|
||||
log.info("[Radio] === 资源包状态 ===");
|
||||
log.info("[Radio] 玩家: "
|
||||
+ p.getName());
|
||||
log.info("[Radio] UUID: " + uuid);
|
||||
log.info("[Radio] 状态: "
|
||||
+ status);
|
||||
log.info("[Radio] 原始状态枚举: "
|
||||
+ e.getStatus().getClass()
|
||||
.getSimpleName()
|
||||
+ "."
|
||||
+ e.getStatus().name());
|
||||
|
||||
if (blocked.containsKey(uuid)) {
|
||||
log.info("[Radio] 已熔断,跳过: "
|
||||
+ p.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case "SUCCESS":
|
||||
log.info("[Radio] " + p.getName()
|
||||
+ " 资源包加载成功");
|
||||
retryCount.remove(uuid);
|
||||
totalAttempts.remove(uuid);
|
||||
log.info("[Radio] "
|
||||
+ p.getName()
|
||||
+ " ✓ 资源包加载成功");
|
||||
failCount.remove(uuid);
|
||||
blocked.remove(uuid);
|
||||
break;
|
||||
|
||||
case "FAILED_DOWNLOAD":
|
||||
int retry = retryCount
|
||||
.getOrDefault(uuid, 0) + 1;
|
||||
int total = totalAttempts
|
||||
.getOrDefault(uuid, 0) + 1;
|
||||
retryCount.put(uuid, retry);
|
||||
totalAttempts.put(uuid, total);
|
||||
|
||||
log.warning("[Radio] " + p.getName()
|
||||
+ " 下载失败 (重试"
|
||||
+ retry + "/" + MAX_RETRY
|
||||
+ ", 总计" + total
|
||||
+ "/" + MAX_TOTAL + ")");
|
||||
|
||||
if (total >= MAX_TOTAL) {
|
||||
p.sendMessage(
|
||||
"§7[Radio] §f资源包加载失败,跳过");
|
||||
retryCount.remove(uuid);
|
||||
totalAttempts.remove(uuid);
|
||||
break;
|
||||
}
|
||||
|
||||
if (retry > MAX_RETRY) {
|
||||
retryCount.put(uuid, 0);
|
||||
Bukkit.getScheduler()
|
||||
.runTaskLater(plugin,
|
||||
() -> {
|
||||
if (p.isOnline()
|
||||
&& plugin
|
||||
.radio
|
||||
!= null) {
|
||||
plugin.radio
|
||||
.sendResourcePack(
|
||||
p);
|
||||
}
|
||||
}, 100L);
|
||||
break;
|
||||
}
|
||||
|
||||
p.sendMessage(
|
||||
"§e[Radio] §f下载失败,重试中...");
|
||||
Bukkit.getScheduler()
|
||||
.runTaskLater(plugin,
|
||||
() -> {
|
||||
if (p.isOnline()
|
||||
&& plugin.radio
|
||||
!= null) {
|
||||
plugin.radio
|
||||
.sendResourcePack(
|
||||
p);
|
||||
}
|
||||
}, 60L);
|
||||
case "ACCEPTED":
|
||||
log.info("[Radio] "
|
||||
+ p.getName()
|
||||
+ " 客户端已接受,"
|
||||
+ "等待下载...");
|
||||
break;
|
||||
|
||||
case "DECLINED":
|
||||
log.info("[Radio] " + p.getName()
|
||||
+ " 拒绝下载");
|
||||
retryCount.remove(uuid);
|
||||
totalAttempts.remove(uuid);
|
||||
log.warning("[Radio] "
|
||||
+ p.getName()
|
||||
+ " ✗ 拒绝了资源包");
|
||||
log.warning("[Radio] "
|
||||
+ "可能原因: "
|
||||
+ "1.客户端设置禁用资源包"
|
||||
+ " 2.已缓存拒绝记录"
|
||||
+ " 3.SSL证书不匹配");
|
||||
handleFailure(p, uuid);
|
||||
break;
|
||||
|
||||
case "FAILED_DOWNLOAD":
|
||||
log.warning("[Radio] "
|
||||
+ p.getName()
|
||||
+ " ✗ 下载失败");
|
||||
log.warning("[Radio] "
|
||||
+ "可能原因: "
|
||||
+ "1.URL无法访问"
|
||||
+ " 2.SSL证书错误"
|
||||
+ " 3.网络超时"
|
||||
+ " 4.文件不存在(404)");
|
||||
handleFailure(p, uuid);
|
||||
break;
|
||||
|
||||
default:
|
||||
retryCount.remove(uuid);
|
||||
totalAttempts.remove(uuid);
|
||||
log.info("[Radio] "
|
||||
+ p.getName()
|
||||
+ " 未知状态: "
|
||||
+ status);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFailure(
|
||||
Player p, UUID uuid) {
|
||||
int failNum = failCount
|
||||
.getOrDefault(uuid, 0) + 1;
|
||||
failCount.put(uuid, failNum);
|
||||
|
||||
log.warning("[Radio] "
|
||||
+ p.getName()
|
||||
+ " 失败次数: "
|
||||
+ failNum + "/" + MAX_FAILED);
|
||||
|
||||
if (failNum >= MAX_FAILED) {
|
||||
blocked.put(uuid, true);
|
||||
log.severe("[Radio] "
|
||||
+ p.getName()
|
||||
+ " 已达最大失败次数,"
|
||||
+ "触发踢出");
|
||||
Bukkit.getScheduler()
|
||||
.runTaskLater(plugin,
|
||||
() -> {
|
||||
Player kp =
|
||||
Bukkit
|
||||
.getPlayer(
|
||||
uuid);
|
||||
if (kp != null
|
||||
&& kp
|
||||
.isOnline()) {
|
||||
kp.kickPlayer(
|
||||
"§c资源包"
|
||||
+ "加载失败"
|
||||
+ "("
|
||||
+ MAX_FAILED
|
||||
+ "次),"
|
||||
+ "请联系管理员");
|
||||
}
|
||||
}, 1L);
|
||||
return;
|
||||
}
|
||||
|
||||
p.sendMessage(
|
||||
"§e§l[Radio] 下载失败,"
|
||||
+ "重试... ("
|
||||
+ failNum + "/"
|
||||
+ MAX_FAILED + ")");
|
||||
|
||||
// 5秒后重试
|
||||
Bukkit.getScheduler()
|
||||
.runTaskLater(plugin,
|
||||
() -> {
|
||||
if (p.isOnline()
|
||||
&& plugin
|
||||
.radio
|
||||
!= null) {
|
||||
plugin.getLogger()
|
||||
.info(
|
||||
"[Radio] "
|
||||
+ "第"
|
||||
+ failNum
|
||||
+ "次重试: "
|
||||
+ p
|
||||
.getName());
|
||||
plugin.radio
|
||||
.sendResourcePack(
|
||||
p);
|
||||
}
|
||||
}, 100L);
|
||||
}
|
||||
}
|
||||
@@ -328,7 +328,7 @@ public class RadioManager {
|
||||
("{\n \"pack\": {\n"
|
||||
+ " \"pack_format\": 54,\n"
|
||||
+ " \"description\":"
|
||||
+ " \"Sdf1 Radio\"\n }\n}")
|
||||
+ " \"为保证服务器正常运行,请勿拒绝资源包!\"\n }\n}")
|
||||
.getBytes("UTF-8"));
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
@@ -412,15 +412,24 @@ public class RadioManager {
|
||||
*/
|
||||
public void sendResourcePack(Player p) {
|
||||
UUID uuid = p.getUniqueId();
|
||||
if (pendingPack.contains(uuid)) return;
|
||||
if (pendingPack.contains(uuid)) {
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 跳过(已等待): "
|
||||
+ p.getName());
|
||||
return;
|
||||
}
|
||||
pendingPack.add(uuid);
|
||||
Bukkit.getScheduler().runTaskLater(
|
||||
plugin, () -> {
|
||||
Bukkit.getScheduler()
|
||||
.runTaskLater(plugin, () -> {
|
||||
pendingPack.remove(uuid);
|
||||
}, 200L);
|
||||
|
||||
String url = getResourcePackUrl();
|
||||
if (url == null || url.isEmpty()) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] URL为空,"
|
||||
+ "跳过发送: "
|
||||
+ p.getName());
|
||||
pendingPack.remove(uuid);
|
||||
return;
|
||||
}
|
||||
@@ -429,98 +438,85 @@ public class RadioManager {
|
||||
plugin.getDataFolder(),
|
||||
"radiopack.zip");
|
||||
if (!zip.exists()) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] radiopack.zip不存在,"
|
||||
+ "跳过: " + p.getName());
|
||||
pendingPack.remove(uuid);
|
||||
return;
|
||||
}
|
||||
|
||||
// 异步:下载远程zip → 算hash → 比对本地hash
|
||||
final Player fp = p;
|
||||
final String finalUrl = url + "?t="
|
||||
+ System.currentTimeMillis();
|
||||
try {
|
||||
byte[] data =
|
||||
Files.readAllBytes(zip.toPath());
|
||||
String hash = sha1(data);
|
||||
long fileSize = zip.length();
|
||||
|
||||
Bukkit.getScheduler()
|
||||
.runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
// 1. 本地hash
|
||||
byte[] localData =
|
||||
Files.readAllBytes(
|
||||
zip.toPath());
|
||||
String localHash =
|
||||
sha1(localData);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] === 发送资源包 ===");
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 玩家: " + p.getName());
|
||||
plugin.getLogger().info(
|
||||
"[Radio] URL: " + url);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] Hash: " + hash);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 文件大小: "
|
||||
+ fileSize + " bytes ("
|
||||
+ String.format("%.1f",
|
||||
fileSize / 1024.0)
|
||||
+ " KB)");
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 客户端版本: "
|
||||
+ p.getProtocolVersion());
|
||||
|
||||
// 2. 远程hash
|
||||
byte[] remoteData =
|
||||
downloadBytes(finalUrl);
|
||||
String remoteHash =
|
||||
sha1(remoteData);
|
||||
sendPackWithHash(p, url, hash);
|
||||
|
||||
// 3. 比对
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 本地hash: "
|
||||
+ localHash);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 远程hash: "
|
||||
+ remoteHash);
|
||||
|
||||
if (!localHash.equals(remoteHash)) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] hash不一致,"
|
||||
+ "跳过发送资源包");
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] 请更新远程"
|
||||
+ "radiopack.zip");
|
||||
Bukkit.getScheduler()
|
||||
.runTask(plugin, () -> {
|
||||
fp.sendMessage(
|
||||
"§7[Radio] §f资源包"
|
||||
+ "版本不一致,"
|
||||
+ "跳过安装");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. hash一致,发送给客户端
|
||||
Bukkit.getScheduler()
|
||||
.runTask(plugin, () -> {
|
||||
sendPackWithHash(
|
||||
fp, finalUrl,
|
||||
remoteHash);
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] 校验失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe(
|
||||
"[Radio] 发送异常: "
|
||||
+ p.getName());
|
||||
e.printStackTrace();
|
||||
pendingPack.remove(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPackWithHash(
|
||||
Player p, String url, String hash) {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
|
||||
// Paper 1.21: setResourcePack(UUID, String, String, Component, boolean)
|
||||
|
||||
private void sendPackWithHash(
|
||||
Player p,
|
||||
String url,
|
||||
String hash) {
|
||||
UUID uuid = UUID.fromString(
|
||||
"a1b2c3d4-e5f6-7890-abcd-"
|
||||
+ "ef1234567890");
|
||||
|
||||
try {
|
||||
Object component = net.kyori.adventure
|
||||
.text.Component.text("Sdf1 Radio");
|
||||
Object component =
|
||||
net.kyori.adventure.text
|
||||
.Component.text(
|
||||
"§d§l服务器需要自定义资源包,请您点击'继续'由客户端自动下载即可");
|
||||
p.getClass().getMethod(
|
||||
"setResourcePack",
|
||||
UUID.class,
|
||||
String.class,
|
||||
String.class,
|
||||
net.kyori.adventure.text
|
||||
net.kyori.adventure
|
||||
.text
|
||||
.Component.class,
|
||||
boolean.class)
|
||||
.invoke(p, uuid, url,
|
||||
hash, component, false);
|
||||
.invoke(
|
||||
p, uuid, url,
|
||||
hash, component,
|
||||
true);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 发送成功: "
|
||||
+ p.getName());
|
||||
+ p.getName()
|
||||
+ " url="
|
||||
+ url);
|
||||
return;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// 降级:旧版 String 签名
|
||||
try {
|
||||
p.getClass().getMethod(
|
||||
"setResourcePack",
|
||||
@@ -529,8 +525,11 @@ public class RadioManager {
|
||||
String.class,
|
||||
String.class,
|
||||
boolean.class)
|
||||
.invoke(p, uuid, url,
|
||||
hash, "Sdf1 Radio", false);
|
||||
.invoke(
|
||||
p, uuid, url,
|
||||
hash,
|
||||
"Sdf1 Radio",
|
||||
true);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 旧版签名成功: "
|
||||
+ p.getName());
|
||||
@@ -538,14 +537,15 @@ public class RadioManager {
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// 降级3参
|
||||
try {
|
||||
p.getClass().getMethod(
|
||||
"setResourcePack",
|
||||
UUID.class,
|
||||
String.class,
|
||||
String.class)
|
||||
.invoke(p, uuid, url, hash);
|
||||
.invoke(
|
||||
p, uuid, url,
|
||||
hash);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 3参签名成功: "
|
||||
+ p.getName());
|
||||
@@ -692,19 +692,86 @@ public class RadioManager {
|
||||
}
|
||||
|
||||
public String getResourcePackUrl() {
|
||||
// 有外部配置 → 随机生成URL
|
||||
if (externalUrl != null
|
||||
&& !externalUrl.isEmpty()) {
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 使用外部地址: "
|
||||
+ externalUrl);
|
||||
return externalUrl;
|
||||
&& !externalUrl.trim().isEmpty()) {
|
||||
return generateRandomUrl(externalUrl);
|
||||
}
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] 未配置外部地址,"
|
||||
+ "跳过资源包");
|
||||
return null;
|
||||
// 无配置 → 用内置HTTP
|
||||
int port = 8080;
|
||||
if (plugin.getConfig2() != null) {
|
||||
port = plugin.getConfig2().httpPort;
|
||||
}
|
||||
return "http://127.0.0.1:"
|
||||
+ port + "/radiopack.zip";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从配置的域名列表中随机选一个
|
||||
* 生成随机、带时间戳的完整资源包URL
|
||||
*
|
||||
* 规则:
|
||||
* *.example.com
|
||||
* → 随机子域+时间戳
|
||||
* 例: a3f7k2.example.com/pack.zip?t=xxx&v=b9c1e4
|
||||
*
|
||||
* cdn.example.com (无*)
|
||||
* → 直接使用+时间戳
|
||||
* 例: cdn.example.com/pack.zip?t=xxx
|
||||
*/
|
||||
private String generateRandomUrl(
|
||||
String raw) {
|
||||
String[] parts = raw.split("\\|");
|
||||
String pick =
|
||||
parts[(int)(Math.random()
|
||||
* parts.length)].trim();
|
||||
|
||||
// 去掉协议 https:// http://
|
||||
String cleaned = pick
|
||||
.replaceFirst("^https?://", "");
|
||||
// 去掉路径 /path/a?v=1
|
||||
if (cleaned.contains("/")) {
|
||||
cleaned = cleaned.substring(
|
||||
0, cleaned.indexOf('/'));
|
||||
}
|
||||
|
||||
// 随机子域
|
||||
String hex = Long.toHexString(
|
||||
Double.doubleToLongBits(
|
||||
Math.random()));
|
||||
String rand = hex.length() > 6
|
||||
? hex.substring(2, 8) : hex;
|
||||
rand = rand.replaceAll(
|
||||
"[^a-z0-9]", "x");
|
||||
|
||||
String host;
|
||||
if (cleaned.startsWith("*")) {
|
||||
host = rand
|
||||
+ cleaned.substring(1);
|
||||
} else {
|
||||
host = cleaned;
|
||||
}
|
||||
|
||||
String ts = String.valueOf(
|
||||
System.currentTimeMillis());
|
||||
String token = Integer.toHexString(
|
||||
(int)(Math.random()
|
||||
* 0xFFFFFF));
|
||||
|
||||
String url = "https://" + host
|
||||
+ "/radiopack.zip"
|
||||
+ "?t=" + ts
|
||||
+ "&v=" + token;
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 生成URL: "
|
||||
+ pick + " → " + url);
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ========== 全服广播 ==========
|
||||
public void startMainRadio() {
|
||||
stopMainRadio();
|
||||
@@ -847,6 +914,12 @@ public class RadioManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 在 init() 或 registerEvents 中保存引用:
|
||||
// this.listener = new RadioDownloadListener(plugin);
|
||||
private RadioDownloadListener radioListener;
|
||||
|
||||
|
||||
|
||||
// ========== 重载 ==========
|
||||
public void reload() {
|
||||
stopMainRadio();
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class AFKManager {
|
||||
|
||||
private final Map<UUID, Long> lastAction = new HashMap<>();
|
||||
private final Map<UUID, Integer> lastStage = new HashMap<>();
|
||||
private final ConfigManager config;
|
||||
private final JavaPlugin plugin;
|
||||
|
||||
public AFKManager(ConfigManager config, JavaPlugin plugin) {
|
||||
this.config = config;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void startCheck() {
|
||||
new BukkitRunnable() {
|
||||
public void run() { tick(); }
|
||||
}.runTaskTimer(plugin, 20L, 20L);
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
if (!config.afkEnabled) return;
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
String name = p.getName();
|
||||
if (!isLoggedIn(name)) continue;
|
||||
if (config.getAfkWhitelist().contains(name)) continue;
|
||||
|
||||
UUID uuid = p.getUniqueId();
|
||||
if (!lastAction.containsKey(uuid)) {
|
||||
lastAction.put(uuid, now);
|
||||
lastStage.put(uuid, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
long last = lastAction.get(uuid);
|
||||
long idle = (now - last) / 1000;
|
||||
int timeout = config.afkTimeout;
|
||||
long remain = timeout - idle;
|
||||
|
||||
// 已超时:踢出
|
||||
if (remain <= 0) {
|
||||
p.kickPlayer("§c§l你已挂机 " + fmtSec(idle) + ",被踢出");
|
||||
lastAction.remove(uuid);
|
||||
lastStage.remove(uuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
int stage = lastStage.getOrDefault(uuid, 0);
|
||||
|
||||
// === 阶段3:最后10秒 ===
|
||||
// 刷屏倒计时 + 大标题 + 每秒音效
|
||||
if (remain <= 10) {
|
||||
if (stage < 3) {
|
||||
lastStage.put(uuid, 3);
|
||||
}
|
||||
p.sendTitle("§e§l挂机了 " + fmtSec(idle),
|
||||
"§c还剩 " + remain + " 秒踢出",
|
||||
0, 22, 3);
|
||||
playSound(p);
|
||||
continue;
|
||||
}
|
||||
|
||||
// === 阶段2:1分钟~10秒 ===
|
||||
// 持续音效(每秒)+ 标题,聊天仅1次
|
||||
if (remain <= 60) {
|
||||
if (stage < 2) {
|
||||
lastStage.put(uuid, 2);
|
||||
p.sendMessage("§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" + fmtSec(idle) + "§f,还剩 §c" + fmtSec(remain) + "§f踢出");
|
||||
}
|
||||
p.sendTitle("§e§l挂机了 " + fmtSec(idle),
|
||||
"§c还剩 " + fmtSec(remain) + " 踢出",
|
||||
5, 25, 5);
|
||||
playSound(p);
|
||||
continue;
|
||||
}
|
||||
|
||||
// === 阶段1:3分钟提醒 ===
|
||||
// 仅1次:聊天 + 标题 + 3次音效循环
|
||||
if (remain <= 180 && stage < 1) {
|
||||
lastStage.put(uuid, 1);
|
||||
p.sendMessage("§e§l⚠ 挂机警告 ⚠ §f你已挂机 §c" + fmtSec(idle) + "§f,还剩 §c" + fmtSec(remain) + "§f踢出");
|
||||
p.sendTitle("§e§l挂机了 " + fmtSec(idle),
|
||||
"§f还剩 " + fmtSec(remain) + " 踢出",
|
||||
10, 60, 20);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final int delay = i * 10;
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
if (p.isOnline()) playSound(p);
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
// 超过3分钟:重置阶段
|
||||
if (remain > 180) {
|
||||
lastStage.remove(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void playSound(Player p) {
|
||||
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_CHIME, 1.0f, 0.79f);
|
||||
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0f, 0.79f);
|
||||
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 0.79f);
|
||||
}
|
||||
|
||||
public void recordAction(UUID uuid) {
|
||||
lastAction.put(uuid, System.currentTimeMillis());
|
||||
lastStage.put(uuid, 0);
|
||||
}
|
||||
|
||||
public void remove(UUID uuid) {
|
||||
lastAction.remove(uuid);
|
||||
lastStage.remove(uuid);
|
||||
}
|
||||
|
||||
private boolean isLoggedIn(String name) {
|
||||
return ((Main) plugin).getLoggedIn().contains(name);
|
||||
}
|
||||
|
||||
private String fmtSec(long sec) {
|
||||
if (sec < 60) return sec + "秒";
|
||||
long m = sec / 60;
|
||||
long s = sec % 60;
|
||||
return m + "分" + (s > 0 ? s + "秒" : "");
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class AccountRequestManager {
|
||||
|
||||
public enum ApprovalMode { AUTO, MANUAL, HYBRID }
|
||||
|
||||
public static class Request {
|
||||
public final int id;
|
||||
public final String requester;
|
||||
public final String target;
|
||||
public final String ip;
|
||||
public final long time;
|
||||
public String status;
|
||||
public String denyReason;
|
||||
|
||||
public Request(int id, String requester, String target, String ip) {
|
||||
this.id = id;
|
||||
this.requester = requester;
|
||||
this.target = target;
|
||||
this.ip = ip;
|
||||
this.time = System.currentTimeMillis();
|
||||
this.status = "PENDING";
|
||||
this.denyReason = "";
|
||||
}
|
||||
}
|
||||
|
||||
private final Main plugin;
|
||||
private final Map<Integer, Request> requests = new ConcurrentHashMap<>();
|
||||
private final Map<String, Integer> pendingByPlayer = new ConcurrentHashMap<>();
|
||||
private int nextId = 1;
|
||||
private ApprovalMode mode = ApprovalMode.MANUAL;
|
||||
private int autoDelayMinutes = 60;
|
||||
|
||||
public AccountRequestManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public ApprovalMode getMode() { return mode; }
|
||||
|
||||
public void setMode(String m) {
|
||||
try { this.mode = ApprovalMode.valueOf(m.toUpperCase()); }
|
||||
catch (Exception e) { this.mode = ApprovalMode.MANUAL; }
|
||||
}
|
||||
|
||||
public void setAutoDelayMinutes(int min) {
|
||||
this.autoDelayMinutes = min;
|
||||
}
|
||||
|
||||
public Request createRequest(String requester, String target, String ip) {
|
||||
int id = nextId++;
|
||||
Request req = new Request(id, requester, target, ip);
|
||||
requests.put(id, req);
|
||||
pendingByPlayer.put(requester, id);
|
||||
|
||||
if (mode == ApprovalMode.AUTO) {
|
||||
scheduleAutoApprove(id);
|
||||
} else if (mode == ApprovalMode.HYBRID) {
|
||||
String targetIp = (String) plugin.getDb().getField(target, "ip_address");
|
||||
if (ip != null && ip.equals(targetIp)) {
|
||||
scheduleAutoApprove(id);
|
||||
}
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
private void scheduleAutoApprove(int id) {
|
||||
long delayTicks = (long) autoDelayMinutes * 60L * 20L;
|
||||
Request req = requests.get(id);
|
||||
if (req == null) return;
|
||||
String requesterName = req.requester;
|
||||
String targetName = req.target;
|
||||
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 将在 " + autoDelayMinutes + " 分钟后自动通过");
|
||||
|
||||
Bukkit.getScheduler().runTaskLater(plugin, () -> {
|
||||
Request r = requests.get(id);
|
||||
if (r != null && "PENDING".equals(r.status)) {
|
||||
approveRequest(id);
|
||||
plugin.getLogger().info("[Sdf1_login] OA#" + id + " 自动通过");
|
||||
Player requester = Bukkit.getPlayer(requesterName);
|
||||
if (requester != null && requester.isOnline()) {
|
||||
requester.sendMessage("§a[Sdf1_login] §f您的删除请求 #" + id + "(§e" + targetName + "§a)已自动通过!");
|
||||
}
|
||||
}
|
||||
}, delayTicks);
|
||||
}
|
||||
|
||||
public boolean approveRequest(int id) {
|
||||
Request req = requests.get(id);
|
||||
if (req == null || !"PENDING".equals(req.status)) return false;
|
||||
req.status = "APPROVED";
|
||||
plugin.getDb().deleteUser(req.target);
|
||||
pendingByPlayer.remove(req.requester);
|
||||
// 清背包+传送出生点+踢出
|
||||
Player target = Bukkit.getPlayer(req.target);
|
||||
if (target != null && target.isOnline()) {
|
||||
target.getInventory().clear();
|
||||
target.getInventory().setArmorContents(new org.bukkit.inventory.ItemStack[4]);
|
||||
target.getInventory().setExtraContents(new org.bukkit.inventory.ItemStack[1]);
|
||||
target.teleport(target.getWorld().getSpawnLocation());
|
||||
target.kickPlayer("§c§l你的账号已被管理员删除,请重新注册");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public boolean denyRequest(int id, String reason) {
|
||||
Request req = requests.get(id);
|
||||
if (req == null || !"PENDING".equals(req.status)) return false;
|
||||
req.status = "DENIED";
|
||||
req.denyReason = reason;
|
||||
pendingByPlayer.remove(req.requester);
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<Request> getPendingRequests() {
|
||||
List<Request> list = new ArrayList<>();
|
||||
for (Request r : requests.values()) {
|
||||
if ("PENDING".equals(r.status)) list.add(r);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public Request getRequest(int id) {
|
||||
return requests.get(id);
|
||||
}
|
||||
|
||||
public boolean hasPending(String player) {
|
||||
return pendingByPlayer.containsKey(player);
|
||||
}
|
||||
|
||||
public void cancelPending(String player) {
|
||||
Integer id = pendingByPlayer.remove(player);
|
||||
if (id != null) {
|
||||
Request r = requests.get(id);
|
||||
if (r != null) r.status = "CANCELLED";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,594 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ChatFilterManager {
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final List<String> whitelistUrls =
|
||||
new ArrayList<>();
|
||||
private final Set<String> whitelistPlayers =
|
||||
new TreeSet<>(
|
||||
String.CASE_INSENSITIVE_ORDER);
|
||||
private final Map<Integer, String>
|
||||
punishmentRules =
|
||||
new LinkedHashMap<>();
|
||||
private final Map<Integer, Integer>
|
||||
punishmentDurations =
|
||||
new LinkedHashMap<>();
|
||||
private final Map<String, Integer>
|
||||
violationCount =
|
||||
new ConcurrentHashMap<>();
|
||||
private final Map<String, Long>
|
||||
mutedPlayers =
|
||||
new ConcurrentHashMap<>();
|
||||
private final Map<String, String> messages =
|
||||
new LinkedHashMap<>();
|
||||
private boolean notifyAdmin = true;
|
||||
private boolean notifyAll = false;
|
||||
private int muteDuration = 300;
|
||||
|
||||
private static final Pattern URL_PATTERN =
|
||||
Pattern.compile(
|
||||
"(?i)(?:https?://\\S+)"
|
||||
+ "|(?:(?:[a-zA-Z0-9]"
|
||||
+ "(?:[a-zA-Z0-9\\-]*"
|
||||
+ "[a-zA-Z0-9])?\\.)+"
|
||||
+ "[a-zA-Z]{2,})"
|
||||
+ "(?:/\\S*)?");
|
||||
private static final Pattern DOT_VARIANTS =
|
||||
Pattern.compile(
|
||||
"[\uff0e\u3002\u2025\u2026\u00b7]");
|
||||
private static final Pattern COLOR_CODES =
|
||||
Pattern.compile(
|
||||
"\u00a7[0-9a-fk-orA-FK-OR]");
|
||||
|
||||
private static final Set<String> FILE_EXT;
|
||||
static {
|
||||
Set<String> s = new HashSet<>(
|
||||
Arrays.asList(
|
||||
"txt", "yml", "yaml",
|
||||
"json", "xml", "java",
|
||||
"class", "jar", "png",
|
||||
"jpg", "jpeg", "gif",
|
||||
"bmp", "svg", "ico",
|
||||
"webp", "mp3", "mp4",
|
||||
"avi", "mkv", "wav",
|
||||
"flac", "ogg", "zip",
|
||||
"rar", "7z", "tar",
|
||||
"gz", "cfg", "conf",
|
||||
"ini", "log", "db",
|
||||
"sql", "sh", "bat",
|
||||
"html", "htm", "css",
|
||||
"js", "md", "pdf",
|
||||
"dll", "so", "exe",
|
||||
"bin", "c", "cpp",
|
||||
"py", "rb", "go",
|
||||
"rs", "kt", "lua",
|
||||
"php", "asp", "jsp"));
|
||||
FILE_EXT = Collections.unmodifiableSet(s);
|
||||
}
|
||||
|
||||
public ChatFilterManager(JavaPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// ========== 配置加载 ==========
|
||||
|
||||
public void loadConfig() {
|
||||
whitelistUrls.clear();
|
||||
whitelistPlayers.clear();
|
||||
punishmentRules.clear();
|
||||
punishmentDurations.clear();
|
||||
messages.clear();
|
||||
|
||||
File f = new File(
|
||||
plugin.getDataFolder(), "chat.txt");
|
||||
if (!f.exists()) writeDefaultFile(f);
|
||||
try (BufferedReader r =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(f),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String section = "";
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
if (line.startsWith("\uFEFF"))
|
||||
line = line.substring(1);
|
||||
String t = line.trim();
|
||||
if (t.isEmpty()
|
||||
|| t.startsWith("#"))
|
||||
continue;
|
||||
if (t.equals("白名单")
|
||||
|| t.equals("白名单:")) {
|
||||
section = "whitelist";
|
||||
continue;
|
||||
}
|
||||
if (t.equals("处罚规则")
|
||||
|| t.equals("处罚规则:")) {
|
||||
section = "punishment";
|
||||
continue;
|
||||
}
|
||||
if (t.equals("白名单玩家")
|
||||
|| t.equals("白名单玩家:")) {
|
||||
section = "players";
|
||||
continue;
|
||||
}
|
||||
if (t.contains(":")) {
|
||||
String[] kv = t.split(":", 2);
|
||||
String k = kv[0].trim();
|
||||
String v = kv[1].trim();
|
||||
if (k.equals("通知管理员")) {
|
||||
notifyAdmin = parseBool(v);
|
||||
continue;
|
||||
}
|
||||
if (k.equals("全服通报")) {
|
||||
notifyAll = parseBool(v);
|
||||
continue;
|
||||
}
|
||||
if (k.equals("禁言时长")) {
|
||||
try {
|
||||
muteDuration =
|
||||
Integer.parseInt(v);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
switch (section) {
|
||||
case "whitelist":
|
||||
whitelistUrls.add(
|
||||
t.toLowerCase());
|
||||
break;
|
||||
case "punishment":
|
||||
String[] parts = t.split(":");
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
int cnt =
|
||||
Integer.parseInt(
|
||||
parts[0]
|
||||
.trim());
|
||||
String type =
|
||||
parts[1].trim()
|
||||
.toLowerCase();
|
||||
int dur =
|
||||
parts.length >= 3
|
||||
? Integer.parseInt(
|
||||
parts[2]
|
||||
.trim())
|
||||
: muteDuration;
|
||||
punishmentRules.put(
|
||||
cnt, type);
|
||||
punishmentDurations.put(
|
||||
cnt, dur);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "players":
|
||||
whitelistPlayers.add(t);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe(
|
||||
"[Sdf1_chat] 配置加载失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
|
||||
// 从消息.txt读取chat_开头的消息
|
||||
loadMessages();
|
||||
}
|
||||
|
||||
private void loadMessages() {
|
||||
File msgFile = new File(
|
||||
plugin.getDataFolder(),
|
||||
"消息.txt");
|
||||
if (!msgFile.exists()) return;
|
||||
try (BufferedReader mr =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(
|
||||
msgFile),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String mline;
|
||||
while ((mline = mr.readLine()) != null) {
|
||||
mline = mline.trim();
|
||||
if (mline.isEmpty()
|
||||
|| mline.startsWith("#"))
|
||||
continue;
|
||||
// 支持 = 和 : 两种分隔符
|
||||
int eqIdx = mline.indexOf('=');
|
||||
int coIdx = mline.indexOf(':');
|
||||
int splitIdx = -1;
|
||||
if (eqIdx >= 0 && coIdx >= 0) {
|
||||
splitIdx = Math.min(eqIdx, coIdx);
|
||||
} else if (eqIdx >= 0) {
|
||||
splitIdx = eqIdx;
|
||||
} else if (coIdx >= 0) {
|
||||
splitIdx = coIdx;
|
||||
}
|
||||
if (splitIdx < 0) continue;
|
||||
String k = mline.substring(0,
|
||||
splitIdx).trim();
|
||||
String v = mline.substring(
|
||||
splitIdx + 1).trim();
|
||||
if (k.startsWith("chat_")) {
|
||||
messages.put(k, v);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void writeDefaultFile(File f) {
|
||||
try (PrintWriter pw = new PrintWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(f),
|
||||
StandardCharsets.UTF_8))) {
|
||||
pw.println("# Sdf1_chat 聊天过滤配置");
|
||||
pw.println("通知管理员: true");
|
||||
pw.println("全服通报: false");
|
||||
pw.println("禁言时长: 300");
|
||||
pw.println();
|
||||
pw.println("白名单:");
|
||||
pw.println("*.minecraft.net");
|
||||
pw.println("baidu.com");
|
||||
pw.println("github.com");
|
||||
pw.println();
|
||||
pw.println("处罚规则:");
|
||||
pw.println("1:warn:0");
|
||||
pw.println("2:warn:0");
|
||||
pw.println("3:mute:300");
|
||||
pw.println("5:mute:600");
|
||||
pw.println("8:kick:0");
|
||||
pw.println("10:ban:0");
|
||||
pw.println();
|
||||
pw.println("白名单玩家:");
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public void saveConfig() {
|
||||
File f = new File(
|
||||
plugin.getDataFolder(), "chat.txt");
|
||||
try (PrintWriter pw = new PrintWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(f),
|
||||
StandardCharsets.UTF_8))) {
|
||||
pw.println("# Sdf1_chat 聊天过滤配置");
|
||||
pw.println("通知管理员: " + notifyAdmin);
|
||||
pw.println("全服通报: " + notifyAll);
|
||||
pw.println("禁言时长: " + muteDuration);
|
||||
pw.println();
|
||||
pw.println("白名单:");
|
||||
for (String u : whitelistUrls)
|
||||
pw.println(u);
|
||||
pw.println();
|
||||
pw.println("处罚规则:");
|
||||
for (Map.Entry<Integer, String> entry
|
||||
: punishmentRules.entrySet()) {
|
||||
int dur = punishmentDurations
|
||||
.getOrDefault(entry.getKey(),
|
||||
muteDuration);
|
||||
pw.println(entry.getKey() + ":"
|
||||
+ entry.getValue()
|
||||
+ ":" + dur);
|
||||
}
|
||||
pw.println();
|
||||
pw.println("白名单玩家:");
|
||||
for (String p : whitelistPlayers)
|
||||
pw.println(p);
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().severe(
|
||||
"[Sdf1_chat] 保存失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 消息 ==========
|
||||
|
||||
public String msg(String key,
|
||||
String... args) {
|
||||
String template = messages.getOrDefault(
|
||||
key, key);
|
||||
for (int i = 0; i < args.length - 1;
|
||||
i += 2) {
|
||||
template = template.replace(
|
||||
"{" + args[i] + "}",
|
||||
args[i + 1]);
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
// ========== URL检测 ==========
|
||||
|
||||
private String normalizeMessage(String msg) {
|
||||
msg = COLOR_CODES.matcher(msg)
|
||||
.replaceAll("");
|
||||
msg = DOT_VARIANTS.matcher(msg)
|
||||
.replaceAll(".");
|
||||
return msg;
|
||||
}
|
||||
|
||||
public List<String> extractUrls(
|
||||
String message) {
|
||||
String normalized =
|
||||
normalizeMessage(message);
|
||||
List<String> urls = new ArrayList<>();
|
||||
Matcher m = URL_PATTERN
|
||||
.matcher(normalized);
|
||||
while (m.find()) {
|
||||
String url = m.group().toLowerCase();
|
||||
url = url.replaceAll(
|
||||
"[\\s.,;!?,。;!?、)》」』】"
|
||||
+ "\\]\\[\\((]+$",
|
||||
"");
|
||||
if (!url.isEmpty()) urls.add(url);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
public boolean isWhitelisted(String url) {
|
||||
String clean = url
|
||||
.replaceAll("^https?://", "")
|
||||
.toLowerCase();
|
||||
int colon = clean.indexOf(':');
|
||||
int slash = clean.indexOf('/');
|
||||
if (colon > 0 && (slash < 0
|
||||
|| colon < slash)) {
|
||||
clean = clean.substring(0, colon);
|
||||
}
|
||||
slash = clean.indexOf('/');
|
||||
if (slash > 0) {
|
||||
clean = clean.substring(0, slash);
|
||||
}
|
||||
for (String entry : whitelistUrls) {
|
||||
String e = entry.toLowerCase().trim();
|
||||
if (e.startsWith("*.")) {
|
||||
String suffix = e.substring(1);
|
||||
if (clean.endsWith(suffix)
|
||||
|| clean.equals(
|
||||
suffix.substring(1)))
|
||||
return true;
|
||||
} else {
|
||||
if (clean.equals(e)
|
||||
|| clean.endsWith(
|
||||
"." + e))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isLikelyDomain(String url) {
|
||||
int lastDot = url.lastIndexOf('.');
|
||||
if (lastDot < 0
|
||||
|| lastDot >= url.length() - 1)
|
||||
return false;
|
||||
String ext = url.substring(lastDot + 1)
|
||||
.toLowerCase();
|
||||
return !FILE_EXT.contains(ext);
|
||||
}
|
||||
|
||||
// ========== 玩家操作 ==========
|
||||
|
||||
public boolean isMuted(String name) {
|
||||
Long expiry = mutedPlayers.get(name);
|
||||
if (expiry == null) return false;
|
||||
if (System.currentTimeMillis()
|
||||
>= expiry) {
|
||||
mutedPlayers.remove(name);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public String fmtDuration(int seconds) {
|
||||
if (seconds <= 0) return "0秒";
|
||||
int day = seconds / 86400;
|
||||
int hr = (seconds % 86400) / 3600;
|
||||
int min = (seconds % 3600) / 60;
|
||||
int sec = seconds % 60;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (day > 0) sb.append(day).append("天");
|
||||
if (hr > 0)
|
||||
sb.append(hr).append("小时");
|
||||
if (min > 0)
|
||||
sb.append(min).append("分");
|
||||
if (sec > 0 || sb.length() == 0)
|
||||
sb.append(sec).append("秒");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public void applyPunishment(Player player,
|
||||
int violation) {
|
||||
String type = "warn";
|
||||
int duration = muteDuration;
|
||||
int maxRule = 0;
|
||||
for (Map.Entry<Integer, String> entry
|
||||
: punishmentRules.entrySet()) {
|
||||
if (violation >= entry.getKey()
|
||||
&& entry.getKey() > maxRule) {
|
||||
maxRule = entry.getKey();
|
||||
type = entry.getValue();
|
||||
duration = punishmentDurations
|
||||
.getOrDefault(
|
||||
entry.getKey(),
|
||||
muteDuration);
|
||||
}
|
||||
}
|
||||
if (type.equals("mute")) {
|
||||
if (isMuted(player.getName()))
|
||||
return;
|
||||
mutedPlayers.put(player.getName(),
|
||||
System.currentTimeMillis()
|
||||
+ (long) duration
|
||||
* 1000L);
|
||||
}
|
||||
final String fType = type;
|
||||
final int fDur = duration;
|
||||
final int fV = violation;
|
||||
final String fName = player.getName();
|
||||
Bukkit.getScheduler().runTask(plugin,
|
||||
() -> {
|
||||
Player p = Bukkit.getPlayer(fName);
|
||||
if (p == null) return;
|
||||
switch (fType) {
|
||||
case "warn":
|
||||
// 警告不处罚,只提示
|
||||
break;
|
||||
case "mute":
|
||||
p.sendMessage(
|
||||
msg("chat_muted"));
|
||||
break;
|
||||
case "kick":
|
||||
p.kickPlayer(
|
||||
msg("chat_muted"));
|
||||
break;
|
||||
case "ban":
|
||||
Bukkit.dispatchCommand(
|
||||
Bukkit.getConsoleSender(),
|
||||
"ban " + fName
|
||||
+ " §c多次发送违规链接");
|
||||
break;
|
||||
case "banip":
|
||||
InetSocketAddress addr =
|
||||
p.getAddress();
|
||||
if (addr != null) {
|
||||
String ip = addr
|
||||
.getAddress()
|
||||
.getHostAddress();
|
||||
Bukkit.dispatchCommand(
|
||||
Bukkit.getConsoleSender(),
|
||||
"ban-ip " + ip
|
||||
+ " §c发送违规链接");
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void unmutePlayer(String name) {
|
||||
mutedPlayers.remove(name);
|
||||
}
|
||||
|
||||
public void resetPlayer(String name) {
|
||||
mutedPlayers.remove(name);
|
||||
violationCount.remove(name);
|
||||
}
|
||||
|
||||
public boolean isPlayerWhitelisted(
|
||||
String name) {
|
||||
return whitelistPlayers.contains(name);
|
||||
}
|
||||
|
||||
public boolean isNotifyAdmin() {
|
||||
return notifyAdmin;
|
||||
}
|
||||
|
||||
public boolean isNotifyAll() {
|
||||
return notifyAll;
|
||||
}
|
||||
|
||||
public int getViolationCount(String name) {
|
||||
return violationCount
|
||||
.getOrDefault(name, 0);
|
||||
}
|
||||
|
||||
public void incrementViolation(
|
||||
String name) {
|
||||
violationCount.put(name,
|
||||
getViolationCount(name) + 1);
|
||||
}
|
||||
|
||||
public List<String> getWhitelistUrls() {
|
||||
return whitelistUrls;
|
||||
}
|
||||
|
||||
public Set<String> getWhitelistPlayers() {
|
||||
return whitelistPlayers;
|
||||
}
|
||||
|
||||
public Map<Integer, String>
|
||||
getPunishmentRules() {
|
||||
return punishmentRules;
|
||||
}
|
||||
|
||||
public Map<Integer, Integer>
|
||||
getPunishmentDurations() {
|
||||
return punishmentDurations;
|
||||
}
|
||||
|
||||
public Map<String, Long>
|
||||
getMutedPlayers() {
|
||||
return mutedPlayers;
|
||||
}
|
||||
|
||||
public void setNotifyAdmin(boolean v) {
|
||||
notifyAdmin = v;
|
||||
}
|
||||
|
||||
public void setNotifyAll(boolean v) {
|
||||
notifyAll = v;
|
||||
}
|
||||
|
||||
public void setMuteDuration(int v) {
|
||||
muteDuration = v;
|
||||
}
|
||||
|
||||
public void addUrl(String url) {
|
||||
whitelistUrls.add(
|
||||
url.toLowerCase().trim());
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public boolean removeUrl(String url) {
|
||||
boolean r = whitelistUrls.remove(
|
||||
url.toLowerCase().trim());
|
||||
saveConfig();
|
||||
return r;
|
||||
}
|
||||
|
||||
public void addWhitelistPlayer(
|
||||
String name) {
|
||||
whitelistPlayers.add(name);
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
public boolean removeWhitelistPlayer(
|
||||
String name) {
|
||||
boolean r =
|
||||
whitelistPlayers.remove(name);
|
||||
saveConfig();
|
||||
return r;
|
||||
}
|
||||
|
||||
public void cleanExpired() {
|
||||
long now = System.currentTimeMillis();
|
||||
mutedPlayers.entrySet().removeIf(
|
||||
e -> e.getValue() <= now);
|
||||
}
|
||||
|
||||
private boolean parseBool(String v) {
|
||||
String s = v.toLowerCase().trim();
|
||||
return s.equals("true")
|
||||
|| s.equals("on")
|
||||
|| s.equals("yes")
|
||||
|| s.equals("1")
|
||||
|| s.contains("开启")
|
||||
|| s.contains("启用")
|
||||
|| s.contains("是");
|
||||
}
|
||||
}
|
||||
@@ -1,823 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChatInputManager {
|
||||
|
||||
public enum InputType {
|
||||
NONE,
|
||||
CHANGE_PWD_STEP1,
|
||||
CHANGE_PWD_STEP2,
|
||||
SET_EMAIL,
|
||||
SET_EMAIL_VERIFY,
|
||||
ADMIN_AUTH,
|
||||
ADMIN_SET_POINTS,
|
||||
ADMIN_SET_PWD,
|
||||
ADMIN_DELETE_CONFIRM,
|
||||
ADMIN_SET_AFK_TIME,
|
||||
SMTP_CONFIG,
|
||||
SMTP_VERIFY,
|
||||
BACK_CHECKIN,
|
||||
INVITE_INPUT_CODE,
|
||||
TICKET_TITLE,
|
||||
TICKET_DESC,
|
||||
TICKET_SCORE,
|
||||
TICKET_REJECT,
|
||||
TICKET_ASSIGN,
|
||||
TICKET_INVITE_PROVIDER,
|
||||
TICKET_REMOVE_PROVIDER,
|
||||
TICKET_COMPLETE,
|
||||
TICKET_REPORT_ISSUE,
|
||||
TICKET_REPLY,
|
||||
TICKET_ADMIN_TYPE,
|
||||
TICKET_ADMIN_TITLE,
|
||||
TICKET_ADMIN_DESC,
|
||||
TICKET_ADMIN_PROVIDER
|
||||
}
|
||||
|
||||
public static class PlayerState {
|
||||
public InputType type = InputType.NONE;
|
||||
public String targetPlayer = "";
|
||||
public String ticketType = "";
|
||||
public String ticketTitle = "";
|
||||
public String ticketDesc = "";
|
||||
public int ticketId = 0;
|
||||
public int smtpStep = 0;
|
||||
public int scoreValue = 0;
|
||||
}
|
||||
|
||||
private final Map<String, PlayerState> states =
|
||||
new HashMap<>();
|
||||
private Main plugin;
|
||||
|
||||
public PlayerState getState(Player p) {
|
||||
return states.computeIfAbsent(
|
||||
p.getName(),
|
||||
k -> new PlayerState());
|
||||
}
|
||||
|
||||
public void reset(Player p) {
|
||||
states.remove(p.getName());
|
||||
}
|
||||
|
||||
public void handleInput(Player p, String msg,
|
||||
Main mainPlugin) {
|
||||
if (this.plugin == null)
|
||||
this.plugin = mainPlugin;
|
||||
PlayerState state = getState(p);
|
||||
ConfigManager config =
|
||||
mainPlugin.getConfig2();
|
||||
DatabaseManager db = mainPlugin.getDb();
|
||||
|
||||
switch (state.type) {
|
||||
|
||||
case CHANGE_PWD_STEP1: {
|
||||
String name = p.getName();
|
||||
String salt = (String) db.getField(
|
||||
name, "password_salt");
|
||||
String hash =
|
||||
PasswordUtils.hash(msg, salt);
|
||||
boolean mainOk =
|
||||
db.checkPassword(name, hash);
|
||||
if (mainOk) {
|
||||
state.type =
|
||||
InputType.CHANGE_PWD_STEP2;
|
||||
state.ticketTitle = "main";
|
||||
p.sendMessage(
|
||||
"§e请输入新密码:");
|
||||
return;
|
||||
}
|
||||
boolean tempOk =
|
||||
db.checkPasswordOrTemp(
|
||||
name, hash);
|
||||
if (tempOk) {
|
||||
state.type =
|
||||
InputType.CHANGE_PWD_STEP2;
|
||||
state.ticketTitle = "temp";
|
||||
p.sendMessage(
|
||||
"§e请输入新密码:");
|
||||
return;
|
||||
}
|
||||
p.sendMessage(config.msg(
|
||||
"password_wrong"));
|
||||
return;
|
||||
}
|
||||
|
||||
case CHANGE_PWD_STEP2: {
|
||||
String name = p.getName();
|
||||
if (!PasswordUtils.validate(msg)) {
|
||||
p.sendMessage(config.msg(
|
||||
"password_format_error"));
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
String oldHash =
|
||||
(String) db.getField(name,
|
||||
"password_hash");
|
||||
String oldSalt =
|
||||
(String) db.getField(name,
|
||||
"password_salt");
|
||||
String newSalt =
|
||||
PasswordUtils.generateSalt();
|
||||
String newHash =
|
||||
PasswordUtils.hash(
|
||||
msg, newSalt);
|
||||
if (newHash.equals(oldHash)) {
|
||||
p.sendMessage(config.msg(
|
||||
"password_same"));
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
mainPlugin.recordPasswordChange(
|
||||
name, oldHash, oldSalt);
|
||||
db.setField(name,
|
||||
"password_hash", newHash);
|
||||
db.setField(name,
|
||||
"password_salt", newSalt);
|
||||
mainPlugin
|
||||
.getNeedsPasswordChange()
|
||||
.remove(name);
|
||||
p.sendMessage(config.msg(
|
||||
"password_changed"));
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
|
||||
case SET_EMAIL: {
|
||||
if (!msg.contains("@")
|
||||
|| !msg.contains(".")) {
|
||||
p.sendMessage(config.msg(
|
||||
"email_format_error"));
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
String oldEmail = (String)
|
||||
db.getField(p.getName(),
|
||||
"email");
|
||||
String emailAddr = msg.trim();
|
||||
// 已绑定邮箱:验证登录密码
|
||||
if (oldEmail != null
|
||||
&& !oldEmail.isEmpty()) {
|
||||
state.targetPlayer = emailAddr;
|
||||
state.type =
|
||||
InputType.SET_EMAIL_VERIFY;
|
||||
state.ticketTitle =
|
||||
"need_pwd";
|
||||
p.sendMessage(
|
||||
"§e您已绑定邮箱: "
|
||||
+ oldEmail);
|
||||
p.sendMessage(
|
||||
"§e请验证登录密码以确认修改:");
|
||||
return;
|
||||
}
|
||||
// 未绑定邮箱:直接发验证码
|
||||
String code = String.valueOf(
|
||||
(int) (Math.random()
|
||||
* 900000
|
||||
+ 100000));
|
||||
state.targetPlayer = emailAddr;
|
||||
state.ticketTitle = code;
|
||||
state.type =
|
||||
InputType.SET_EMAIL_VERIFY;
|
||||
final String to = emailAddr;
|
||||
final String c = code;
|
||||
final Player fp = p;
|
||||
Bukkit.getScheduler()
|
||||
.runTaskAsynchronously(
|
||||
mainPlugin, () -> {
|
||||
boolean sent = mainPlugin
|
||||
.getEmail()
|
||||
.sendVerifyCode(
|
||||
to, c);
|
||||
Bukkit.getScheduler()
|
||||
.runTask(mainPlugin,
|
||||
() -> {
|
||||
if (sent) {
|
||||
fp.sendMessage(
|
||||
"§a验证码已发送到 "
|
||||
+ to);
|
||||
fp.sendMessage(
|
||||
"§e输入6位验证码(0取消):");
|
||||
} else {
|
||||
fp.sendMessage(
|
||||
"§c发送失败");
|
||||
reset(fp);
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
case SET_EMAIL_VERIFY: {
|
||||
if ("0".equals(msg.trim())
|
||||
|| "取消".equals(
|
||||
msg.trim())) {
|
||||
p.sendMessage(
|
||||
"§c邮箱设置已取消");
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
String expected =
|
||||
state.ticketTitle;
|
||||
if (msg.trim().equals(expected)) {
|
||||
db.setField(p.getName(),
|
||||
"email",
|
||||
state.targetPlayer);
|
||||
p.sendMessage(
|
||||
"§a邮箱已设置: "
|
||||
+ state.targetPlayer);
|
||||
reset(p);
|
||||
} else {
|
||||
p.sendMessage(
|
||||
"§c验证码错误(0取消):");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case ADMIN_AUTH: {
|
||||
if (msg.equals(
|
||||
config.adminPassword)) {
|
||||
Bukkit.getScheduler().runTask(
|
||||
mainPlugin, () ->
|
||||
mainPlugin
|
||||
.getGui()
|
||||
.openAdmin(p));
|
||||
} else {
|
||||
p.sendMessage(
|
||||
"§c管理密码错误");
|
||||
}
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
|
||||
case ADMIN_SET_POINTS: {
|
||||
try {
|
||||
int pts = Integer.parseInt(
|
||||
msg.trim());
|
||||
db.setField(
|
||||
state.targetPlayer,
|
||||
"points", pts);
|
||||
p.sendMessage("§a已将 "
|
||||
+ state.targetPlayer
|
||||
+ " 积分设为 " + pts);
|
||||
} catch (
|
||||
NumberFormatException e) {
|
||||
p.sendMessage(
|
||||
"§c请输入有效数字");
|
||||
}
|
||||
String tgt =
|
||||
state.targetPlayer;
|
||||
Bukkit.getScheduler().runTask(
|
||||
mainPlugin, () ->
|
||||
mainPlugin
|
||||
.getGui()
|
||||
.openUserDetail(
|
||||
p, tgt));
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
|
||||
case ADMIN_SET_PWD: {
|
||||
if (!PasswordUtils.validate(msg)) {
|
||||
p.sendMessage(config.msg(
|
||||
"password_format_error"));
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
String tgt = state.targetPlayer;
|
||||
String salt =
|
||||
PasswordUtils
|
||||
.generateSalt();
|
||||
String hash =
|
||||
PasswordUtils.hash(
|
||||
msg, salt);
|
||||
db.setField(tgt,
|
||||
"temp_password", hash);
|
||||
db.setField(tgt, "temp_pw_expire",
|
||||
System.currentTimeMillis()
|
||||
+ 300000L);
|
||||
db.setField(tgt,
|
||||
"temp_pw_used", 0);
|
||||
p.sendMessage("§a已为 " + tgt
|
||||
+ " 设置临时密码");
|
||||
Player tp =
|
||||
Bukkit.getPlayer(tgt);
|
||||
if (tp != null && tp.isOnline())
|
||||
tp.kickPlayer(
|
||||
"§c管理员已设置临时密码,"
|
||||
+ "请使用临时密码登录");
|
||||
// 邮件通知
|
||||
String emailAddr = (String)
|
||||
db.getField(tgt, "email");
|
||||
if (emailAddr != null
|
||||
&& !emailAddr.isEmpty()) {
|
||||
String time =
|
||||
new java.text.SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss")
|
||||
.format(new java.util.Date());
|
||||
String body = "玩家 " + tgt
|
||||
+ ":\n您的密码已于 "
|
||||
+ time
|
||||
+ " 被管理员重置为临时密码。\n"
|
||||
+ "如果是本人操作请忽略。\n"
|
||||
+ "如非本人操作请立即联系管理员。";
|
||||
final String to = emailAddr;
|
||||
final String fBody = body;
|
||||
final String fName = tgt;
|
||||
Bukkit.getScheduler()
|
||||
.runTaskAsynchronously(
|
||||
mainPlugin, () ->
|
||||
mainPlugin.getEmail()
|
||||
.sendTempPassword(
|
||||
to,
|
||||
fName,
|
||||
fBody));
|
||||
}
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
|
||||
case ADMIN_DELETE_CONFIRM: {
|
||||
if (msg.equals(
|
||||
state.targetPlayer)) {
|
||||
db.deleteUser(
|
||||
state.targetPlayer);
|
||||
p.sendMessage("§a已删除 "
|
||||
+ state.targetPlayer);
|
||||
Player tgt =
|
||||
Bukkit.getPlayer(
|
||||
state.targetPlayer);
|
||||
if (tgt != null
|
||||
&& tgt.isOnline())
|
||||
tgt.kickPlayer(
|
||||
"§c账号已被删除");
|
||||
} else {
|
||||
p.sendMessage(
|
||||
"§c输入不匹配,已取消");
|
||||
}
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
|
||||
case ADMIN_SET_AFK_TIME: {
|
||||
try {
|
||||
int minutes =
|
||||
Integer.parseInt(
|
||||
msg.trim());
|
||||
config.afkTimeout =
|
||||
minutes * 60;
|
||||
config.saveSettings();
|
||||
p.sendMessage(
|
||||
"§a挂机时长已设为 "
|
||||
+ minutes
|
||||
+ " 分钟");
|
||||
} catch (
|
||||
NumberFormatException e) {
|
||||
p.sendMessage(
|
||||
"§c请输入有效数字");
|
||||
}
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
|
||||
case SMTP_CONFIG: {
|
||||
String m = msg.trim();
|
||||
if ("000".equals(m)
|
||||
|| "取消取消取消"
|
||||
.equals(m)) {
|
||||
p.sendMessage(
|
||||
"§cSMTP配置已取消");
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
if ("取消".equals(m)) {
|
||||
p.sendMessage(
|
||||
"§cSMTP配置已取消");
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
int step = state.smtpStep;
|
||||
if (step <= 1) {
|
||||
if (!"0".equals(m))
|
||||
config.setSmtp(
|
||||
"smtp地址", m);
|
||||
state.smtpStep = 2;
|
||||
p.sendMessage("§e当前端口: "
|
||||
+ config.getSmtp(
|
||||
"smtp端口"));
|
||||
p.sendMessage(
|
||||
"§e输入端口(0跳过):");
|
||||
} else if (step == 2) {
|
||||
if (!"0".equals(m))
|
||||
config.setSmtp(
|
||||
"smtp端口", m);
|
||||
state.smtpStep = 3;
|
||||
p.sendMessage(
|
||||
"§e输入发件邮箱:");
|
||||
} else if (step == 3) {
|
||||
if (!"0".equals(m))
|
||||
config.setSmtp(
|
||||
"smtp账号", m);
|
||||
state.smtpStep = 4;
|
||||
p.sendMessage(
|
||||
"§e输入SMTP密码(0跳过):");
|
||||
} else if (step == 4) {
|
||||
if (!"0".equals(m))
|
||||
config.setSmtp(
|
||||
"smtp密码", m);
|
||||
state.smtpStep = 5;
|
||||
p.sendMessage(
|
||||
"§e输入接收邮箱(留空发到发件邮箱):");
|
||||
} else if (step == 5) {
|
||||
String recv = m;
|
||||
if (recv.isEmpty()
|
||||
|| "0".equals(recv))
|
||||
recv = config.getSmtp(
|
||||
"smtp账号");
|
||||
config.setSmtp(
|
||||
"验证码接收邮箱", recv);
|
||||
if (!config
|
||||
.canSendSmtpVerify()) {
|
||||
p.sendMessage(
|
||||
"§c发送间隔不足1分钟");
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
String code = config
|
||||
.generateSmtpVerifyCode();
|
||||
String addr = config.getSmtp(
|
||||
"smtp地址");
|
||||
String acct = config.getSmtp(
|
||||
"smtp账号");
|
||||
if (acct.isEmpty()
|
||||
|| addr.isEmpty()) {
|
||||
p.sendMessage(
|
||||
"§c地址或账号为空");
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
config.recordSmtpSendTime();
|
||||
final String fRecv = recv;
|
||||
final String fCode = code;
|
||||
final Player fp = p;
|
||||
Bukkit.getScheduler()
|
||||
.runTaskAsynchronously(
|
||||
mainPlugin, () -> {
|
||||
Bukkit.getScheduler()
|
||||
.runTask(
|
||||
mainPlugin,
|
||||
() -> fp.sendMessage(
|
||||
"§e正在发送验证码..."));
|
||||
boolean sent =
|
||||
mainPlugin.getEmail()
|
||||
.sendVerifyCode(
|
||||
fRecv,
|
||||
fCode);
|
||||
final boolean fSent = sent;
|
||||
Bukkit.getScheduler()
|
||||
.runTask(
|
||||
mainPlugin,
|
||||
() -> {
|
||||
if (fSent) {
|
||||
state.type =
|
||||
InputType
|
||||
.SMTP_VERIFY;
|
||||
state.smtpStep = 0;
|
||||
fp.sendMessage(
|
||||
"§a验证码已发送到 "
|
||||
+ fRecv);
|
||||
fp.sendMessage(
|
||||
"§e输入6位验证码(0取消):");
|
||||
} else {
|
||||
fp.sendMessage(
|
||||
"§c发送失败,查看控制台");
|
||||
reset(fp);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case SMTP_VERIFY: {
|
||||
if ("0".equals(msg.trim())
|
||||
|| "取消".equals(
|
||||
msg.trim())) {
|
||||
p.sendMessage(
|
||||
"§cSMTP配置已取消");
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
if (config.checkSmtpVerifyCode(
|
||||
msg.trim())) {
|
||||
config.saveSmtp();
|
||||
config.clearSmtpVerifyCode();
|
||||
String addr = config.getSmtp(
|
||||
"smtp地址");
|
||||
String port = config.getSmtp(
|
||||
"smtp端口");
|
||||
String acct = config.getSmtp(
|
||||
"smtp账号");
|
||||
String recv = config.getSmtp(
|
||||
"验证码接收邮箱");
|
||||
boolean ssl = config.smtpSsl;
|
||||
p.sendMessage("§a§l=== SMTP配置完成 ===");
|
||||
p.sendMessage("§7地址: §f" + addr);
|
||||
p.sendMessage("§7端口: §f" + port);
|
||||
p.sendMessage("§7账号: §f" + acct);
|
||||
p.sendMessage("§7接收: §f" + recv);
|
||||
p.sendMessage("§7加密: §f" + ssl);
|
||||
p.sendMessage("§a§l====================");
|
||||
reset(p);
|
||||
} else {
|
||||
p.sendMessage(
|
||||
"§c验证码错误(0取消):");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case BACK_CHECKIN: {
|
||||
int cost = 20;
|
||||
if (!mainPlugin.getDb()
|
||||
.deductPoints(
|
||||
p.getName(), cost)) {
|
||||
p.sendMessage(
|
||||
"§c积分不足,补签需 "
|
||||
+ cost + " 积分");
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
p.sendMessage(
|
||||
"§a补签成功!扣除 "
|
||||
+ cost + " 积分。"
|
||||
+ "日期: " + msg);
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
|
||||
case INVITE_INPUT_CODE: {
|
||||
String code = msg.trim();
|
||||
String myName = p.getName();
|
||||
String myCode =
|
||||
(String) db.getField(
|
||||
myName,
|
||||
"invite_code");
|
||||
if (code.isEmpty()) {
|
||||
p.sendMessage(
|
||||
"§c邀请码不能为空");
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
if (code.equals(myCode)) {
|
||||
p.sendMessage(
|
||||
"§c不能用自己邀请码");
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
boolean found = false;
|
||||
for (Map<String, Object> u :
|
||||
db.getAllUsers()) {
|
||||
String uc = (String)
|
||||
u.getOrDefault(
|
||||
"invite_code", "");
|
||||
if (code.equals(uc)) {
|
||||
String inv = (String)
|
||||
u.get("player_name");
|
||||
db.setField(myName,
|
||||
"invited_by", inv);
|
||||
db.addPoints(inv, 50);
|
||||
p.sendMessage(
|
||||
"§a邀请成功!邀请人: "
|
||||
+ inv);
|
||||
Player ip =
|
||||
Bukkit.getPlayer(inv);
|
||||
if (ip != null
|
||||
&& ip.isOnline())
|
||||
ip.sendMessage("§a"
|
||||
+ myName
|
||||
+ " 使用了您的邀请码");
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
p.sendMessage(
|
||||
"§c邀请码不存在");
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_TITLE: {
|
||||
state.ticketTitle = msg;
|
||||
state.type =
|
||||
InputType.TICKET_DESC;
|
||||
p.sendMessage(
|
||||
"§e请输入工单详细描述:");
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_DESC: {
|
||||
String tt = state.ticketType;
|
||||
String tl = state.ticketTitle;
|
||||
reset(p);
|
||||
mainPlugin.getTicket()
|
||||
.submitTicket(
|
||||
p, tt, tl, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_SCORE: {
|
||||
int tid = state.ticketId;
|
||||
try {
|
||||
int sc = Integer.parseInt(
|
||||
msg.trim());
|
||||
reset(p);
|
||||
mainPlugin.getTicket()
|
||||
.confirmAndScore(
|
||||
p, tid, sc);
|
||||
} catch (
|
||||
NumberFormatException e) {
|
||||
reset(p);
|
||||
p.sendMessage(
|
||||
"§c请输入数字1-5");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_REJECT: {
|
||||
int tid = state.ticketId;
|
||||
reset(p);
|
||||
mainPlugin.getTicket()
|
||||
.rejectTicket(
|
||||
p, tid, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_ASSIGN: {
|
||||
int tid = state.ticketId;
|
||||
String[] parts = msg.trim()
|
||||
.split("\\s+");
|
||||
String prov = parts[0];
|
||||
int price = 0;
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
price = Integer.parseInt(
|
||||
parts[1]);
|
||||
} catch (
|
||||
NumberFormatException e) {
|
||||
p.sendMessage(
|
||||
"§c价格格式错误");
|
||||
}
|
||||
}
|
||||
reset(p);
|
||||
mainPlugin.getTicket()
|
||||
.assignToProvider(
|
||||
p, tid, prov,
|
||||
price);
|
||||
final int fTid = tid;
|
||||
Bukkit.getScheduler().runTask(
|
||||
mainPlugin, () ->
|
||||
mainPlugin
|
||||
.getTicket()
|
||||
.openDetail(
|
||||
p, fTid));
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_INVITE_PROVIDER: {
|
||||
String tgt = msg.trim();
|
||||
reset(p);
|
||||
if (!db.userExists(tgt)) {
|
||||
p.sendMessage(
|
||||
"§c玩家不存在");
|
||||
return;
|
||||
}
|
||||
db.addServiceProvider(
|
||||
tgt, "waiter");
|
||||
p.sendMessage("§a已邀请 " + tgt
|
||||
+ " 成为服务商");
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_REMOVE_PROVIDER: {
|
||||
String tgt = msg.trim();
|
||||
reset(p);
|
||||
if (!db.isServiceProvider(tgt)) {
|
||||
p.sendMessage("§c" + tgt
|
||||
+ " 不是服务商");
|
||||
return;
|
||||
}
|
||||
db.removeServiceProvider(tgt);
|
||||
p.sendMessage("§a已移除 " + tgt);
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_COMPLETE: {
|
||||
try {
|
||||
int tid = Integer.parseInt(
|
||||
msg.trim());
|
||||
mainPlugin.getTicket()
|
||||
.completeAsProvider(
|
||||
p, tid);
|
||||
} catch (
|
||||
NumberFormatException e) {
|
||||
p.sendMessage(
|
||||
"§c请输入有效工单号");
|
||||
}
|
||||
reset(p);
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_REPORT_ISSUE: {
|
||||
String report = msg.trim();
|
||||
reset(p);
|
||||
p.sendMessage("§e异常已上报");
|
||||
String tag = config.adminTag;
|
||||
for (Player op :
|
||||
Bukkit.getOnlinePlayers()) {
|
||||
if (op.getScoreboardTags()
|
||||
.contains(tag)) {
|
||||
op.sendMessage(
|
||||
"§c§l[工单异常] §f"
|
||||
+ p.getName()
|
||||
+ ": " + report);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_REPLY: {
|
||||
int replyTid = state.ticketId;
|
||||
reset(p);
|
||||
mainPlugin.getTicket()
|
||||
.replyTicket(
|
||||
p, replyTid, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_ADMIN_TYPE: {
|
||||
state.ticketType = msg.trim();
|
||||
state.type =
|
||||
InputType
|
||||
.TICKET_ADMIN_TITLE;
|
||||
p.sendMessage(
|
||||
"§e请输入工单标题:");
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_ADMIN_TITLE: {
|
||||
state.ticketTitle = msg;
|
||||
state.type =
|
||||
InputType
|
||||
.TICKET_ADMIN_DESC;
|
||||
p.sendMessage(
|
||||
"§e请输入工单描述:");
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_ADMIN_DESC: {
|
||||
state.targetPlayer = msg;
|
||||
state.type =
|
||||
InputType
|
||||
.TICKET_ADMIN_PROVIDER;
|
||||
p.sendMessage(
|
||||
"§e输入服务商名和价格:");
|
||||
p.sendMessage(
|
||||
"§7例: playerA 20");
|
||||
return;
|
||||
}
|
||||
|
||||
case TICKET_ADMIN_PROVIDER: {
|
||||
String[] parts2 = msg.trim()
|
||||
.split("\\s+");
|
||||
String prov2 = parts2[0];
|
||||
int price2 = 0;
|
||||
if (parts2.length >= 2) {
|
||||
try {
|
||||
price2 = Integer.parseInt(
|
||||
parts2[1]);
|
||||
} catch (
|
||||
NumberFormatException e) {
|
||||
}
|
||||
}
|
||||
String t2 = state.ticketType;
|
||||
String ti2 = state.ticketTitle;
|
||||
String d2 = state.targetPlayer;
|
||||
reset(p);
|
||||
mainPlugin.getTicket()
|
||||
.adminCreateAndAssign(
|
||||
p, t2, ti2,
|
||||
d2, prov2, price2);
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
reset(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class CheckInManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
public CheckInManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public boolean isCheckedInToday(String name) {
|
||||
String last = (String) plugin.getDb()
|
||||
.getField(name, "last_checkin_date");
|
||||
String today = new SimpleDateFormat("yyyy-MM-dd")
|
||||
.format(new Date());
|
||||
return today.equals(last);
|
||||
}
|
||||
|
||||
public String checkIn(Player p) {
|
||||
String name = p.getName();
|
||||
if (isCheckedInToday(name)) {
|
||||
return plugin.getConfig2().msg("checkin_already");
|
||||
}
|
||||
|
||||
int streak = ((Number) plugin.getDb()
|
||||
.getField(name, "checkin_streak")).intValue();
|
||||
String lastDate = (String) plugin.getDb()
|
||||
.getField(name, "last_checkin_date");
|
||||
String today = new SimpleDateFormat("yyyy-MM-dd")
|
||||
.format(new Date());
|
||||
|
||||
// 检查是否连续
|
||||
if (lastDate != null && !lastDate.isEmpty()) {
|
||||
try {
|
||||
java.util.Date lastD = new SimpleDateFormat(
|
||||
"yyyy-MM-dd").parse(lastDate);
|
||||
java.util.Date todayD = new SimpleDateFormat(
|
||||
"yyyy-MM-dd").parse(today);
|
||||
long diff = todayD.getTime() - lastD.getTime();
|
||||
if (diff > 86400000L * 2) {
|
||||
streak = 0; // 断签
|
||||
} else if (diff > 86400000L) {
|
||||
streak++; // 连续
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
if (streak == 0) streak = 1;
|
||||
else streak++;
|
||||
|
||||
double multi = 1.0;
|
||||
if (streak >= 21) multi = 1.5;
|
||||
else if (streak >= 14) multi = 1.3;
|
||||
else if (streak >= 7) multi = 1.1;
|
||||
|
||||
int points = (int) (1 * multi);
|
||||
plugin.getDb().addPoints(name, points);
|
||||
plugin.getDb().setField(
|
||||
name, "checkin_streak", streak);
|
||||
plugin.getDb().setField(
|
||||
name, "last_checkin_date", today);
|
||||
int total = ((Number) plugin.getDb()
|
||||
.getField(name, "total_checkin_days"))
|
||||
.intValue();
|
||||
plugin.getDb().setField(
|
||||
name, "total_checkin_days", total + 1);
|
||||
|
||||
return plugin.getConfig2().msg("checkin_success",
|
||||
"points", String.valueOf(points),
|
||||
"streak", String.valueOf(streak),
|
||||
"multi", String.valueOf(multi));
|
||||
}
|
||||
|
||||
public String backCheckIn(Player p, String dateStr) {
|
||||
String name = p.getName();
|
||||
if (!plugin.getDb().deductPoints(name, 10)) {
|
||||
return plugin.getConfig2().msg("backcheckin_no_point");
|
||||
}
|
||||
try {
|
||||
java.util.Date target = new SimpleDateFormat(
|
||||
"yyyy-MM-dd").parse(dateStr);
|
||||
java.util.Date now = new SimpleDateFormat(
|
||||
"yyyy-MM-dd").parse(
|
||||
new SimpleDateFormat("yyyy-MM-dd")
|
||||
.format(new Date()));
|
||||
long diff = now.getTime() - target.getTime();
|
||||
if (diff < 0 || diff > 3 * 86400000L) {
|
||||
plugin.getDb().addPoints(name, 10); // 退还
|
||||
return plugin.getConfig2().msg(
|
||||
"backcheckin_expired");
|
||||
}
|
||||
plugin.getDb().addPoints(name, 1); // 补签也给1分
|
||||
return plugin.getConfig2().msg("backcheckin_success");
|
||||
} catch (Exception e) {
|
||||
plugin.getDb().addPoints(name, 10);
|
||||
return "§c[Sdf1_login] §f日期格式错误,请用 yyyy-MM-dd";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
import static java.lang.Double.parseDouble;
|
||||
|
||||
public class ConfigManager {
|
||||
|
||||
private final File dataFolder;
|
||||
private final Map<String, String> messages = new LinkedHashMap<>();
|
||||
private final Map<String, String> smtpSettings = new LinkedHashMap<>();
|
||||
|
||||
public String adminTag = "admin";
|
||||
public String adminPassword = "qweasd";
|
||||
public int maxAccountsPerIP = 3;
|
||||
public String approvalMode = "manual";
|
||||
public int autoApproveDelayMinutes = 30;
|
||||
public int loginTimeout = 60;
|
||||
public boolean afkEnabled = false;
|
||||
public int afkTimeout = 300;
|
||||
public boolean smtpSsl = true;
|
||||
public String smtpVerifyCode = "";
|
||||
public long smtpVerifyExpire = 0;
|
||||
public long smtpLastSendTime = 0;
|
||||
public String smtpConfiguringPlayer = "";
|
||||
|
||||
|
||||
public ConfigManager(File dataFolder) {
|
||||
this.dataFolder = dataFolder;
|
||||
}
|
||||
|
||||
// ==================== 消息 ====================
|
||||
|
||||
public void loadMessages() {
|
||||
messages.clear();
|
||||
File file = new File(dataFolder, "消息.txt");
|
||||
if (!file.exists()) createDefaultMessages(file);
|
||||
loadMap(file, messages);
|
||||
}
|
||||
|
||||
public String msg(String key) {
|
||||
String val = messages.getOrDefault(key, key);
|
||||
val = val.replace("<b>", "&l");
|
||||
val = val.replace("</b>", "&r");
|
||||
val = val.replace("<br>", "\n");
|
||||
val = val.replace("\\n", "\n");
|
||||
// &颜色码 转换为 §颜色码
|
||||
val = val.replaceAll(
|
||||
"&([0-9a-fk-orA-FK-OR])", "§$1");
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
private void createDefaultMessages(File f) {
|
||||
List<String> L = new ArrayList<>();
|
||||
L.add("# ==========================================");
|
||||
L.add("# Sdf1_login 消息配置文件");
|
||||
L.add("# ==========================================");
|
||||
L.add("# 颜色代码使用 § 符号");
|
||||
L.add("# §0黑色 §1深蓝 §2深绿 §3深青");
|
||||
L.add("# §4深红 §5深紫 §6金色 §7灰色");
|
||||
L.add("# §8深灰 §9蓝色 §a绿色 §b浅蓝");
|
||||
L.add("# §c红色 §d粉色 §e黄色 §f白色");
|
||||
L.add("# §l粗体 §n下划线 §o斜体 §r重置");
|
||||
L.add("#");
|
||||
L.add("# 占位符用 {xxx} 表示,运行时自动替换");
|
||||
L.add("# 支持 <b>粗体</b> 标签");
|
||||
L.add("# 支持 \\n 或 <br> 换行");
|
||||
L.add("# 修改后执行 /sdf1_login reload 重载");
|
||||
L.add("# ==========================================");
|
||||
L.add("");
|
||||
|
||||
// --- 登录/注册 ---
|
||||
L.add("# ---- 登录注册相关 ----");
|
||||
L.add("# login_timeout: 登录超时被踢出时的提示");
|
||||
L.add("login_timeout=§c登录超时,请重新登录");
|
||||
L.add("# not_registered: 未注册玩家收到的提示");
|
||||
L.add("not_registered=§e您尚未注册,请使用 /reg <密码>");
|
||||
L.add("# not_logged_in: 已注册但未登录的提示");
|
||||
L.add("not_logged_in=§e您尚未登录,请使用 /login <密码>");
|
||||
L.add("# already_registered: 已注册玩家再次注册时的提示");
|
||||
L.add("already_registered=§c您已注册过账号");
|
||||
L.add("# already_logged_in: 已登录玩家再次登录时的提示");
|
||||
L.add("already_logged_in=§c您已登录");
|
||||
L.add("# reg_success: 注册成功提示");
|
||||
L.add("reg_success=§a注册成功!欢迎来到服务器");
|
||||
L.add("# login_success: 登录成功提示");
|
||||
L.add("login_success=§a登录成功!");
|
||||
L.add("# login_failed: 密码错误提示");
|
||||
L.add("login_failed=§c密码错误,请重试");
|
||||
L.add("");
|
||||
|
||||
// --- 密码 ---
|
||||
L.add("# ---- 密码相关 ----");
|
||||
L.add("# password_wrong: 旧密码验证失败");
|
||||
L.add("password_wrong=§c密码错误");
|
||||
L.add("# password_changed: 密码修改成功");
|
||||
L.add("password_changed=§a密码修改成功!");
|
||||
L.add("# password_format_error: 密码格式不符合要求");
|
||||
L.add("password_format_error=§c密码需6位以上,含大小写字母和数字");
|
||||
L.add("# password_same: 新密码与旧密码相同");
|
||||
L.add("password_same=§c新密码不能与旧密码相同");
|
||||
L.add("# reset_no_email: 找回密码时未绑定邮箱");
|
||||
L.add("reset_no_email=§c您未绑定邮箱,无法找回密码");
|
||||
L.add("# reset_sent: 临时密码已发送到邮箱");
|
||||
L.add("reset_sent=§a临时密码已发送到您的邮箱,请查收");
|
||||
L.add("");
|
||||
|
||||
// --- 签到积分 ---
|
||||
L.add("# ---- 签到积分相关 ----");
|
||||
L.add("# checkin_already: 今日已签到");
|
||||
L.add("checkin_already=§c您今日已签到");
|
||||
L.add("# checkin_success: 签到成功,{points}=获得积分 {streak}=连续天数 {multi}=倍率");
|
||||
L.add("checkin_success=§a签到成功!获得 {points} 积分,连续 {streak} 天,倍率 x{multi}");
|
||||
L.add("# points_insufficient: 积分不足");
|
||||
L.add("points_insufficient=§c积分不足,当前: {points}");
|
||||
L.add("# points_purchase_success: 积分购买成功");
|
||||
L.add("points_purchase_success=§a购买成功!消耗 {count} 积分");
|
||||
L.add("");
|
||||
|
||||
// --- 挂机 ---
|
||||
L.add("# ---- 挂机检测相关 ----");
|
||||
L.add("# afk_set_enabled: 挂机检测开启");
|
||||
L.add("afk_set_enabled=§a挂机检测已开启");
|
||||
L.add("# afk_set_disabled: 挂机检测关闭");
|
||||
L.add("afk_set_disabled=§c挂机检测已关闭");
|
||||
L.add("# afk_kick: 因挂机被踢出");
|
||||
L.add("afk_kick=§c因挂机过久被踢出");
|
||||
L.add("");
|
||||
|
||||
// --- 邮箱 ---
|
||||
L.add("# ---- 邮箱相关 ----");
|
||||
L.add("# email_set: 邮箱设置成功");
|
||||
L.add("email_set=§a邮箱设置成功:{email}");
|
||||
L.add("# email_format_error: 邮箱格式错误");
|
||||
L.add("email_format_error=§c邮箱格式不正确");
|
||||
L.add("");
|
||||
|
||||
// --- 邀请 ---
|
||||
L.add("# ---- 邀请相关 ----");
|
||||
L.add("# invite_code_generated: 邀请码生成");
|
||||
L.add("invite_code_generated=§a邀请码:{code}");
|
||||
L.add("# invite_used: 使用邀请码成功");
|
||||
L.add("invite_used=§a邀请成功,邀请人:{player}");
|
||||
L.add("# invite_self: 使用自己的邀请码");
|
||||
L.add("invite_self=§c不能使用自己的邀请码");
|
||||
L.add("# invite_not_found: 邀请码不存在");
|
||||
L.add("invite_not_found=§c邀请码不存在");
|
||||
L.add("# invite_referral_bonus: 被邀请人签到后邀请人获得奖励");
|
||||
L.add("invite_referral_bonus=§a邀请奖励!获得 {points} 积分");
|
||||
L.add("");
|
||||
|
||||
// --- 礼包 ---
|
||||
L.add("# ---- 新人礼包相关 ----");
|
||||
L.add("# gift_not_ready: 礼包条件未满足");
|
||||
L.add("gift_not_ready=§c礼包条件未满足");
|
||||
L.add("# gift_claimed: 礼包领取成功");
|
||||
L.add("gift_claimed=§a礼包领取成功!");
|
||||
L.add("");
|
||||
|
||||
// --- 审批/IP ---
|
||||
L.add("# ---- 审批与IP限制 ----");
|
||||
L.add("# need_approval: 注册需审批");
|
||||
L.add("need_approval=§e注册需审批 (工单#{id})");
|
||||
L.add("# ip_max_accounts: IP注册已达上限");
|
||||
L.add("ip_max_accounts=§cIP注册已达上限");
|
||||
L.add("");
|
||||
|
||||
// --- 管理员 ---
|
||||
L.add("# ---- 管理员操作 ----");
|
||||
L.add("# admin_delete_confirm: 管理员删除账号确认提示");
|
||||
L.add("admin_delete_confirm=§c请在聊天中输入玩家名确认删除");
|
||||
L.add("");
|
||||
|
||||
// --- 聊天过滤 ---
|
||||
L.add("# ---- 聊天过滤 ----");
|
||||
L.add("# chat_muted: 禁言玩家尝试聊天");
|
||||
L.add("chat_muted=§c§l[Sdf1_chat] §f你已被禁言,无法聊天");
|
||||
L.add("# chat_url_blocked: 发送违规链接被拦截");
|
||||
L.add("# {url}=检测到的链接地址");
|
||||
L.add("chat_url_blocked=§c§l[Sdf1_chat] §f检测到违规第三方链接: §e{url}");
|
||||
L.add("# chat_url_violation: 当前违规次数");
|
||||
L.add("# {count}=当前违规次数");
|
||||
L.add("chat_url_violation=§7当前违规次数: §e{count}");
|
||||
L.add("# chat_url_admin_notify: 通知管理员,{player}=玩家名 {url}=链接 {count}=次数");
|
||||
L.add("chat_url_admin_notify=§e§l[Sdf1_chat] §f{player} 发送违规链接: {url} (第{count}次)");
|
||||
L.add("# chat_url_broadcast: 全服通报,{player}=玩家名");
|
||||
L.add("chat_url_broadcast=§c§l[Sdf1_chat] §e{player} §f因发送违规链接被处罚");
|
||||
L.add("");
|
||||
|
||||
// --- 通用 ---
|
||||
L.add("# ---- 通用 ----");
|
||||
L.add("# 未知参数: 未知命令或权限不足时的提示");
|
||||
L.add("未知参数=§c未知参数或权限不足");
|
||||
L.add("# only_player: 仅玩家可用的命令");
|
||||
L.add("# only_player=§c此命令仅玩家可用");
|
||||
|
||||
writeLines(f, L);
|
||||
}
|
||||
|
||||
|
||||
// ==================== SMTP ====================
|
||||
|
||||
public void loadSmtp() {
|
||||
smtpSettings.clear();
|
||||
File file = new File(dataFolder, "SMTP设置.txt");
|
||||
if (!file.exists()) createDefaultSmtp(file);
|
||||
loadMap(file, smtpSettings);
|
||||
}
|
||||
private double parseDouble(String s, double def) {
|
||||
try { return Double.parseDouble(s.trim()); }
|
||||
catch (Exception e) { return def; }
|
||||
}
|
||||
|
||||
public String getSmtp(String key) {
|
||||
return smtpSettings.getOrDefault(key, "");
|
||||
}
|
||||
/** 获取挂机白名单列表 */
|
||||
public List<String> getAfkWhitelist() {
|
||||
List<String> list = new ArrayList<>();
|
||||
File file = new File(dataFolder, "挂机白名单.txt");
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
file.createNewFile();
|
||||
try (BufferedWriter w = new BufferedWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(file),
|
||||
StandardCharsets.UTF_8))) {
|
||||
w.write("# 每行一个玩家名,前面加 # 为注释");
|
||||
w.newLine();
|
||||
w.write("# 以下玩家不会被挂机踢出");
|
||||
w.newLine();
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
try (BufferedReader r = new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(file),
|
||||
StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty() && !line.startsWith("#")) {
|
||||
list.add(line);
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 设置SMTP单项(聊天输入用) */
|
||||
public void setSmtp(String key, String value) {
|
||||
smtpSettings.put(key, value);
|
||||
}
|
||||
|
||||
/** 将当前SMTP设置写回文件 */
|
||||
public void saveSmtp() {
|
||||
File file = new File(dataFolder, "SMTP设置.txt");
|
||||
List<String> L = new ArrayList<>();
|
||||
L.add("# ===== SMTP 邮件服务器配置 =====");
|
||||
for (Map.Entry<String, String> e : smtpSettings.entrySet()) {
|
||||
L.add(e.getKey() + "=" + e.getValue());
|
||||
}
|
||||
writeLines(file, L);
|
||||
}
|
||||
|
||||
private void createDefaultSmtp(File f) {
|
||||
List<String> L = new ArrayList<>();
|
||||
L.add("# ===== SMTP 邮件服务器配置 =====");
|
||||
L.add("smtp地址=smtp.example.com");
|
||||
L.add("smtp端口=465");
|
||||
L.add("smtp账号=your_email@example.com");
|
||||
L.add("smtp密码=");
|
||||
L.add("发件人名称=Sdf1_login");
|
||||
L.add("smtp加密=true");
|
||||
writeLines(f, L);
|
||||
}
|
||||
|
||||
// ==================== 插件设置 ====================
|
||||
|
||||
public void loadSettings() {
|
||||
File file = new File(dataFolder, "插件设置.txt");
|
||||
if (!file.exists()) createDefaultSettings(file);
|
||||
Map<String, String> m = new LinkedHashMap<>();
|
||||
loadMap(file, m);
|
||||
adminTag = m.getOrDefault("管理标签", adminTag);
|
||||
adminPassword = m.getOrDefault("管理密码", adminPassword);
|
||||
maxAccountsPerIP = parseInt(
|
||||
m.getOrDefault("每IP最大账号数", "3"), 3);
|
||||
approvalMode = m.getOrDefault("审批模式", approvalMode);
|
||||
autoApproveDelayMinutes = parseInt(
|
||||
m.getOrDefault("自动审批延迟分钟", "30"), 30);
|
||||
loginTimeout = parseInt(
|
||||
m.getOrDefault("登录超时秒数", "60"), 60);
|
||||
afkEnabled = Boolean.parseBoolean(
|
||||
m.getOrDefault("挂机踢出", "false"));
|
||||
afkTimeout = parseInt(
|
||||
m.getOrDefault("挂机超时秒数", "300"), 300);
|
||||
smtpSsl = Boolean.parseBoolean(
|
||||
m.getOrDefault("smtp加密", "true"));
|
||||
providerPointsPerScore = parseDouble(
|
||||
m.getOrDefault("服务商积分倍率", "2.0"),
|
||||
2.0);
|
||||
baseEconomyReward = parseInt(
|
||||
m.getOrDefault("基础经济奖励", "100"),
|
||||
100);
|
||||
providerEconomyPerScore = parseDouble(
|
||||
m.getOrDefault("经济奖励每评分", "50.0"),
|
||||
50.0);
|
||||
requesterPointsReward = parseInt(
|
||||
m.getOrDefault("报单人积分奖励", "10"),
|
||||
10);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void saveSettings() {
|
||||
File file = new File(dataFolder, "插件设置.txt");
|
||||
List<String> L = new ArrayList<>();
|
||||
L.add("# ===== Sdf1_login 插件设置 =====");
|
||||
L.add("管理标签=" + adminTag);
|
||||
L.add("管理密码=" + adminPassword);
|
||||
L.add("每IP最大账号数=" + maxAccountsPerIP);
|
||||
L.add("审批模式=" + approvalMode);
|
||||
L.add("自动审批延迟分钟=" + autoApproveDelayMinutes);
|
||||
L.add("登录超时秒数=" + loginTimeout);
|
||||
L.add("挂机踢出=" + afkEnabled);
|
||||
L.add("挂机超时秒数=" + afkTimeout);
|
||||
// 垃圾站设置
|
||||
L.add("# ---- 垃圾站 ----");
|
||||
L.add("垃圾站_启用: "
|
||||
+ garbageEnabled);
|
||||
L.add("垃圾站_清理间隔秒: "
|
||||
+ garbageInterval);
|
||||
L.add("垃圾站_保留轮数: "
|
||||
+ garbageMaxRounds);
|
||||
|
||||
writeLines(file, L);
|
||||
}
|
||||
public double providerPointsPerScore = 2.0;
|
||||
public int baseEconomyReward = 100;
|
||||
public double providerEconomyPerScore = 50.0;
|
||||
public int requesterPointsReward = 10;
|
||||
public boolean garbageEnabled = true;
|
||||
public int garbageInterval = 300;
|
||||
public int garbageMaxRounds = 1;
|
||||
|
||||
|
||||
private void createDefaultSettings(File f) {
|
||||
List<String> L = new ArrayList<>();
|
||||
L.add("# ===== Sdf1_login 插件设置 =====");
|
||||
L.add("管理标签=admin");
|
||||
L.add("管理密码=qweasd");
|
||||
L.add("每IP最大账号数=3");
|
||||
L.add("审批模式=manual");
|
||||
L.add("自动审批延迟分钟=30");
|
||||
L.add("登录超时秒数=60");
|
||||
L.add("挂机踢出=false");
|
||||
L.add("挂机超时秒数=300");
|
||||
L.add("服务商积分倍率=" + providerPointsPerScore);
|
||||
L.add("基础经济奖励=" + baseEconomyReward);
|
||||
L.add("经济奖励每评分=" + providerEconomyPerScore);
|
||||
L.add("报单人积分奖励=" + requesterPointsReward);
|
||||
|
||||
writeLines(f, L);
|
||||
}
|
||||
|
||||
// ==================== 通用读写 ====================
|
||||
|
||||
private void loadMap(File file, Map<String, String> map) {
|
||||
try (BufferedReader r = new BufferedReader(
|
||||
new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = r.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
int eq = line.indexOf('=');
|
||||
if (eq > 0) map.put(line.substring(0, eq).trim(), line.substring(eq + 1).trim());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("[Sdf1_login] 读取失败: " + file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLines(File file, List<String> lines) {
|
||||
try (BufferedWriter w = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
|
||||
for (String l : lines) { w.write(l); w.newLine(); }
|
||||
} catch (IOException e) {
|
||||
System.err.println("[Sdf1_login] 写入失败: " + file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private int parseInt(String s, int def) {
|
||||
try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; }
|
||||
}
|
||||
/**
|
||||
* 生成6位验证码(字母+数字)
|
||||
*/
|
||||
public String generateSmtpVerifyCode() {
|
||||
String chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
+ "abcdefghijklmnopqrstuvwxyz"
|
||||
+ "0123456789";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
java.util.Random r = new java.util.Random();
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sb.append(chars.charAt(r.nextInt(
|
||||
chars.length())));
|
||||
}
|
||||
smtpVerifyCode = sb.toString();
|
||||
smtpVerifyExpire =
|
||||
System.currentTimeMillis() + 300000L;
|
||||
return smtpVerifyCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查验证码是否有效
|
||||
*/
|
||||
public boolean checkSmtpVerifyCode(String code) {
|
||||
if (System.currentTimeMillis() > smtpVerifyExpire) {
|
||||
smtpVerifyCode = "";
|
||||
return false;
|
||||
}
|
||||
return smtpVerifyCode.equalsIgnoreCase(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除验证码
|
||||
*/
|
||||
public void clearSmtpVerifyCode() {
|
||||
smtpVerifyCode = "";
|
||||
smtpVerifyExpire = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查发送间隔是否>=1分钟
|
||||
*/
|
||||
public boolean canSendSmtpVerify() {
|
||||
return System.currentTimeMillis()
|
||||
- smtpLastSendTime >= 60000L;
|
||||
}
|
||||
|
||||
public void recordSmtpSendTime() {
|
||||
smtpLastSendTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,210 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.Socket;
|
||||
import java.util.Base64;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class EmailManager {
|
||||
|
||||
private final ConfigManager config;
|
||||
private final Logger logger;
|
||||
/**
|
||||
* 发送SMTP验证码
|
||||
*/
|
||||
public boolean sendVerifyCode(String to, String code) {
|
||||
if (!isConfigured()) {
|
||||
plugin.getLogger().severe(
|
||||
"[Sdf1_login] SMTP未配置完整,"
|
||||
+ "请检查SMTP设置.txt");
|
||||
return false;
|
||||
}
|
||||
String subject =
|
||||
"[Sdf1_login] SMTP配置验证码";
|
||||
String body = "您的SMTP配置验证码是: "
|
||||
+ code + "\n"
|
||||
+ "验证码5分钟内有效。\n"
|
||||
+ "如非本人操作请忽略此邮件。";
|
||||
try {
|
||||
return sendEmail(to, subject, body);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe(
|
||||
"[Sdf1_login] 邮件发送异常: "
|
||||
+ e.getClass().getSimpleName()
|
||||
+ ": " + e.getMessage());
|
||||
if (e.getCause() != null) {
|
||||
plugin.getLogger().severe(
|
||||
"[Sdf1_login] 根因: "
|
||||
+ e.getCause().getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public EmailManager(ConfigManager config) {
|
||||
this.config = config;
|
||||
this.logger = Logger.getLogger("Sdf1_login");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查SMTP是否已配置
|
||||
*/
|
||||
public boolean isConfigured() {
|
||||
String host = config.getSmtp("smtp地址");
|
||||
String user = config.getSmtp("发件邮箱");
|
||||
String pass = config.getSmtp("授权码");
|
||||
return host != null && !host.isEmpty()
|
||||
&& user != null && !user.isEmpty()
|
||||
&& pass != null && !pass.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码,返回是否成功发起
|
||||
*/
|
||||
public boolean sendVerifyCode(String to, String player, String code) {
|
||||
if (!isConfigured()) {
|
||||
logger.warning("[Sdf1_login] SMTP未配置,无法发送验证码到 " + to);
|
||||
return false;
|
||||
}
|
||||
logger.info("[Sdf1_login] 发送验证码到 " + to + " (玩家: " + player + ")");
|
||||
return sendEmailSync(to, "Sdf1_login 验证码",
|
||||
"玩家 " + player + " 的验证码: " + code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送临时密码,返回是否成功发起
|
||||
*/
|
||||
public boolean sendTempPassword(String to, String player, String tempPwd) {
|
||||
if (!isConfigured()) {
|
||||
logger.warning("[Sdf1_login] SMTP未配置,无法发送临时密码到 " + to);
|
||||
return false;
|
||||
}
|
||||
logger.info("[Sdf1_login] 发送临时密码到 " + to + " (玩家: " + player + ")");
|
||||
return sendEmailSync(to, "Sdf1_login 临时密码",
|
||||
"玩家 " + player + " 的临时密码: " + tempPwd + "\n请尽快登录并修改密码!");
|
||||
}
|
||||
|
||||
private boolean sendEmailSync(String to, String subject, String body) {
|
||||
final boolean[] result = {false};
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
result[0] = sendEmail(to, subject, body);
|
||||
} catch (Exception e) {
|
||||
logger.severe("[Sdf1_login] 邮件线程异常: " + e.getMessage());
|
||||
}
|
||||
}, "Sdf1_login-SMTP");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
try { t.join(15000); } catch (InterruptedException ignored) {}
|
||||
if (t.isAlive()) {
|
||||
t.interrupt();
|
||||
logger.severe("[Sdf1_login] SMTP超时(15秒)");
|
||||
return false;
|
||||
}
|
||||
return result[0];
|
||||
}
|
||||
|
||||
private boolean sendEmail(String to, String subject, String body) {
|
||||
String host = config.getSmtp("smtp地址");
|
||||
String portStr = config.getSmtp("smtp端口");
|
||||
String user = config.getSmtp("发件邮箱");
|
||||
String pass = config.getSmtp("授权码");
|
||||
String fromName = config.getSmtp("发件人名");
|
||||
|
||||
int port;
|
||||
try { port = Integer.parseInt(portStr); } catch (Exception e) { port = 465; }
|
||||
|
||||
try {
|
||||
String encSubject = "=?UTF-8?B?" + Base64.getEncoder().encodeToString(subject.getBytes("UTF-8")) + "?=";
|
||||
String encBody = Base64.getEncoder().encodeToString(body.getBytes("UTF-8"));
|
||||
|
||||
if (port == 465) {
|
||||
SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
|
||||
SSLSocket socket = (SSLSocket) factory.createSocket(host, port);
|
||||
socket.setSoTimeout(10000);
|
||||
doSmtp(socket, host, user, pass, to, encSubject, encBody, fromName);
|
||||
socket.close();
|
||||
} else {
|
||||
Socket plainSocket = new Socket(host, port);
|
||||
plainSocket.setSoTimeout(10000);
|
||||
BufferedReader preReader = new BufferedReader(new InputStreamReader(plainSocket.getInputStream(), "UTF-8"));
|
||||
PrintWriter preWriter = new PrintWriter(plainSocket.getOutputStream(), true);
|
||||
checkResponse(preReader, "220");
|
||||
sendLine(preWriter, "EHLO " + host);
|
||||
checkResponse(preReader, "250");
|
||||
sendLine(preWriter, "STARTTLS");
|
||||
checkResponse(preReader, "220");
|
||||
SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
|
||||
SSLSocket sslSocket = (SSLSocket) factory.createSocket(plainSocket, host, port, true);
|
||||
sslSocket.startHandshake();
|
||||
doSmtp(sslSocket, host, user, pass, to, encSubject, encBody, fromName);
|
||||
sslSocket.close();
|
||||
}
|
||||
logger.info("[Sdf1_login] 邮件发送成功 -> " + to);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.severe("[Sdf1_login] 邮件异常: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void doSmtp(SSLSocket socket, String host, String user, String pass,
|
||||
String to, String subject, String body, String fromName) throws Exception {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
|
||||
OutputStream rawOut = socket.getOutputStream();
|
||||
PrintWriter writer = new PrintWriter(new java.io.BufferedOutputStream(rawOut), true);
|
||||
|
||||
checkResponse(reader, "220");
|
||||
sendLine(writer, "EHLO " + host);
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "AUTH LOGIN");
|
||||
checkResponse(reader, "334");
|
||||
sendLine(writer, Base64.getEncoder().encodeToString(user.getBytes("UTF-8")));
|
||||
checkResponse(reader, "334");
|
||||
sendLine(writer, Base64.getEncoder().encodeToString(pass.getBytes("UTF-8")));
|
||||
checkResponse(reader, "235");
|
||||
sendLine(writer, "MAIL FROM:<" + user + ">");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "RCPT TO:<" + to + ">");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "DATA");
|
||||
checkResponse(reader, "354");
|
||||
sendLine(writer, "From: " + fromName + " <" + user + ">");
|
||||
sendLine(writer, "To: <" + to + ">");
|
||||
sendLine(writer, "Reply-To: <" + user + ">");
|
||||
sendLine(writer, "Subject: " + subject);
|
||||
sendLine(writer, "MIME-Version: 1.0");
|
||||
sendLine(writer, "Content-Type: text/plain; charset=UTF-8");
|
||||
sendLine(writer, "Content-Transfer-Encoding: base64");
|
||||
sendLine(writer, "");
|
||||
sendLine(writer, body);
|
||||
sendLine(writer, ".");
|
||||
checkResponse(reader, "250");
|
||||
sendLine(writer, "QUIT");
|
||||
}
|
||||
|
||||
private void sendLine(PrintWriter writer, String line) {
|
||||
writer.print(line + "\r\n");
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
private void checkResponse(BufferedReader reader, String expected) throws Exception {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
sb.append(line).append("\n");
|
||||
if (line.length() >= 4 && Character.isDigit(line.charAt(0)) && line.charAt(3) == ' ') break;
|
||||
}
|
||||
String resp = sb.toString().trim();
|
||||
if (!resp.startsWith(expected)) {
|
||||
if (resp.startsWith("535")) logger.severe("[Sdf1_login] SMTP 535 认证失败");
|
||||
throw new Exception("SMTP响应异常: 期望 " + expected + ",实际: " + resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,861 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.*;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class GUIManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
public static final String T_MAIN =
|
||||
"§6§l登录面板";
|
||||
public static final String T_MY_INFO =
|
||||
"§a§l我的信息";
|
||||
public static final String T_ADMIN =
|
||||
"§c§l管理员面板";
|
||||
public static final String T_USER_MGMT =
|
||||
"§e§l用户管理";
|
||||
public static final String T_INVITE =
|
||||
"§b§l邀请数据";
|
||||
public static final String T_TASK_CENTER = "§d§l任务中心";
|
||||
public static final String T_GIFT_STAGES = "§d§l新人任务";
|
||||
|
||||
|
||||
public GUIManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void openMain(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 27, T_MAIN);
|
||||
fillBg(g);
|
||||
g.setItem(10, mkItem(Material.PLAYER_HEAD,
|
||||
"§a§l我的信息"));
|
||||
g.setItem(12, mkItem(Material.PAPER,
|
||||
"§b§l邀请数据"));
|
||||
g.setItem(14, mkItem(Material.EMERALD,
|
||||
"§d§l积分商城"));
|
||||
g.setItem(16, mkItem(Material.CHEST,
|
||||
"§e§l任务中心"));
|
||||
g.setItem(18, mkItem(Material.BOOK,
|
||||
"§6工单系统",
|
||||
"§7提交bug、求助、举报"));
|
||||
if (p.isOp() || isAdmin(p)) {
|
||||
g.setItem(20, mkItem(Material.CHEST,
|
||||
"§6垃圾回收站",
|
||||
"§7查看和取回清理的物品"));
|
||||
}
|
||||
if (isAdmin(p)) {
|
||||
g.setItem(22, mkItem(Material.REDSTONE_BLOCK,
|
||||
"§c§l管理员面板"));
|
||||
}
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
public void openMyInfo(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 27, T_MY_INFO);
|
||||
fillBg(g);
|
||||
Map<String, Object> user = plugin.getDb()
|
||||
.getUser(p.getName());
|
||||
boolean checkedIn = plugin.getCheckIn()
|
||||
.isCheckedInToday(p.getName());
|
||||
Material checkMat = checkedIn
|
||||
? Material.EMERALD : Material.COAL;
|
||||
String checkName = checkedIn
|
||||
? "§a已签到" : "§7点击签到";
|
||||
int streak = ((Number) user.getOrDefault(
|
||||
"checkin_streak", 0)).intValue();
|
||||
int points = ((Number) user.getOrDefault(
|
||||
"points", 0)).intValue();
|
||||
int totalDays = ((Number) user.getOrDefault(
|
||||
"total_checkin_days", 0)).intValue();
|
||||
String email = (String) user.getOrDefault(
|
||||
"email", "");
|
||||
g.setItem(4, mkItem(Material.NAME_TAG,
|
||||
"§e" + p.getName()));
|
||||
g.setItem(10, mkItem(checkMat, checkName,
|
||||
"§7连续签到: " + streak + "天",
|
||||
"§7累计签到: " + totalDays + "天",
|
||||
"§7当前积分: " + points));
|
||||
g.setItem(11, mkItem(Material.PAPER,
|
||||
"§e修改密码", "§7双击修改密码"));
|
||||
g.setItem(12, mkItem(Material.CLOCK,
|
||||
"§e补签", "§7消耗10积分补签3天内"));
|
||||
g.setItem(13, mkItem(Material.ENDER_PEARL,
|
||||
"§e邀请码",
|
||||
"§7你的邀请码: "
|
||||
+ user.getOrDefault(
|
||||
"invite_code", "无"),
|
||||
"§7点击生成/查看"));
|
||||
g.setItem(14, mkItem(Material.BOOK,
|
||||
"§e邮箱: "
|
||||
+ (email.isEmpty()
|
||||
? "未绑定" : email),
|
||||
"§7点击绑定邮箱"));
|
||||
g.setItem(15, mkItem(Material.EMERALD_BLOCK,
|
||||
"§a§l积分商城",
|
||||
"§7当前: " + points + "积分"));
|
||||
g.setItem(26, mkItem(Material.ARROW, "§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开账号删除选择面板
|
||||
* 展示该玩家IP下所有账号,供选择删除
|
||||
*/
|
||||
public void openAccountDelete(Player p) {
|
||||
String ip = plugin.getPlayerIP(p);
|
||||
if (ip == null) {
|
||||
p.sendMessage("§c无法获取你的IP地址");
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> accounts =
|
||||
plugin.getDb().getAccountDetailsByIP(ip);
|
||||
|
||||
// 54格大箱子
|
||||
org.bukkit.inventory.Inventory g =
|
||||
org.bukkit.Bukkit.createInventory(
|
||||
null, 54,
|
||||
"§c§l选择要删除的账号");
|
||||
|
||||
// 填充玻璃面板
|
||||
org.bukkit.inventory.ItemStack glass =
|
||||
new org.bukkit.inventory.ItemStack(
|
||||
org.bukkit.Material
|
||||
.RED_STAINED_GLASS_PANE);
|
||||
org.bukkit.inventory.meta.ItemMeta gm =
|
||||
glass.getItemMeta();
|
||||
if (gm != null) {
|
||||
gm.setDisplayName(" ");
|
||||
glass.setItemMeta(gm);
|
||||
}
|
||||
for (int i = 0; i < 54; i++) {
|
||||
g.setItem(i, glass);
|
||||
}
|
||||
|
||||
// 展示账号列表(从0号位开始放)
|
||||
java.text.SimpleDateFormat sdf =
|
||||
new java.text.SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm");
|
||||
int slot = 0;
|
||||
for (Map<String, Object> acc : accounts) {
|
||||
if (slot >= 45) break;
|
||||
String accName = (String) acc
|
||||
.get("player_name");
|
||||
long regTime = ((Number) acc
|
||||
.get("register_time")).longValue();
|
||||
String dateStr = regTime > 0
|
||||
? sdf.format(
|
||||
new java.util.Date(regTime))
|
||||
: "未知";
|
||||
|
||||
// 当前登录的账号用不同颜色标记
|
||||
boolean isCurrent =
|
||||
accName.equalsIgnoreCase(
|
||||
p.getName());
|
||||
|
||||
org.bukkit.inventory.ItemStack item =
|
||||
new org.bukkit.inventory.ItemStack(
|
||||
isCurrent
|
||||
? org.bukkit.Material
|
||||
.RED_WOOL
|
||||
: org.bukkit.Material
|
||||
.ORANGE_WOOL);
|
||||
org.bukkit.inventory.meta.ItemMeta im =
|
||||
item.getItemMeta();
|
||||
if (im != null) {
|
||||
im.setDisplayName(
|
||||
(isCurrent ? "§c" : "§e")
|
||||
+ accName);
|
||||
im.setLore(java.util.Arrays.asList(
|
||||
"§7注册时间: " + dateStr,
|
||||
"§7IP: " + ip,
|
||||
isCurrent
|
||||
? "§c§l(当前账号)"
|
||||
: "§a点击选择删除此账号"));
|
||||
item.setItemMeta(im);
|
||||
}
|
||||
g.setItem(slot, item);
|
||||
slot++;
|
||||
}
|
||||
|
||||
// 49号位:返回
|
||||
org.bukkit.inventory.ItemStack back =
|
||||
new org.bukkit.inventory.ItemStack(
|
||||
org.bukkit.Material.ARROW);
|
||||
org.bukkit.inventory.meta.ItemMeta backMeta =
|
||||
back.getItemMeta();
|
||||
if (backMeta != null) {
|
||||
backMeta.setDisplayName("§7返回");
|
||||
back.setItemMeta(backMeta);
|
||||
}
|
||||
g.setItem(49, back);
|
||||
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开删除确认面板
|
||||
*/
|
||||
public void openDeleteConfirm(Player p,
|
||||
String targetName) {
|
||||
org.bukkit.inventory.Inventory g =
|
||||
org.bukkit.Bukkit.createInventory(
|
||||
null, 27,
|
||||
"§c§l确认删除: " + targetName);
|
||||
|
||||
org.bukkit.inventory.ItemStack glass =
|
||||
new org.bukkit.inventory.ItemStack(
|
||||
org.bukkit.Material
|
||||
.GRAY_STAINED_GLASS_PANE);
|
||||
org.bukkit.inventory.meta.ItemMeta gm =
|
||||
glass.getItemMeta();
|
||||
if (gm != null) {
|
||||
gm.setDisplayName(" ");
|
||||
glass.setItemMeta(gm);
|
||||
}
|
||||
for (int i = 0; i < 27; i++) {
|
||||
g.setItem(i, glass);
|
||||
}
|
||||
|
||||
// 11号位:确认删除
|
||||
org.bukkit.inventory.ItemStack confirm =
|
||||
new org.bukkit.inventory.ItemStack(
|
||||
org.bukkit.Material.LIME_WOOL);
|
||||
org.bukkit.inventory.meta.ItemMeta cm =
|
||||
confirm.getItemMeta();
|
||||
if (cm != null) {
|
||||
cm.setDisplayName("§a§l确认删除");
|
||||
cm.setLore(java.util.Arrays.asList(
|
||||
"§7删除账号: §c" + targetName,
|
||||
"§c此操作不可撤销!"));
|
||||
confirm.setItemMeta(cm);
|
||||
}
|
||||
g.setItem(11, confirm);
|
||||
|
||||
// 15号位:取消
|
||||
org.bukkit.inventory.ItemStack cancel =
|
||||
new org.bukkit.inventory.ItemStack(
|
||||
org.bukkit.Material.RED_WOOL);
|
||||
org.bukkit.inventory.meta.ItemMeta cml =
|
||||
cancel.getItemMeta();
|
||||
if (cml != null) {
|
||||
cml.setDisplayName("§c§l取消");
|
||||
cancel.setItemMeta(cml);
|
||||
}
|
||||
g.setItem(15, cancel);
|
||||
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
public void openShopAdmin(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 54, "§c§l商城管理");
|
||||
ItemStack glass = new ItemStack(
|
||||
Material.GRAY_STAINED_GLASS_PANE);
|
||||
ItemMeta glm = glass.getItemMeta();
|
||||
if (glm != null) {
|
||||
glm.setDisplayName(" ");
|
||||
glass.setItemMeta(glm);
|
||||
}
|
||||
for (int i = 0; i < 54; i++)
|
||||
g.setItem(i, glass);
|
||||
g.setItem(49, mkItem(Material.ARROW,
|
||||
"§7返回商城"));
|
||||
g.setItem(50, mkItem(Material.EMERALD,
|
||||
"§a添加商品",
|
||||
"§7格式: /sdf1_login shopadd <名称> <价格> <命令>"));
|
||||
g.setItem(51, mkItem(Material.BARRIER,
|
||||
"§c删除商品",
|
||||
"§7格式: /sdf1_login shopdel <序号>"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
|
||||
public void openAdmin(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 27, T_ADMIN);
|
||||
fillBg(g);
|
||||
g.setItem(4, mkItem(Material.NAME_TAG,
|
||||
"§e§l管理员面板"));
|
||||
g.setItem(10, mkItem(Material.REDSTONE,
|
||||
"§6SMTP配置",
|
||||
"§7配置邮件服务器"));
|
||||
g.setItem(11, mkItem(Material.ARMOR_STAND,
|
||||
"§a用户管理",
|
||||
"§7管理玩家数据"));
|
||||
g.setItem(12, mkItem(Material.PLAYER_HEAD,
|
||||
"§a§l我的信息"));
|
||||
g.setItem(14, mkItem(Material.EMERALD,
|
||||
"§d§l积分商城"));
|
||||
boolean afkOn = plugin.getConfig2().afkEnabled;
|
||||
int afkMin = plugin.getConfig2().afkTimeout / 60;
|
||||
g.setItem(16, mkItem(
|
||||
afkOn ? Material.REDSTONE_TORCH
|
||||
: Material.TORCH,
|
||||
afkOn ? "§a§l挂机踢出: 开启"
|
||||
: "§c§l挂机踢出: 关闭",
|
||||
"§7左键: 切换开关",
|
||||
"§7右键: 设置时长",
|
||||
"§7当前: " + afkMin + "分钟"));
|
||||
g.setItem(22, mkItem(Material.ARROW, "§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
|
||||
public void openInvite(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 27, T_INVITE);
|
||||
fillBg(g);
|
||||
String code = (String) plugin.getDb()
|
||||
.getField(p.getName(), "invite_code");
|
||||
if (code == null || code.isEmpty()) {
|
||||
code = plugin.getInvite().generateCode(p);
|
||||
}
|
||||
int monthly = ((Number) plugin.getDb()
|
||||
.getField(p.getName(),
|
||||
"monthly_invite_count"))
|
||||
.intValue();
|
||||
g.setItem(10, mkItem(Material.PAPER,
|
||||
"§b§l我的邀请码",
|
||||
"§7" + code,
|
||||
"",
|
||||
"§e点击刷新邀请码"));
|
||||
g.setItem(12, mkItem(Material.PLAYER_HEAD,
|
||||
"§a本月邀请",
|
||||
"§7已邀请 §e" + monthly + " §7人"));
|
||||
g.setItem(16, mkItem(Material.BOOK,
|
||||
"§e输入邀请码",
|
||||
"§7绑定他人的邀请码",
|
||||
"",
|
||||
"§e点击输入"));
|
||||
g.setItem(26, mkItem(Material.ARROW, "§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
|
||||
private String getInviteRate(int count) {
|
||||
if (count > 15) return "60%";
|
||||
if (count > 10) return "35%";
|
||||
if (count > 5) return "15%";
|
||||
if (count > 0) return "10%";
|
||||
return "0%";
|
||||
}
|
||||
|
||||
public void openUserManagement(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 54, T_USER_MGMT);
|
||||
fillBg(g);
|
||||
List<Map<String, Object>> users = plugin.getDb()
|
||||
.getAllUsers();
|
||||
int slot = 0;
|
||||
for (Map<String, Object> user : users) {
|
||||
if (slot >= 45) break;
|
||||
String name = (String) user.get("player_name");
|
||||
int pts = ((Number) user.getOrDefault(
|
||||
"points", 0)).intValue();
|
||||
int stage = ((Number) user.getOrDefault(
|
||||
"gift_stage", 0)).intValue();
|
||||
g.setItem(slot, mkItem(Material.PLAYER_HEAD,
|
||||
"§e" + name,
|
||||
"§7积分: " + pts
|
||||
+ " 礼包阶段: " + stage,
|
||||
"§7双击管理"));
|
||||
slot++;
|
||||
}
|
||||
|
||||
g.setItem(53, mkItem(Material.ARROW, "§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
public void openTaskCenter(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 27, T_TASK_CENTER);
|
||||
fillBg(g);
|
||||
|
||||
QuestTracker qt =
|
||||
plugin.getQuestTracker();
|
||||
String player = p.getName();
|
||||
|
||||
// 新人任务进度
|
||||
List<QuestTracker.QuestFile> newQuests =
|
||||
qt.getQuests("新人任务");
|
||||
int newTotal = newQuests.size();
|
||||
int newDone =
|
||||
qt.getCompletedStageCount(
|
||||
player, newQuests);
|
||||
QuestTracker.QuestFile newCurrent =
|
||||
qt.getCurrentStage(
|
||||
player, newQuests);
|
||||
|
||||
String newInfo = newDone + "/" + newTotal;
|
||||
List<String> newLore = new ArrayList<>();
|
||||
newLore.add("§7进度: §e" + newInfo);
|
||||
if (newCurrent != null) {
|
||||
newLore.add("§7当前: §f"
|
||||
+ newCurrent.displayName);
|
||||
if (newCurrent.hasConditions()) {
|
||||
List<String> unmet =
|
||||
qt.getUnmetConditions(
|
||||
player,
|
||||
newCurrent.conditions);
|
||||
if (!unmet.isEmpty()) {
|
||||
for (String c : unmet) {
|
||||
newLore.add("§7 "
|
||||
+ qt.getProgress(
|
||||
player, c));
|
||||
}
|
||||
} else {
|
||||
newLore.add("§a✓ 条件已满足");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newLore.add("§a§l全部完成!");
|
||||
}
|
||||
|
||||
g.setItem(10, mkItem(Material.BOOK,
|
||||
"§e§l新人任务 §7["
|
||||
+ newInfo + "]",
|
||||
newLore.toArray(new String[0])));
|
||||
|
||||
// 主线任务
|
||||
List<QuestTracker.QuestFile> mainQuests =
|
||||
qt.getQuests("主线任务");
|
||||
g.setItem(12, mkItem(Material.COMPASS,
|
||||
"§b§l主线任务 §7["
|
||||
+ qt.getCompletedStageCount(
|
||||
player, mainQuests)
|
||||
+ "/" + mainQuests.size()
|
||||
+ "]",
|
||||
"§7查看主线任务列表"));
|
||||
|
||||
// 支线任务
|
||||
List<QuestTracker.QuestFile> sideQuests =
|
||||
qt.getQuests("支线任务");
|
||||
g.setItem(14, mkItem(Material.PAPER,
|
||||
"§a§l支线任务 §7["
|
||||
+ qt.getCompletedStageCount(
|
||||
player, sideQuests)
|
||||
+ "/" + sideQuests.size()
|
||||
+ "]",
|
||||
"§7查看支线任务列表"));
|
||||
|
||||
g.setItem(16, mkItem(Material.EMERALD,
|
||||
"§d§l每日签到",
|
||||
"§7签到获取积分"));
|
||||
|
||||
// 打卡上报按钮(slot 21)
|
||||
g.setItem(21, mkItem(
|
||||
Material.EMERALD,
|
||||
"§a§l打卡上报",
|
||||
"§7验证当前阶段任务完成情况",
|
||||
"§7满足条件将自动标记完成"));
|
||||
|
||||
// 返回按钮(slot 26)
|
||||
g.setItem(26, mkItem(
|
||||
Material.ARROW, "§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
|
||||
public void openGiftStages(Player p) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 54, T_GIFT_STAGES);
|
||||
fillBg(g);
|
||||
|
||||
QuestTracker qt =
|
||||
plugin.getQuestTracker();
|
||||
List<QuestTracker.QuestFile> quests =
|
||||
qt.getQuests("新人任务");
|
||||
int total = quests.size();
|
||||
int completed =
|
||||
qt.getCompletedStageCount(
|
||||
p.getName(), quests);
|
||||
QuestTracker.QuestFile current =
|
||||
qt.getCurrentStage(
|
||||
p.getName(), quests);
|
||||
|
||||
g.setItem(4, mkItem(Material.BOOK,
|
||||
"§e§l新人任务进度",
|
||||
"§7已完成: §e" + completed
|
||||
+ "/" + total,
|
||||
current != null
|
||||
? "§7当前: §f"
|
||||
+ current.displayName
|
||||
: "§a§l全部完成!"));
|
||||
|
||||
for (int i = 0;
|
||||
i < quests.size() && i < 44; i++) {
|
||||
QuestTracker.QuestFile qf =
|
||||
quests.get(i);
|
||||
boolean done =
|
||||
qt.isStageCompleted(
|
||||
p.getName(), qf);
|
||||
boolean isCurrent =
|
||||
(current != null
|
||||
&& current == qf);
|
||||
|
||||
Material mat;
|
||||
List<String> lore = new ArrayList<>();
|
||||
|
||||
if (done) {
|
||||
mat = Material.LIME_WOOL;
|
||||
} else if (isCurrent) {
|
||||
mat = Material.YELLOW_WOOL;
|
||||
} else {
|
||||
mat = Material.RED_WOOL;
|
||||
}
|
||||
|
||||
if (!qf.details.isEmpty()) {
|
||||
lore.add("§7---- 任务详情 ----");
|
||||
for (String d : qf.details) {
|
||||
lore.add("§f" + d);
|
||||
}
|
||||
}
|
||||
|
||||
if (qf.hasConditions()
|
||||
&& (done || isCurrent)) {
|
||||
lore.add("");
|
||||
for (String c : qf.conditions) {
|
||||
if (qt.isOptional(c))
|
||||
continue;
|
||||
if (qt.isConditionMet(
|
||||
p.getName(), c)) {
|
||||
lore.add("§7 §a✓ "
|
||||
+ qt.getProgress(
|
||||
p.getName(), c));
|
||||
} else {
|
||||
lore.add("§7 §c✗ "
|
||||
+ qt.getProgress(
|
||||
p.getName(), c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!qf.rewards.isEmpty()) {
|
||||
lore.add("");
|
||||
lore.add("§7---- 奖励 ----");
|
||||
for (String r : qf.rewards) {
|
||||
lore.add("§a" + r);
|
||||
}
|
||||
}
|
||||
|
||||
if (done
|
||||
&& !qf.rewards.isEmpty()
|
||||
&& !qt.hasClaimed(
|
||||
p.getName(), qf)) {
|
||||
lore.add("");
|
||||
lore.add("§e§l点击领取奖励");
|
||||
}
|
||||
if (done && qt.hasClaimed(
|
||||
p.getName(), qf)) {
|
||||
lore.add("");
|
||||
lore.add("§7已领取奖励");
|
||||
}
|
||||
|
||||
g.setItem(10 + i, mkItem(mat,
|
||||
"§e" + qf.displayName,
|
||||
lore.toArray(new String[0])));
|
||||
}
|
||||
// 返回按钮
|
||||
g.setItem(53, mkItem(
|
||||
Material.ARROW, "§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public void openTaskList(Player p,
|
||||
String folderName) {
|
||||
QuestTracker qt =
|
||||
plugin.getQuestTracker();
|
||||
List<QuestTracker.QuestFile> tasks =
|
||||
qt.getQuests(folderName);
|
||||
|
||||
int size = Math.min(54,
|
||||
Math.max(54,
|
||||
((tasks.size() / 9) + 1) * 9));
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, size,
|
||||
"§d§l" + folderName);
|
||||
fillBg(g);
|
||||
|
||||
for (int i = 0;
|
||||
i < tasks.size()
|
||||
&& i < 45; i++) {
|
||||
QuestTracker.QuestFile qf =
|
||||
tasks.get(i);
|
||||
boolean done =
|
||||
qt.isStageCompleted(
|
||||
p.getName(), qf);
|
||||
Material mat = done
|
||||
? Material.LIME_WOOL
|
||||
: Material.BOOK;
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
|
||||
// 解锁条件
|
||||
if (qf.hasConditions()) {
|
||||
lore.add("§7──── 解锁条件 ────");
|
||||
for (String c : qf.conditions) {
|
||||
if (qt.isOptional(c))
|
||||
continue;
|
||||
if (qt.isConditionMet(
|
||||
p.getName(), c)) {
|
||||
lore.add("§a ✓ "
|
||||
+ qt.getProgress(
|
||||
p.getName(), c));
|
||||
} else {
|
||||
lore.add("§c ✗ "
|
||||
+ qt.getProgress(
|
||||
p.getName(), c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 任务详情
|
||||
if (!qf.details.isEmpty()) {
|
||||
lore.add("");
|
||||
lore.add("§7──── 任务详情 ────");
|
||||
for (String d : qf.details) {
|
||||
lore.add("§f" + d);
|
||||
}
|
||||
}
|
||||
|
||||
// 任务奖励
|
||||
if (!qf.rewards.isEmpty()) {
|
||||
lore.add("");
|
||||
lore.add("§7──── 任务奖励 ────");
|
||||
for (String r : qf.rewards) {
|
||||
lore.add("§a" + r);
|
||||
}
|
||||
}
|
||||
|
||||
g.setItem(i, mkItem(mat,
|
||||
(done ? "§a✓ " : "§e")
|
||||
+ qf.displayName,
|
||||
lore.toArray(new String[0])));
|
||||
}
|
||||
|
||||
if (tasks.isEmpty()) {
|
||||
g.setItem(22, mkItem(
|
||||
Material.BARRIER,
|
||||
"§7暂无任务"));
|
||||
}
|
||||
g.setItem(size - 1, mkItem(
|
||||
Material.ARROW, "§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
|
||||
public void openUserDetail(Player p, String target) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 27, "§e§l管理: " + target);
|
||||
fillBg(g);
|
||||
Map<String, Object> user = plugin.getDb()
|
||||
.getUser(target);
|
||||
int pts = ((Number) user.getOrDefault(
|
||||
"points", 0)).intValue();
|
||||
String email = (String) user.getOrDefault(
|
||||
"email", "");
|
||||
g.setItem(4, mkItem(Material.NAME_TAG,
|
||||
"§e" + target));
|
||||
g.setItem(10, mkItem(Material.EMERALD,
|
||||
"§a积分: " + pts,
|
||||
"§7点击设置积分"));
|
||||
g.setItem(11, mkItem(Material.PAPER,
|
||||
"§e邮箱: "
|
||||
+ (email.isEmpty() ? "无" : email)));
|
||||
g.setItem(12, mkItem(Material.BOOK,
|
||||
"§e设置密码"));
|
||||
g.setItem(16, mkItem(Material.BARRIER,
|
||||
"§c删除用户", "§7二次验证删除"));
|
||||
// 雪球菜单检测(slot 14)
|
||||
g.setItem(14, mkItem(
|
||||
Material.SNOWBALL,
|
||||
"§e§l雪球菜单",
|
||||
"§7点击为该玩家发放菜单雪球"));
|
||||
g.setItem(16, mkItem(
|
||||
Material.ENDER_EYE,
|
||||
"§b§l打卡记录",
|
||||
"§7查看该玩家的群系打卡记录"));
|
||||
|
||||
g.setItem(22, mkItem(Material.ARROW, "§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
public void openCheckInRecords(
|
||||
Player p, String target) {
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 54,
|
||||
"§b§l打卡记录: " + target);
|
||||
fillBg(g);
|
||||
|
||||
QuestTracker qt =
|
||||
plugin.getQuestTracker();
|
||||
List<String> records =
|
||||
qt.getPlayerRecords(target);
|
||||
|
||||
java.text.SimpleDateFormat sdf =
|
||||
new java.text.SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
int slot = 0;
|
||||
for (int i = records.size() - 1;
|
||||
i >= 0 && slot < 45; i--) {
|
||||
String r = records.get(i);
|
||||
String[] parts = r.split("\\|");
|
||||
if (parts.length < 7) continue;
|
||||
|
||||
String world = parts[1];
|
||||
String biome = parts[2];
|
||||
String category = parts[3];
|
||||
String x = parts[4];
|
||||
String y = parts[5];
|
||||
String z = parts[6];
|
||||
String timeStr = "";
|
||||
if (parts.length >= 8) {
|
||||
try {
|
||||
long ts = Long.parseLong(
|
||||
parts[7]);
|
||||
timeStr = sdf.format(
|
||||
new java.util.Date(
|
||||
ts));
|
||||
} catch (Exception e) {
|
||||
timeStr = parts[7];
|
||||
}
|
||||
}
|
||||
|
||||
g.setItem(slot, mkItem(
|
||||
Material.PAPER,
|
||||
"§e" + category + " - "
|
||||
+ biome,
|
||||
"§7时间: " + timeStr,
|
||||
"§7世界: " + world,
|
||||
"§7坐标: " + x
|
||||
+ ", " + y
|
||||
+ ", " + z));
|
||||
slot++;
|
||||
}
|
||||
|
||||
if (slot == 0) {
|
||||
g.setItem(22, mkItem(
|
||||
Material.BARRIER,
|
||||
"§7暂无打卡记录"));
|
||||
}
|
||||
|
||||
g.setItem(53, mkItem(
|
||||
Material.ARROW, "§7返回"));
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 打开账号请求面板
|
||||
* 玩家通过 /oa 命令触发
|
||||
*/
|
||||
public void openAccountRequest(Player p) {
|
||||
org.bukkit.inventory.Inventory g =
|
||||
org.bukkit.Bukkit.createInventory(
|
||||
null, 27,
|
||||
"§e§l账号请求");
|
||||
|
||||
// 填充玻璃面板
|
||||
org.bukkit.inventory.ItemStack glass =
|
||||
new org.bukkit.inventory.ItemStack(
|
||||
org.bukkit.Material
|
||||
.YELLOW_STAINED_GLASS_PANE);
|
||||
org.bukkit.inventory.meta.ItemMeta gm =
|
||||
glass.getItemMeta();
|
||||
if (gm != null) {
|
||||
gm.setDisplayName(" ");
|
||||
glass.setItemMeta(gm);
|
||||
}
|
||||
for (int i = 0; i < 27; i++) {
|
||||
g.setItem(i, glass);
|
||||
}
|
||||
|
||||
// 10号位:申请删除账号
|
||||
g.setItem(10, makeItem(
|
||||
org.bukkit.Material.BARRIER,
|
||||
"§c§l申请删除账号",
|
||||
"§7点击后确认删除您的账号",
|
||||
"§7此操作不可撤销"));
|
||||
|
||||
// 13号位:找回密码
|
||||
g.setItem(13, makeItem(
|
||||
org.bukkit.Material.TRIPWIRE_HOOK,
|
||||
"§e§l找回密码",
|
||||
"§7通过绑定邮箱获取临时密码",
|
||||
"§7需要先绑定邮箱"));
|
||||
|
||||
// 16号位:联系管理员
|
||||
g.setItem(16, makeItem(
|
||||
org.bukkit.Material.EMERALD,
|
||||
"§a§l联系管理员",
|
||||
"§7遇到问题?向管理员提交工单"));
|
||||
|
||||
// 22号位:返回主菜单
|
||||
g.setItem(22, makeItem(
|
||||
org.bukkit.Material.ARROW,
|
||||
"§7返回"));
|
||||
|
||||
p.openInventory(g);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建物品的工具方法
|
||||
*/
|
||||
private org.bukkit.inventory.ItemStack makeItem(
|
||||
org.bukkit.Material mat,
|
||||
String displayName,
|
||||
String... lore) {
|
||||
org.bukkit.inventory.ItemStack item =
|
||||
new org.bukkit.inventory.ItemStack(mat);
|
||||
org.bukkit.inventory.meta.ItemMeta meta =
|
||||
item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.setDisplayName(displayName);
|
||||
if (lore.length > 0) {
|
||||
meta.setLore(
|
||||
java.util.Arrays.asList(lore));
|
||||
}
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
private boolean isAdmin(Player p) {
|
||||
return p.getScoreboardTags().contains(
|
||||
plugin.getConfig2().adminTag);
|
||||
}
|
||||
|
||||
private void fillBg(Inventory inv) {
|
||||
ItemStack gl = mkItem(
|
||||
Material.GRAY_STAINED_GLASS_PANE, " ");
|
||||
for (int i = 0; i < inv.getSize(); i++)
|
||||
inv.setItem(i, gl);
|
||||
}
|
||||
|
||||
private ItemStack mkItem(Material mat, String name,
|
||||
String... lore) {
|
||||
ItemStack it = new ItemStack(mat);
|
||||
ItemMeta im = it.getItemMeta();
|
||||
if (im != null) {
|
||||
im.setDisplayName(name);
|
||||
if (lore.length > 0)
|
||||
im.setLore(Arrays.asList(lore));
|
||||
it.setItemMeta(im);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
}
|
||||
@@ -1,568 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.*;
|
||||
import java.sql.Date;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
public class GarbageManager {
|
||||
|
||||
private final Main plugin;
|
||||
private Connection db;
|
||||
private int cleanupInterval = 1800;
|
||||
private int maxRounds = 1;
|
||||
private boolean enabled = true;
|
||||
private int currentRound = 0;
|
||||
private final Map<String, Integer>
|
||||
recyclePages = new HashMap<>();
|
||||
|
||||
|
||||
public GarbageManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
File dbFile = new File(
|
||||
plugin.getDataFolder(),
|
||||
"garbage.db");
|
||||
db = DriverManager.getConnection(
|
||||
"jdbc:sqlite:"
|
||||
+ dbFile.getAbsolutePath());
|
||||
Statement st = db.createStatement();
|
||||
st.execute("PRAGMA journal_mode=WAL");
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
+ "items ("
|
||||
+ "id INTEGER PRIMARY KEY "
|
||||
+ "AUTOINCREMENT,"
|
||||
+ "item_data TEXT NOT NULL,"
|
||||
+ "amount INTEGER DEFAULT 1,"
|
||||
+ "round INTEGER DEFAULT 0,"
|
||||
+ "create_time INTEGER NOT NULL"
|
||||
+ ")");
|
||||
st.execute("CREATE TABLE IF NOT EXISTS "
|
||||
+ "config ("
|
||||
+ "key TEXT PRIMARY KEY,"
|
||||
+ "value TEXT NOT NULL"
|
||||
+ ")");
|
||||
st.close();
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().severe(
|
||||
"[Garbage] DB失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void loadConfig() {
|
||||
// 从 ConfigManager 读取
|
||||
cleanupInterval =
|
||||
plugin.getConfig2()
|
||||
.garbageInterval;
|
||||
enabled =
|
||||
plugin.getConfig2()
|
||||
.garbageEnabled;
|
||||
maxRounds =
|
||||
plugin.getConfig2()
|
||||
.garbageMaxRounds;
|
||||
|
||||
// 从数据库读取当前轮次
|
||||
try {
|
||||
String val = getConfigValue("round");
|
||||
if (val != null)
|
||||
currentRound =
|
||||
Integer.parseInt(val);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Garbage] 间隔: "
|
||||
+ cleanupInterval + "秒, "
|
||||
+ "轮次保留: "
|
||||
+ maxRounds + "轮, "
|
||||
+ "启用: " + enabled);
|
||||
}
|
||||
|
||||
|
||||
private String getConfigValue(String key) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT value FROM config "
|
||||
+ "WHERE key=?");
|
||||
ps.setString(1, key);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
String val = rs.next()
|
||||
? rs.getString("value") : null;
|
||||
rs.close();
|
||||
ps.close();
|
||||
return val;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void setConfigValue(String key,
|
||||
String value) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT OR REPLACE INTO config "
|
||||
+ "(key, value) "
|
||||
+ "VALUES (?, ?)");
|
||||
ps.setString(1, key);
|
||||
ps.setString(2, value);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void startAutoCleanup() {
|
||||
if (!enabled) return;
|
||||
new BukkitRunnable() {
|
||||
int countdown = 0;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// 在线人数≤0时暂停
|
||||
if (Bukkit.getOnlinePlayers()
|
||||
.isEmpty()) {
|
||||
countdown = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// 重新读取配置(支持热更)
|
||||
cleanupInterval =
|
||||
plugin.getConfig2()
|
||||
.garbageInterval;
|
||||
|
||||
int remain =
|
||||
cleanupInterval - countdown;
|
||||
if (remain == 300)
|
||||
broadcast("§e§l[垃圾站] §f距离清理还有 §e5分钟");
|
||||
else if (remain == 180)
|
||||
broadcast("§e§l[垃圾站] §f距离清理还有 §e3分钟");
|
||||
else if (remain == 60)
|
||||
broadcast("§e§l[垃圾站] §f距离清理还有 §e1分钟");
|
||||
else if (remain == 30)
|
||||
broadcast("§e§l[垃圾站] §f距离清理还有 §e30秒");
|
||||
else if (remain == 10)
|
||||
broadcast("§e§l[垃圾站] §f距离清理还有 §e10秒");
|
||||
else if (remain <= 5 && remain > 0)
|
||||
broadcast("§e§l[垃圾站] §c" + remain + "§f...");
|
||||
else if (remain == 0) {
|
||||
doCleanup();
|
||||
countdown = 0;
|
||||
return;
|
||||
}
|
||||
countdown++;
|
||||
}
|
||||
}.runTaskTimer(plugin, 20L, 20L);
|
||||
}
|
||||
|
||||
private void doCleanup() {
|
||||
// 删除过期轮次的物品
|
||||
int deleteRound =
|
||||
currentRound - maxRounds;
|
||||
if (deleteRound >= 0) {
|
||||
int deleted = deleteOldItems(
|
||||
deleteRound);
|
||||
if (deleted > 0) {
|
||||
broadcast("§7[垃圾站] §f永久清理了 §e"
|
||||
+ deleted + " §f个过期物品");
|
||||
}
|
||||
}
|
||||
|
||||
// 递增轮次
|
||||
currentRound++;
|
||||
setConfigValue("round",
|
||||
String.valueOf(currentRound));
|
||||
|
||||
// 收集当前掉落物
|
||||
int count = collectItems();
|
||||
broadcast("§a§l[垃圾站] §f清理完成,"
|
||||
+ "共清理 §e" + count
|
||||
+ " §f个掉落物,存入回收站");
|
||||
|
||||
// 打印下次清理时间
|
||||
long nextMs = System.currentTimeMillis()
|
||||
+ (long) cleanupInterval * 1000L;
|
||||
String nextTime =
|
||||
new SimpleDateFormat("HH:mm:ss")
|
||||
.format(new Date(nextMs));
|
||||
broadcast("§e§l[垃圾站] §f下次清理时间: §a"
|
||||
+ nextTime);
|
||||
}
|
||||
|
||||
public int collectItems() {
|
||||
int count = 0;
|
||||
for (org.bukkit.World w :
|
||||
Bukkit.getWorlds()) {
|
||||
Collection<Item> items =
|
||||
w.getEntitiesByClass(Item.class);
|
||||
for (Item item : items) {
|
||||
ItemStack stack =
|
||||
item.getItemStack();
|
||||
if (stack == null
|
||||
|| stack.getType()
|
||||
== Material.AIR)
|
||||
continue;
|
||||
saveItem(stack);
|
||||
item.remove();
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public void saveItem(ItemStack stack) {
|
||||
{
|
||||
try {
|
||||
String data =
|
||||
stackToBase64(stack);
|
||||
if (data.isEmpty()) return;
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"INSERT INTO items "
|
||||
+ "(item_data, amount, "
|
||||
+ "round, create_time) "
|
||||
+ "VALUES (?,?,?,?)");
|
||||
ps.setString(1, data);
|
||||
ps.setInt(2, stack.getAmount());
|
||||
ps.setInt(3, currentRound);
|
||||
ps.setLong(4,
|
||||
System.currentTimeMillis());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}}
|
||||
|
||||
private int deleteOldItems(int beforeRound) {
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"DELETE FROM items "
|
||||
+ "WHERE round <= ?");
|
||||
ps.setInt(1, beforeRound);
|
||||
int rows = ps.executeUpdate();
|
||||
ps.close();
|
||||
return rows;
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 序列化 ==========
|
||||
|
||||
private String stackToBase64(ItemStack stack) {
|
||||
try {
|
||||
Map<String, Object> map =
|
||||
stack.serialize();
|
||||
String json = new Gson().toJson(map);
|
||||
return Base64.getEncoder()
|
||||
.encodeToString(
|
||||
json.getBytes(
|
||||
StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack base64ToStack(String base64) {
|
||||
try {
|
||||
byte[] bytes = Base64.getDecoder()
|
||||
.decode(base64);
|
||||
String json = new String(bytes,
|
||||
StandardCharsets.UTF_8);
|
||||
Type type = new TypeToken<
|
||||
Map<String, Object>>() {}
|
||||
.getType();
|
||||
Map<String, Object> map =
|
||||
new Gson().fromJson(json, type);
|
||||
map = fixNumberTypes(map);
|
||||
return ItemStack.deserialize(map);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> fixNumberTypes(
|
||||
Map<String, Object> map) {
|
||||
Map<String, Object> result =
|
||||
new LinkedHashMap<>();
|
||||
for (Map.Entry<String, Object> e
|
||||
: map.entrySet()) {
|
||||
Object v = e.getValue();
|
||||
if (v instanceof Double) {
|
||||
double d = (Double) v;
|
||||
if (d == (long) d) {
|
||||
result.put(e.getKey(), (long) d);
|
||||
} else {
|
||||
result.put(e.getKey(), d);
|
||||
}
|
||||
} else if (v instanceof Map) {
|
||||
result.put(e.getKey(),
|
||||
fixNumberTypes(
|
||||
(Map<String, Object>) v));
|
||||
} else if (v instanceof List) {
|
||||
List<Object> list =
|
||||
new ArrayList<>();
|
||||
for (Object item : (List<?>) v) {
|
||||
if (item instanceof Map) {
|
||||
list.add(fixNumberTypes(
|
||||
(Map<String, Object>) item));
|
||||
} else {
|
||||
list.add(item);
|
||||
}
|
||||
}
|
||||
result.put(e.getKey(), list);
|
||||
} else {
|
||||
result.put(e.getKey(), v);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ========== 回收站GUI ==========
|
||||
public void openRecycle(Player p) {
|
||||
openRecyclePage(p, 0);
|
||||
}
|
||||
|
||||
public void openRecyclePage(Player p, int page) {
|
||||
|
||||
Inventory g = Bukkit.createInventory(
|
||||
null, 54, "§6§l垃圾回收站");
|
||||
|
||||
// 获取全部存储物品
|
||||
List<Map<String, Object>> allItems =
|
||||
getStoredItems(999);
|
||||
int total = allItems.size();
|
||||
int pageSize = 40;
|
||||
int totalPages = Math.max(1,
|
||||
(int) Math.ceil(
|
||||
(double) total / pageSize));
|
||||
|
||||
// 修正页码
|
||||
if (page < 0) page = 0;
|
||||
if (page >= totalPages)
|
||||
page = totalPages - 1;
|
||||
|
||||
recyclePages.put(p.getName(), page);
|
||||
|
||||
// 填充物品(0-39 格 = 第1页,40-79 = 第2页...)
|
||||
int start = page * pageSize;
|
||||
int end = Math.min(start + pageSize,
|
||||
total);
|
||||
int slot = 0;
|
||||
for (int i = start; i < end; i++) {
|
||||
if (slot >= 40) break;
|
||||
Map<String, Object> item =
|
||||
allItems.get(i);
|
||||
int id = ((Number) item.get("id"))
|
||||
.intValue();
|
||||
String data = (String) item
|
||||
.get("item_data");
|
||||
int amount = ((Number) item
|
||||
.get("amount")).intValue();
|
||||
ItemStack stack = base64ToStack(data);
|
||||
if (stack != null) {
|
||||
stack.setAmount(amount);
|
||||
ItemMeta im =
|
||||
stack.getItemMeta();
|
||||
if (im != null) {
|
||||
List<String> lore =
|
||||
new ArrayList<>();
|
||||
lore.add("§7ID: #" + id);
|
||||
lore.add("§a点击取出");
|
||||
im.setLore(lore);
|
||||
stack.setItemMeta(im);
|
||||
}
|
||||
g.setItem(slot, stack);
|
||||
}
|
||||
slot++;
|
||||
}
|
||||
|
||||
// 45-48 格:额外投放区(空位)
|
||||
// 52-53 格:额外投放区(空位)
|
||||
|
||||
// 49 格:翻页-上一页
|
||||
if (page > 0) {
|
||||
g.setItem(49, mk(Material.ARROW,
|
||||
"§e上一页 §7("
|
||||
+ page + "/"
|
||||
+ totalPages + ")"));
|
||||
} else {
|
||||
g.setItem(49, mk(Material.ARROW,
|
||||
"§e第一页 §7("
|
||||
+ "1/" + totalPages + ")"));
|
||||
}
|
||||
|
||||
// 50 格:统计信息
|
||||
g.setItem(50, mk(Material.CHEST,
|
||||
"§e回收站统计",
|
||||
"§7总存量: §f" + total + " 个",
|
||||
"§7当前页: §f" + (page + 1)
|
||||
+ "/" + totalPages));
|
||||
|
||||
// 51 格:翻页-下一页
|
||||
boolean canNext = total >= pageSize
|
||||
&& page < totalPages - 1;
|
||||
if (canNext) {
|
||||
g.setItem(51, mk(Material.ARROW,
|
||||
"§e下一页 §7("
|
||||
+ (page + 2) + "/"
|
||||
+ totalPages + ")"));
|
||||
} else {
|
||||
g.setItem(51, mk(Material.ARROW,
|
||||
"§7已是最后一页"));
|
||||
}
|
||||
|
||||
p.openInventory(g);
|
||||
}
|
||||
public int getRecyclePage(String name) {
|
||||
return recyclePages
|
||||
.getOrDefault(name, 0);
|
||||
}
|
||||
|
||||
|
||||
public boolean handleClick(Player p,
|
||||
String title, int slot) {
|
||||
if (!title.equals("§6§l垃圾回收站"))
|
||||
return false;
|
||||
// 49:上一页
|
||||
if (slot == 49) {
|
||||
int page = recyclePages
|
||||
.getOrDefault(p.getName(), 0);
|
||||
if (page > 0)
|
||||
openRecyclePage(p, page - 1);
|
||||
return true;
|
||||
}
|
||||
// 51:下一页
|
||||
if (slot == 51) {
|
||||
int page = recyclePages
|
||||
.getOrDefault(p.getName(), 0);
|
||||
openRecyclePage(p, page + 1);
|
||||
return true;
|
||||
}
|
||||
// 50/52/53:不处理
|
||||
if (slot >= 49 && slot <= 53)
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>>
|
||||
getStoredItems(int limit) {
|
||||
List<Map<String, Object>> list =
|
||||
new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps =
|
||||
db.prepareStatement(
|
||||
"SELECT id, item_data, "
|
||||
+ "amount "
|
||||
+ "FROM items "
|
||||
+ "ORDER BY "
|
||||
+ "create_time "
|
||||
+ "DESC LIMIT ?");
|
||||
ps.setInt(1, limit);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> row =
|
||||
new LinkedHashMap<>();
|
||||
row.put("id",
|
||||
rs.getInt("id"));
|
||||
row.put("item_data",
|
||||
rs.getString("item_data"));
|
||||
row.put("amount",
|
||||
rs.getInt("amount"));
|
||||
list.add(row);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public boolean removeItem(int id) {
|
||||
try {
|
||||
PreparedStatement ps =
|
||||
db.prepareStatement(
|
||||
"DELETE FROM items "
|
||||
+ "WHERE id=?");
|
||||
ps.setInt(1, id);
|
||||
int rows = ps.executeUpdate();
|
||||
ps.close();
|
||||
return rows > 0;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public int getItemCount() {
|
||||
try {
|
||||
Statement st = db.createStatement();
|
||||
ResultSet rs = st.executeQuery(
|
||||
"SELECT COUNT(*) FROM items");
|
||||
int count = 0;
|
||||
if (rs.next())
|
||||
count = rs.getInt(1);
|
||||
rs.close();
|
||||
st.close();
|
||||
return count;
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcast(String msg) {
|
||||
// 同步打印到控制台
|
||||
plugin.getLogger().info(
|
||||
msg.replaceAll("§[0-9a-fk-orA-FK-OR]", ""));
|
||||
// 同步打印到聊天栏
|
||||
Bukkit.broadcastMessage(msg);
|
||||
}
|
||||
|
||||
|
||||
private ItemStack mk(Material mat,
|
||||
String name, String... lore) {
|
||||
ItemStack it = new ItemStack(mat);
|
||||
ItemMeta im = it.getItemMeta();
|
||||
if (im != null) {
|
||||
im.setDisplayName(name);
|
||||
if (lore.length > 0) {
|
||||
List<String> list =
|
||||
new ArrayList<>();
|
||||
for (String l : lore)
|
||||
if (!l.isEmpty()) list.add(l);
|
||||
if (!list.isEmpty())
|
||||
im.setLore(list);
|
||||
}
|
||||
it.setItemMeta(im);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
if (db != null && !db.isClosed())
|
||||
db.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
|
||||
public class GiftManager {
|
||||
|
||||
private final File giftFolder;
|
||||
private final Main plugin;
|
||||
private final List<Map<String, Object>> stages =
|
||||
new ArrayList<>();
|
||||
|
||||
public GiftManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.giftFolder = new File(
|
||||
plugin.getDataFolder(), "新人礼包");
|
||||
}
|
||||
|
||||
public void loadStages() {
|
||||
stages.clear();
|
||||
if (!giftFolder.exists()) {
|
||||
giftFolder.mkdirs();
|
||||
writeDefaultStages();
|
||||
}
|
||||
File[] files = giftFolder.listFiles(
|
||||
(dir, name) -> name.endsWith(".md"));
|
||||
if (files == null) return;
|
||||
Arrays.sort(files);
|
||||
for (File f : files) {
|
||||
stages.add(parseMarkdown(f));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> parseMarkdown(File f) {
|
||||
Map<String, Object> stage = new LinkedHashMap<>();
|
||||
stage.put("stage", stages.size() + 1);
|
||||
stage.put("name",
|
||||
f.getName().replace(".md", ""));
|
||||
stage.put("rewards", new ArrayList<String>());
|
||||
try {
|
||||
BufferedReader r = new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(f),
|
||||
StandardCharsets.UTF_8));
|
||||
String line;
|
||||
List<String> rewards = new ArrayList<>();
|
||||
while ((line = r.readLine()) != null) {
|
||||
if (line.contains("礼包内容")
|
||||
|| line.contains("奖励")
|
||||
|| line.contains("任务内容")) {
|
||||
int colon = line.indexOf(':');
|
||||
if (colon < 0)
|
||||
colon = line.indexOf('\uff1a');
|
||||
if (colon >= 0) {
|
||||
rewards.add(
|
||||
line.substring(colon + 1)
|
||||
.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
stage.put("rewards", rewards);
|
||||
r.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return stage;
|
||||
}
|
||||
private void writeStage(String name, String content) {
|
||||
File f = new File(giftFolder, name);
|
||||
if (f.exists()) return;
|
||||
try (PrintWriter pw = new PrintWriter(
|
||||
new OutputStreamWriter(
|
||||
new FileOutputStream(f),
|
||||
StandardCharsets.UTF_8))) {
|
||||
pw.print(content);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
private void writeDefaultStages() {
|
||||
writeStage("第一阶段.md",
|
||||
"# 第一阶段\n\n## 领取条件\n\n- 无前级条件\n\n## 礼包内容\n\n- 皮革套装 × 1\n- 木制武器工具套装 × 1\n");
|
||||
writeStage("第二阶段.md",
|
||||
"# 第二阶段\n\n## 领取条件\n\n- 在线时长>1小时\n\n## 礼包内容\n\n- 500 游戏币\n");
|
||||
writeStage("第三阶段.md",
|
||||
"# 第三阶段\n\n## 领取条件\n\n- 破坏100个方块\n- 在线>1天\n\n## 礼包内容\n\n- 1500 游戏币\n- 铁质工具套装 × 1\n- 铁质防具 × 1\n");
|
||||
writeStage("第四阶段.md",
|
||||
"# 第四阶段\n\n## 领取条件\n\n- 破坏/放置300方块\n- 在线≥7天\n\n## 礼包内容\n\n- 钻石套装 × 1\n- 2000 游戏币\n");
|
||||
}
|
||||
|
||||
/** 从任意文件夹加载任务列表 */
|
||||
public List<Map<String, Object>> loadTasksFromFolder(
|
||||
File folder) {
|
||||
List<Map<String, Object>> tasks =
|
||||
new ArrayList<>();
|
||||
if (!folder.exists() || !folder.isDirectory())
|
||||
return tasks;
|
||||
File[] files = folder.listFiles(
|
||||
(dir, name) -> name.endsWith(".md"));
|
||||
if (files == null) return tasks;
|
||||
Arrays.sort(files);
|
||||
for (File f : files) {
|
||||
Map<String, Object> task =
|
||||
new LinkedHashMap<>();
|
||||
task.put("name",
|
||||
f.getName().replace(".md", ""));
|
||||
task.put("conditions",
|
||||
new ArrayList<String>());
|
||||
task.put("rewards",
|
||||
new ArrayList<String>());
|
||||
try {
|
||||
BufferedReader r = new BufferedReader(
|
||||
new InputStreamReader(
|
||||
new FileInputStream(f),
|
||||
StandardCharsets.UTF_8));
|
||||
String line;
|
||||
List<String> current = null;
|
||||
while ((line = r.readLine()) != null) {
|
||||
if (line.startsWith("# ")) {
|
||||
task.put("name",
|
||||
line.substring(2)
|
||||
.trim());
|
||||
} else if (line.contains("条件")
|
||||
|| line.contains("领取条件")
|
||||
|| line.contains("完成条件")) {
|
||||
current = (List<String>) task
|
||||
.get("conditions");
|
||||
} else if (line.contains("内容")
|
||||
|| line.contains("奖励")
|
||||
|| line.contains("任务内容")
|
||||
|| line.contains("礼包内容")) {
|
||||
current = (List<String>) task
|
||||
.get("rewards");
|
||||
} else if (line.startsWith("- ")
|
||||
&& current != null) {
|
||||
current.add(line.substring(2)
|
||||
.trim());
|
||||
}
|
||||
}
|
||||
r.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
tasks.add(task);
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
public boolean canClaim(Player p, int stage) {
|
||||
Map<String, Object> user = plugin.getDb()
|
||||
.getUser(p.getName());
|
||||
if (user.isEmpty()) return false;
|
||||
int curStage = ((Number) user.getOrDefault(
|
||||
"gift_stage", 0)).intValue();
|
||||
if (stage > curStage + 1) return false;
|
||||
if (stage < 1 || stage > stages.size())
|
||||
return false;
|
||||
String claimed = (String) user.getOrDefault(
|
||||
"gift_claimed", "");
|
||||
if (claimed.contains("[" + stage + "]"))
|
||||
return false;
|
||||
|
||||
// debug 绕过:检查 tasks_completed
|
||||
String completed = (String) user.getOrDefault(
|
||||
"tasks_completed", "");
|
||||
if (completed.contains("[" + stage + "]"))
|
||||
return true;
|
||||
|
||||
// 正常条件检查
|
||||
long dbTime = ((Number) user.getOrDefault(
|
||||
"total_online_time", 0L)).longValue();
|
||||
long sessionTime = 0;
|
||||
Object lastObj = plugin.getDb().getField(
|
||||
p.getName(), "last_online_check");
|
||||
if (lastObj != null && plugin.getLoggedIn()
|
||||
.contains(p.getName())) {
|
||||
long last = ((Number) lastObj).longValue();
|
||||
if (last > 0) {
|
||||
sessionTime =
|
||||
System.currentTimeMillis() - last;
|
||||
}
|
||||
}
|
||||
long onlineTime = dbTime + sessionTime;
|
||||
int broken = ((Number) user.getOrDefault(
|
||||
"blocks_broken", 0)).intValue();
|
||||
int placed = ((Number) user.getOrDefault(
|
||||
"blocks_placed", 0)).intValue();
|
||||
|
||||
switch (stage) {
|
||||
case 1: return true;
|
||||
case 2: return onlineTime >= 3600000;
|
||||
case 3: return broken >= 100
|
||||
&& onlineTime >= 86400000L;
|
||||
case 4: return (broken + placed) >= 300
|
||||
&& onlineTime >= 604800000L;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void claimReward(Player p, int stage) {
|
||||
if (!canClaim(p, stage)) return;
|
||||
Map<String, Object> user = plugin.getDb()
|
||||
.getUser(p.getName());
|
||||
String claimed = (String) user.getOrDefault(
|
||||
"gift_claimed", "");
|
||||
claimed += "[" + stage + "]";
|
||||
plugin.getDb().setField(
|
||||
p.getName(), "gift_claimed", claimed);
|
||||
plugin.getDb().setField(
|
||||
p.getName(), "gift_stage",
|
||||
Math.max(stage,
|
||||
((Number) user.getOrDefault(
|
||||
"gift_stage", 0))
|
||||
.intValue()));
|
||||
|
||||
if (stage == 1) {
|
||||
giveItem(p, Material.LEATHER_HELMET);
|
||||
giveItem(p, Material.LEATHER_CHESTPLATE);
|
||||
giveItem(p, Material.LEATHER_LEGGINGS);
|
||||
giveItem(p, Material.LEATHER_BOOTS);
|
||||
giveItem(p, Material.WOODEN_SWORD);
|
||||
giveItem(p, Material.WOODEN_PICKAXE);
|
||||
giveItem(p, Material.WOODEN_AXE);
|
||||
giveItem(p, Material.WOODEN_SHOVEL);
|
||||
} else if (stage == 2) {
|
||||
if (plugin.getEconomy() != null) {
|
||||
double before = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 500);
|
||||
double after = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
p.sendMessage("§a§l[礼包] §f游戏币: §e"
|
||||
+ (long) before
|
||||
+ "§f → §a" + (long) after);
|
||||
}
|
||||
} else if (stage == 3) {
|
||||
if (plugin.getEconomy() != null) {
|
||||
double before = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 1500);
|
||||
double after = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
p.sendMessage("§a§l[礼包] §f游戏币: §e"
|
||||
+ (long) before
|
||||
+ "§f → §a" + (long) after);
|
||||
}
|
||||
giveItem(p, Material.IRON_PICKAXE);
|
||||
giveItem(p, Material.IRON_AXE);
|
||||
giveItem(p, Material.IRON_SWORD);
|
||||
giveItem(p, Material.IRON_HELMET);
|
||||
giveItem(p, Material.IRON_CHESTPLATE);
|
||||
giveItem(p, Material.IRON_LEGGINGS);
|
||||
giveItem(p, Material.IRON_BOOTS);
|
||||
} else if (stage == 4) {
|
||||
if (plugin.getEconomy() != null) {
|
||||
double before = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 2000);
|
||||
double after = plugin.getEconomy()
|
||||
.getBalance(p);
|
||||
p.sendMessage("§a§l[礼包] §f游戏币: §e"
|
||||
+ (long) before
|
||||
+ "§f → §a" + (long) after);
|
||||
}
|
||||
giveItem(p, Material.DIAMOND_HELMET);
|
||||
giveItem(p, Material.DIAMOND_CHESTPLATE);
|
||||
giveItem(p, Material.DIAMOND_LEGGINGS);
|
||||
giveItem(p, Material.DIAMOND_BOOTS);
|
||||
}
|
||||
p.sendMessage(plugin.getConfig2().msg(
|
||||
"gift_claimed"));
|
||||
}
|
||||
|
||||
private void giveItem(Player p, Material mat) {
|
||||
p.getInventory().addItem(new ItemStack(mat));
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getStages() {
|
||||
return stages;
|
||||
}
|
||||
|
||||
public int getMaxStage() {
|
||||
return stages.size();
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IPGroupManager {
|
||||
|
||||
private final Main plugin;
|
||||
private int maxAccounts;
|
||||
|
||||
public IPGroupManager(Main plugin, int maxAccounts) {
|
||||
this.plugin = plugin;
|
||||
this.maxAccounts = maxAccounts;
|
||||
}
|
||||
|
||||
public int getMaxAccounts() {
|
||||
return maxAccounts;
|
||||
}
|
||||
|
||||
public void setMaxAccounts(int max) {
|
||||
this.maxAccounts = max;
|
||||
}
|
||||
|
||||
public List<String> getAccountsByIP(String ip) {
|
||||
return plugin.getDb().getAccountsByIP(ip);
|
||||
}
|
||||
|
||||
public int getAccountCount(String ip) {
|
||||
if (ip == null) return 0;
|
||||
return getAccountsByIP(ip).size();
|
||||
}
|
||||
|
||||
public boolean canRegister(String ip) {
|
||||
if (maxAccounts <= 0) return true;
|
||||
return getAccountCount(ip) < maxAccounts;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
public class InventorySerializer {
|
||||
|
||||
public static String toBase64(ItemStack[] items) throws IOException {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
BukkitObjectOutputStream dataOutput = new BukkitObjectOutputStream(outputStream);
|
||||
dataOutput.writeInt(items.length);
|
||||
for (ItemStack item : items) {
|
||||
dataOutput.writeObject(item);
|
||||
}
|
||||
dataOutput.close();
|
||||
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Unable to serialize item stacks.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static ItemStack[] fromBase64(String data) throws IOException {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
try {
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(data));
|
||||
BukkitObjectInputStream dataInput = new BukkitObjectInputStream(inputStream);
|
||||
ItemStack[] items = new ItemStack[dataInput.readInt()];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
items[i] = (ItemStack) dataInput.readObject();
|
||||
}
|
||||
dataInput.close();
|
||||
return items;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to deserialize item stacks.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
public class InviteManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
public InviteManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public String generateCode(Player p) {
|
||||
String code = PasswordUtils.generateInviteCode();
|
||||
plugin.getDb().setField(
|
||||
p.getName(), "invite_code", code);
|
||||
return code;
|
||||
}
|
||||
|
||||
public void onInviteeCheckIn(String inviteeName) {
|
||||
String invitedBy = (String) plugin.getDb()
|
||||
.getField(inviteeName, "invited_by");
|
||||
if (invitedBy == null || invitedBy.isEmpty()) return;
|
||||
|
||||
String month = new SimpleDateFormat("yyyy-MM")
|
||||
.format(new Date());
|
||||
String lastMonth = (String) plugin.getDb()
|
||||
.getField(invitedBy, "invite_month");
|
||||
|
||||
if (!month.equals(lastMonth)) {
|
||||
plugin.getDb().setField(
|
||||
invitedBy, "invite_month", month);
|
||||
plugin.getDb().setField(
|
||||
invitedBy, "monthly_invite_count", 0);
|
||||
}
|
||||
|
||||
int count = ((Number) plugin.getDb()
|
||||
.getField(invitedBy,
|
||||
"monthly_invite_count")).intValue();
|
||||
plugin.getDb().setField(
|
||||
invitedBy, "monthly_invite_count", count + 1);
|
||||
|
||||
double rate = 0.10;
|
||||
if (count + 1 > 15) {
|
||||
rate = 0.60;
|
||||
} else if (count + 1 > 10) {
|
||||
rate = 0.35;
|
||||
} else if (count + 1 > 5) {
|
||||
rate = 0.15;
|
||||
}
|
||||
|
||||
int bonus = (int) (1 * rate);
|
||||
if (bonus > 0) {
|
||||
plugin.getDb().addPoints(invitedBy, bonus);
|
||||
Player inviterPlayer = Bukkit
|
||||
.getPlayerExact(invitedBy);
|
||||
if (inviterPlayer != null
|
||||
&& inviterPlayer.isOnline()) {
|
||||
inviterPlayer.sendMessage(
|
||||
plugin.getConfig2().msg(
|
||||
"invite_referral_bonus",
|
||||
"points",
|
||||
String.valueOf(bonus)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getAllInviteData() {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Map<String, Object> user
|
||||
: plugin.getDb().getAllUsers()) {
|
||||
String code = (String) user.getOrDefault(
|
||||
"invite_code", "");
|
||||
if (!code.isEmpty()) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("player",
|
||||
user.get("player_name"));
|
||||
row.put("code", code);
|
||||
row.put("monthly",
|
||||
user.getOrDefault(
|
||||
"monthly_invite_count", 0));
|
||||
result.add(row);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class LoginManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
public LoginManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public boolean handleRegister(Player p, String[] args) {
|
||||
String name = p.getName();
|
||||
|
||||
if (plugin.getplugin.getDb()().userExists(name)) {
|
||||
p.sendMessage(plugin.getConfig2().msg("already_registered"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== IP注册限制检查 =====
|
||||
String ip = plugin.getPlayerIP(p);
|
||||
if (ip != null && plugin.getConfig2().maxAccountsPerIP > 0) {
|
||||
if (!plugin.getIPGroup().canRegister(ip)) {
|
||||
int count = plugin.getIPGroup().getAccountCount(ip);
|
||||
int max = plugin.getConfig2().maxAccountsPerIP;
|
||||
p.sendMessage("§c[Sdf1_login] §f该IP注册已达上限(" + count + "/" + max + ")");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (args.length < 1) {
|
||||
p.sendMessage("§c用法: /reg <密码>");
|
||||
p.sendMessage("§7密码要求: 6位以上,包含大小写字母和数字");
|
||||
return true;
|
||||
}
|
||||
|
||||
String pwd = args[0];
|
||||
if (pwd.length() < 6) {
|
||||
p.sendMessage("§c[Sdf1_login] §f密码长度不足6位");
|
||||
return true;
|
||||
}
|
||||
if (!pwd.matches(".*[a-z].*") || !pwd.matches(".*[A-Z].*") || !pwd.matches(".*[0-9].*")) {
|
||||
p.sendMessage("§c[Sdf1_login] §f密码必须包含大写字母、小写字母和数字");
|
||||
return true;
|
||||
}
|
||||
|
||||
String salt = PasswordUtils.generateSalt();
|
||||
String hash = PasswordUtils.hash(pwd, salt);
|
||||
plugin.getplugin.getDb()().createUser(name, hash, salt);
|
||||
plugin.getLoggedIn().add(name);
|
||||
plugin.getplugin.getDb()().setLoggedIn(name, true);
|
||||
plugin.getplugin.getDb()().setField(name, "last_login_time", System.currentTimeMillis());
|
||||
plugin.getplugin.getDb()().setField(name, "last_online_check", System.currentTimeMillis());
|
||||
plugin.getplugin.getDb()().recordIP(name, ip);
|
||||
plugin.getplugin.getDb()().setField(name, "register_time", System.currentTimeMillis());
|
||||
plugin.restoreInventory(p);
|
||||
plugin.recordIPLogin(p);
|
||||
|
||||
p.sendMessage(plugin.getConfig2().msg("reg_success"));
|
||||
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_CHIME, 1.0f, 0.79f);
|
||||
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1.0f, 0.79f);
|
||||
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 0.79f);
|
||||
|
||||
try {
|
||||
org.bukkit.plugin.Plugin cy = org.bukkit.Bukkit.getPluginManager().getPlugin("CY_beibao");
|
||||
if (cy != null && cy.isEnabled()) {
|
||||
cy.getClass().getMethod("onSdf1Activation", String.class, int.class, int.class)
|
||||
.invoke(cy, name, 9, 0);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean handleLogin(Player p,
|
||||
String[] args) {
|
||||
String name = p.getName();
|
||||
if (plugin.getLoggedIn().contains(name)) {
|
||||
p.sendMessage(plugin.getConfig2().msg(
|
||||
"already_logged_in"));
|
||||
return true;
|
||||
}
|
||||
if (!plugin.getplugin.getDb()().userExists(name)) {
|
||||
p.sendMessage(plugin.getConfig2().msg(
|
||||
"not_registered"));
|
||||
return true;
|
||||
}
|
||||
if (args.length < 1) {
|
||||
p.sendMessage("§c用法: /login <密码>");
|
||||
return true;
|
||||
}
|
||||
String pwd = args[0];
|
||||
|
||||
// 诊断:显示账号信息
|
||||
String salt = (String) plugin.getplugin.getDb()()
|
||||
.getField(name, "password_salt");
|
||||
String regType = (String) plugin.getplugin.getDb()()
|
||||
.getField(name, "register_type");
|
||||
String storedHash = (String) plugin.getplugin.getDb()()
|
||||
.getField(name, "password_hash");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] [登录诊断] 玩家: " + name);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] [登录诊断] 注册类型: "
|
||||
+ regType);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] [登录诊断] 盐值: " + salt);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] [登录诊断] 存储哈希: "
|
||||
+ (storedHash != null
|
||||
? storedHash.substring(0,
|
||||
Math.min(16,
|
||||
storedHash.length()))
|
||||
+ "..."
|
||||
: "null"));
|
||||
|
||||
if (salt == null) {
|
||||
p.sendMessage(
|
||||
"§c[Sdf1_login] §f数据异常,请联系管理员");
|
||||
plugin.getLogger().severe(
|
||||
"[Sdf1_login] 玩家 " + name
|
||||
+ " 盐值为null");
|
||||
return true;
|
||||
}
|
||||
|
||||
String hash = PasswordUtils.hash(pwd, salt);
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] [登录诊断] 输入密码哈希: "
|
||||
+ hash.substring(0,
|
||||
Math.min(16, hash.length()))
|
||||
+ "...");
|
||||
|
||||
// 检查是否匹配主密码
|
||||
boolean matchMain =
|
||||
plugin.getplugin.getDb()().checkPassword(name, hash);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] [登录诊断] 主密码匹配: "
|
||||
+ matchMain);
|
||||
|
||||
if (matchMain) {
|
||||
// 主密码匹配 → 正常登录
|
||||
plugin.getLoggedIn().add(name);
|
||||
plugin.getplugin.getDb()().setLoggedIn(name, true);
|
||||
plugin.getplugin.getDb()().setField(name,
|
||||
"last_login_time",
|
||||
System.currentTimeMillis());
|
||||
plugin.getplugin.getDb()().setField(name,
|
||||
"last_online_check",
|
||||
System.currentTimeMillis());
|
||||
plugin.restoreInventory(p);
|
||||
plugin.recordIPLogin(p);
|
||||
p.sendMessage(plugin.getConfig2().msg("login_success"));
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] " + name
|
||||
+ " 主密码登录成功");
|
||||
try {
|
||||
org.bukkit.plugin.Plugin cy =
|
||||
org.bukkit.Bukkit
|
||||
.getPluginManager()
|
||||
.getPlugin("CY_beibao");
|
||||
if (cy != null && cy.isEnabled()) {
|
||||
java.lang.reflect.Method m =
|
||||
cy.getClass().getMethod(
|
||||
"onSdf1Activation",
|
||||
String.class,
|
||||
int.class, int.class);
|
||||
m.invoke(cy, name, 0, 0);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 主密码不匹配 → 检查临时密码
|
||||
boolean matchTemp =
|
||||
plugin.getplugin.getDb()().checkPasswordOrTemp(
|
||||
name, hash);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] [登录诊断] 临时密码匹配: "
|
||||
+ matchTemp);
|
||||
|
||||
if (matchTemp) {
|
||||
// 临时密码匹配 → 登录 + 强制改密
|
||||
plugin.getLoggedIn().add(name);
|
||||
plugin.getplugin.getDb()().setLoggedIn(name, true);
|
||||
plugin.getplugin.getDb()().setField(name,
|
||||
"last_login_time",
|
||||
System.currentTimeMillis());
|
||||
plugin.getplugin.getDb()().setField(name,
|
||||
"last_online_check",
|
||||
System.currentTimeMillis());
|
||||
plugin.restoreInventory(p);
|
||||
plugin.recordIPLogin(p);
|
||||
plugin.getNeedsPasswordChange().add(name);
|
||||
p.sendMessage(plugin.getConfig2().msg("login_success"));
|
||||
p.sendMessage(
|
||||
"§c§l[警告] §f您使用的是临时密码,"
|
||||
+ "请尽快修改密码!");
|
||||
p.sendMessage(
|
||||
"§7用法: /sdf1_login pw");
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] " + name
|
||||
+ " 临时密码登录成功,"
|
||||
+ "强制要求改密");
|
||||
try {
|
||||
org.bukkit.plugin.Plugin cy =
|
||||
org.bukkit.Bukkit
|
||||
.getPluginManager()
|
||||
.getPlugin("CY_beibao");
|
||||
if (cy != null && cy.isEnabled()) {
|
||||
java.lang.reflect.Method m =
|
||||
cy.getClass().getMethod(
|
||||
"onSdf1Activation",
|
||||
String.class,
|
||||
int.class, int.class);
|
||||
m.invoke(cy, name, 0, 0);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 都不匹配 → 登录失败
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] " + name
|
||||
+ " 密码验证失败");
|
||||
p.sendMessage(plugin.getConfig2().msg(
|
||||
"login_failed"));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void handleReset(Player p) {
|
||||
String name = p.getName();
|
||||
String plugin.getEmail()Addr = (String) plugin.getDb().getField(
|
||||
name, "plugin.getEmail()");
|
||||
if (plugin.getEmail()Addr == null
|
||||
|| plugin.getEmail()Addr.isEmpty()) {
|
||||
p.sendMessage(
|
||||
"§c未设置邮箱,无法重置密码");
|
||||
return;
|
||||
}
|
||||
String tempPwd = PasswordUtils
|
||||
.generateTempPassword();
|
||||
String salt = (String) plugin.getDb().getField(
|
||||
name, "password_salt");
|
||||
String hash = PasswordUtils.hash(
|
||||
tempPwd, salt);
|
||||
plugin.getDb().setField(name,
|
||||
"temp_password", hash);
|
||||
plugin.getDb().setField(name,
|
||||
"temp_pw_expire",
|
||||
System.currentTimeMillis()
|
||||
+ 3600000L);
|
||||
plugin.getDb().setField(name,
|
||||
"temp_pw_used", 0);
|
||||
p.sendMessage("§e正在发送临时密码...");
|
||||
final String to = plugin.getEmail()Addr;
|
||||
final String fName = name;
|
||||
final String pwd = tempPwd;
|
||||
Bukkit.getScheduler()
|
||||
.Bukkit.getScheduler().runTask(plugin, ...)Asynchronously(this,
|
||||
() -> {
|
||||
final boolean[] sent = {false};
|
||||
Thread t = new Thread(() -> {
|
||||
sent[0] = plugin.getEmail()
|
||||
.sendTempPassword(
|
||||
to, fName, pwd);
|
||||
}, "Sdf1_login-reset");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
try {
|
||||
t.join(15000);
|
||||
} catch (
|
||||
InterruptedException ignored) {
|
||||
}
|
||||
if (t.isAlive()) {
|
||||
t.interrupt();
|
||||
Bukkit.getScheduler().Bukkit.getScheduler().runTask(plugin, ...)(
|
||||
this, () ->
|
||||
p.sendMessage(
|
||||
"§c邮件发送超时"));
|
||||
return;
|
||||
}
|
||||
Bukkit.getScheduler().Bukkit.getScheduler().runTask(plugin, ...)(
|
||||
this, () -> {
|
||||
if (sent[0]) {
|
||||
p.sendMessage(
|
||||
"§a临时密码已发送到 "
|
||||
+ to);
|
||||
} else {
|
||||
p.sendMessage(
|
||||
"§c邮件发送失败");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
-3712
File diff suppressed because it is too large
Load Diff
@@ -1,72 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
public class PasswordUtils {
|
||||
|
||||
private static final SecureRandom RANDOM =
|
||||
new SecureRandom();
|
||||
|
||||
public static String generateSalt() {
|
||||
byte[] salt = new byte[16];
|
||||
RANDOM.nextBytes(salt);
|
||||
return Base64.getEncoder().encodeToString(salt);
|
||||
}
|
||||
|
||||
public static String hash(String password, String salt) {
|
||||
try {
|
||||
MessageDigest md =
|
||||
MessageDigest.getInstance("SHA-256");
|
||||
md.update(Base64.getDecoder().decode(salt));
|
||||
byte[] hash =
|
||||
md.digest(password.getBytes("UTF-8"));
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean validate(String password) {
|
||||
if (password == null) return false;
|
||||
if (password.length() < 6 || password.length() > 20)
|
||||
return false;
|
||||
boolean hasUpper = false;
|
||||
boolean hasLower = false;
|
||||
boolean hasDigit = false;
|
||||
for (char c : password.toCharArray()) {
|
||||
if (Character.isUpperCase(c)) hasUpper = true;
|
||||
if (Character.isLowerCase(c)) hasLower = true;
|
||||
if (Character.isDigit(c)) hasDigit = true;
|
||||
}
|
||||
return hasUpper && hasLower && hasDigit;
|
||||
}
|
||||
|
||||
public static String generateTempPassword() {
|
||||
String chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
StringBuilder sb = new StringBuilder(8);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
sb.append(chars.charAt(
|
||||
RANDOM.nextInt(chars.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String generateInviteCode() {
|
||||
String chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
StringBuilder sb = new StringBuilder(6);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
sb.append(chars.charAt(
|
||||
RANDOM.nextInt(chars.length())));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String generateVerifyCode() {
|
||||
int code = 100000 + RANDOM.nextInt(900000);
|
||||
return String.valueOf(code);
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
|
||||
public class PointsManager {
|
||||
|
||||
private final Main plugin;
|
||||
private Object cyInstance = null;
|
||||
private Method cyActivationMethod = null;
|
||||
private Method cyGetShopItemsMethod = null;
|
||||
|
||||
private final List<Map<String, Object>> cyItems =
|
||||
new ArrayList<>();
|
||||
|
||||
public PointsManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
discoverCY();
|
||||
}
|
||||
|
||||
private void discoverCY() {
|
||||
try {
|
||||
org.bukkit.plugin.Plugin p = Bukkit
|
||||
.getPluginManager()
|
||||
.getPlugin("CY_beibao");
|
||||
if (p != null && p.isEnabled()) {
|
||||
cyInstance = p;
|
||||
cyGetShopItemsMethod = p.getClass()
|
||||
.getMethod("onSdf1GetShopItems");
|
||||
cyActivationMethod = p.getClass()
|
||||
.getMethod("onSdf1Activation",
|
||||
String.class,
|
||||
int.class, int.class);
|
||||
plugin.getLogger().info(
|
||||
"[Sdf1_login] CY_beibao 联控已连接");
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// ===== CY 商品拉取 =====
|
||||
|
||||
private void refreshCYItems() {
|
||||
cyItems.clear();
|
||||
if (cyInstance == null
|
||||
|| cyGetShopItemsMethod == null) return;
|
||||
try {
|
||||
Object result =
|
||||
cyGetShopItemsMethod.invoke(cyInstance);
|
||||
if (result == null) return;
|
||||
String data = result.toString();
|
||||
if (data.isEmpty()) return;
|
||||
String[] items = data.split(";");
|
||||
for (String itemStr : items) {
|
||||
String[] p = itemStr.split("\\|");
|
||||
if (p.length < 8) continue;
|
||||
Map<String, Object> item =
|
||||
new LinkedHashMap<>();
|
||||
item.put("id", p[0]);
|
||||
item.put("name", p[1]);
|
||||
item.put("days",
|
||||
Integer.parseInt(p[2]));
|
||||
item.put("slots",
|
||||
Integer.parseInt(p[3]));
|
||||
item.put("price",
|
||||
Integer.parseInt(p[4]));
|
||||
item.put("stock",
|
||||
Integer.parseInt(p[5]));
|
||||
item.put("icon", p[6]);
|
||||
item.put("lifetime",
|
||||
"1".equals(p[7]));
|
||||
cyItems.add(item);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] 拉取CY商品失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void activateCY(String name,
|
||||
int slots, int days) {
|
||||
try {
|
||||
if (cyInstance == null
|
||||
|| cyActivationMethod == null) return;
|
||||
cyActivationMethod.invoke(
|
||||
cyInstance, name, slots, days);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Sdf1_login] CY激活失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 积分查询 =====
|
||||
|
||||
public int getPoints(String name) {
|
||||
return ((Number) plugin.getDb()
|
||||
.getField(name, "points")).intValue();
|
||||
}
|
||||
|
||||
// ===== 主面板 =====
|
||||
|
||||
public Inventory createShopGUI(Player p) {
|
||||
int points = getPoints(p.getName());
|
||||
Inventory inv = Bukkit.createInventory(
|
||||
null, 54,
|
||||
"§d§l积分商城 | 当前积分: " + points);
|
||||
fillBg(inv);
|
||||
inv.setItem(10, mkItem(Material.EMERALD,
|
||||
"§a游戏币充值",
|
||||
"§7消耗积分兑换游戏币",
|
||||
"§7100积分 = $1000",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
inv.setItem(12, mkItem(Material.ENDER_CHEST,
|
||||
"§b背包空间",
|
||||
"§7消耗积分兑换背包格子",
|
||||
"§750积分 = 9格永久空间",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
inv.setItem(14, mkItem(Material.EXPERIENCE_BOTTLE,
|
||||
"§e经验瓶",
|
||||
"§7消耗积分兑换经验",
|
||||
"§720积分 = 10级经验",
|
||||
"",
|
||||
"§e点击兑换"));
|
||||
if (cyInstance != null) {
|
||||
inv.setItem(31, mkItem(Material.CHEST,
|
||||
"§6背包商城",
|
||||
"§7来自CY_beibao的商品",
|
||||
"",
|
||||
"§e点击查看"));
|
||||
}
|
||||
inv.setItem(49, mkItem(Material.ARROW, "§7返回"));
|
||||
return inv;
|
||||
}
|
||||
|
||||
// ===== CY 商品面板 =====
|
||||
|
||||
public Inventory createCYShopGUI(Player p) {
|
||||
refreshCYItems();
|
||||
int points = getPoints(p.getName());
|
||||
int size = Math.max(54,
|
||||
((cyItems.size() / 9) + 1) * 9);
|
||||
if (size > 54) size = 54;
|
||||
Inventory inv = Bukkit.createInventory(
|
||||
null, size,
|
||||
"§d§lCY背包商城 | 当前积分: " + points);
|
||||
fillBg(inv);
|
||||
for (int i = 0; i < cyItems.size()
|
||||
&& i < 45; i++) {
|
||||
Map<String, Object> item = cyItems.get(i);
|
||||
String name = (String) item.get("name");
|
||||
int price = ((Number) item.get("price"))
|
||||
.intValue();
|
||||
int stock = ((Number) item.get("stock"))
|
||||
.intValue();
|
||||
int days = ((Number) item.get("days"))
|
||||
.intValue();
|
||||
int slots = ((Number) item.get("slots"))
|
||||
.intValue();
|
||||
boolean lifetime = (boolean) item
|
||||
.get("lifetime");
|
||||
|
||||
List<String> lore = new ArrayList<>();
|
||||
lore.add("§7价格: §e" + price + " 积分");
|
||||
if (lifetime) {
|
||||
lore.add("§7格子: §e终身 +" + slots);
|
||||
} else {
|
||||
lore.add("§7格子: §e+" + slots);
|
||||
lore.add("§7时间: §e" + days + "天");
|
||||
}
|
||||
if (stock == 0) {
|
||||
lore.add("§7库存: §a无限");
|
||||
} else if (stock == -1) {
|
||||
lore.add("§7库存: §c已下架");
|
||||
} else {
|
||||
lore.add("§7库存: §e" + stock);
|
||||
}
|
||||
lore.add("");
|
||||
if (points >= price && stock != -1) {
|
||||
lore.add("§e点击购买");
|
||||
} else if (stock == -1) {
|
||||
lore.add("§c已下架");
|
||||
} else {
|
||||
lore.add("§c积分不足");
|
||||
}
|
||||
inv.setItem(i, mkItem(Material.CHEST,
|
||||
"§a" + name,
|
||||
lore.toArray(new String[0])));
|
||||
}
|
||||
inv.setItem(size - 1,
|
||||
mkItem(Material.ARROW, "§7返回"));
|
||||
return inv;
|
||||
}
|
||||
|
||||
// ===== 主面板点击 =====
|
||||
|
||||
public void handleClick(Player p, int slot) {
|
||||
int points = getPoints(p.getName());
|
||||
switch (slot) {
|
||||
case 10:
|
||||
if (points >= 100) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 100);
|
||||
if (plugin.getEconomy() != null) {
|
||||
plugin.getEconomy()
|
||||
.depositPlayer(p, 1000);
|
||||
}
|
||||
p.sendMessage(plugin.getConfig2().msg(
|
||||
"points_purchase_success",
|
||||
"count", "100"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2().msg(
|
||||
"points_insufficient",
|
||||
"points",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 12:
|
||||
if (points >= 50) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 50);
|
||||
activateCY(p.getName(), 9, 0);
|
||||
p.sendMessage(plugin.getConfig2().msg(
|
||||
"points_purchase_success",
|
||||
"count", "50"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2().msg(
|
||||
"points_insufficient",
|
||||
"points",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 14:
|
||||
if (points >= 20) {
|
||||
plugin.getDb().deductPoints(
|
||||
p.getName(), 20);
|
||||
p.setLevel(p.getLevel() + 10);
|
||||
p.sendMessage(plugin.getConfig2().msg(
|
||||
"points_purchase_success",
|
||||
"count", "20"));
|
||||
} else {
|
||||
p.sendMessage(plugin.getConfig2().msg(
|
||||
"points_insufficient",
|
||||
"points",
|
||||
String.valueOf(points)));
|
||||
}
|
||||
break;
|
||||
case 31:
|
||||
if (cyInstance != null) {
|
||||
p.openInventory(
|
||||
createCYShopGUI(p));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CY 商品面板点击 =====
|
||||
|
||||
public void handleCYClick(Player p, int slot) {
|
||||
if (slot < 0 || slot >= cyItems.size()
|
||||
|| slot >= 45) return;
|
||||
Map<String, Object> item = cyItems.get(slot);
|
||||
int price = ((Number) item.get("price"))
|
||||
.intValue();
|
||||
int stock = ((Number) item.get("stock"))
|
||||
.intValue();
|
||||
int days = ((Number) item.get("days"))
|
||||
.intValue();
|
||||
int slots = ((Number) item.get("slots"))
|
||||
.intValue();
|
||||
boolean lifetime = (boolean) item
|
||||
.get("lifetime");
|
||||
|
||||
if (stock == -1) {
|
||||
p.sendMessage(
|
||||
"§c[Sdf1_login] §f该商品已下架");
|
||||
return;
|
||||
}
|
||||
int points = getPoints(p.getName());
|
||||
if (points < price) {
|
||||
p.sendMessage(plugin.getConfig2().msg(
|
||||
"points_insufficient",
|
||||
"points",
|
||||
String.valueOf(points)));
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getDb().deductPoints(p.getName(), price);
|
||||
int actDays = lifetime ? 0 : days;
|
||||
activateCY(p.getName(), slots, actDays);
|
||||
|
||||
String tl = lifetime
|
||||
? "终身" : (days + "天");
|
||||
p.sendMessage("§a[Sdf1_login] §f购买成功!获得 "
|
||||
+ tl + " +" + slots + "格 背包空间");
|
||||
}
|
||||
|
||||
// ===== 工具 =====
|
||||
|
||||
private void fillBg(Inventory inv) {
|
||||
ItemStack gl = mkItem(
|
||||
Material.GRAY_STAINED_GLASS_PANE, " ");
|
||||
for (int i = 0; i < inv.getSize(); i++)
|
||||
inv.setItem(i, gl);
|
||||
}
|
||||
|
||||
private ItemStack mkItem(Material mat, String name,
|
||||
String... lore) {
|
||||
ItemStack it = new ItemStack(mat);
|
||||
ItemMeta im = it.getItemMeta();
|
||||
if (im != null) {
|
||||
im.setDisplayName(name);
|
||||
if (lore.length > 0)
|
||||
im.setLore(Arrays.asList(lore));
|
||||
it.setItemMeta(im);
|
||||
}
|
||||
return it;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,113 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class RadioConverter {
|
||||
|
||||
private final Logger logger;
|
||||
private final boolean ffmpegAvailable;
|
||||
|
||||
public RadioConverter(Logger logger) {
|
||||
this.logger = logger;
|
||||
this.ffmpegAvailable = checkFfmpeg();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换音频文件为 ogg 格式
|
||||
* 支持: mp3, wav, flac, ogg
|
||||
*/
|
||||
public boolean convert(File input,
|
||||
File output) {
|
||||
String name =
|
||||
input.getName().toLowerCase();
|
||||
|
||||
// ogg 文件直接复制
|
||||
if (name.endsWith(".ogg")) {
|
||||
return copyFile(input, output);
|
||||
}
|
||||
|
||||
// 有 ffmpeg 则转换
|
||||
if (ffmpegAvailable) {
|
||||
return convertFfmpeg(input, output);
|
||||
}
|
||||
|
||||
// 无 ffmpeg 无法转换
|
||||
logger.warning(
|
||||
"[Radio] 无法转换: "
|
||||
+ input.getName()
|
||||
+ " (需要安装ffmpeg)");
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean copyFile(File src,
|
||||
File dest) {
|
||||
try {
|
||||
dest.getParentFile().mkdirs();
|
||||
Files.copy(src.toPath(),
|
||||
dest.toPath(),
|
||||
StandardCopyOption
|
||||
.REPLACE_EXISTING);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean convertFfmpeg(
|
||||
File input, File output) {
|
||||
try {
|
||||
output.getParentFile().mkdirs();
|
||||
ProcessBuilder pb =
|
||||
new ProcessBuilder(
|
||||
"ffmpeg", "-y",
|
||||
"-i",
|
||||
input
|
||||
.getAbsolutePath(),
|
||||
"-c:a",
|
||||
"libvorbis",
|
||||
"-q:a", "5",
|
||||
output
|
||||
.getAbsolutePath());
|
||||
pb.redirectErrorStream(true);
|
||||
Process p = pb.start();
|
||||
p.waitFor();
|
||||
return output.exists();
|
||||
} catch (Exception e) {
|
||||
logger.warning(
|
||||
"[Radio] ffmpeg转换失败: "
|
||||
+ input.getName());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkFfmpeg() {
|
||||
try {
|
||||
Process p =
|
||||
new ProcessBuilder(
|
||||
"ffmpeg",
|
||||
"-version")
|
||||
.start();
|
||||
boolean ok = p.waitFor() == 0;
|
||||
if (ok) {
|
||||
logger.info(
|
||||
"[Radio] ffmpeg可用");
|
||||
} else {
|
||||
logger.info(
|
||||
"[Radio] ffmpeg不可用"
|
||||
+ " (仅支持ogg文件)");
|
||||
}
|
||||
return ok;
|
||||
} catch (Exception e) {
|
||||
logger.info(
|
||||
"[Radio] ffmpeg未安装"
|
||||
+ " (仅支持ogg文件)");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isFfmpegAvailable() {
|
||||
return ffmpegAvailable;
|
||||
}
|
||||
}
|
||||
@@ -1,563 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
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 BukkitTask mainRadioTask;
|
||||
private BukkitTask loginRadioTask;
|
||||
private final Map<String, Boolean>
|
||||
loginPlayers =
|
||||
new ConcurrentHashMap<>();
|
||||
private int radioIndex = 0;
|
||||
private RadioConverter converter;
|
||||
|
||||
private final List<File> mainFiles =
|
||||
new ArrayList<>();
|
||||
private final List<File> loginFiles =
|
||||
new ArrayList<>();
|
||||
|
||||
public RadioManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
this.radioDir = new File(
|
||||
plugin.getDataFolder(),
|
||||
"radio");
|
||||
this.oggDir = new File(
|
||||
plugin.getDataFolder(),
|
||||
"radio/ogg");
|
||||
this.packDir = new File(
|
||||
plugin.getDataFolder(),
|
||||
"radiopack");
|
||||
radioDir.mkdirs();
|
||||
oggDir.mkdirs();
|
||||
packDir.mkdirs();
|
||||
}
|
||||
|
||||
// ========== 初始化 ==========
|
||||
|
||||
public void init() {
|
||||
converter = new RadioConverter(
|
||||
plugin.getLogger());
|
||||
|
||||
convertAll();
|
||||
buildResourcePack();
|
||||
loadFiles();
|
||||
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 初始化完成: 全服="
|
||||
+ mainFiles.size()
|
||||
+ " 登录=" + loginFiles.size());
|
||||
}
|
||||
|
||||
// ========== 格式转换 ==========
|
||||
|
||||
private void convertAll() {
|
||||
// 根目录
|
||||
convertDir(radioDir,
|
||||
new File(oggDir, "radio"));
|
||||
// login 目录
|
||||
File loginSrc = new File(
|
||||
radioDir, "login");
|
||||
if (loginSrc.exists()) {
|
||||
convertDir(loginSrc,
|
||||
new File(oggDir,
|
||||
"radio/login"));
|
||||
}
|
||||
// reg 目录
|
||||
File regSrc = new File(
|
||||
radioDir, "reg");
|
||||
if (regSrc.exists()) {
|
||||
convertDir(regSrc,
|
||||
new File(oggDir,
|
||||
"radio/reg"));
|
||||
}
|
||||
}
|
||||
|
||||
private void convertDir(File src,
|
||||
File dest) {
|
||||
dest.mkdirs();
|
||||
File[] files = src.listFiles(
|
||||
(d, n) -> {
|
||||
String l =
|
||||
n.toLowerCase();
|
||||
return l.endsWith(".mp3")
|
||||
|| l.endsWith(".wav")
|
||||
|| l.endsWith(".flac")
|
||||
|| l.endsWith(".ogg");
|
||||
});
|
||||
if (files == null) return;
|
||||
|
||||
for (File f : files) {
|
||||
String oggName = f.getName()
|
||||
.replaceAll(
|
||||
"\\.(mp3|wav|flac"
|
||||
+ "|ogg)$",
|
||||
".ogg");
|
||||
File oggFile =
|
||||
new File(dest, oggName);
|
||||
if (oggFile.exists()
|
||||
&& oggFile.lastModified()
|
||||
>= f.lastModified()) {
|
||||
continue;
|
||||
}
|
||||
boolean ok = converter.convert(
|
||||
f, oggFile);
|
||||
if (ok) {
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 转换成功: "
|
||||
+ f.getName());
|
||||
} else {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] 转换失败: "
|
||||
+ f.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 资源包 ==========
|
||||
private void buildResourcePack() {
|
||||
// 清理旧文件
|
||||
deleteDir(packDir);
|
||||
packDir.mkdirs();
|
||||
|
||||
List<File> allOggs = new ArrayList<>();
|
||||
collectOggs(oggDir, allOggs);
|
||||
|
||||
if (allOggs.isEmpty()) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] 无 ogg 文件,跳过资源包");
|
||||
return;
|
||||
}
|
||||
|
||||
// assets/sdf1radio/sounds/ 下面直接放文件
|
||||
File soundsDir = new File(
|
||||
packDir,
|
||||
"assets/sdf1radio/sounds");
|
||||
soundsDir.mkdirs();
|
||||
|
||||
// 构建 sounds.json
|
||||
StringBuilder json =
|
||||
new StringBuilder("{\n");
|
||||
|
||||
for (int i = 0;
|
||||
i < allOggs.size(); i++) {
|
||||
File ogg = allOggs.get(i);
|
||||
// 简化路径:只取文件名(去掉所有目录前缀)
|
||||
String simpleName =
|
||||
ogg.getName()
|
||||
.replace(".ogg", "");
|
||||
// 检查是否有同名文件,加前缀区分
|
||||
String soundKey = simpleName;
|
||||
|
||||
// 复制 ogg 到资源包
|
||||
File dest =
|
||||
new File(soundsDir,
|
||||
simpleName + ".ogg");
|
||||
try {
|
||||
Files.copy(ogg.toPath(),
|
||||
dest.toPath(),
|
||||
StandardCopyOption
|
||||
.REPLACE_EXISTING);
|
||||
} catch (Exception e) {
|
||||
// 忽略
|
||||
}
|
||||
|
||||
// 写 JSON
|
||||
json.append(" \"sdf1radio:")
|
||||
.append(soundKey)
|
||||
.append("\": {\n");
|
||||
json.append(" \"sounds\": [{\n");
|
||||
json.append(" \"name\": \"")
|
||||
.append("sdf1radio/")
|
||||
.append(soundKey)
|
||||
.append("\",\n");
|
||||
json.append(
|
||||
" \"stream\": true\n");
|
||||
json.append(" }]\n");
|
||||
json.append(" }");
|
||||
if (i < allOggs.size() - 1)
|
||||
json.append(",");
|
||||
json.append("\n");
|
||||
}
|
||||
json.append("}");
|
||||
|
||||
// 写 sounds.json
|
||||
try {
|
||||
Files.write(
|
||||
new File(packDir,
|
||||
"assets/sdf1radio/"
|
||||
+ "sounds.json")
|
||||
.toPath(),
|
||||
json.toString()
|
||||
.getBytes("UTF-8"));
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] sounds.json写入失败");
|
||||
}
|
||||
|
||||
// 写 pack.mcmeta
|
||||
try {
|
||||
Files.write(
|
||||
new File(packDir,
|
||||
"pack.mcmeta")
|
||||
.toPath(),
|
||||
("{\n"
|
||||
+ " \"pack\": {\n"
|
||||
+ " \"pack_format\": 34,\n"
|
||||
+ " \"description\": "
|
||||
+ "\"Sdf1 Radio\"\n"
|
||||
+ " }\n"
|
||||
+ "}")
|
||||
.getBytes("UTF-8"));
|
||||
} catch (Exception e) {
|
||||
// 忽略
|
||||
}
|
||||
|
||||
// 打包 zip
|
||||
try {
|
||||
File zipFile = new File(
|
||||
plugin.getDataFolder(),
|
||||
"radiopack.zip");
|
||||
packZip(packDir, zipFile);
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 资源包生成: "
|
||||
+ allOggs.size()
|
||||
+ " 个音频");
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] zip打包失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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 collectOggs(File dir,
|
||||
List<File> list) {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) return;
|
||||
for (File f : files) {
|
||||
if (f.isDirectory()) {
|
||||
collectOggs(f, list);
|
||||
} else if (f.getName()
|
||||
.endsWith(".ogg")) {
|
||||
list.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getRelativePath(
|
||||
File base, File file) {
|
||||
return base.toURI()
|
||||
.relativize(file.toURI())
|
||||
.getPath();
|
||||
}
|
||||
|
||||
private void packZip(File root,
|
||||
File zipFile) throws Exception {
|
||||
try (ZipOutputStream zos =
|
||||
new ZipOutputStream(
|
||||
new FileOutputStream(
|
||||
zipFile))) {
|
||||
zipDir(root, root, zos);
|
||||
}
|
||||
}
|
||||
|
||||
private void zipDir(File root,
|
||||
File current,
|
||||
ZipOutputStream zos)
|
||||
throws Exception {
|
||||
File[] files = current.listFiles();
|
||||
if (files == null) return;
|
||||
for (File f : files) {
|
||||
if (f.isDirectory()) {
|
||||
zipDir(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 加载文件列表 ==========
|
||||
|
||||
private void loadFiles() {
|
||||
mainFiles.clear();
|
||||
loginFiles.clear();
|
||||
|
||||
File radioOgg = new File(
|
||||
oggDir, "radio");
|
||||
loadOggs(radioOgg, mainFiles);
|
||||
|
||||
File loginOgg = new File(
|
||||
oggDir, "radio/login");
|
||||
loadOggs(loginOgg, loginFiles);
|
||||
|
||||
File regOgg = new File(
|
||||
oggDir, "radio/reg");
|
||||
loadOggs(regOgg, loginFiles);
|
||||
}
|
||||
|
||||
private void loadOggs(File dir,
|
||||
List<File> list) {
|
||||
File[] files = dir.listFiles(
|
||||
(d, n) -> n.toLowerCase()
|
||||
.endsWith(".ogg"));
|
||||
if (files != null) {
|
||||
Arrays.sort(files);
|
||||
for (File f : files)
|
||||
list.add(f);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 资源包发送 ==========
|
||||
public void sendResourcePack(Player p) {
|
||||
File zip = new File(
|
||||
plugin.getDataFolder(),
|
||||
"radiopack.zip");
|
||||
if (!zip.exists()) return;
|
||||
try {
|
||||
byte[] data =
|
||||
Files.readAllBytes(
|
||||
zip.toPath());
|
||||
java.security.MessageDigest md =
|
||||
java.security.MessageDigest
|
||||
.getInstance("SHA-1");
|
||||
md.update(data);
|
||||
byte[] hashBytes = md.digest();
|
||||
StringBuilder sb =
|
||||
new StringBuilder();
|
||||
for (byte b : hashBytes)
|
||||
sb.append(String.format(
|
||||
"%02x", b));
|
||||
String hash = sb.toString();
|
||||
UUID uuid = UUID.randomUUID();
|
||||
|
||||
// 用反射兼容不同版本
|
||||
try {
|
||||
java.lang.reflect.Method m =
|
||||
p.getClass().getMethod(
|
||||
"setResourcePack",
|
||||
UUID.class,
|
||||
byte[].class,
|
||||
String.class,
|
||||
String.class,
|
||||
boolean.class);
|
||||
m.invoke(p, uuid, data,
|
||||
hash,
|
||||
"Sdf1 Radio",
|
||||
false);
|
||||
} catch (NoSuchMethodException ex) {
|
||||
try {
|
||||
java.lang.reflect.Method m2 =
|
||||
p.getClass().getMethod(
|
||||
"setResourcePack",
|
||||
UUID.class,
|
||||
byte[].class,
|
||||
String.class);
|
||||
m2.invoke(p, uuid,
|
||||
data, hash);
|
||||
} catch (Exception ex2) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] 服务端不支持"
|
||||
+ "发送资源包");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 全服广播 ==========
|
||||
|
||||
public void startMainRadio() {
|
||||
stopMainRadio();
|
||||
if (mainFiles.isEmpty()) {
|
||||
plugin.getLogger().warning(
|
||||
"[Radio] radio/ 目录"
|
||||
+ "无 .ogg 文件");
|
||||
return;
|
||||
}
|
||||
plugin.getLogger().info(
|
||||
"[Radio] 启动全服广播"
|
||||
+ " (" + mainFiles.size()
|
||||
+ " 个文件)");
|
||||
|
||||
mainRadioTask = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
playNext();
|
||||
}
|
||||
}.runTaskTimer(
|
||||
plugin, 100L, 2400L);
|
||||
}
|
||||
private void playNext() {
|
||||
if (mainFiles.isEmpty()) return;
|
||||
File file = mainFiles.get(
|
||||
radioIndex % mainFiles.size());
|
||||
String name = file.getName()
|
||||
.replace(".ogg", "");
|
||||
String key = "sdf1radio:" + name;
|
||||
|
||||
for (Player p
|
||||
: Bukkit.getOnlinePlayers()) {
|
||||
p.playSound(
|
||||
p.getLocation(),
|
||||
key,
|
||||
SoundCategory.RECORDS,
|
||||
2.0f, 1.0f);
|
||||
}
|
||||
|
||||
Bukkit.broadcastMessage(
|
||||
"§6§l[广播] §f正在播放: §e"
|
||||
+ name);
|
||||
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;
|
||||
if (loginFiles.isEmpty()) return;
|
||||
|
||||
loginRadioTask = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (loginPlayers.isEmpty()) {
|
||||
cancel();
|
||||
loginRadioTask = null;
|
||||
return;
|
||||
}
|
||||
int idx = (int)
|
||||
(System.currentTimeMillis()
|
||||
/ 2400L
|
||||
% loginFiles.size());
|
||||
File file =
|
||||
loginFiles.get(idx);
|
||||
String rel =
|
||||
getRelativePath(
|
||||
oggDir, file)
|
||||
.replace("\\", "/");
|
||||
String key =
|
||||
"sdf1radio:"
|
||||
+ rel.replace(".ogg", "");
|
||||
|
||||
for (String name
|
||||
: loginPlayers.keySet()) {
|
||||
Player lp = Bukkit
|
||||
.getPlayer(name);
|
||||
if (lp != null
|
||||
&& lp.isOnline()) {
|
||||
lp.playSound(
|
||||
lp.getLocation(),
|
||||
key,
|
||||
SoundCategory
|
||||
.RECORDS,
|
||||
1.5f, 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(
|
||||
plugin, 0L, 24L);
|
||||
}
|
||||
|
||||
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 boolean playSpecific(
|
||||
Player sender, String name) {
|
||||
List<File> all = new ArrayList<>();
|
||||
collectOggs(oggDir, all);
|
||||
for (File f : all) {
|
||||
String fn = f.getName()
|
||||
.replace(".ogg", "");
|
||||
if (fn.equalsIgnoreCase(name)) {
|
||||
String key = "sdf1radio:" + name;
|
||||
for (Player p
|
||||
: Bukkit.getOnlinePlayers()) {
|
||||
p.playSound(
|
||||
p.getLocation(),
|
||||
key,
|
||||
SoundCategory.RECORDS,
|
||||
2.0f, 1.0f);
|
||||
}
|
||||
Bukkit.broadcastMessage(
|
||||
"§6§l[广播] §f正在播放: §e"
|
||||
+ name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
sender.sendMessage("§c未找到: " + name);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ========== 重载 ==========
|
||||
|
||||
public void reload() {
|
||||
stopMainRadio();
|
||||
stopAllLoginRadio();
|
||||
convertAll();
|
||||
buildResourcePack();
|
||||
loadFiles();
|
||||
// 不自动启动,由管理员手动启动
|
||||
} }
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class RiskControlManager {
|
||||
|
||||
private final Main plugin;
|
||||
|
||||
// 玩家当前是否处于风控状态
|
||||
private final Map<String, Boolean> riskActive = new ConcurrentHashMap<>();
|
||||
// 密码验证连续错误次数
|
||||
private final Map<String, Integer> passwordAttempts = new ConcurrentHashMap<>();
|
||||
// 邮箱验证码连续错误次数
|
||||
private final Map<String, Integer> emailAttempts = new ConcurrentHashMap<>();
|
||||
// 风控触发时间(5分钟过期)
|
||||
private final Map<String, Long> riskTriggerTime = new ConcurrentHashMap<>();
|
||||
// 临时封禁到期时间
|
||||
private final Map<String, Long> tempBanExpiry = new ConcurrentHashMap<>();
|
||||
|
||||
private static final long RISK_TIMEOUT = 5 * 60 * 1000L; // 5分钟
|
||||
private static final int MAX_ATTEMPTS = 3;
|
||||
private static final long TEMP_BAN_DURATION = 10 * 60 * 1000L; // 10分钟
|
||||
|
||||
public RiskControlManager(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否被临时封禁
|
||||
*/
|
||||
public boolean isTempBanned(String name) {
|
||||
Long expiry = tempBanExpiry.get(name);
|
||||
if (expiry == null) return false;
|
||||
if (System.currentTimeMillis() >= expiry) {
|
||||
tempBanExpiry.remove(name);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取临时封禁剩余秒数
|
||||
*/
|
||||
public int getTempBanRemaining(String name) {
|
||||
Long expiry = tempBanExpiry.get(name);
|
||||
if (expiry == null) return 0;
|
||||
return (int) ((expiry - System.currentTimeMillis()) / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家是否需要风控验证
|
||||
* 条件:IP变化 + 有背包数据(老玩家)
|
||||
*/
|
||||
public boolean shouldTrigger(String name, String currentIP) {
|
||||
if (isTempBanned(name)) return false;
|
||||
if (riskActive.getOrDefault(name, false)) return false;
|
||||
|
||||
// 获取上次记录的IP
|
||||
String lastIP = (String) plugin.getDb().getField(name, "last_ip");
|
||||
if (lastIP == null || lastIP.isEmpty()) return false; // 新玩家不触发
|
||||
|
||||
// IP相同不触发
|
||||
if (lastIP.equals(currentIP)) return false;
|
||||
|
||||
// 有背包数据的老玩家 + IP变化 → 触发风控
|
||||
String invData = (String) plugin.getDb().getField(name, "inventory_data");
|
||||
if (invData != null && !invData.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发风控:冻结玩家,保存并隐藏背包
|
||||
*/
|
||||
public void trigger(Player p, String currentIP) {
|
||||
String name = p.getName();
|
||||
riskActive.put(name, true);
|
||||
riskTriggerTime.put(name, System.currentTimeMillis());
|
||||
passwordAttempts.put(name, 0);
|
||||
emailAttempts.put(name, 0);
|
||||
|
||||
// 保存背包并冻结
|
||||
plugin.hideInventory(p);
|
||||
|
||||
p.sendMessage("§c§l[风控系统] §f检测到异常登录!");
|
||||
p.sendMessage("§e当前IP: §f" + currentIP);
|
||||
p.sendMessage("§e请选择验证方式:");
|
||||
p.sendMessage("§a1. §f密码验证 §7(输入: 密码 <你的密码>)");
|
||||
p.sendMessage("§a2. §f邮箱验证 §7(输入: 邮箱)");
|
||||
p.sendMessage("§7风控有效期: 5分钟");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理密码验证
|
||||
*/
|
||||
public boolean handlePassword(Player p, String input) {
|
||||
String name = p.getName();
|
||||
if (!riskActive.getOrDefault(name, false)) return false;
|
||||
|
||||
// 检查过期
|
||||
if (isExpired(name)) { expireRisk(name); return false; }
|
||||
|
||||
int attempts = passwordAttempts.getOrDefault(name, 0) + 1;
|
||||
passwordAttempts.put(name, attempts);
|
||||
|
||||
String salt = (String) plugin.getDb().getField(name, "password_salt");
|
||||
if (salt == null) return false;
|
||||
String hash = PasswordUtils.hash(input, salt);
|
||||
|
||||
if (plugin.getDb().checkPassword(name, hash)) {
|
||||
passRisk(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attempts >= MAX_ATTEMPTS) {
|
||||
failRisk(p);
|
||||
} else {
|
||||
p.sendMessage("§c§l[风控系统] §f密码错误!剩余 " + (MAX_ATTEMPTS - attempts) + " 次机会");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮箱验证码
|
||||
*/
|
||||
public void sendEmailCode(Player p) {
|
||||
String name = p.getName();
|
||||
if (!riskActive.getOrDefault(name, false)) return;
|
||||
if (isExpired(name)) { expireRisk(name); return; }
|
||||
|
||||
String emailAddr = (String) plugin.getDb().getField(name, "email");
|
||||
if (emailAddr == null || emailAddr.isEmpty()) {
|
||||
p.sendMessage("§c§l[风控系统] §f你未绑定邮箱,无法使用邮箱验证");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!plugin.getEmail().isConfigured()) {
|
||||
p.sendMessage("§c§l[风控系统] §f邮件服务未配置,请使用密码验证");
|
||||
return;
|
||||
}
|
||||
|
||||
String code = PasswordUtils.generateVerifyCode();
|
||||
plugin.getChatInput().setVerifyCode(p, code, "risk_control");
|
||||
boolean sent = plugin.getEmail().sendVerifyCode(emailAddr, name, code);
|
||||
if (sent) {
|
||||
p.sendMessage("§a§l[风控系统] §f验证码已发送到 " + emailAddr);
|
||||
p.sendMessage("§e请输入: 验证 <验证码>");
|
||||
} else {
|
||||
p.sendMessage("§c§l[风控系统] §f验证码发送失败,请使用密码验证");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理邮箱验证码
|
||||
*/
|
||||
public boolean handleEmailCode(Player p, String code) {
|
||||
String name = p.getName();
|
||||
if (!riskActive.getOrDefault(name, false)) return false;
|
||||
if (isExpired(name)) { expireRisk(name); return false; }
|
||||
|
||||
int attempts = emailAttempts.getOrDefault(name, 0) + 1;
|
||||
emailAttempts.put(name, attempts(p, code, "risk_control")) {
|
||||
passRisk(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (attempts >= MAX_ATTEMPTS) {
|
||||
failRisk(p);
|
||||
} else {
|
||||
p.sendMessage("§c§l[风控系统] §f验证码);
|
||||
|
||||
if (plugin.getChatInput().checkVerifyCode错误!剩余 " + (MAX_ATTEMPTS - attempts) + " 次机会");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证通过:解冻,记录新IP
|
||||
*/
|
||||
private void passRisk(Player p) {
|
||||
String name = p.getName();
|
||||
riskActive.remove(name);
|
||||
passwordAttempts.remove(name);
|
||||
emailAttempts.remove(name);
|
||||
riskTriggerTime.remove(name);
|
||||
|
||||
// 记录新IP
|
||||
String ip = plugin.getPlayerIP(p);
|
||||
if (ip != null) plugin.getDb().setField(name, "last_ip", ip);
|
||||
plugin.recordIPLogin(p);
|
||||
|
||||
// 还原背包
|
||||
plugin.restoreInventory(p);
|
||||
plugin.getLoggedIn().add(name);
|
||||
plugin.getDb().setLoggedIn(name, true);
|
||||
|
||||
p.sendMessage("§a§l[风控系统] §f验证通过!已解除风控限制");
|
||||
p.playSound(p.getLocation(), org.bukkit.Sound.BLOCK_NOTE_BLOCK_CHIME, 1.0f, 0.79f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证失败3次:临时封禁 + 邮件报警
|
||||
*/
|
||||
private void failRisk(Player p) {
|
||||
String name = p.getName();
|
||||
riskActive.remove(name);
|
||||
passwordAttempts.remove(name);
|
||||
emailAttempts.remove(name);
|
||||
riskTriggerTime.remove(name);
|
||||
|
||||
// 临时封禁10分钟
|
||||
tempBanExpiry.put(name, System.currentTimeMillis() + TEMP_BAN_DURATION);
|
||||
|
||||
p.sendMessage("§c§l[风控系统] §f验证失败次数过多,临时封禁10分钟");
|
||||
|
||||
// 邮件报警
|
||||
String emailAddr = (String) plugin.getDb().getField(name, "email");
|
||||
if (emailAddr != null && !emailAddr.isEmpty() && plugin.getEmail().isConfigured()) {
|
||||
String ip = plugin.getPlayerIP(p);
|
||||
plugin.getEmail().sendVerifyCode(emailAddr, name,
|
||||
"[Sdf1_login] 风控报警: 玩家 " + name + " IP(" + ip + ")验证失败3次,已临时封禁10分钟");
|
||||
}
|
||||
|
||||
// 踢出
|
||||
p.kickPlayer("§c§l[风控系统] §f验证失败,临时封禁10分钟,请稍后再试");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查风控是否过期
|
||||
*/
|
||||
private boolean isExpired(String name) {
|
||||
Long trigger = riskTriggerTime.get(name);
|
||||
if (trigger == null) return true;
|
||||
return System.currentTimeMillis() - trigger > RISK_TIMEOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 风控过期处理
|
||||
*/
|
||||
private void expireRisk(String name) {
|
||||
riskActive.remove(name);
|
||||
passwordAttempts.remove(name);
|
||||
emailAttempts.remove(name);
|
||||
riskTriggerTime.remove(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家是否处于风控状态
|
||||
*/
|
||||
public boolean isRiskActive(String name) {
|
||||
return riskActive.getOrDefault(name, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时清理过期数据
|
||||
*/
|
||||
public void cleanup() {
|
||||
long now = System.currentTimeMillis();
|
||||
riskTriggerTime.entrySet().removeIf(e -> {
|
||||
if (now - e.getValue() > RISK_TIMEOUT) {
|
||||
riskActive.remove(e.getKey());
|
||||
passwordAttempts.remove(e.getKey());
|
||||
emailAttempts.remove(e.getKey());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
tempBanExpiry.entrySet().removeIf(e -> now >= e.getValue());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,137 +0,0 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class VerificationManager {
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final Logger logger;
|
||||
|
||||
public VerificationManager(JavaPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
this.logger = plugin.getLogger();
|
||||
}
|
||||
|
||||
public boolean isBedrockPlayer(Player player) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
if (uuid.toString().startsWith("00000000-0000-0000-0009-")) {
|
||||
logger.info("[Sdf1_login] Geyser: UUID前缀匹配 → 基岩版");
|
||||
return true;
|
||||
}
|
||||
if (player.hasMetadata("isgeyserplayer")) {
|
||||
logger.info("[Sdf1_login] Geyser: metadata匹配 → 基岩版");
|
||||
return true;
|
||||
}
|
||||
if (player.hasPermission("geyser.player")) {
|
||||
logger.info("[Sdf1_login] Geyser: permission匹配 → 基岩版");
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate");
|
||||
if (floodgate == null) floodgate = Bukkit.getPluginManager().getPlugin("Floodgate");
|
||||
if (floodgate != null && floodgate.isEnabled()) {
|
||||
Class<?> apiClass = Class.forName("org.geysermc.floodgate.api.FloodgateApi");
|
||||
Object api = apiClass.getMethod("getInstance").invoke(null);
|
||||
boolean isBedrock = (boolean) apiClass.getMethod("isFloodgatePlayer", UUID.class).invoke(api, uuid);
|
||||
if (isBedrock) {
|
||||
logger.info("[Sdf1_login] Geyser: Floodgate API确认 → 基岩版");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
logger.info("[Sdf1_login] Geyser: 非基岩版");
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isOnlineMode() {
|
||||
return Bukkit.getOnlineMode();
|
||||
}
|
||||
|
||||
public void verifyPremiumAsync(Player player, Consumer<Boolean> callback) {
|
||||
String name = player.getName();
|
||||
UUID playerUUID = player.getUniqueId();
|
||||
logger.info("[Sdf1_login] Mojang验证: 查询 " + name + " ...");
|
||||
logger.info("[Sdf1_login] Mojang验证: 玩家UUID = " + playerUUID);
|
||||
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
UUID premiumUUID = queryMojangUUID(name);
|
||||
|
||||
if (premiumUUID == null) {
|
||||
logger.info("[Sdf1_login] Mojang验证: " + name + " 无记录 → 非正版");
|
||||
Bukkit.getScheduler().runTask(plugin, () -> callback.accept(false));
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("[Sdf1_login] Mojang验证: 正版UUID = " + premiumUUID);
|
||||
boolean match = playerUUID.equals(premiumUUID);
|
||||
|
||||
if (match) {
|
||||
logger.info("[Sdf1_login] Mojang验证: ✓ UUID匹配 → 正版");
|
||||
} else {
|
||||
logger.info("[Sdf1_login] Mojang验证: ✗ UUID不匹配 → 非正版");
|
||||
logger.info("[Sdf1_login] Mojang验证: 玩家: " + playerUUID);
|
||||
logger.info("[Sdf1_login] Mojang验证: 正版: " + premiumUUID);
|
||||
}
|
||||
|
||||
final boolean finalResult = match;
|
||||
Bukkit.getScheduler().runTask(plugin, () -> callback.accept(finalResult));
|
||||
});
|
||||
}
|
||||
|
||||
private UUID queryMojangUUID(String name) {
|
||||
try {
|
||||
URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + name);
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("GET");
|
||||
conn.setRequestProperty("User-Agent", "Sdf1_login");
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(5000);
|
||||
|
||||
int code = conn.getResponseCode();
|
||||
if (code != 200) {
|
||||
conn.disconnect();
|
||||
return null;
|
||||
}
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) sb.append(line);
|
||||
reader.close();
|
||||
conn.disconnect();
|
||||
|
||||
String json = sb.toString();
|
||||
logger.info("[Sdf1_login] Mojang响应: " + json);
|
||||
|
||||
int idIndex = json.indexOf("\"id\"");
|
||||
if (idIndex < 0) return null;
|
||||
|
||||
int colonIndex = json.indexOf(":", idIndex);
|
||||
int quoteStart = json.indexOf("\"", colonIndex + 1);
|
||||
int quoteEnd = json.indexOf("\"", quoteStart + 1);
|
||||
|
||||
String rawUUID = json.substring(quoteStart + 1, quoteEnd);
|
||||
String formatted = rawUUID.replaceFirst(
|
||||
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
|
||||
"$1-$2-$3-$4-$5");
|
||||
|
||||
return UUID.fromString(formatted);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.log(Level.WARNING, "[Sdf1_login] Mojang查询异常: " + name, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
playSpecific无结果,位置:Main.java
|
||||
Reference in New Issue
Block a user