feat: 新增死亡报告与快速返回功能 - DeathReport记录死亡坐标并发送消息 - QuickBack每分钟记录坐标支持/back传送 - 更新plugin.yml添加back命令
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 死亡报告系统
|
||||
* - 记录玩家死亡坐标(一次性,新死亡覆盖)
|
||||
* - 死亡后发送包含坐标的消息
|
||||
*/
|
||||
public class DeathReport implements Listener {
|
||||
private final Main plugin;
|
||||
|
||||
// 内存缓存: player_name → DeathData
|
||||
private final ConcurrentHashMap<String, DeathData> deathReports = new ConcurrentHashMap<>();
|
||||
|
||||
public static class DeathData {
|
||||
public final String playerName;
|
||||
public String worldName;
|
||||
public final double x, y, z;
|
||||
public long deathTime;
|
||||
public final UUID playerId;
|
||||
|
||||
DeathData(String playerName, UUID playerId, Location loc) {
|
||||
this.playerName = playerName;
|
||||
this.playerId = playerId;
|
||||
this.worldName = loc.getWorld() != null ? loc.getWorld().getName() : "world";
|
||||
this.x = loc.getX();
|
||||
this.y = loc.getY();
|
||||
this.z = loc.getZ();
|
||||
this.deathTime = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
public DeathReport(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
initTable();
|
||||
loadAllFromDB();
|
||||
}
|
||||
|
||||
// ==================== 事件监听 ====================
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerDeath(PlayerDeathEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
Location deathLoc = player.getLocation();
|
||||
if (deathLoc == null) return;
|
||||
|
||||
DeathData data = new DeathData(player.getName(), player.getUniqueId(), deathLoc);
|
||||
deathReports.put(player.getName(), data);
|
||||
saveToDB(data);
|
||||
|
||||
// 发送死亡报告消息
|
||||
String locMsg = String.format("§c✦ 你已死亡 ✦§7 世界: §f%s §| §7坐标: §e%.1f, %.1f, %.1f§7 时间: §f%s",
|
||||
data.worldName, data.x, data.y, data.z,
|
||||
new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date(data.deathTime)));
|
||||
|
||||
player.sendMessage(locMsg);
|
||||
}
|
||||
|
||||
// ==================== 查询/获取 ====================
|
||||
|
||||
/**
|
||||
* 获取玩家的最新死亡坐标(如果有的话)
|
||||
*/
|
||||
public DeathData getDeathReport(String playerName) {
|
||||
return deathReports.get(playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除玩家的死亡报告
|
||||
*/
|
||||
public void clearDeathReport(String playerName) {
|
||||
deathReports.remove(playerName);
|
||||
}
|
||||
|
||||
// ==================== 数据库操作 ====================
|
||||
|
||||
private void loadAllFromDB() {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return;
|
||||
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT player_name, player_uuid, world, x, y, z, death_time FROM death_reports");
|
||||
ResultSet rs = ps.executeQuery();
|
||||
|
||||
while (rs.next()) {
|
||||
DeathData data = new DeathData(
|
||||
rs.getString("player_name"),
|
||||
UUID.fromString(rs.getString("player_uuid")),
|
||||
new Location(null, rs.getDouble("x"), rs.getDouble("y"), rs.getDouble("z"))
|
||||
);
|
||||
data.worldName = rs.getString("world");
|
||||
data.deathTime = rs.getLong("death_time");
|
||||
deathReports.put(data.playerName, data);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
// 表可能不存在,忽略(会在第一次死亡时自动创建)
|
||||
}
|
||||
}
|
||||
|
||||
private void saveToDB(DeathData data) {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return;
|
||||
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"INSERT OR REPLACE INTO death_reports (player_name, player_uuid, world, x, y, z, death_time) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
ps.setString(1, data.playerName);
|
||||
ps.setString(2, data.playerId.toString());
|
||||
ps.setString(3, data.worldName);
|
||||
ps.setDouble(4, data.x);
|
||||
ps.setDouble(5, data.y);
|
||||
ps.setDouble(6, data.z);
|
||||
ps.setLong(7, data.deathTime);
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().warning("[死亡报告] 保存失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void persistAll() {
|
||||
for (DeathData data : deathReports.values()) {
|
||||
saveToDB(data);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
public void initTable() {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return;
|
||||
|
||||
java.sql.Statement st = conn.createStatement();
|
||||
st.execute("CREATE TABLE IF NOT EXISTS death_reports ("
|
||||
+ "player_name TEXT PRIMARY KEY, "
|
||||
+ "player_uuid TEXT NOT NULL, "
|
||||
+ "world TEXT, "
|
||||
+ "x REAL, y REAL, z REAL, "
|
||||
+ "death_time LONG)");
|
||||
st.close();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().warning("[死亡报告] 建表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityDeathEvent;
|
||||
import org.bukkit.event.entity.ProjectileLaunchEvent;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.event.inventory.CraftItemEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
@@ -147,6 +148,8 @@ public class Main extends JavaPlugin
|
||||
public AreaCLIManager areaCLIManager;
|
||||
private TeleportManager teleportMgr;
|
||||
private HomeManager homeMgr;
|
||||
private DeathReport deathReport;
|
||||
private QuickBack quickBack;
|
||||
|
||||
|
||||
// 钱包流水查看目标(玩家名 → 查看目标)
|
||||
@@ -575,6 +578,17 @@ public class Main extends JavaPlugin
|
||||
getCommand("home").setTabCompleter(this);
|
||||
getCommand("homes").setTabCompleter(this);
|
||||
|
||||
// ===== 死亡报告系统 =====
|
||||
deathReport = new DeathReport(this);
|
||||
deathReport.initTable();
|
||||
getServer().getPluginManager().registerEvents(deathReport, this);
|
||||
|
||||
// ===== 快速返回系统 =====
|
||||
quickBack = new QuickBack(this);
|
||||
quickBack.initTable();
|
||||
getCommand("back").setExecutor(quickBack);
|
||||
getCommand("back").setTabCompleter(quickBack);
|
||||
|
||||
// 注册传送命令已在 onCommand 中通过条件分支完成
|
||||
|
||||
getLogger().info("§b[Sdf1_login]启动完毕\n§lS欢迎使用sdf1系列插件,如有问题,您可在\nGitHub和Gitee提交反馈");
|
||||
@@ -688,6 +702,10 @@ public class Main extends JavaPlugin
|
||||
questTracker.shutdown();
|
||||
if (homeMgr != null)
|
||||
homeMgr.shutdown();
|
||||
if (deathReport != null)
|
||||
deathReport = null;
|
||||
if (quickBack != null)
|
||||
quickBack = null;
|
||||
if (areaProtection != null) {
|
||||
areaProtection.stopEnforceTask();
|
||||
}
|
||||
@@ -4668,6 +4686,18 @@ public class Main extends JavaPlugin
|
||||
}
|
||||
}
|
||||
|
||||
// ===== /back 快速返回 =====
|
||||
if (label.equalsIgnoreCase("back")) {
|
||||
if (!(sender instanceof Player)) {
|
||||
sender.sendMessage("§c此命令仅限玩家使用");
|
||||
return true;
|
||||
}
|
||||
if (quickBack == null) {
|
||||
sender.sendMessage("§c快速返回系统未初始化");
|
||||
return true;
|
||||
}
|
||||
return quickBack.onCommand(sender, cmd, label, args);
|
||||
}
|
||||
|
||||
// ===== /printer [玩家名] =====
|
||||
if (label.equalsIgnoreCase("printer")) {
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package Sdf1_login;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 快速返回系统 (/back)
|
||||
* - 每分钟记录一次在线玩家的坐标
|
||||
* - /back 命令传送玩家到最近的一个记录坐标
|
||||
*/
|
||||
public class QuickBack implements Listener, CommandExecutor, TabCompleter {
|
||||
private final Main plugin;
|
||||
|
||||
// 内存缓存: player_name → Location (每分钟刷新)
|
||||
private final Map<String, Location> lastKnownLocations = new ConcurrentHashMap<>();
|
||||
|
||||
// 冷却: player_name → next_available_time
|
||||
private final Map<String, Long> cooldowns = new ConcurrentHashMap<>();
|
||||
private static final long COOLDOWN_MS = 5 * 60 * 1000; // 5分钟冷却
|
||||
|
||||
// 最大记录数 per player
|
||||
private static final int MAX_RECORDS = 50;
|
||||
|
||||
public QuickBack(Main plugin) {
|
||||
this.plugin = plugin;
|
||||
loadAllFromDB();
|
||||
|
||||
// 每分钟记录所有在线玩家的坐标
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (Player p : Bukkit.getOnlinePlayers()) {
|
||||
recordLocation(p);
|
||||
}
|
||||
}
|
||||
}.runTaskTimer(plugin, 1200L, 1200L);
|
||||
}
|
||||
|
||||
// ==================== 事件监听 ====================
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onPlayerMove(PlayerMoveEvent event) {
|
||||
// 移动超过8格时才记录(减少数据库写入压力)
|
||||
Location from = event.getFrom();
|
||||
Location to = event.getTo();
|
||||
if (to == null || from.getWorld() != to.getWorld()) return;
|
||||
|
||||
double distance = from.distance(to);
|
||||
if (distance < 8.0) return;
|
||||
|
||||
recordLocation(event.getPlayer());
|
||||
}
|
||||
|
||||
// ==================== 命令处理 ====================
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (!(sender instanceof Player)) {
|
||||
sender.sendMessage("§c此命令仅限玩家使用");
|
||||
return true;
|
||||
}
|
||||
|
||||
Player player = (Player) sender;
|
||||
|
||||
// 检查冷却
|
||||
Long nextAvailable = cooldowns.get(player.getName());
|
||||
if (nextAvailable != null && System.currentTimeMillis() < nextAvailable) {
|
||||
long remaining = (nextAvailable - System.currentTimeMillis()) / 1000;
|
||||
player.sendMessage("§c快速返回冷却中,请等待 §e" + remaining + " §c秒");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取最近的已知位置
|
||||
Location loc = getLastKnownLocation(player);
|
||||
if (loc == null) {
|
||||
player.sendMessage("§c没有找到可返回的位置记录");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 传送
|
||||
if (loc.getWorld() == null) {
|
||||
player.sendMessage("§c位置世界信息无效");
|
||||
return true;
|
||||
}
|
||||
|
||||
Location dest = loc.clone().add(0.5, 0, 0.5); // 对齐格子中心
|
||||
player.teleport(dest);
|
||||
|
||||
// 设置冷却
|
||||
cooldowns.put(player.getName(), System.currentTimeMillis() + COOLDOWN_MS);
|
||||
player.sendMessage("§a§l[快速返回] §f已传送到最近记录点");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==================== 位置记录 ====================
|
||||
|
||||
private void recordLocation(Player player) {
|
||||
Location loc = player.getLocation();
|
||||
lastKnownLocations.put(player.getName(), loc);
|
||||
saveToDB(player.getName(), loc);
|
||||
}
|
||||
|
||||
public Location getLastKnownLocation(Player player) {
|
||||
// 优先从内存获取
|
||||
Location loc = lastKnownLocations.get(player.getName());
|
||||
if (loc != null && loc.getWorld() != null) {
|
||||
return loc;
|
||||
}
|
||||
|
||||
// 从数据库获取
|
||||
return loadFromDB(player.getName());
|
||||
}
|
||||
|
||||
// ==================== 数据库操作 ====================
|
||||
|
||||
private void loadAllFromDB() {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return;
|
||||
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT player_name, world, x, y, z FROM quick_back_records");
|
||||
ResultSet rs = ps.executeQuery();
|
||||
|
||||
while (rs.next()) {
|
||||
String playerName = rs.getString("player_name");
|
||||
Location loc = new Location(
|
||||
Bukkit.getWorld(rs.getString("world")),
|
||||
rs.getDouble("x"),
|
||||
rs.getDouble("y"),
|
||||
rs.getDouble("z")
|
||||
);
|
||||
lastKnownLocations.put(playerName, loc);
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
// 表可能不存在
|
||||
}
|
||||
}
|
||||
|
||||
private void saveToDB(String playerName, Location loc) {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return;
|
||||
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"INSERT OR REPLACE INTO quick_back_records (player_name, world, x, y, z) VALUES (?, ?, ?, ?, ?)");
|
||||
ps.setString(1, playerName);
|
||||
ps.setString(2, loc.getWorld() != null ? loc.getWorld().getName() : "world");
|
||||
ps.setDouble(3, loc.getX());
|
||||
ps.setDouble(4, loc.getY());
|
||||
ps.setDouble(5, loc.getZ());
|
||||
ps.executeUpdate();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
|
||||
private Location loadFromDB(String playerName) {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return null;
|
||||
|
||||
PreparedStatement ps = conn.prepareStatement(
|
||||
"SELECT world, x, y, z FROM quick_back_records WHERE player_name=? LIMIT 1");
|
||||
ps.setString(1, playerName);
|
||||
ResultSet rs = ps.executeQuery();
|
||||
|
||||
if (rs.next()) {
|
||||
Location loc = new Location(
|
||||
Bukkit.getWorld(rs.getString("world")),
|
||||
rs.getDouble("x"),
|
||||
rs.getDouble("y"),
|
||||
rs.getDouble("z")
|
||||
);
|
||||
rs.close();
|
||||
ps.close();
|
||||
return loc;
|
||||
}
|
||||
rs.close();
|
||||
ps.close();
|
||||
} catch (SQLException e) {
|
||||
// 忽略
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== Tab补全 ====================
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
public void initTable() {
|
||||
try {
|
||||
java.sql.Connection conn = plugin.getDb().getConnection();
|
||||
if (conn == null) return;
|
||||
|
||||
java.sql.Statement st = conn.createStatement();
|
||||
st.execute("CREATE TABLE IF NOT EXISTS quick_back_records ("
|
||||
+ "player_name TEXT PRIMARY KEY, "
|
||||
+ "world TEXT, "
|
||||
+ "x REAL, y REAL, z REAL)");
|
||||
st.close();
|
||||
} catch (SQLException e) {
|
||||
plugin.getLogger().warning("[快速返回] 建表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,9 +42,9 @@ commands:
|
||||
签到:
|
||||
description: 每日签到(中文)
|
||||
usage: /签到
|
||||
#oa:
|
||||
# description: 申请删除账号
|
||||
# usage: /oa [玩家名]
|
||||
#oa:
|
||||
# description: 申请删除账号
|
||||
# usage: /oa [玩家名]
|
||||
删除账号:
|
||||
description: 申请删除账号(中文)
|
||||
usage: /删除账号 [玩家名]
|
||||
@@ -97,8 +97,8 @@ commands:
|
||||
description: 打开商店
|
||||
usage: /商店
|
||||
# testorder:
|
||||
# description: 测试订单中心
|
||||
# usage: /testorder
|
||||
# description: 测试订单中心
|
||||
# usage: /testorder
|
||||
protect:
|
||||
description: 区域保护
|
||||
usage: /protect
|
||||
@@ -150,4 +150,6 @@ commands:
|
||||
group:
|
||||
description: 用户组管理
|
||||
usage: /group <buy|renew|info> [组名]
|
||||
|
||||
back:
|
||||
description: 快速返回最近记录点
|
||||
usage: /back
|
||||
|
||||
Reference in New Issue
Block a user