diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f88539..1343c70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +## 未发布 + +### 优化 + +- 扩展管理员玩家信息查询,增加身份、权限状态、登录历史、实时状态、客户端与网络信息。 + ## 1.3.1 - 2026-08-16 ### 移除 diff --git a/README.md b/README.md index 1199ae0..70153b5 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ - `/hat` - `/suicide` `(/die)` - `/vanish` `(/v)` -- `/seen` `(/info)` +- `/seen` `(/info)` 管理员玩家详细信息查询 - `/tpsbar` - `/essc admin` 管理模式切换 diff --git a/src/main/java/cn/infstar/essentialsC/commands/SeenCommand.java b/src/main/java/cn/infstar/essentialsC/commands/SeenCommand.java index cdc3c66..3fbd356 100644 --- a/src/main/java/cn/infstar/essentialsC/commands/SeenCommand.java +++ b/src/main/java/cn/infstar/essentialsC/commands/SeenCommand.java @@ -2,11 +2,15 @@ package cn.infstar.essentialsC.commands; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; +import org.bukkit.Statistic; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Locale; import java.util.Map; public class SeenCommand extends BaseCommand { @@ -49,13 +53,24 @@ public class SeenCommand extends BaseCommand { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); sender.sendMessage(getLang().getPrefixedComponent("messages.seen-header", Map.of("player", String.valueOf(target.getName())))); + sender.sendMessage(getLang().getComponent("messages.seen-uuid", + Map.of("uuid", target.getUniqueId().toString()))); + sender.sendMessage(getLang().getComponent("messages.seen-operator", + Map.of("value", getLang().getString(target.isOp() ? "messages.seen-yes" : "messages.seen-no")))); + sender.sendMessage(getLang().getComponent("messages.seen-whitelisted", + Map.of("value", getLang().getString(target.isWhitelisted() ? "messages.seen-yes" : "messages.seen-no")))); + sender.sendMessage(getLang().getComponent("messages.seen-banned", + Map.of("value", getLang().getString(target.isBanned() ? "messages.seen-yes" : "messages.seen-no")))); + if (target.getLastLogin() > 0) { + sender.sendMessage(getLang().getComponent("messages.seen-last-login", + Map.of("time", format.format(new Date(target.getLastLogin()))))); + } if (target.isOnline()) { sender.sendMessage(getLang().getComponent("messages.seen-status-online")); Player onlinePlayer = target.getPlayer(); if (onlinePlayer != null) { - sender.sendMessage(getLang().getComponent("messages.seen-world", - Map.of("world", onlinePlayer.getWorld().getName()))); + sendOnlineDetails(sender, onlinePlayer); } } else { sender.sendMessage(getLang().getComponent("messages.seen-status-offline")); @@ -70,4 +85,74 @@ public class SeenCommand extends BaseCommand { Map.of("time", format.format(new Date(target.getFirstPlayed()))))); return true; } + + private void sendOnlineDetails(CommandSender sender, Player player) { + var location = player.getLocation(); + String address = player.getAddress() == null || player.getAddress().getAddress() == null + ? getLang().getString("messages.seen-unknown") + : player.getAddress().getAddress().getHostAddress(); + String clientBrand = player.getClientBrandName(); + if (clientBrand == null || clientBrand.isBlank()) { + clientBrand = getLang().getString("messages.seen-unknown"); + } + AttributeInstance maxHealthAttribute = player.getAttribute(Attribute.MAX_HEALTH); + double maxHealth = maxHealthAttribute == null ? player.getHealth() : maxHealthAttribute.getValue(); + + sender.sendMessage(getLang().getComponent("messages.seen-world", + Map.of("world", player.getWorld().getName()))); + sender.sendMessage(getLang().getComponent("messages.seen-location", Map.of( + "x", formatNumber(location.getX()), + "y", formatNumber(location.getY()), + "z", formatNumber(location.getZ()) + ))); + sender.sendMessage(getLang().getComponent("messages.seen-gamemode", Map.of( + "mode", getLang().getString("messages.seen-gamemode-values." + + player.getGameMode().name().toLowerCase(Locale.ROOT)) + ))); + sender.sendMessage(getLang().getComponent("messages.seen-health", Map.of( + "health", formatNumber(player.getHealth()), + "max_health", formatNumber(maxHealth) + ))); + sender.sendMessage(getLang().getComponent("messages.seen-food", Map.of( + "food", Integer.toString(player.getFoodLevel()) + ))); + sender.sendMessage(getLang().getComponent("messages.seen-level", Map.of( + "level", Integer.toString(player.getLevel()), + "experience", formatNumber(player.getExp() * 100.0D) + ))); + sender.sendMessage(getLang().getComponent("messages.seen-ping", Map.of( + "ping", Integer.toString(Math.max(0, player.getPing())) + ))); + sender.sendMessage(getLang().getComponent("messages.seen-locale", Map.of( + "locale", player.locale().toLanguageTag() + ))); + sender.sendMessage(getLang().getComponent("messages.seen-client", Map.of( + "client", clientBrand + ))); + sender.sendMessage(getLang().getComponent("messages.seen-ip", Map.of("address", address))); + sender.sendMessage(getLang().getComponent("messages.seen-flying", Map.of( + "value", getLang().getString(player.isFlying() ? "messages.seen-yes" : "messages.seen-no") + ))); + sender.sendMessage(getLang().getComponent("messages.seen-playtime", Map.of( + "time", formatDuration(player.getStatistic(Statistic.PLAY_ONE_MINUTE)) + ))); + } + + private String formatNumber(double value) { + return String.format(Locale.ROOT, "%.1f", value); + } + + private String formatDuration(int ticks) { + long[] duration = durationParts(ticks); + return getLang().getString("messages.seen-duration", Map.of( + "days", Long.toString(duration[0]), + "hours", Long.toString(duration[1]), + "minutes", Long.toString(duration[2]) + )); + } + + static long[] durationParts(int ticks) { + long minutes = Math.max(0L, ticks) / (20L * 60L); + return new long[]{minutes / (24L * 60L), minutes % (24L * 60L) / 60L, minutes % 60L}; + } } diff --git a/src/main/resources/lang/en_US.yml b/src/main/resources/lang/en_US.yml index 75aaf0e..1e310db 100644 --- a/src/main/resources/lang/en_US.yml +++ b/src/main/resources/lang/en_US.yml @@ -37,9 +37,34 @@ messages: seen-usage: '<#FF3300>Error: <#FF7E5E>Usage: /seen [player]' seen-usage-console: '<#FF3300>Error: <#FF7E5E>Usage: /seen [player]' seen-header: '<#00FB9A>Player information: {player}' + seen-uuid: 'UUID: {uuid}' + seen-operator: 'Operator: {value}' + seen-whitelisted: 'Whitelisted: {value}' + seen-banned: 'Banned: {value}' + seen-yes: 'Yes' + seen-no: 'No' + seen-unknown: 'Unknown' seen-status-online: 'Status: <#00FB9A>Online' seen-status-offline: 'Status: <#FF7E5E>Offline' seen-world: 'World: {world}' + seen-location: 'Location: {x}, {y}, {z}' + seen-gamemode: 'Game mode: {mode}' + seen-gamemode-values: + survival: 'Survival' + creative: 'Creative' + adventure: 'Adventure' + spectator: 'Spectator' + seen-health: 'Health: {health}/{max_health}' + seen-food: 'Food: {food}/20' + seen-level: 'Experience level: {level} Progress: {experience}%' + seen-ping: 'Ping: {ping} ms' + seen-locale: 'Client locale: {locale}' + seen-client: 'Client: {client}' + seen-ip: 'IP address: {address}' + seen-flying: 'Flying: {value}' + seen-playtime: 'Total playtime: {time}' + seen-duration: '{days}d {hours}h {minutes}m' + seen-last-login: 'Last login: {time}' seen-last-online: 'Last online: {time}' seen-first-joined: 'First joined: {time}' player-not-found: '<#FF3300>Error: <#FF7E5E>Could not find the player {player}.' @@ -95,7 +120,7 @@ help: glow: '/glow - Toggle glowing' heal: '/heal [player] - Restore health and hunger' vanish: '/vanish - Toggle vanish mode' - seen: '/seen [player] - View player information' + seen: '/seen [player] - View detailed player information' feed: '/feed [player] - Restore hunger' repair: '/repair - Repair held or all items' tpa: '/tpa [player] - Request to teleport to a player' diff --git a/src/main/resources/lang/zh_CN.yml b/src/main/resources/lang/zh_CN.yml index 0a82d4b..445a677 100644 --- a/src/main/resources/lang/zh_CN.yml +++ b/src/main/resources/lang/zh_CN.yml @@ -37,9 +37,34 @@ messages: seen-usage: '<#FF3300>错误: <#FF7E5E>用法: /seen [玩家]' seen-usage-console: '<#FF3300>错误: <#FF7E5E>用法: /seen [玩家]' seen-header: '<#00FB9A>玩家信息: {player}' + seen-uuid: 'UUID: {uuid}' + seen-operator: '管理员: {value}' + seen-whitelisted: '白名单: {value}' + seen-banned: '封禁状态: {value}' + seen-yes: '是' + seen-no: '否' + seen-unknown: '未知' seen-status-online: '状态: <#00FB9A>在线' seen-status-offline: '状态: <#FF7E5E>离线' seen-world: '所在世界: {world}' + seen-location: '坐标: {x}, {y}, {z}' + seen-gamemode: '游戏模式: {mode}' + seen-gamemode-values: + survival: '生存' + creative: '创造' + adventure: '冒险' + spectator: '旁观' + seen-health: '生命值: {health}/{max_health}' + seen-food: '饥饿值: {food}/20' + seen-level: '经验等级: {level} 进度: {experience}%' + seen-ping: '延迟: {ping} ms' + seen-locale: '客户端语言: {locale}' + seen-client: '客户端: {client}' + seen-ip: 'IP 地址: {address}' + seen-flying: '正在飞行: {value}' + seen-playtime: '累计游戏时间: {time}' + seen-duration: '{days}天 {hours}小时 {minutes}分钟' + seen-last-login: '最近登录: {time}' seen-last-online: '最后在线: {time}' seen-first-joined: '首次加入: {time}' player-not-found: '<#FF3300>错误: <#FF7E5E>没有找到玩家 {player}.' @@ -95,7 +120,7 @@ help: glow: '/glow - 切换发光' heal: '/heal [玩家] - 恢复生命值和饥饿值' vanish: '/vanish - 切换隐身模式' - seen: '/seen [玩家] - 查看玩家信息' + seen: '/seen [玩家] - 查看玩家详细信息' feed: '/feed [玩家] - 恢复饥饿值' repair: '/repair - 修复手中或全部物品' tpa: '/tpa [玩家] - 请求传送到玩家身边' diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index acf08a9..875d120 100644 --- a/src/main/resources/paper-plugin.yml +++ b/src/main/resources/paper-plugin.yml @@ -66,7 +66,7 @@ permissions: description: 允许看见隐身玩家 default: op essentialsc.command.seen: - description: 允许使用 /seen + description: 允许查看玩家详细信息 default: op essentialsc.command.feed: description: 允许使用 /feed diff --git a/src/test/java/cn/infstar/essentialsC/commands/SeenCommandTest.java b/src/test/java/cn/infstar/essentialsC/commands/SeenCommandTest.java new file mode 100644 index 0000000..5a9e8cb --- /dev/null +++ b/src/test/java/cn/infstar/essentialsC/commands/SeenCommandTest.java @@ -0,0 +1,16 @@ +package cn.infstar.essentialsC.commands; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class SeenCommandTest { + + @Test + void convertsPlayTicksToDaysHoursAndMinutes() { + int ticks = (2 * 24 * 60 + 3 * 60 + 4) * 60 * 20; + + assertArrayEquals(new long[]{2, 3, 4}, SeenCommand.durationParts(ticks)); + assertArrayEquals(new long[]{0, 0, 0}, SeenCommand.durationParts(-1)); + } +}