feat: 完善管理员玩家信息查询
Build / build (push) Canceled after 0s
Build / Paper 1.21.11 / Java 21 (push) Canceled after 0s
Build / Paper 26.1.2 / Java 25 (push) Canceled after 0s
Build / Paper 26.2 / Java 25 (push) Canceled after 0s

This commit is contained in:
2026-10-01 15:52:39 +08:00
parent 94197e6bfa
commit 37f29b50b8
7 changed files with 163 additions and 6 deletions
+6
View File
@@ -1,5 +1,11 @@
# 更新日志
## 未发布
### 优化
- 扩展管理员玩家信息查询,增加身份、权限状态、登录历史、实时状态、客户端与网络信息。
## 1.3.1 - 2026-08-16
### 移除
+1 -1
View File
@@ -40,7 +40,7 @@
- `/hat`
- `/suicide` `(/die)`
- `/vanish` `(/v)`
- `/seen` `(/info)`
- `/seen` `(/info)` 管理员玩家详细信息查询
- `/tpsbar`
- `/essc admin` 管理模式切换
@@ -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};
}
}
+26 -1
View File
@@ -37,9 +37,34 @@ messages:
seen-usage: '<#FF3300>Error:</#FF3300> <#FF7E5E>Usage: /seen [player]</#FF7E5E>'
seen-usage-console: '<#FF3300>Error:</#FF3300> <#FF7E5E>Usage: /seen [player]</#FF7E5E>'
seen-header: '<#00FB9A>Player information:</#00FB9A> <white>{player}</white>'
seen-uuid: '<gray>UUID: <white>{uuid}</white></gray>'
seen-operator: '<gray>Operator: <white>{value}</white></gray>'
seen-whitelisted: '<gray>Whitelisted: <white>{value}</white></gray>'
seen-banned: '<gray>Banned: <white>{value}</white></gray>'
seen-yes: 'Yes'
seen-no: 'No'
seen-unknown: 'Unknown'
seen-status-online: '<gray>Status: <#00FB9A>Online</#00FB9A></gray>'
seen-status-offline: '<gray>Status: <#FF7E5E>Offline</#FF7E5E></gray>'
seen-world: '<gray>World: <white>{world}</white></gray>'
seen-location: '<gray>Location: <white>{x}, {y}, {z}</white></gray>'
seen-gamemode: '<gray>Game mode: <white>{mode}</white></gray>'
seen-gamemode-values:
survival: 'Survival'
creative: 'Creative'
adventure: 'Adventure'
spectator: 'Spectator'
seen-health: '<gray>Health: <white>{health}/{max_health}</white></gray>'
seen-food: '<gray>Food: <white>{food}/20</white></gray>'
seen-level: '<gray>Experience level: <white>{level}</white> <gray>Progress: <white>{experience}%</white></gray>'
seen-ping: '<gray>Ping: <white>{ping} ms</white></gray>'
seen-locale: '<gray>Client locale: <white>{locale}</white></gray>'
seen-client: '<gray>Client: <white>{client}</white></gray>'
seen-ip: '<gray>IP address: <white>{address}</white></gray>'
seen-flying: '<gray>Flying: <white>{value}</white></gray>'
seen-playtime: '<gray>Total playtime: <white>{time}</white></gray>'
seen-duration: '{days}d {hours}h {minutes}m'
seen-last-login: '<gray>Last login: <white>{time}</white></gray>'
seen-last-online: '<gray>Last online: <white>{time}</white></gray>'
seen-first-joined: '<gray>First joined: <white>{time}</white></gray>'
player-not-found: '<#FF3300>Error:</#FF3300> <#FF7E5E>Could not find the player {player}.</#FF7E5E>'
@@ -95,7 +120,7 @@ help:
glow: '<white>/glow</white> <gray>- Toggle glowing</gray>'
heal: '<white>/heal [player]</white> <gray>- Restore health and hunger</gray>'
vanish: '<white>/vanish</white> <gray>- Toggle vanish mode</gray>'
seen: '<white>/seen [player]</white> <gray>- View player information</gray>'
seen: '<white>/seen [player]</white> <gray>- View detailed player information</gray>'
feed: '<white>/feed [player]</white> <gray>- Restore hunger</gray>'
repair: '<white>/repair</white> <gray>- Repair held or all items</gray>'
tpa: '<white>/tpa [player]</white> <gray>- Request to teleport to a player</gray>'
+26 -1
View File
@@ -37,9 +37,34 @@ messages:
seen-usage: '<#FF3300>错误:</#FF3300> <#FF7E5E>用法: /seen [玩家]</#FF7E5E>'
seen-usage-console: '<#FF3300>错误:</#FF3300> <#FF7E5E>用法: /seen [玩家]</#FF7E5E>'
seen-header: '<#00FB9A>玩家信息:</#00FB9A> <white>{player}</white>'
seen-uuid: '<gray>UUID: <white>{uuid}</white></gray>'
seen-operator: '<gray>管理员: <white>{value}</white></gray>'
seen-whitelisted: '<gray>白名单: <white>{value}</white></gray>'
seen-banned: '<gray>封禁状态: <white>{value}</white></gray>'
seen-yes: '是'
seen-no: '否'
seen-unknown: '未知'
seen-status-online: '<gray>状态: <#00FB9A>在线</#00FB9A></gray>'
seen-status-offline: '<gray>状态: <#FF7E5E>离线</#FF7E5E></gray>'
seen-world: '<gray>所在世界: <white>{world}</white></gray>'
seen-location: '<gray>坐标: <white>{x}, {y}, {z}</white></gray>'
seen-gamemode: '<gray>游戏模式: <white>{mode}</white></gray>'
seen-gamemode-values:
survival: '生存'
creative: '创造'
adventure: '冒险'
spectator: '旁观'
seen-health: '<gray>生命值: <white>{health}/{max_health}</white></gray>'
seen-food: '<gray>饥饿值: <white>{food}/20</white></gray>'
seen-level: '<gray>经验等级: <white>{level}</white> <gray>进度: <white>{experience}%</white></gray>'
seen-ping: '<gray>延迟: <white>{ping} ms</white></gray>'
seen-locale: '<gray>客户端语言: <white>{locale}</white></gray>'
seen-client: '<gray>客户端: <white>{client}</white></gray>'
seen-ip: '<gray>IP 地址: <white>{address}</white></gray>'
seen-flying: '<gray>正在飞行: <white>{value}</white></gray>'
seen-playtime: '<gray>累计游戏时间: <white>{time}</white></gray>'
seen-duration: '{days}天 {hours}小时 {minutes}分钟'
seen-last-login: '<gray>最近登录: <white>{time}</white></gray>'
seen-last-online: '<gray>最后在线: <white>{time}</white></gray>'
seen-first-joined: '<gray>首次加入: <white>{time}</white></gray>'
player-not-found: '<#FF3300>错误:</#FF3300> <#FF7E5E>没有找到玩家 {player}.</#FF7E5E>'
@@ -95,7 +120,7 @@ help:
glow: '<white>/glow</white> <gray>- 切换发光</gray>'
heal: '<white>/heal [玩家]</white> <gray>- 恢复生命值和饥饿值</gray>'
vanish: '<white>/vanish</white> <gray>- 切换隐身模式</gray>'
seen: '<white>/seen [玩家]</white> <gray>- 查看玩家信息</gray>'
seen: '<white>/seen [玩家]</white> <gray>- 查看玩家详细信息</gray>'
feed: '<white>/feed [玩家]</white> <gray>- 恢复饥饿值</gray>'
repair: '<white>/repair</white> <gray>- 修复手中或全部物品</gray>'
tpa: '<white>/tpa [玩家]</white> <gray>- 请求传送到玩家身边</gray>'
+1 -1
View File
@@ -66,7 +66,7 @@ permissions:
description: 允许看见隐身玩家
default: op
essentialsc.command.seen:
description: 允许使用 /seen
description: 允许查看玩家详细信息
default: op
essentialsc.command.feed:
description: 允许使用 /feed
@@ -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));
}
}