feat: 新增虚拟货币系统和商城系统

This commit is contained in:
ypsdf1
2026-05-23 19:01:10 +08:00
parent 325ec9f877
commit a75be0ebae
18 changed files with 3614 additions and 1982 deletions
+197 -197
View File
@@ -1,197 +1,197 @@
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<>();
// 是否已进入挂机检测(false=还在活动,不计时)
private final Map<UUID, Boolean> afkTracking =
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();
UUID uuid = p.getUniqueId();
if (!isLoggedIn(name)) continue;
if (config.getAfkWhitelist().contains(name))
continue;
// 确保有记录
if (!lastAction.containsKey(uuid)) {
lastAction.put(uuid, now);
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
continue;
}
long last = lastAction.get(uuid);
long idle = (now - last) / 1000;
int timeout = config.afkTimeout;
// 还在活动范围内:不进入挂机检测
if (idle < 60) {
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
continue;
}
// 超过60秒无活动:开始挂机检测
afkTracking.put(uuid, true);
long remain = timeout - idle;
// 已超时:踢出
if (remain <= 0) {
p.kickPlayer("§c§l你已挂机 "
+ fmtSec(idle) + ",被踢出");
lastAction.remove(uuid);
lastStage.remove(uuid);
afkTracking.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秒
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分钟提醒
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);
}
}
if (remain > 180) {
lastStage.remove(uuid);
}
}
}
/**
* 玩家有活动时调用(移动/交互/放置/挖掘等)
* 重置计时,退出挂机检测
*/
public void recordAction(UUID uuid) {
lastAction.put(uuid, System.currentTimeMillis());
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
}
/**
* 玩家下线时调用
* 彻底停止计时,清理所有数据
*/
public void remove(UUID uuid) {
lastAction.remove(uuid);
lastStage.remove(uuid);
afkTracking.remove(uuid);
}
/**
* 获取玩家当前是否在挂机检测中
*/
public boolean isAfkTracking(UUID uuid) {
return afkTracking.getOrDefault(uuid, false);
}
private boolean isLoggedIn(String name) {
return ((Main) plugin).getLoggedIn()
.contains(name);
}
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);
}
private String fmtSec(long sec) {
if (sec < 60) return sec + "秒";
long m = sec / 60;
long s = sec % 60;
return m + "分" + (s > 0 ? s + "秒" : "");
}
}
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<>();
// 是否已进入挂机检测(false=还在活动,不计时)
private final Map<UUID, Boolean> afkTracking =
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();
UUID uuid = p.getUniqueId();
if (!isLoggedIn(name)) continue;
if (config.getAfkWhitelist().contains(name))
continue;
// 确保有记录
if (!lastAction.containsKey(uuid)) {
lastAction.put(uuid, now);
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
continue;
}
long last = lastAction.get(uuid);
long idle = (now - last) / 1000;
int timeout = config.afkTimeout;
// 还在活动范围内:不进入挂机检测
if (idle < 60) {
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
continue;
}
// 超过60秒无活动:开始挂机检测
afkTracking.put(uuid, true);
long remain = timeout - idle;
// 已超时:踢出
if (remain <= 0) {
p.kickPlayer("§c§l你已挂机 "
+ fmtSec(idle) + ",被踢出");
lastAction.remove(uuid);
lastStage.remove(uuid);
afkTracking.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秒
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分钟提醒
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);
}
}
if (remain > 180) {
lastStage.remove(uuid);
}
}
}
/**
* 玩家有活动时调用(移动/交互/放置/挖掘等)
* 重置计时,退出挂机检测
*/
public void recordAction(UUID uuid) {
lastAction.put(uuid, System.currentTimeMillis());
afkTracking.put(uuid, false);
lastStage.put(uuid, 0);
}
/**
* 玩家下线时调用
* 彻底停止计时,清理所有数据
*/
public void remove(UUID uuid) {
lastAction.remove(uuid);
lastStage.remove(uuid);
afkTracking.remove(uuid);
}
/**
* 获取玩家当前是否在挂机检测中
*/
public boolean isAfkTracking(UUID uuid) {
return afkTracking.getOrDefault(uuid, false);
}
private boolean isLoggedIn(String name) {
return ((Main) plugin).getLoggedIn()
.contains(name);
}
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);
}
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 +1,146 @@
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";
}
}
}
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";
}
}
}
+275 -275
View File
@@ -1,275 +1,275 @@
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;
public EmailManager(ConfigManager config) {
this.config = config;
this.logger = Logger.getLogger("Sdf1_login");
}
public boolean isConfigured() {
String host = config.getSmtp("smtp地址");
String user = config.getSmtp("smtp账号");
String pass = config.getSmtp("smtp密码");
return host != null && !host.isEmpty()
&& user != null && !user.isEmpty()
&& pass != null && !pass.isEmpty();
}
public boolean sendVerifyCode(String to,
String code) {
if (!isConfigured()) {
logger.warning(
"[Sdf1_login] SMTP未配置,"
+ "无法发送验证码");
return false;
}
logger.info("[Sdf1_login] 发送验证码到 " + to);
return sendEmailSync(to,
"[Sdf1_login] 验证码",
"您的验证码: " + code
+ "\n5分钟内有效。\n"
+ "如非本人操作请忽略。");
}
public boolean sendTempPassword(String to,
String player, String tempPwd) {
if (!isConfigured()) {
logger.warning(
"[Sdf1_login] SMTP未配置");
return false;
}
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("smtp账号");
String pass = config.getSmtp("smtp密码");
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.getClass()
.getSimpleName()
+ ": " + e.getMessage());
if (e.getCause() != null) {
logger.severe(
"[Sdf1_login] 根因: "
+ e.getCause()
.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);
}
}
}
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;
public EmailManager(ConfigManager config) {
this.config = config;
this.logger = Logger.getLogger("Sdf1_login");
}
public boolean isConfigured() {
String host = config.getSmtp("smtp地址");
String user = config.getSmtp("smtp账号");
String pass = config.getSmtp("smtp密码");
return host != null && !host.isEmpty()
&& user != null && !user.isEmpty()
&& pass != null && !pass.isEmpty();
}
public boolean sendVerifyCode(String to,
String code) {
if (!isConfigured()) {
logger.warning(
"[Sdf1_login] SMTP未配置,"
+ "无法发送验证码");
return false;
}
logger.info("[Sdf1_login] 发送验证码到 " + to);
return sendEmailSync(to,
"[Sdf1_login] 验证码",
"您的验证码: " + code
+ "\n5分钟内有效。\n"
+ "如非本人操作请忽略。");
}
public boolean sendTempPassword(String to,
String player, String tempPwd) {
if (!isConfigured()) {
logger.warning(
"[Sdf1_login] SMTP未配置");
return false;
}
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("smtp账号");
String pass = config.getSmtp("smtp密码");
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.getClass()
.getSimpleName()
+ ": " + e.getMessage());
if (e.getCause() != null) {
logger.severe(
"[Sdf1_login] 根因: "
+ e.getCause()
.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);
}
}
}
+768 -28
View File
@@ -1,69 +1,797 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.*;
import org.bukkit.entity.Player;
import org.bukkit.inventory.*;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.*;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.io.File;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.scheduler.BukkitTask;
import java.util.*;
public class GUIManager {
public class GUIManager implements Listener {
private final Main plugin;
public static final String T_MAIN =
"§6§l登录面板";
"§6§l服务器菜单";
public static final String T_MORE =
"§6§l更多功能";
public static final String T_EDITOR =
"§6§l编辑菜单项";
public static final String T_BIOME =
"§b§l目标群系传送";
public static final String T_MY_INFO =
"§a§l我的信息";
"§6§l我的信息";
public static final String T_GIFT_STAGES =
"§6§l礼包阶段";
public static final String T_ADMIN =
"§c§l管理员面板";
public static final String T_USER_MGMT =
"§e§l用户管理";
"§6§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新人任务";
"§6§l邀请数据";
public static final String T_USER_MGMT =
"§6§l用户管理";
public static final String T_TASK_CENTER =
"§6§l任务中心";
public GUIManager(Main plugin) {
this.plugin = plugin;
}
public void openMain(Player p) {
Inventory g = Bukkit.createInventory(
null, 27, T_MAIN);
null, 54, T_MAIN);
// ===== 自定义菜单项 =====
if (plugin.getMenu() != null) {
List<MenuManager.MenuItem> customs =
plugin.getMenu().getItems();
// 已占用的槽位
Set<Integer> occupied = new HashSet<>();
occupied.add(10);
occupied.add(12);
occupied.add(14);
occupied.add(16);
occupied.add(19);
occupied.add(21);
occupied.add(23);
occupied.add(25);
occupied.add(29);
occupied.add(31);
occupied.add(32);
occupied.add(34);
// 自定义区域:第1排(0-8) + 第5排(36-44) + 第6排(45-53)
int[] customArea = {
0, 1, 2, 3, 4, 5, 6, 7, 8,
36, 37, 38, 39, 40, 41, 42, 43, 44,
45, 46, 47, 48, 49, 50, 51, 52, 53
};
int autoIdx = 0;
for (int ci = 0;
ci < customs.size() && ci < 27; ci++) {
MenuManager.MenuItem mi =
customs.get(ci);
// 权限过滤
if ("OP".equals(mi.permType)
&& !p.isOp()
&& !isAdmin(p)) continue;
int targetSlot = -1;
// 玩家指定了格子?
if (mi.slot != null) {
targetSlot = findBestSlot(
mi.slot - 1, occupied);
}
// 未指定或冲突 → 自动排队
if (targetSlot < 0) {
while (autoIdx < customArea.length) {
int s = customArea[autoIdx++];
if (!occupied.contains(s)) {
targetSlot = s;
break;
}
}
}
if (targetSlot < 0
|| targetSlot >= 54) continue;
occupied.add(targetSlot);
// 放置物品
ItemStack icon =
mi.icon.clone();
ItemMeta im =
icon.getItemMeta();
im.setDisplayName(
"§e§l" + mi.title);
im.setLore(java.util.Arrays.asList(
"§7指令: §f"
+ mi.command,
"§7权限: §f"
+ mi.permType,
"",
"§a左键执行"));
icon.setItemMeta(im);
g.setItem(targetSlot, icon);
}
}
// 背景填充
fillBg(g);
p.openInventory(g);
// ===== Row1(9-17): 核心功能 =====
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.EMERALD_BLOCK,
g.setItem(16, mkItem(Material.COMPASS,
"§b§l群系传送",
"§7传送到指定群系"));
// ===== Row2(18-26): 日常功能 =====
g.setItem(19, mkItem(Material.BOOK,
"§6工单系统"));
g.setItem(21, mkItem(Material.EMERALD_BLOCK,
"§a§l每日签到",
"§7签到获取积分和惊喜奖励"));
g.setItem(20, mkItem(Material.BOOK,
"§6工单系统",
"§7提交bug、求助、举报"));
g.setItem(23, mkItem(Material.ENDER_CHEST,
"§6§l任务中心"));
if (p.isOp() || isAdmin(p)) {
g.setItem(22, mkItem(Material.CHEST,
"§6垃圾回收站",
g.setItem(25, mkItem(Material.HOPPER,
"§6§l垃圾回收站",
"§7查看和取回清理的物品"));
}
// ===== Row3(27-35): 管理功能 =====
if (isAdmin(p)) {
g.setItem(24, mkItem(
g.setItem(29, mkItem(
Material.REDSTONE_BLOCK,
"§c§l管理员面板"));
}
if (p.isOp()) {
g.setItem(31, mkItem(Material.ECHO_SHARD,
"§c§l管理菜单",
"§7添加、编辑、删除菜单项"));
}
// 在 openMain 方法中,Row3 区域加:
// ★ 余额查询 - 所有人都能看 ★
double myBal = 0;
try {
var reg = plugin.getServer()
.getServicesManager()
.getRegistration(
net.milkbowl.vault.economy
.Economy.class);
if (reg != null)
myBal = reg.getProvider()
.getBalance(p);
} catch (Exception ignored) {}
g.setItem(32, mkItem(Material.DIAMOND,
"§a§l余额查询",
"§7我的余额: §a$"
+ String.format("%.2f", myBal)));
// ★ 余额操作 - 仅OP和tag玩家 ★
if (isAdmin(p)) {
g.setItem(34, mkItem(Material.GOLD_INGOT,
"§6§l余额操作",
"§7给钱、扣钱"));
}
// ===== Row0(0-8): 自定义菜单项 =====
if (plugin.getMenu() != null) {
List<MenuManager.MenuItem> customs =
plugin.getMenu().getItems();
for (int i = 0;
i < customs.size() && i < 9;
i++) {
MenuManager.MenuItem mi =
customs.get(i);
if ("OP".equals(mi.permType)
&& !p.isOp()
&& !isAdmin(p)) continue;
ItemStack icon =
mi.icon.clone();
ItemMeta im =
icon.getItemMeta();
im.setDisplayName(
"§e§l" + mi.title);
im.setLore(Arrays.asList(
"§7指令: §f"
+ mi.command,
"§7权限: §f"
+ mi.permType,
"",
"§a左键执行"));
icon.setItemMeta(im);
g.setItem(i, icon);
}
}
// 背景最后填充(不覆盖物品)
fillBg(g);
p.openInventory(g);
}
private int findBestSlot(
int requested,
Set<Integer> occupied) {
// 目标位空闲直接用
if (requested >= 0 && requested < 54
&& !occupied.contains(requested)) {
return requested;
}
// BFS上下左右找最近空位
int row = requested / 9;
int col = requested % 9;
boolean[][] visited =
new boolean[6][9];
java.util.Queue<int[]> queue =
new java.util.LinkedList<>();
queue.add(new int[]{row, col});
visited[row][col] = true;
int[][] dirs = {
{-1, 0}, {1, 0},
{0, -1}, {0, 1}};
while (!queue.isEmpty()) {
int[] cur = queue.poll();
for (int[] d : dirs) {
int nr = cur[0] + d[0];
int nc = cur[1] + d[1];
if (nr >= 0 && nr < 6
&& nc >= 0 && nc < 9
&& !visited[nr][nc]) {
visited[nr][nc] = true;
int s = nr * 9 + nc;
if (!occupied.contains(s)) {
return s;
}
queue.add(new int[]{nr, nc});
}
}
}
return -1;
}
public void openMenuManager(Player p) {
List<MenuManager.MenuItem> list =
plugin.getMenu().getItems();
Inventory g = Bukkit.createInventory(
null, 27,
"§c§l菜单管理");
fillBg(g);
for (int i = 0;
i < list.size() && i < 9; i++) {
MenuManager.MenuItem mi = list.get(i);
// ★ 直接用 mi.icon,不要随机 ★
g.setItem(i, mkItem(
mi.icon.getType(),
"§e" + mi.title,
"§7指令: " + mi.command,
"§7权限: " + mi.permType,
"",
"§a左键编辑",
"§7右键切换权限"));
}
g.setItem(22, mkItem(Material.LIME_WOOL,
"§a§l添加菜单项"));
g.setItem(26, mkItem(Material.ARROW,
"§7返回"));
p.openInventory(g);
}
public void openBalanceOps(Player p) {
Inventory g = Bukkit.createInventory(
null, 54,
"§d§l余额操作");
fillBg(g);
net.milkbowl.vault.economy.Economy eco = null;
try {
var reg = plugin.getServer()
.getServicesManager()
.getRegistration(
net.milkbowl
.vault
.economy
.Economy
.class);
if (reg != null) eco = reg.getProvider();
} catch (Exception ignored) {}
int slot = 0;
for (Player op :
Bukkit.getOnlinePlayers()) {
if (slot >= 45) break;
double bal = 0;
if (eco != null) bal =
eco.getBalance(op);
boolean isSelf =
op.getName().equals(p.getName());
g.setItem(slot, mkItem(
isSelf ? Material.EMERALD_BLOCK
: Material.PLAYER_HEAD,
"§e" + op.getName(),
"§7余额: §a$"
+ String.format(
"%.2f", bal),
"",
"§a左键给钱 §c右键扣钱"));
slot++;
}
g.setItem(49, mkItem(Material.ARROW,
"§7返回"));
p.openInventory(g);
}
public void openShop(Player p) {
p.closeInventory();
p.performCommand("sdf1_login shop");
}
public void openTicket(Player p) {
p.closeInventory();
p.performCommand("sdf1_login ticket");
}
@EventHandler
public void onInvClose(
InventoryCloseEvent e) {
if (!(e.getPlayer() instanceof Player))
return;
Player p = (Player) e.getPlayer();
String t = e.getView().getTitle();
if (!t.equals(T_EDITOR)) return;
// ★ 编辑器关闭时清理空项 ★
List<MenuManager.MenuItem> list =
plugin.getMenu().getItems();
if (!list.isEmpty()) {
MenuManager.MenuItem last =
list.get(list.size() - 1);
if ((last.command == null
|| last.command.trim().isEmpty()
|| last.command.trim()
.equals("/"))
&& "新菜单项"
.equals(last.title)) {
list.remove(list.size() - 1);
}
}
}
public void openSubMenu(
Player p, String fileName) {
String menuTitle =
"§6§l" + fileName
.replace(".txt", "");
Inventory g = Bukkit.createInventory(
null, 54, menuTitle);
fillBg(g);
java.io.File f = new java.io.File(
plugin.getDataFolder(), fileName);
if (!f.exists()) {
p.sendMessage("§c文件不存在: "
+ fileName);
return;
}
try {
List<String> lines =
java.nio.file.Files
.readAllLines(
f.toPath(),
java.nio.charset
.StandardCharsets
.UTF_8);
List<String[]> items =
new ArrayList<>();
StringBuilder buf =
new StringBuilder();
boolean inItem = false;
for (String raw : lines) {
String l = raw.trim();
if (l.startsWith("#")
|| l.startsWith("//"))
continue;
if (l.contains("<!--")
|| l.contains("/*"))
continue;
int h = l.indexOf('#');
int s = l.indexOf("//");
if (h >= 0)
l = l.substring(0, h)
.trim();
if (s >= 0 && (h < 0 || s < h))
l = l.substring(0, s)
.trim();
if (l.equals("{")) {
inItem = true;
buf.setLength(0);
continue;
}
if (l.equals("}") && inItem) {
String[] parsed =
parseSubItem(
buf.toString());
if (parsed != null)
items.add(parsed);
inItem = false;
continue;
}
if (inItem)
buf.append(l).append("\n");
}
for (int i = 0;
i < items.size() && i < 45;
i++) {
String[] item = items.get(i);
Material mat = Material.PAPER;
try {
mat = Material.valueOf(
item.length > 2
? item[2]
: "PAPER");
} catch (Exception ignored) {}
ItemStack it =
new ItemStack(mat);
ItemMeta im =
it.getItemMeta();
im.setDisplayName(
"§e§l" + item[0]);
im.setLore(Arrays.asList(
"§7指令: §f"
+ item[1]));
it.setItemMeta(im);
g.setItem(i, it);
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Menu] 子菜单读取失败: "
+ e.getMessage());
}
g.setItem(49, mkItem(
Material.ARROW,
"§c返回主菜单"));
p.openInventory(g);
}
private String[] parseSubItem(
String block) {
String title = "";
String cmd = "";
String icon = "PAPER";
for (String line : block.split("\n")) {
line = line.trim();
String v = "";
int idx = line.indexOf('\uff1a');
if (idx < 0)
idx = line.indexOf(':');
if (idx >= 0) {
v = line.substring(idx + 1)
.trim();
if (v.startsWith("\"")
&& v.endsWith("\""))
v = v.substring(1,
v.length() - 1);
}
if (line.startsWith("标题"))
title = v;
else if (line.startsWith("指令")) {
cmd = v;
if (!cmd.isEmpty()
&& !cmd.startsWith("/"))
cmd = "/" + cmd;
} else if (line.startsWith("图标"))
icon = v;
}
if (title.isEmpty()) return null;
return new String[]{title, cmd, icon};
}
public void openBiomeMenu(Player p) {
Inventory g = Bukkit.createInventory(
null, 54, T_BIOME);
fillBg(g);
String[][] biomes = {
{"海洋", "WATER_BUCKET"},
{"平原", "GRASS_BLOCK"},
{"森林", "OAK_LOG"},
{"沙漠", "SAND"},
{"恶地", "TERRACOTTA"},
{"雪原", "SNOW_BLOCK"},
{"沼泽", "LILY_PAD"},
{"河流", "COD"},
{"地下", "DEEPSLATE"},
{"蘑菇岛", "RED_MUSHROOM"},
{"下界", "NETHERRACK"},
{"末地", "END_STONE"},
};
String[] colors = {
"§b", "§a", "§2", "§e",
"§6", "§f", "§a", "§9",
"§8", "§c", "§4", "§5"
};
for (int i = 0;
i < biomes.length; i++) {
ItemStack it = new ItemStack(
Material.valueOf(
biomes[i][1]));
ItemMeta im = it.getItemMeta();
im.setDisplayName(
colors[i] + biomes[i][0]);
im.setLore(Arrays.asList(
"§7点击传送"));
it.setItemMeta(im);
g.setItem(i, it);
}
g.setItem(49, mkItem(
Material.ARROW,
"§c返回主菜单"));
p.openInventory(g);
}
// 常量
private static final int BIOME_SCAN_STEP = 32;
private static final int BIOME_SCAN_MAX = 512;
private static final int BIOME_BATCH = 30;
public void teleportToBiome(
Player p, String category) {
p.sendMessage("§e正在搜索 "
+ category + " 群系...");
// 1. 纯计算收集候选坐标(不碰Bukkit API)
int baseX = p.getLocation().getBlockX();
int baseZ = p.getLocation().getBlockZ();
List<int[]> candidates = new ArrayList<>();
for (int r = BIOME_SCAN_STEP;
r <= BIOME_SCAN_MAX;
r += BIOME_SCAN_STEP) {
for (int dx = -r; dx <= r;
dx += BIOME_SCAN_STEP) {
for (int dz = -r; dz <= r;
dz += BIOME_SCAN_STEP) {
if (dx * dx + dz * dz > r * r)
continue;
candidates.add(new int[]{
baseX + dx, baseZ + dz});
}
}
}
// 2. 打乱顺序,避免每次往同一方向搜
Collections.shuffle(candidates);
// 3. 主线程分批检测,每tick处理30个坐标
final int[] idx = {0};
new BukkitRunnable() {
@Override
public void run() {
World w = p.getWorld();
int end = Math.min(
idx[0] + BIOME_BATCH,
candidates.size());
for (int i = idx[0]; i < end; i++) {
int x = candidates.get(i)[0];
int z = candidates.get(i)[1];
// 跳过未加载区块
if (!w.isChunkLoaded(x >> 4, z >> 4))
continue;
// 获取地表高度
int surfaceY =
w.getHighestBlockYAt(x, z);
// 在地表Y取群系(避免地下误判)
String key = w.getBlockAt(
x, surfaceY, z)
.getBiome()
.getKey().toString();
String cat =
mapBiomeCategory(
key, w.getName());
if (category.equals(cat)) {
this.cancel();
Bukkit.getScheduler()
.runTask(plugin, () -> {
Location tp =
new Location(
w,
x + 0.5,
surfaceY + 1,
z + 0.5);
p.teleport(tp);
p.sendMessage(
"§a已传送到 "
+ category
+ " ("
+ x + ","
+ (surfaceY + 1)
+ ","
+ z + ")");
});
return;
}
}
idx[0] = end;
if (idx[0] >= candidates.size()) {
this.cancel();
Bukkit.getScheduler()
.runTask(plugin, () ->
p.sendMessage(
"§c附近未找到 "
+ category
+ ",请换个位置再试"));
}
}
}.runTaskTimer(plugin, 0L, 1L);
}
private String mapBiomeCategory(
String biome, String worldName) {
if (biome == null) return "平原";
String b = biome.toLowerCase();
// 海洋
if (b.contains("ocean")) return "海洋";
// 下界
if (b.contains("nether")) return "下界";
// 末地
if (b.contains("end")) return "末地";
// 洞穴群系
if (b.contains("deep_dark")
|| b.contains("lush_caves")
|| b.contains("dripstone_caves")
|| b.contains("stony_peaks"))
return "地下";
// 蘑菇岛
if (b.contains("mushroom")) return "蘑菇岛";
// 沼泽(先于森林)
if (b.contains("swamp")) return "沼泽";
// 沙漠
if (b.contains("desert")) return "沙漠";
// 恶地
if (b.contains("badlands")
|| b.contains("mesa"))
return "恶地";
// 雪原
if (b.contains("snow")
|| b.contains("ice")
|| b.contains("frozen")
|| b.contains("tundra"))
return "雪原";
// 河流
if (b.contains("river")) return "河流";
// 森林(排除沼泽/恶地/雪原后匹配)
if (b.contains("forest")
|| b.contains("taiga")
|| b.contains("jungle")
|| b.contains("cherry")
|| b.contains("birch")
|| b.contains("dark_forest")
|| b.contains("mangrove"))
return "森林";
return "平原";
}
private boolean titleStartsWith6(String t) {
return t != null
&& t.length() >= 3
&& t.charAt(1) == '6'
&& t.charAt(2) == 'l';
}
@EventHandler
public void onInvDrag(
InventoryDragEvent e) {
if (!(e.getWhoClicked()
instanceof Player)) return;
Player p = (Player) e.getWhoClicked();
if (!T_EDITOR.equals(
e.getView().getTitle())) return;
if (e.getRawSlots().contains(12)) {
List<MenuManager.MenuItem> list =
plugin.getMenu().getItems();
int idx = list.size() - 1;
if (idx >= 0)
list.get(idx).icon =
e.getOldCursor().clone();
}
}
public void openEditor(
Player p, int idx) {
List<MenuManager.MenuItem> list =
plugin.getMenu().getItems();
MenuManager.MenuItem item;
if (idx < 0 || idx >= list.size()) {
item = new MenuManager.MenuItem();
list.add(item);
idx = list.size() - 1;
} else {
item = list.get(idx);
}
Inventory g = Bukkit.createInventory(
null, 27, T_EDITOR);
fillBg(g);
g.setItem(10, mkItem(Material.NAME_TAG,
"§e§l标题",
"§7当前: §f" + item.title));
g.setItem(11, mkItem(
Material.COMMAND_BLOCK,
"§e§l指令",
"§7当前: §f" + item.command));
ItemStack ic = item.icon.clone();
ItemMeta im = ic.getItemMeta();
im.setDisplayName("§e§l菜单图标");
im.setLore(Arrays.asList(
"§7拖拽替换"));
ic.setItemMeta(im);
g.setItem(12, ic);
g.setItem(20, mkItem(
"OP".equals(item.permType)
? Material.RED_BANNER
: Material.GREEN_BANNER,
"§e§l权限: " + item.permType,
"§a左键切换"));
g.setItem(22, mkItem(
Material.LIME_STAINED_GLASS_PANE,
"§a§l保存"));
g.setItem(24, mkItem(
Material.RED_STAINED_GLASS_PANE,
"§c§l取消"));
p.openInventory(g);
}
public void openMyInfo(Player p) {
Inventory g = Bukkit.createInventory(
null, 27, T_MY_INFO);
null, 54, T_MY_INFO);
fillBg(g);
Map<String, Object> user = plugin.getDb()
.getUser(p.getName());
@@ -400,6 +1128,11 @@ public class GUIManager {
"§7右键: 设置时长",
"§7当前: " + afkMin + "分钟"));
g.setItem(22, mkItem(Material.ARROW, "§7返回"));
// openAdmin 方法中加:
g.setItem(15, mkItem(Material.EMERALD_BLOCK,
"§d§l余额操作",
"§7给钱、扣钱"));
p.openInventory(g);
}
@@ -694,12 +1427,19 @@ public class GUIManager {
}
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);
ItemStack glass = new ItemStack(
Material.GRAY_STAINED_GLASS_PANE);
ItemMeta gm = glass.getItemMeta();
gm.setDisplayName(" ");
glass.setItemMeta(gm);
for (int i = 0; i < inv.getSize(); i++) {
if (inv.getItem(i) == null) {
inv.setItem(i, glass);
}
}
}
private ItemStack mkItem(Material mat, String name,
String... lore) {
ItemStack it = new ItemStack(mat);
+67 -67
View File
@@ -1,67 +1,67 @@
package Sdf1_login;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
public class IPGroupManager {
private final Main plugin;
private final int maxAccounts;
public IPGroupManager(Main plugin, int maxAccounts) {
this.plugin = plugin;
this.maxAccounts = maxAccounts;
}
/**
* 检查该IP是否还能注册新账号
*/
public boolean canRegister(String ip) {
if (maxAccounts <= 0) return true;
if (ip == null || ip.isEmpty()) return true;
return queryAccountsByIP(ip).size() < maxAccounts;
}
/**
* 获取该IP下已注册的账号数量
*/
public int getAccountCount(String ip) {
if (ip == null || ip.isEmpty()) return 0;
return queryAccountsByIP(ip).size();
}
/**
* 获取该IP下所有账号名
*/
public List<String> getAccounts(String ip) {
return queryAccountsByIP(ip);
}
/**
* 直接通过数据库连接查询指定IP下的所有账号
*/
private List<String> queryAccountsByIP(String ip) {
List<String> accounts = new ArrayList<>();
Connection conn = plugin.getDb().getDb();
if (conn == null) return accounts;
try {
PreparedStatement ps = conn.prepareStatement(
"SELECT player_name FROM users "
+ "WHERE ip_address = ?");
ps.setString(1, ip);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
accounts.add(
rs.getString("player_name"));
}
rs.close();
ps.close();
} catch (Exception e) {
e.printStackTrace();
}
return accounts;
}
}
package Sdf1_login;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
public class IPGroupManager {
private final Main plugin;
private final int maxAccounts;
public IPGroupManager(Main plugin, int maxAccounts) {
this.plugin = plugin;
this.maxAccounts = maxAccounts;
}
/**
* 检查该IP是否还能注册新账号
*/
public boolean canRegister(String ip) {
if (maxAccounts <= 0) return true;
if (ip == null || ip.isEmpty()) return true;
return queryAccountsByIP(ip).size() < maxAccounts;
}
/**
* 获取该IP下已注册的账号数量
*/
public int getAccountCount(String ip) {
if (ip == null || ip.isEmpty()) return 0;
return queryAccountsByIP(ip).size();
}
/**
* 获取该IP下所有账号名
*/
public List<String> getAccounts(String ip) {
return queryAccountsByIP(ip);
}
/**
* 直接通过数据库连接查询指定IP下的所有账号
*/
private List<String> queryAccountsByIP(String ip) {
List<String> accounts = new ArrayList<>();
Connection conn = plugin.getDb().getDb();
if (conn == null) return accounts;
try {
PreparedStatement ps = conn.prepareStatement(
"SELECT player_name FROM users "
+ "WHERE ip_address = ?");
ps.setString(1, ip);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
accounts.add(
rs.getString("player_name"));
}
rs.close();
ps.close();
} catch (Exception e) {
e.printStackTrace();
}
return accounts;
}
}
@@ -1,46 +1,46 @@
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);
}
}
}
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);
}
}
}
File diff suppressed because it is too large Load Diff
+449 -1
View File
@@ -1,4 +1,452 @@
package Sdf1_login;
public class MenuManager {
import org.bukkit.*;
import org.bukkit.entity.Player;
import org.bukkit.event.*;
import org.bukkit.event.inventory.*;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.nio.file.Files;
public class MenuManager implements Listener {
private final Main plugin;
private final List<MenuItem> items =
new ArrayList<>();
private final Set<String> chatWait =
ConcurrentHashMap.newKeySet();
private final Map<String, String> chatField =
new ConcurrentHashMap<>();
private final Map<String, Integer> chatIdx =
new ConcurrentHashMap<>();
// ===== 菜单项 =====
public static class MenuItem {
public String title = "新菜单项";
public String command = "";
public String permType = "玩家";
public Integer slot = null;
public ItemStack icon;
public MenuItem() {
icon = randomIcon();
}
public MenuItem(String t,
String c, String p) {
title = t;
command = c;
permType = p;
icon = randomIcon();
}
static ItemStack randomIcon() {
Material[] pool = {
Material.COMPASS,
Material.ENDER_PEARL,
Material.DIAMOND,
Material.GOLD_INGOT,
Material.EMERALD,
Material.NETHER_STAR,
Material.BEACON,
Material.ENCHANTING_TABLE,
Material.CHEST,
Material.FIREWORK_ROCKET,
Material.BOOK,
Material.ELYTRA,
Material.TOTEM_OF_UNDYING
};
Material m = pool[
(int) (Math.random()
* pool.length)];
ItemStack it =
new ItemStack(m);
ItemMeta im = it.getItemMeta();
im.setDisplayName("§e默认图标");
it.setItemMeta(im);
return it;
}
}
public MenuManager(Main plugin) {
this.plugin = plugin;
}
public List<MenuItem> getItems() {
return items;
}
// ===== 读取菜单.txt =====
public void loadMenu() {
items.clear();
File f = new File(
plugin.getDataFolder(),
"菜单.txt");
if (!f.exists()) createDefault(f);
parseFile(f);
plugin.getLogger().info(
"[Menu] 加载 "
+ items.size()
+ " 个菜单项");
}
private void parseFile(File f) {
plugin.getLogger().info("[Menu] ===== 扫描 =====");
plugin.getLogger().info("[Menu] 路径: "
+ f.getAbsolutePath());
plugin.getLogger().info("[Menu] 大小: "
+ f.length());
try {
List<String> lines = Files.readAllLines(
f.toPath(), StandardCharsets.UTF_8);
plugin.getLogger().info("[Menu] 总行数: "
+ lines.size());
boolean inBlock = false;
boolean inHtml = false;
StringBuilder buf = new StringBuilder();
boolean inItem = false;
int parsed = 0;
for (int lineNum = 1;
lineNum <= lines.size();
lineNum++) {
String raw = lines.get(lineNum - 1);
String l = raw.trim();
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": 原文[" + l.length()
+ "字符] " + l);
// ===== 第1优先:跨行块注释 =====
if (inBlock) {
if (l.contains("*/")) {
inBlock = false;
l = l.substring(
l.indexOf("*/") + 2)
.trim();
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": 块注释结束");
} else {
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": [块注释中]");
continue;
}
}
// ===== 第2优先:跨行HTML注释 =====
if (inHtml) {
if (l.contains("-->")) {
inHtml = false;
l = l.substring(
l.indexOf("-->") + 3)
.trim();
} else {
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": [HTML注释中]");
continue;
}
}
// ===== 第3优先:整行注释(最高裁决) =====
// 以 # 或 // 开头的行,直接跳过
// 不管后面包含什么 /* <!-- 等
if (l.startsWith("#")
|| l.startsWith("//")) {
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": [整行注释,跳过]");
continue;
}
// ===== 第4优先:块注释开始 =====
if (l.contains("/*")) {
l = l.substring(0, l.indexOf("/*"))
.trim();
inBlock = true;
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": 进入/*块注释"
+ " 剩余[" + l + "]");
if (l.isEmpty()) {
continue;
}
}
// ===== 第5优先:HTML注释开始 =====
if (l.contains("<!--")) {
l = l.substring(0,
l.indexOf("<!--")).trim();
inHtml = true;
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": 进入<!--注释");
if (l.isEmpty()) {
continue;
}
}
// ===== 第6优先:行内注释 =====
int hashPos = l.indexOf("#");
int slashPos = l.indexOf("//");
if (hashPos >= 0) {
l = l.substring(0, hashPos).trim();
}
if (slashPos >= 0) {
if (hashPos < 0
|| slashPos < hashPos) {
l = l.substring(0, slashPos)
.trim();
}
}
if (l.isEmpty()) {
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": 空");
continue;
}
plugin.getLogger().info(
"[Menu] L" + lineNum
+ ": [保留] " + l);
if (l.equals("{")) {
inItem = true;
buf.setLength(0);
plugin.getLogger().info(
"[Menu] === 块开始 ===");
continue;
}
if (l.equals("}")) {
if (inItem) {
plugin.getLogger().info(
"[Menu] === 块结束 ===");
parseBlock(buf.toString());
parsed++;
plugin.getLogger().info(
"[Menu] 当前菜单项: "
+ items.size());
}
inItem = false;
continue;
}
if (inItem) {
buf.append(l).append("\n");
}
}
plugin.getLogger().info(
"[Menu] ===== 完毕 =====");
plugin.getLogger().info(
"[Menu] 块数: " + parsed);
plugin.getLogger().info(
"[Menu] 菜单项: "
+ items.size());
} catch (Exception e) {
plugin.getLogger().warning(
"[Menu] 异常: "
+ e.getMessage());
}
}
private void parseBlock(String block) {
String title = "";
String cmd = "";
String perm = "玩家";
String iconMat = "";
int slotNum = -1;
for (String line : block.split("\n")) {
line = line.trim();
if (line.isEmpty()) continue;
String v = "";
int idx = line.indexOf(":");
if (idx < 0) idx = line.indexOf(":");
if (idx >= 0) {
v = line.substring(idx + 1).trim();
if (v.startsWith("\"")
&& v.endsWith("\"")
&& v.length() >= 2) {
v = v.substring(1, v.length() - 1);
}
}
if (line.startsWith("标题")) {
title = v;
} else if (line.startsWith("指令")) {
cmd = v;
if (!cmd.isEmpty()
&& !cmd.startsWith("/")) {
cmd = "/" + cmd;
}
} else if (line.startsWith("权限")) {
perm = v;
} else if (line.startsWith("图标")) {
iconMat = v;
} else if (line.startsWith("格子")) {
try {
int s = Integer.parseInt(v);
if (s >= 1 && s <= 54) {
slotNum = s;
}
} catch (NumberFormatException ignored) {
}
}
}
if (title.isEmpty()) return;
MenuItem mi = new MenuItem(title, cmd, perm);
if (slotNum >= 1 && slotNum <= 54) {
mi.slot = slotNum;
}
if (!iconMat.isEmpty()) {
try {
String matName = iconMat
.replace("minecraft:", "")
.replace("bukkit:", "")
.trim().toUpperCase();
Material m =
Material.valueOf(matName);
mi.icon = new ItemStack(m);
ItemMeta im =
mi.icon.getItemMeta();
im.setDisplayName(
"§e" + title);
mi.icon.setItemMeta(im);
} catch (Exception e) {
plugin.getLogger().warning(
"[Menu] 无法解析: "
+ iconMat);
}
}
items.add(mi);
}
private void createDefault(File f) {
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets
.UTF_8))) {
pw.println("# 服务器菜单");
pw.println("# {}定义菜单项");
pw.println("# 支持"
+ " # // /* */ <!-- -->");
pw.println();
pw.println("{");
pw.println(" 标题:主城传送");
pw.println(" 指令:/spawn");
pw.println(" 权限类型:玩家");
pw.println(" 图标:COMPASS");
pw.println("}");
pw.println();
pw.println("{");
pw.println(" 标题:随机传送");
pw.println(" 指令:/rtp");
pw.println(" 权限类型:玩家");
pw.println(" 图标:ENDER_PEARL");
pw.println("}");
} catch (Exception ignored) {
}
}
// ===== 保存 =====
public void saveMenu() {
File f = new File(
plugin.getDataFolder(),
"菜单.txt");
try (PrintWriter pw = new PrintWriter(
new OutputStreamWriter(
new FileOutputStream(f),
StandardCharsets.UTF_8))) {
pw.println("# 服务器菜单");
pw.println();
for (MenuItem mi : items) {
pw.println("{");
pw.println(" 标题:"
+ mi.title);
pw.println(" 指令:"
+ mi.command);
pw.println(" 权限类型:"
+ mi.permType);
// ★ 保存实际材质名 ★
pw.println(" 图标:"
+ mi.icon.getType().name());
pw.println("}");
pw.println();
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Menu] 保存失败: "
+ e.getMessage());
}
}
// ===== 聊天编辑 =====
public boolean isEditing(String name) {
return chatWait.contains(name);
}
public void editTitle(
Player p, int idx) {
chatIdx.put(p.getName(), idx);
chatWait.add(p.getName());
chatField.put(p.getName(), "title");
p.closeInventory();
p.sendMessage("§e请输入菜单标题:");
}
public void editCommand(
Player p, int idx) {
chatIdx.put(p.getName(), idx);
chatWait.add(p.getName());
chatField.put(p.getName(), "command");
p.closeInventory();
p.sendMessage("§e请输入指令"
+ "(自动补全/):");
}
public void onChat(Player p, String input) {
String name = p.getName();
if (!chatWait.remove(name)) return;
String field = chatField.remove(name);
Integer idx = chatIdx.get(name);
if (idx == null || idx >= items.size()) return;
MenuItem mi = items.get(idx);
if ("title".equals(field)) {
mi.title = input;
p.sendMessage("§a标题: " + input);
} else if ("command".equals(field)) {
if (!input.startsWith("/"))
input = "/" + input;
mi.command = input;
p.sendMessage("§a指令: " + input);
}
Bukkit.getScheduler().runTask(plugin, () ->
plugin.gui.openEditor(p, idx));
}
}
+72 -72
View File
@@ -1,72 +1,72 @@
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);
}
}
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);
}
}
+374 -374
View File
@@ -1,374 +1,374 @@
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.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
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) {
}
}
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;
}
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 addItem(String name,
int price, String cmd2) {
// 追加到商品配置文件
File file = new File(
plugin.getDataFolder(),
"ShopItems.txt");
try {
java.io.FileWriter fw =
new java.io.FileWriter(file, true);
fw.write(name + ":" + price
+ ":" + cmd2 + "\n");
fw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void removeItem(int index) {
File file = new File(
plugin.getDataFolder(),
"ShopItems.txt");
try {
java.util.List<String> lines =
new java.util.ArrayList<>();
java.io.BufferedReader br =
new java.io.BufferedReader(
new java.io.FileReader(
file));
String line;
int i = 0;
while ((line = br.readLine()) != null) {
if (line.trim().isEmpty()
|| line.startsWith("#")) {
lines.add(line);
continue;
}
if (i != index) {
lines.add(line);
}
i++;
}
br.close();
java.io.PrintWriter pw =
new java.io.PrintWriter(file);
for (String l : lines) pw.println(l);
pw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
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")
.replace("{count}", "100"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{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")
.replace("{count}", "50"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{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")
.replace("{count}", "20"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
}
break;
case 31:
if (cyInstance != null) {
p.openInventory(
createCYShopGUI(p));
}
break;
}
}
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该商品已下架");
return;
}
int points = getPoints(p.getName());
if (points < price) {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{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购买成功!获得 "
+ 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;
}
}
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.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
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) {
}
}
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;
}
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 addItem(String name,
int price, String cmd2) {
// 追加到商品配置文件
File file = new File(
plugin.getDataFolder(),
"ShopItems.txt");
try {
java.io.FileWriter fw =
new java.io.FileWriter(file, true);
fw.write(name + ":" + price
+ ":" + cmd2 + "\n");
fw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void removeItem(int index) {
File file = new File(
plugin.getDataFolder(),
"ShopItems.txt");
try {
java.util.List<String> lines =
new java.util.ArrayList<>();
java.io.BufferedReader br =
new java.io.BufferedReader(
new java.io.FileReader(
file));
String line;
int i = 0;
while ((line = br.readLine()) != null) {
if (line.trim().isEmpty()
|| line.startsWith("#")) {
lines.add(line);
continue;
}
if (i != index) {
lines.add(line);
}
i++;
}
br.close();
java.io.PrintWriter pw =
new java.io.PrintWriter(file);
for (String l : lines) pw.println(l);
pw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
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")
.replace("{count}", "100"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{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")
.replace("{count}", "50"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{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")
.replace("{count}", "20"));
} else {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{points}",
String.valueOf(points)));
}
break;
case 31:
if (cyInstance != null) {
p.openInventory(
createCYShopGUI(p));
}
break;
}
}
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该商品已下架");
return;
}
int points = getPoints(p.getName());
if (points < price) {
p.sendMessage(plugin.getConfig2()
.msg("points_insufficient")
.replace("{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购买成功!获得 "
+ 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;
}
}
@@ -110,71 +110,35 @@ public class RadioDownloadListener
}
}
private void handleFailure(
Player p, UUID uuid) {
int failNum = failCount
.getOrDefault(uuid, 0) + 1;
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);
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);
log.severe("[Radio] " + p.getName()
+ " 已达最大失败次数,停止重试,"
+ "玩家继续留在服务器");
// 不踢出,只记录熔断,玩家正常游玩
p.sendMessage("§e§l[Radio] 资源包加载失败"
+ ",跳过资源包,部分画面、音频可能出现问题"
+ "§d§l若想继续下载,请退出重进");
return;
}
p.sendMessage(
"§e§l[Radio] 下载失败,"
+ "重试... ("
+ failNum + "/"
+ MAX_FAILED + ")");
// 打了包没测试
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);
.runTaskLater(plugin, () -> {
if (p.isOnline() && plugin.radio != null) {
plugin.radio.sendResourcePack(p);
}
}, 100L);
}
}
}
+1 -1
View File
@@ -328,7 +328,7 @@ public class RadioManager {
("{\n \"pack\": {\n"
+ " \"pack_format\": 54,\n"
+ " \"description\":"
+ " \"为保证服务器正常运行,请勿拒绝资源包!\"\n }\n}")
+ " \"§d§l草原探险服务器资源包,请勿拒绝资源包\"\n }\n}")
.getBytes("UTF-8"));
} catch (Exception e) {
// ignore
+248 -248
View File
@@ -1,248 +1,248 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警
*/
public class RiskControlManager {
private final Main plugin;
// 每个玩家名 -> 连续失败次数
private final Map<String, Integer> failAttempts =
new ConcurrentHashMap<>();
// 每个IP -> 关联的可疑玩家列表
private final Map<String, Set<String>> ipSuspicious =
new ConcurrentHashMap<>();
// 每个玩家 -> 上次验证时间(用于冷却)
private final Map<String, Long> verifyCooldown =
new ConcurrentHashMap<>();
// 配置项
private static final int MAX_FAIL_ATTEMPTS = 5;
private static final int MAX_ACCOUNTS_PER_IP = 5;
private static final long COOLDOWN_MS = 60000L;
public RiskControlManager(Main plugin) {
this.plugin = plugin;
}
/**
* 记录一次失败的登录尝试
*
* @param playerName 玩家名
* @return 剩余尝试次数
*/
public int recordFailAttempt(String playerName) {
int current = failAttempts
.getOrDefault(playerName, 0) + 1;
failAttempts.put(playerName, current);
return Math.max(0, MAX_FAIL_ATTEMPTS - current);
}
/**
* 重置指定玩家的失败计数(登录成功时调用)
*/
public void resetFailAttempts(String playerName) {
failAttempts.remove(playerName);
}
/**
* 获取指定玩家的失败次数
*/
public int getFailAttempts(String playerName) {
return failAttempts
.getOrDefault(playerName, 0);
}
/**
* 判断玩家是否已被锁定(失败次数达到上限)
*/
public boolean isLocked(String playerName) {
return failAttempts
.getOrDefault(playerName, 0)
>= MAX_FAIL_ATTEMPTS;
}
/**
* 检查IP是否达到注册上限
* 直接查数据库,不依赖 DatabaseManager 的方法
*/
public boolean canRegisterFromIP(String ip) {
if (ip == null || ip.isEmpty()) return true;
try {
java.sql.Connection conn =
plugin.getDb().getDb();
if (conn == null) return true;
java.sql.PreparedStatement ps =
conn.prepareStatement(
"SELECT COUNT(*) FROM users "
+ "WHERE ip_address = ?");
ps.setString(1, ip);
java.sql.ResultSet rs = ps.executeQuery();
int count = 0;
if (rs.next()) count = rs.getInt(1);
rs.close();
ps.close();
return count < 5;
} catch (Exception e) {
e.printStackTrace();
return true;
}
}
/**
* 记录IP关联的可疑行为
*/
public void recordSuspiciousIP(String ip,
String playerName) {
if (ip == null || ip.isEmpty()) return;
ipSuspicious
.computeIfAbsent(ip,
k -> ConcurrentHashMap.newKeySet())
.add(playerName);
}
/**
* 获取某IP关联的所有可疑玩家
*/
public Set<String> getSuspiciousAccounts(String ip) {
return ipSuspicious
.getOrDefault(ip, Collections.emptySet());
}
/**
* 检查是否在验证冷却期内
*/
public boolean isOnCooldown(String playerName) {
Long last = verifyCooldown.get(playerName);
if (last == null) return false;
return System.currentTimeMillis() - last
< COOLDOWN_MS;
}
/**
* 设置验证冷却时间
*/
public void setCooldown(String playerName) {
verifyCooldown.put(playerName,
System.currentTimeMillis());
}
/**
* 当检测到可疑登录时触发报警
* - 全服广播
* - 有邮箱则发邮件
* - 记录到数据库
*/
public void triggerAlert(Player player,
String suspiciousIP) {
String name = player.getName();
// 记录到数据库
plugin.getDb().recordSecurityAlert(
name, suspiciousIP);
// 记录IP可疑行为
recordSuspiciousIP(suspiciousIP, name);
// 全服广播
String serverMsg = "§c§l[安全警告] §e" + name
+ " §f的账号疑似遭到 §c" + suspiciousIP
+ " §f的盗号尝试, 请各位玩家注意账号安全!";
Bukkit.broadcastMessage(serverMsg);
// 有邮箱则发邮件
String emailAddr = (String) plugin.getDb()
.getField(name, "email");
if (emailAddr != null
&& !emailAddr.isEmpty()) {
String subject =
"[安全警告] 您的账号疑似遭到盗号尝试";
String body = "尊敬的玩家 " + name
+ ":\n\n"
+ "您的账号在 "
+ new java.text.SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss")
.format(new java.util.Date())
+ " 疑似遭到 IP: " + suspiciousIP
+ " 的盗号尝试。\n\n"
+ "建议您:\n"
+ "1. 立即修改密码"
+ "(使用 /sdf1_login pw)\n"
+ "2. 如非本人操作,"
+ "请联系管理员处理\n\n"
+ "--Sdf1_login 安全系统";
plugin.getEmail().sendTempPassword(
emailAddr, name, body);
}
}
/**
* 处理登录失败时的风控逻辑
*
* @param player 玩家
* @param suspiciousIP 可疑来源IP
* @return true=已被锁定(应踢出), false=仍可继续尝试
*/
public boolean handleLoginFailure(Player player,
String suspiciousIP) {
String name = player.getName();
int remaining = recordFailAttempt(name);
if (remaining <= 0) {
// 达到上限,触发报警
triggerAlert(player, suspiciousIP);
player.kickPlayer(
"§c§l[风控] §f登录失败次数过多,"
+ " 账号已被临时锁定, 请联系管理员");
return true;
}
// 未达上限,提示剩余次数
player.sendMessage(
"§c§l[风控] §f密码错误! "
+ "剩余" + remaining + "次机会");
return false;
}
/**
* 登录成功后重置风控状态
*/
public void onLoginSuccess(String playerName) {
resetFailAttempts(playerName);
}
/**
* 清理过期的冷却记录(可由定时任务调用)
*/
public void cleanup() {
long now = System.currentTimeMillis();
verifyCooldown.entrySet().removeIf(
e -> now - e.getValue() > COOLDOWN_MS * 10);
}
/**
* 获取所有玩家的失败尝试记录(调试用)
*/
public Map<String, Integer> getAllFailAttempts() {
return Collections.unmodifiableMap(failAttempts);
}
/**
* 清除所有风控数据(管理员调试用)
*/
public void clearAll() {
failAttempts.clear();
ipSuspicious.clear();
verifyCooldown.clear();
}
}
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 风控管理器:检测异常登录行为,记录尝试次数,触发安全报警
*/
public class RiskControlManager {
private final Main plugin;
// 每个玩家名 -> 连续失败次数
private final Map<String, Integer> failAttempts =
new ConcurrentHashMap<>();
// 每个IP -> 关联的可疑玩家列表
private final Map<String, Set<String>> ipSuspicious =
new ConcurrentHashMap<>();
// 每个玩家 -> 上次验证时间(用于冷却)
private final Map<String, Long> verifyCooldown =
new ConcurrentHashMap<>();
// 配置项
private static final int MAX_FAIL_ATTEMPTS = 5;
private static final int MAX_ACCOUNTS_PER_IP = 5;
private static final long COOLDOWN_MS = 60000L;
public RiskControlManager(Main plugin) {
this.plugin = plugin;
}
/**
* 记录一次失败的登录尝试
*
* @param playerName 玩家名
* @return 剩余尝试次数
*/
public int recordFailAttempt(String playerName) {
int current = failAttempts
.getOrDefault(playerName, 0) + 1;
failAttempts.put(playerName, current);
return Math.max(0, MAX_FAIL_ATTEMPTS - current);
}
/**
* 重置指定玩家的失败计数(登录成功时调用)
*/
public void resetFailAttempts(String playerName) {
failAttempts.remove(playerName);
}
/**
* 获取指定玩家的失败次数
*/
public int getFailAttempts(String playerName) {
return failAttempts
.getOrDefault(playerName, 0);
}
/**
* 判断玩家是否已被锁定(失败次数达到上限)
*/
public boolean isLocked(String playerName) {
return failAttempts
.getOrDefault(playerName, 0)
>= MAX_FAIL_ATTEMPTS;
}
/**
* 检查IP是否达到注册上限
* 直接查数据库,不依赖 DatabaseManager 的方法
*/
public boolean canRegisterFromIP(String ip) {
if (ip == null || ip.isEmpty()) return true;
try {
java.sql.Connection conn =
plugin.getDb().getDb();
if (conn == null) return true;
java.sql.PreparedStatement ps =
conn.prepareStatement(
"SELECT COUNT(*) FROM users "
+ "WHERE ip_address = ?");
ps.setString(1, ip);
java.sql.ResultSet rs = ps.executeQuery();
int count = 0;
if (rs.next()) count = rs.getInt(1);
rs.close();
ps.close();
return count < 5;
} catch (Exception e) {
e.printStackTrace();
return true;
}
}
/**
* 记录IP关联的可疑行为
*/
public void recordSuspiciousIP(String ip,
String playerName) {
if (ip == null || ip.isEmpty()) return;
ipSuspicious
.computeIfAbsent(ip,
k -> ConcurrentHashMap.newKeySet())
.add(playerName);
}
/**
* 获取某IP关联的所有可疑玩家
*/
public Set<String> getSuspiciousAccounts(String ip) {
return ipSuspicious
.getOrDefault(ip, Collections.emptySet());
}
/**
* 检查是否在验证冷却期内
*/
public boolean isOnCooldown(String playerName) {
Long last = verifyCooldown.get(playerName);
if (last == null) return false;
return System.currentTimeMillis() - last
< COOLDOWN_MS;
}
/**
* 设置验证冷却时间
*/
public void setCooldown(String playerName) {
verifyCooldown.put(playerName,
System.currentTimeMillis());
}
/**
* 当检测到可疑登录时触发报警
* - 全服广播
* - 有邮箱则发邮件
* - 记录到数据库
*/
public void triggerAlert(Player player,
String suspiciousIP) {
String name = player.getName();
// 记录到数据库
plugin.getDb().recordSecurityAlert(
name, suspiciousIP);
// 记录IP可疑行为
recordSuspiciousIP(suspiciousIP, name);
// 全服广播
String serverMsg = "§c§l[安全警告] §e" + name
+ " §f的账号疑似遭到 §c" + suspiciousIP
+ " §f的盗号尝试, 请各位玩家注意账号安全!";
Bukkit.broadcastMessage(serverMsg);
// 有邮箱则发邮件
String emailAddr = (String) plugin.getDb()
.getField(name, "email");
if (emailAddr != null
&& !emailAddr.isEmpty()) {
String subject =
"[安全警告] 您的账号疑似遭到盗号尝试";
String body = "尊敬的玩家 " + name
+ ":\n\n"
+ "您的账号在 "
+ new java.text.SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss")
.format(new java.util.Date())
+ " 疑似遭到 IP: " + suspiciousIP
+ " 的盗号尝试。\n\n"
+ "建议您:\n"
+ "1. 立即修改密码"
+ "(使用 /sdf1_login pw)\n"
+ "2. 如非本人操作,"
+ "请联系管理员处理\n\n"
+ "--Sdf1_login 安全系统";
plugin.getEmail().sendTempPassword(
emailAddr, name, body);
}
}
/**
* 处理登录失败时的风控逻辑
*
* @param player 玩家
* @param suspiciousIP 可疑来源IP
* @return true=已被锁定(应踢出), false=仍可继续尝试
*/
public boolean handleLoginFailure(Player player,
String suspiciousIP) {
String name = player.getName();
int remaining = recordFailAttempt(name);
if (remaining <= 0) {
// 达到上限,触发报警
triggerAlert(player, suspiciousIP);
player.kickPlayer(
"§c§l[风控] §f登录失败次数过多,"
+ " 账号已被临时锁定, 请联系管理员");
return true;
}
// 未达上限,提示剩余次数
player.sendMessage(
"§c§l[风控] §f密码错误! "
+ "剩余" + remaining + "次机会");
return false;
}
/**
* 登录成功后重置风控状态
*/
public void onLoginSuccess(String playerName) {
resetFailAttempts(playerName);
}
/**
* 清理过期的冷却记录(可由定时任务调用)
*/
public void cleanup() {
long now = System.currentTimeMillis();
verifyCooldown.entrySet().removeIf(
e -> now - e.getValue() > COOLDOWN_MS * 10);
}
/**
* 获取所有玩家的失败尝试记录(调试用)
*/
public Map<String, Integer> getAllFailAttempts() {
return Collections.unmodifiableMap(failAttempts);
}
/**
* 清除所有风控数据(管理员调试用)
*/
public void clearAll() {
failAttempts.clear();
ipSuspicious.clear();
verifyCooldown.clear();
}
}
+158 -158
View File
@@ -1,158 +1,158 @@
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.function.Consumer;
public class VerificationManager {
private final Main plugin;
public VerificationManager(Main plugin) {
this.plugin = plugin;
}
public boolean isBedrockPlayer(Player p) {
String uuid = p.getUniqueId().toString();
return uuid.contains("f851ba4")
|| uuid.startsWith("00000000-0000-0000");
}
public boolean isOnlineMode() {
return Bukkit.getServer().getOnlineMode();
}
public void verifyPremiumAsync(Player player,
Consumer<Boolean> callback) {
Thread t = new Thread(() -> doVerify(player, callback),
"Sdf1_login-PremiumVerify");
t.setDaemon(true);
t.start();
}
private void doVerify(Player player,
Consumer<Boolean> callback) {
String name = player.getName();
String uuid = player.getUniqueId().toString();
plugin.getLogger().info(
"[Sdf1_login] ====== 验证开始 ======");
plugin.getLogger().info(
"[Sdf1_login] 玩家: " + name);
plugin.getLogger().info(
"[Sdf1_login] 当前UUID: " + uuid);
try {
UUID offlineUuid = UUID.nameUUIDFromBytes(
("OfflinePlayer:" + name)
.getBytes(StandardCharsets.UTF_8));
String offlineStr = offlineUuid.toString();
plugin.getLogger().info(
"[Sdf1_login] 离线UUID: " + offlineStr);
boolean isOfflineUuid =
player.getUniqueId().equals(offlineUuid);
plugin.getLogger().info(
"[Sdf1_login] UUID匹配离线格式: "
+ isOfflineUuid);
if (!isOfflineUuid) {
plugin.getLogger().info(
"[Sdf1_login] 结论: 正版玩家");
plugin.getLogger().info(
"[Sdf1_login] ====== 验证结束 ======");
callback.accept(true);
return;
}
plugin.getLogger().info(
"[Sdf1_login] UUID为离线格式,"
+ "查询Mojang API...");
UUID mojangUuid = fetchMojangUuid(name);
if (mojangUuid != null) {
plugin.getLogger().info(
"[Sdf1_login] Mojang返回UUID: "
+ mojangUuid);
plugin.getLogger().info(
"[Sdf1_login] 结论: 盗版客户端"
+ "使用了正版名字");
} else {
plugin.getLogger().info(
"[Sdf1_login] Mojang无此名字");
plugin.getLogger().info(
"[Sdf1_login] 结论: 纯离线玩家");
}
plugin.getLogger().info(
"[Sdf1_login] ====== 验证结束 ======");
callback.accept(false);
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] 验证异常: "
+ e.getMessage());
callback.accept(false);
}
}
private UUID fetchMojangUuid(String name) {
HttpURLConnection conn = null;
try {
URL url = new URL(
"https://api.mojang.com/users/profiles/minecraft/"
+ name);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
plugin.getLogger().info(
"[Sdf1_login] Mojang API响应: " + code);
if (code == 200) {
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
conn.getInputStream(),
StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
reader.close();
String json = sb.toString();
String uuidStr = json.replaceAll(
".*\"id\"\\s*:\\s*\"([^\"]+)\".*",
"$1");
plugin.getLogger().info(
"[Sdf1_login] Mojang原始UUID: "
+ uuidStr);
String formatted = uuidStr.replaceFirst(
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
"$1-$2-$3-$4-$5");
return UUID.fromString(formatted);
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] Mojang查询异常: "
+ e.getMessage());
} finally {
if (conn != null) conn.disconnect();
}
return null;
}
}
package Sdf1_login;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.function.Consumer;
public class VerificationManager {
private final Main plugin;
public VerificationManager(Main plugin) {
this.plugin = plugin;
}
public boolean isBedrockPlayer(Player p) {
String uuid = p.getUniqueId().toString();
return uuid.contains("f851ba4")
|| uuid.startsWith("00000000-0000-0000");
}
public boolean isOnlineMode() {
return Bukkit.getServer().getOnlineMode();
}
public void verifyPremiumAsync(Player player,
Consumer<Boolean> callback) {
Thread t = new Thread(() -> doVerify(player, callback),
"Sdf1_login-PremiumVerify");
t.setDaemon(true);
t.start();
}
private void doVerify(Player player,
Consumer<Boolean> callback) {
String name = player.getName();
String uuid = player.getUniqueId().toString();
plugin.getLogger().info(
"[Sdf1_login] ====== 验证开始 ======");
plugin.getLogger().info(
"[Sdf1_login] 玩家: " + name);
plugin.getLogger().info(
"[Sdf1_login] 当前UUID: " + uuid);
try {
UUID offlineUuid = UUID.nameUUIDFromBytes(
("OfflinePlayer:" + name)
.getBytes(StandardCharsets.UTF_8));
String offlineStr = offlineUuid.toString();
plugin.getLogger().info(
"[Sdf1_login] 离线UUID: " + offlineStr);
boolean isOfflineUuid =
player.getUniqueId().equals(offlineUuid);
plugin.getLogger().info(
"[Sdf1_login] UUID匹配离线格式: "
+ isOfflineUuid);
if (!isOfflineUuid) {
plugin.getLogger().info(
"[Sdf1_login] 结论: 正版玩家");
plugin.getLogger().info(
"[Sdf1_login] ====== 验证结束 ======");
callback.accept(true);
return;
}
plugin.getLogger().info(
"[Sdf1_login] UUID为离线格式,"
+ "查询Mojang API...");
UUID mojangUuid = fetchMojangUuid(name);
if (mojangUuid != null) {
plugin.getLogger().info(
"[Sdf1_login] Mojang返回UUID: "
+ mojangUuid);
plugin.getLogger().info(
"[Sdf1_login] 结论: 盗版客户端"
+ "使用了正版名字");
} else {
plugin.getLogger().info(
"[Sdf1_login] Mojang无此名字");
plugin.getLogger().info(
"[Sdf1_login] 结论: 纯离线玩家");
}
plugin.getLogger().info(
"[Sdf1_login] ====== 验证结束 ======");
callback.accept(false);
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] 验证异常: "
+ e.getMessage());
callback.accept(false);
}
}
private UUID fetchMojangUuid(String name) {
HttpURLConnection conn = null;
try {
URL url = new URL(
"https://api.mojang.com/users/profiles/minecraft/"
+ name);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
plugin.getLogger().info(
"[Sdf1_login] Mojang API响应: " + code);
if (code == 200) {
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
conn.getInputStream(),
StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
reader.close();
String json = sb.toString();
String uuidStr = json.replaceAll(
".*\"id\"\\s*:\\s*\"([^\"]+)\".*",
"$1");
plugin.getLogger().info(
"[Sdf1_login] Mojang原始UUID: "
+ uuidStr);
String formatted = uuidStr.replaceFirst(
"(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
"$1-$2-$3-$4-$5");
return UUID.fromString(formatted);
}
} catch (Exception e) {
plugin.getLogger().warning(
"[Sdf1_login] Mojang查询异常: "
+ e.getMessage());
} finally {
if (conn != null) conn.disconnect();
}
return null;
}
}