新增/tempban临时封禁、/chat mute禁言、异地登录检测与账号冻结解冻
- DurationParser: 通用时长解析(时间戳/日期/相对时长) - /tempban: Bukkit原生封禁(1秒~10年) - ChatFilterManager: 持久化禁言(muted_until)+loadMutes启动加载 - 异地登录检测: onJoin异步通知PHP比对IP归属(ip9.com.cn) - freeze/unfreeze: web_admin_changes通道执行/解除Bukkit原生封禁 - PVPArenaManager: 移植PVP全服连杀/终结播报逻辑 - DatabaseManager/plugin.yml: 风控列 + tempban命令注册
This commit is contained in:
@@ -36,6 +36,10 @@ public class ChatFilterManager {
|
||||
private final Map<String, Long>
|
||||
mutedPlayers =
|
||||
new ConcurrentHashMap<>();
|
||||
// ★ 禁言理由(内存镜像,持久化到 users.muted_until / mute_reason)
|
||||
private final Map<String, String>
|
||||
muteReasons =
|
||||
new ConcurrentHashMap<>();
|
||||
private final Map<String, String> messages =
|
||||
new LinkedHashMap<>();
|
||||
// ★ 广告机检测:记录每个玩家发送链接的频率
|
||||
@@ -889,6 +893,8 @@ public class ChatFilterManager {
|
||||
if (System.currentTimeMillis()
|
||||
>= expiry) {
|
||||
mutedPlayers.remove(name);
|
||||
muteReasons.remove(name);
|
||||
clearMuteDb(name);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -981,6 +987,76 @@ public class ChatFilterManager {
|
||||
|
||||
public void unmutePlayer(String name) {
|
||||
mutedPlayers.remove(name);
|
||||
muteReasons.remove(name);
|
||||
clearMuteDb(name);
|
||||
}
|
||||
|
||||
// ========== 禁言持久化(重启后仍生效) ==========
|
||||
|
||||
/** 禁言指定玩家,到期时间戳为 expireMs(绝对毫秒) */
|
||||
public void mutePlayer(String name, long expireMs,
|
||||
String reason) {
|
||||
mutedPlayers.put(name, expireMs);
|
||||
if (reason != null && !reason.isEmpty())
|
||||
muteReasons.put(name, reason);
|
||||
persistMute(name, expireMs, reason);
|
||||
plugin.getLogger().info(
|
||||
"[聊天] 已禁言 " + name + " 至 "
|
||||
+ new java.util.Date(expireMs));
|
||||
}
|
||||
|
||||
private void persistMute(String name, long expireMs,
|
||||
String reason) {
|
||||
try {
|
||||
Main mainPlugin = (Main) plugin;
|
||||
DatabaseManager db = mainPlugin.getDb();
|
||||
if (db == null) return;
|
||||
db.setField(name, "muted_until", expireMs);
|
||||
db.setField(name, "mute_reason",
|
||||
reason == null ? "" : reason);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void clearMuteDb(String name) {
|
||||
try {
|
||||
Main mainPlugin = (Main) plugin;
|
||||
DatabaseManager db = mainPlugin.getDb();
|
||||
if (db == null) return;
|
||||
db.setField(name, "muted_until", 0);
|
||||
db.setField(name, "mute_reason", "");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/** 插件启动时从数据库加载仍有效的禁言 */
|
||||
public void loadMutes() {
|
||||
try {
|
||||
Main mainPlugin = (Main) plugin;
|
||||
DatabaseManager db = mainPlugin.getDb();
|
||||
if (db == null) return;
|
||||
for (Object[] row : db.getActiveMutes()) {
|
||||
String name = (String) row[0];
|
||||
long expire = (Long) row[1];
|
||||
String reason = (String) row[2];
|
||||
mutedPlayers.put(name, expire);
|
||||
if (reason != null
|
||||
&& !reason.isEmpty())
|
||||
muteReasons.put(name, reason);
|
||||
}
|
||||
if (!mutedPlayers.isEmpty())
|
||||
plugin.getLogger().info(
|
||||
"[聊天] 已加载 " + mutedPlayers.size()
|
||||
+ " 条有效禁言");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public String getMuteReason(String name) {
|
||||
return muteReasons.get(name);
|
||||
}
|
||||
|
||||
public void resetPlayer(String name) {
|
||||
|
||||
@@ -91,6 +91,22 @@ public class DatabaseManager {
|
||||
// UI偏好:1=CLI(默认), 0=GUI
|
||||
safeAdd(st, "ui_mode",
|
||||
"INTEGER DEFAULT 1");
|
||||
// ===== 风控 / 封禁相关列 =====
|
||||
// IP归属(省+市+区),用于异地登录检测
|
||||
safeAdd(st, "last_login_location",
|
||||
"TEXT DEFAULT ''");
|
||||
// 账号冻结(玩家主动冻结防被盗):0=正常 1=已冻结
|
||||
safeAdd(st, "frozen",
|
||||
"INTEGER DEFAULT 0");
|
||||
// 冻结校验 token(邮件链接防猜测)
|
||||
safeAdd(st, "freeze_token",
|
||||
"TEXT DEFAULT ''");
|
||||
// 禁言到期时间戳(毫秒),0=未禁言
|
||||
safeAdd(st, "muted_until",
|
||||
"INTEGER DEFAULT 0");
|
||||
// 禁言理由
|
||||
safeAdd(st, "mute_reason",
|
||||
"TEXT DEFAULT ''");
|
||||
|
||||
// ===== 背包备份表:安全迁移 =====
|
||||
// 检查旧表是否存在(用 inventory_data 列判断)
|
||||
@@ -684,6 +700,36 @@ public class DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** 读取当前仍有效的禁言列表(muted_until > now) */
|
||||
public List<Object[]> getActiveMutes() {
|
||||
List<Object[]> list =
|
||||
new ArrayList<>();
|
||||
try {
|
||||
PreparedStatement ps = db.prepareStatement(
|
||||
"SELECT player_name, muted_until, "
|
||||
+ "mute_reason FROM users "
|
||||
+ "WHERE muted_until > ?");
|
||||
ps.setLong(1,
|
||||
System.currentTimeMillis());
|
||||
ResultSet rs = ps.executeQuery();
|
||||
while (rs.next()) {
|
||||
Object[] row = new Object[3];
|
||||
row[0] = rs.getString(
|
||||
"player_name");
|
||||
row[1] = rs.getLong(
|
||||
"muted_until");
|
||||
row[2] = rs.getString(
|
||||
"mute_reason");
|
||||
list.add(row);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public Object getField(String name,
|
||||
String field) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 统一时长解析器(供 /tempban 与 /chat mute 共用)
|
||||
*
|
||||
* parseToExpireMs(String) 返回「绝对到期时间戳(毫秒)」,调用方自行校验最小/最大区间。
|
||||
*
|
||||
* 支持的写法:
|
||||
* 1) 纯数字(Unix 秒时间戳): 253402271999
|
||||
* 2) 中文日期(绝对): 2026年7月9日
|
||||
* 3) ISO 日期(绝对): 2025-07-09
|
||||
* 4) 相对时长(中文/英文单位): 1秒 / 1s / 1年 / 1年2月3天 / 2天12小时
|
||||
*
|
||||
* 解析规则优先级:纯数字 → 时间戳;含「年/月/日」且为完整日期格式 → 绝对日期;
|
||||
* 否则按「数字+单位」逐段累加为相对时长。
|
||||
*/
|
||||
public class DurationParser {
|
||||
|
||||
private static final Map<String, Long> UNIT_MS = new HashMap<>();
|
||||
static {
|
||||
// 秒
|
||||
UNIT_MS.put("秒", 1000L);
|
||||
UNIT_MS.put("秒钟", 1000L);
|
||||
UNIT_MS.put("s", 1000L);
|
||||
UNIT_MS.put("sec", 1000L);
|
||||
// 分
|
||||
UNIT_MS.put("分", 60000L);
|
||||
UNIT_MS.put("分钟", 60000L);
|
||||
UNIT_MS.put("mins", 60000L);
|
||||
UNIT_MS.put("min", 60000L);
|
||||
UNIT_MS.put("m", 60000L);
|
||||
// 时
|
||||
UNIT_MS.put("时", 3600000L);
|
||||
UNIT_MS.put("小时", 3600000L);
|
||||
UNIT_MS.put("钟头", 3600000L);
|
||||
UNIT_MS.put("h", 3600000L);
|
||||
UNIT_MS.put("hr", 3600000L);
|
||||
// 天
|
||||
UNIT_MS.put("天", 86400000L);
|
||||
UNIT_MS.put("日", 86400000L);
|
||||
UNIT_MS.put("d", 86400000L);
|
||||
// 周
|
||||
UNIT_MS.put("周", 604800000L);
|
||||
UNIT_MS.put("星期", 604800000L);
|
||||
UNIT_MS.put("礼拜", 604800000L);
|
||||
UNIT_MS.put("w", 604800000L);
|
||||
// 月(按30天近似)
|
||||
UNIT_MS.put("个月", 2592000000L);
|
||||
UNIT_MS.put("月", 2592000000L);
|
||||
UNIT_MS.put("mo", 2592000000L);
|
||||
// 年(按365天近似)
|
||||
UNIT_MS.put("年", 31536000000L);
|
||||
UNIT_MS.put("y", 31536000000L);
|
||||
}
|
||||
|
||||
/** 解析为绝对到期毫秒;无法解析抛出 IllegalArgumentException */
|
||||
public static long parseToExpireMs(String input)
|
||||
throws IllegalArgumentException {
|
||||
if (input == null || input.trim().isEmpty())
|
||||
throw new IllegalArgumentException("时长为空");
|
||||
String s = input.trim().replace(" ", "");
|
||||
|
||||
// 1) 纯数字 → Unix 秒时间戳
|
||||
if (s.matches("\\d+")) {
|
||||
long ts = Long.parseLong(s);
|
||||
return ts * 1000L;
|
||||
}
|
||||
|
||||
// 2) 中文日期 YYYY年M月D日
|
||||
Matcher mc = Pattern.compile(
|
||||
"(\\d{4})年(\\d{1,2})月(\\d{1,2})日").matcher(s);
|
||||
if (mc.find()) {
|
||||
return buildDateMillis(Integer.parseInt(mc.group(1)),
|
||||
Integer.parseInt(mc.group(2)),
|
||||
Integer.parseInt(mc.group(3)));
|
||||
}
|
||||
|
||||
// 3) ISO 日期 YYYY-MM-DD / YYYY/MM/DD
|
||||
Matcher mi = Pattern.compile(
|
||||
"(\\d{4})[-/](\\d{1,2})[-/](\\d{1,2})").matcher(s);
|
||||
if (mi.find()) {
|
||||
return buildDateMillis(Integer.parseInt(mi.group(1)),
|
||||
Integer.parseInt(mi.group(2)),
|
||||
Integer.parseInt(mi.group(3)));
|
||||
}
|
||||
|
||||
// 4) 相对时长(可多段组合)
|
||||
long rel = parseRelative(s);
|
||||
if (rel >= 0) {
|
||||
return System.currentTimeMillis() + rel;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("无法解析时长: " + input);
|
||||
}
|
||||
|
||||
private static long parseRelative(String s) {
|
||||
// 按「数字 + 单位」逐段匹配并累加
|
||||
StringBuilder units = new StringBuilder();
|
||||
for (String u : UNIT_MS.keySet()) {
|
||||
if (units.length() > 0) units.append("|");
|
||||
// 转义,避免正则特殊字符
|
||||
units.append(Pattern.quote(u));
|
||||
}
|
||||
Pattern p = Pattern.compile(
|
||||
"([0-9零一二两三四五六七八九十百千万]+)(" + units + ")");
|
||||
Matcher m = p.matcher(s);
|
||||
long total = 0;
|
||||
int found = 0;
|
||||
while (m.find()) {
|
||||
found++;
|
||||
long num = parseNumber(m.group(1));
|
||||
Long u = UNIT_MS.get(m.group(2));
|
||||
if (u == null) continue;
|
||||
total += num * u;
|
||||
}
|
||||
return found > 0 ? total : -1;
|
||||
}
|
||||
|
||||
private static long buildDateMillis(int y, int mo, int d) {
|
||||
Calendar c = Calendar.getInstance();
|
||||
c.set(y, mo - 1, d, 0, 0, 0);
|
||||
c.set(Calendar.MILLISECOND, 0);
|
||||
return c.getTimeInMillis();
|
||||
}
|
||||
|
||||
/** 解析数字(阿拉伯数字优先,其次中文数字 零-九/十百千万/两/壹贰叁…) */
|
||||
private static long parseNumber(String s) {
|
||||
if (s.matches("\\d+")) return Long.parseLong(s);
|
||||
|
||||
Map<Character, Long> cn = new HashMap<>();
|
||||
cn.put('零', 0L); cn.put('〇', 0L);
|
||||
cn.put('一', 1L); cn.put('壹', 1L);
|
||||
cn.put('二', 2L); cn.put('贰', 2L); cn.put('两', 2L);
|
||||
cn.put('三', 3L); cn.put('叁', 3L);
|
||||
cn.put('四', 4L); cn.put('肆', 4L);
|
||||
cn.put('五', 5L); cn.put('伍', 5L);
|
||||
cn.put('六', 6L); cn.put('陆', 6L);
|
||||
cn.put('七', 7L); cn.put('柒', 7L);
|
||||
cn.put('八', 8L); cn.put('捌', 8L);
|
||||
cn.put('九', 9L); cn.put('玖', 9L);
|
||||
|
||||
long section = 0, num = 0;
|
||||
boolean hasNum = false;
|
||||
for (char ch : s.toCharArray()) {
|
||||
if (cn.containsKey(ch)) {
|
||||
num = cn.get(ch);
|
||||
hasNum = true;
|
||||
} else if (ch == '十') {
|
||||
if (!hasNum) num = 1;
|
||||
section += num * 10; hasNum = false; num = 0;
|
||||
} else if (ch == '百') {
|
||||
if (!hasNum) num = 1;
|
||||
section += num * 100; hasNum = false; num = 0;
|
||||
} else if (ch == '千') {
|
||||
if (!hasNum) num = 1;
|
||||
section += num * 1000; hasNum = false; num = 0;
|
||||
} else if (ch == '万') {
|
||||
if (!hasNum) num = 1;
|
||||
section = (section + num) * 10000; hasNum = false; num = 0;
|
||||
} else {
|
||||
num = 0;
|
||||
}
|
||||
}
|
||||
return section + (hasNum ? num : 0);
|
||||
}
|
||||
|
||||
/** 格式化绝对到期时间戳为「yyyy年M月d日 HH:mm」 */
|
||||
public static String formatExpire(long ms) {
|
||||
Calendar c = Calendar.getInstance();
|
||||
c.setTimeInMillis(ms);
|
||||
return String.format("%d年%d月%d日 %02d:%02d",
|
||||
c.get(Calendar.YEAR),
|
||||
c.get(Calendar.MONTH) + 1,
|
||||
c.get(Calendar.DAY_OF_MONTH),
|
||||
c.get(Calendar.HOUR_OF_DAY),
|
||||
c.get(Calendar.MINUTE));
|
||||
}
|
||||
}
|
||||
@@ -382,6 +382,7 @@ public class Main extends JavaPlugin
|
||||
chatFilter = new ChatFilterManager(this);
|
||||
chatFilter.loadConfig();
|
||||
chatFilter.loadIllegalDomains(); // 加载非法域名后缀
|
||||
chatFilter.loadMutes(); // 启动加载持久化禁言(muted_until 字段)
|
||||
welcome = new WelcomeManager(this);
|
||||
|
||||
// ===== 5.5 Web通信管理器 =====
|
||||
@@ -1835,6 +1836,20 @@ public class Main extends JavaPlugin
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 异地登录检测:异步通知PHP比较IP归属并发提醒邮件 =====
|
||||
if (webManager != null && ip != null
|
||||
&& !ip.isEmpty()) {
|
||||
final String alertName = name;
|
||||
final String alertIp = ip;
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
webManager.reportLoginLocation(
|
||||
alertName, alertIp);
|
||||
}
|
||||
}.runTaskAsynchronously(this);
|
||||
}
|
||||
|
||||
// ★ 传送联动:登录阶段自动开启接受传送(基岩版玩家跳过)
|
||||
if (teleportMgr != null) {
|
||||
teleportMgr.onPlayerLogin(p);
|
||||
@@ -4656,6 +4671,67 @@ public class Main extends JavaPlugin
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== /tempban 临时封禁 =====
|
||||
if (cmd.getName().equalsIgnoreCase("tempban")) {
|
||||
if (sender instanceof Player
|
||||
&& !isAdmin(sender)) {
|
||||
sender.sendMessage("§c权限不足");
|
||||
return true;
|
||||
}
|
||||
if (args.length < 2) {
|
||||
sender.sendMessage(
|
||||
"§e/tempban <玩家名> <时长> [理由]");
|
||||
sender.sendMessage(
|
||||
"§7时长示例: 1秒 / 1s / 1年 / 2026年7月9日 / 253402271999");
|
||||
return true;
|
||||
}
|
||||
String target = args[0];
|
||||
String durStr = args[1];
|
||||
StringBuilder reason = new StringBuilder();
|
||||
for (int i = 2; i < args.length; i++) {
|
||||
if (reason.length() > 0)
|
||||
reason.append(" ");
|
||||
reason.append(args[i]);
|
||||
}
|
||||
long expireMs;
|
||||
try {
|
||||
expireMs = DurationParser
|
||||
.parseToExpireMs(durStr);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
sender.sendMessage("§c时长格式无法解析: "
|
||||
+ ex.getMessage());
|
||||
return true;
|
||||
}
|
||||
long durMs = expireMs
|
||||
- System.currentTimeMillis();
|
||||
long MIN = 1000L;
|
||||
long MAX = 10L * 365 * 24 * 3600 * 1000; // 约10年
|
||||
if (durMs < MIN) {
|
||||
sender.sendMessage("§c封禁时长至少 1 秒");
|
||||
return true;
|
||||
}
|
||||
if (durMs > MAX) {
|
||||
sender.sendMessage("§c封禁时长最长 10 年");
|
||||
return true;
|
||||
}
|
||||
String r = reason.length() > 0
|
||||
? reason.toString()
|
||||
: "您已被服务器封禁,效期至: "
|
||||
+ DurationParser.formatExpire(expireMs);
|
||||
Bukkit.getBanList(
|
||||
org.bukkit.BanList.Type.NAME).addBan(
|
||||
target, r,
|
||||
new java.util.Date(expireMs),
|
||||
sender.getName());
|
||||
sender.sendMessage("§a已封禁 " + target
|
||||
+ " 至 " + DurationParser.formatExpire(expireMs));
|
||||
org.bukkit.entity.Player tp =
|
||||
Bukkit.getPlayerExact(target);
|
||||
if (tp != null)
|
||||
tp.kickPlayer("§c§l" + r);
|
||||
return true;
|
||||
}
|
||||
|
||||
String sub = (args.length > 0) ? args[0].toLowerCase() : "";
|
||||
|
||||
// ===== 在 onCommand 方法体内,最开头加: =====
|
||||
@@ -5128,6 +5204,56 @@ public class Main extends JavaPlugin
|
||||
return true;
|
||||
}
|
||||
String cs = args[0].toLowerCase();
|
||||
if (cs.equals("mute")) {
|
||||
if (args.length < 3) {
|
||||
sender.sendMessage(
|
||||
"§e/chat mute <玩家名> <时长> [理由]");
|
||||
sender.sendMessage(
|
||||
"§7时长示例: 1秒 / 1s / 1年 / 2026年7月9日 / 253402271999");
|
||||
return true;
|
||||
}
|
||||
String target = args[1];
|
||||
String durStr = args[2];
|
||||
StringBuilder reason = new StringBuilder();
|
||||
for (int i = 3; i < args.length; i++) {
|
||||
if (reason.length() > 0)
|
||||
reason.append(" ");
|
||||
reason.append(args[i]);
|
||||
}
|
||||
long expireMs;
|
||||
try {
|
||||
expireMs = DurationParser
|
||||
.parseToExpireMs(durStr);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
sender.sendMessage("§c时长格式无法解析: "
|
||||
+ ex.getMessage());
|
||||
return true;
|
||||
}
|
||||
long durMs = expireMs
|
||||
- System.currentTimeMillis();
|
||||
long MIN = 1000L;
|
||||
long MAX = 30L * 24 * 3600 * 1000; // 1个月
|
||||
if (durMs < MIN) {
|
||||
sender.sendMessage("§c禁言时长至少 1 秒");
|
||||
return true;
|
||||
}
|
||||
if (durMs > MAX) {
|
||||
sender.sendMessage("§c禁言时长最长 1 个月");
|
||||
return true;
|
||||
}
|
||||
String r = reason.length() > 0
|
||||
? reason.toString()
|
||||
: "您已被管理员禁言,效期至:"
|
||||
+ DurationParser.formatExpire(expireMs);
|
||||
chatFilter.mutePlayer(target, expireMs, r);
|
||||
sender.sendMessage("§a已禁言 " + target
|
||||
+ " 至 " + DurationParser.formatExpire(expireMs));
|
||||
org.bukkit.entity.Player tp =
|
||||
Bukkit.getPlayerExact(target);
|
||||
if (tp != null)
|
||||
tp.sendMessage("§c§l" + r);
|
||||
return true;
|
||||
}
|
||||
if (cs.equals("reload")) {
|
||||
chatFilter.loadConfig();
|
||||
sender.sendMessage("§a聊天配置已重载");
|
||||
@@ -6856,7 +6982,7 @@ public class Main extends JavaPlugin
|
||||
list.addAll(Arrays.asList(
|
||||
"reload", "add", "remove",
|
||||
"addplayer", "takeplayer",
|
||||
"unmute", "reset"));
|
||||
"mute", "unmute", "reset"));
|
||||
}
|
||||
if (cmd.getName().equalsIgnoreCase("cypay")) {
|
||||
return cypayCommand.onTabComplete(sender, cmd, label, args);
|
||||
|
||||
@@ -110,6 +110,14 @@ public class PVPArenaManager implements Listener {
|
||||
private final Map<String, Integer> pvpKills = new ConcurrentHashMap<>();
|
||||
private final Map<String, Integer> pvpDeaths = new ConcurrentHashMap<>();
|
||||
|
||||
// 连杀播报(移植自 PVPManager,全服播报)
|
||||
private final Map<String, PVPManager.KillSession>
|
||||
arenaKillSessions =
|
||||
new ConcurrentHashMap<>();
|
||||
private final Set<String> arenaTripleAnnounced =
|
||||
ConcurrentHashMap.newKeySet();
|
||||
private static final String ARENA_LABEL = "PVP竞技场";
|
||||
|
||||
// 记分板条目缓存:玩家名 -> 侧边栏条目字符串(用于精确清除)
|
||||
private final Map<String, String> scoreEntries = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -1094,8 +1102,54 @@ public class PVPArenaManager implements Listener {
|
||||
// paper-api 1.21.4: getKiller() 已移除,改用 DamageSource.getCausingEntity()
|
||||
org.bukkit.entity.Entity killer = event.getDamageSource().getCausingEntity();
|
||||
if (killer instanceof Player && inPVPArena.contains(killer.getName())) {
|
||||
pvpKills.put(killer.getName(), pvpKills.getOrDefault(killer.getName(), 0) + 1);
|
||||
((Player) killer).sendMessage("\u00a7a\u00a7l击杀 " + player.getName() + "\u00a7a 当前击杀: " + pvpKills.get(killer.getName()));
|
||||
String kName = killer.getName();
|
||||
String vName = player.getName();
|
||||
// 总榜击杀数
|
||||
pvpKills.put(kName, pvpKills.getOrDefault(kName, 0) + 1);
|
||||
|
||||
// 击杀者连杀会话
|
||||
PVPManager.KillSession ks = arenaKillSessions.get(kName);
|
||||
if (ks == null
|
||||
|| System.currentTimeMillis() - ks.lastKill
|
||||
> 5 * 60 * 1000) {
|
||||
arenaTripleAnnounced.remove(kName);
|
||||
PVPManager.KillSession ns =
|
||||
new PVPManager.KillSession();
|
||||
ns.region = ARENA_LABEL;
|
||||
ns.count = 1;
|
||||
ns.firstKill = System.currentTimeMillis();
|
||||
ns.lastKill = System.currentTimeMillis();
|
||||
arenaKillSessions.put(kName, ns);
|
||||
} else {
|
||||
ks.count++;
|
||||
ks.lastKill = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// 终结检测:受害者连杀 >= 3 被终结
|
||||
PVPManager.KillSession victimSession =
|
||||
arenaKillSessions.get(vName);
|
||||
if (victimSession != null
|
||||
&& victimSession.count >= 3) {
|
||||
String endMsg = "§b§l" + kName + " §e终结了 "
|
||||
+ vName + " §e的 " + victimSession.count
|
||||
+ "连杀!§a恭喜终结者 " + kName
|
||||
+ "§e!§7快来挑战!";
|
||||
arenaKillSessions.remove(vName);
|
||||
broadcastWithSound(endMsg,
|
||||
org.bukkit.Sound.ENTITY_ENDER_DRAGON_GROWL);
|
||||
}
|
||||
// 无论如何,死者连杀清零
|
||||
arenaKillSessions.remove(vName);
|
||||
|
||||
// 多杀通报(全服播报)
|
||||
PVPManager.KillSession killerSession =
|
||||
arenaKillSessions.get(kName);
|
||||
if (killerSession != null) {
|
||||
checkArenaMultiKill((Player) killer,
|
||||
killerSession);
|
||||
}
|
||||
|
||||
((Player) killer).sendMessage("\u00a7a\u00a7l击杀 " + vName + "\u00a7a 当前击杀: " + pvpKills.get(kName));
|
||||
}
|
||||
pvpDeaths.put(player.getName(), pvpDeaths.getOrDefault(player.getName(), 0) + 1);
|
||||
refreshPVPScoreboard();
|
||||
@@ -1119,6 +1173,41 @@ public class PVPArenaManager implements Listener {
|
||||
}.runTaskLater(plugin, 20L); // 1秒后
|
||||
}
|
||||
|
||||
// ===== 全服击杀播报(移植自 PVPManager) =====
|
||||
private void broadcastWithSound(String msg,
|
||||
org.bukkit.Sound sound) {
|
||||
Bukkit.broadcastMessage(msg);
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
p.playSound(p.getLocation(), sound,
|
||||
1.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkArenaMultiKill(Player killer,
|
||||
PVPManager.KillSession session) {
|
||||
int count = session.count;
|
||||
String kName = killer.getName();
|
||||
String msg = null;
|
||||
if (count >= 5) {
|
||||
msg = "§c§l" + kName + " §e在「" + ARENA_LABEL
|
||||
+ "」达成 " + count + "连杀!§7§l快来挑战!";
|
||||
} else if (count == 3) {
|
||||
if (arenaTripleAnnounced.add(kName)) {
|
||||
msg = "§6§l三杀!" + kName
|
||||
+ " 势不可挡!§7快来挑战!";
|
||||
}
|
||||
}
|
||||
if (msg != null) {
|
||||
if (count >= 5) {
|
||||
broadcastWithSound(msg,
|
||||
org.bukkit.Sound.ENTITY_ENDER_DRAGON_GROWL);
|
||||
} else {
|
||||
broadcastWithSound(msg,
|
||||
org.bukkit.Sound.BLOCK_END_PORTAL_SPAWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家加入事件 — 如果上次在PVP中断线,还原背包
|
||||
*/
|
||||
|
||||
@@ -3908,6 +3908,39 @@ public class WebManager {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "freeze": {
|
||||
// ★ 玩家主动冻结账号(异地登录邮件触发):Bukkit 原生封禁
|
||||
String target = targetName;
|
||||
if (!target.isEmpty()) {
|
||||
String reason = "§c§l您的账号已被临时冻结(疑似被盗)\n§7请到网页修改密码后自动解冻";
|
||||
Bukkit.getBanList(
|
||||
org.bukkit.BanList.Type.NAME)
|
||||
.addBan(target, reason,
|
||||
null, "security");
|
||||
org.bukkit.entity.Player fp =
|
||||
Bukkit.getPlayerExact(target);
|
||||
if (fp != null)
|
||||
fp.kickPlayer(reason);
|
||||
plugin.getLogger().info(
|
||||
"[安全] 已按玩家请求冻结账号: "
|
||||
+ target);
|
||||
applied = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "unfreeze": {
|
||||
// ★ 玩家改密后解冻:解除 Bukkit 原生封禁
|
||||
String target = targetName;
|
||||
if (!target.isEmpty()) {
|
||||
Bukkit.getBanList(
|
||||
org.bukkit.BanList.Type.NAME)
|
||||
.pardon(target);
|
||||
plugin.getLogger().info(
|
||||
"[安全] 已解冻账号: " + target);
|
||||
applied = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "group_buy": {
|
||||
// ★ 玩家端付费加入用户组:PHP写入pending → Java拉取执行
|
||||
String buyGroup = String.valueOf(changeData.getOrDefault("group_name", ""));
|
||||
@@ -5289,6 +5322,29 @@ public class WebManager {
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步通知 PHP:玩家加入服务器,触发异地登录检测与提醒邮件。
|
||||
* PHP 端比较 IP 归属并决定是否发邮件(含冻结/改密链接)。
|
||||
*/
|
||||
public void reportLoginLocation(String name, String ip) {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
String url = webBaseUrl + "/api/security_alert.php"
|
||||
+ "?action=login_location_alert"
|
||||
+ "&secret=" + java.net.URLEncoder
|
||||
.encode(secretKey, "UTF-8")
|
||||
+ "&name=" + java.net.URLEncoder
|
||||
.encode(name, "UTF-8")
|
||||
+ "&ip=" + java.net.URLEncoder
|
||||
.encode(ip, "UTF-8");
|
||||
doGet(url);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning(
|
||||
"[安全] 通知PHP异地登录检测失败: "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Web登录轮询 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -153,3 +153,7 @@ commands:
|
||||
back:
|
||||
description: 快速返回最近记录点
|
||||
usage: /back
|
||||
|
||||
tempban:
|
||||
description: 临时封禁玩家
|
||||
usage: /tempban <玩家名> <时长> [理由]
|
||||
|
||||
Reference in New Issue
Block a user