feat: 新增死亡报告与快速返回功能 - DeathReport记录死亡坐标并发送消息 - QuickBack每分钟记录坐标支持/back传送 - 更新plugin.yml添加back命令

This commit is contained in:
2026-07-03 19:24:57 +08:00
parent abaf54470c
commit fc495802f2
4 changed files with 435 additions and 6 deletions
+163
View File
@@ -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());
}
}
}
+30
View File
@@ -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")) {
+234
View File
@@ -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());
}
}
}
+8 -6
View File
@@ -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