利用已有api添加新元素

This commit is contained in:
BRanulf 2025-06-09 18:27:05 +08:00
parent 0d9e2db5ee
commit 75bacc1ec7
9 changed files with 237 additions and 84 deletions

View File

@ -6,7 +6,7 @@ minecraft_version=1.21.4
yarn_mappings=1.21.4+build.8 yarn_mappings=1.21.4+build.8
loader_version=0.16.10 loader_version=0.16.10
# Mod Properties # Mod Properties
mod_version=1.14.514.133 mod_version=1.14.514.134
maven_group=org.example1 maven_group=org.example1
archives_base_name=ServerPlayerOnlineTracker archives_base_name=ServerPlayerOnlineTracker
# Dependencies # Dependencies

View File

@ -39,9 +39,7 @@ public class PlayerTimeMod implements ModInitializer {
private static ModConfig config; private static ModConfig config;
public static LocalizationManager localizationManager; public static LocalizationManager localizationManager;
// RSS Feed URL for releases
private static final String RSS_FEED_URL = "https://git.branulf.top/Branulf/ServerPlayerOnlineTracker/releases.rss"; private static final String RSS_FEED_URL = "https://git.branulf.top/Branulf/ServerPlayerOnlineTracker/releases.rss";
// Executor for update check to avoid blocking server startup
private static final ExecutorService updateCheckExecutor = Executors.newSingleThreadExecutor(); private static final ExecutorService updateCheckExecutor = Executors.newSingleThreadExecutor();
@ -53,12 +51,10 @@ public class PlayerTimeMod implements ModInitializer {
try { try {
LOGGER.info("[在线时间] 初始化 玩家在线时长视奸Mod"); LOGGER.info("[在线时间] 初始化 玩家在线时长视奸Mod");
// Check for updates asynchronously on mod initialization
checkForUpdates(); checkForUpdates();
// SERVER_STARTING 阶段创建 Tracker WebServer 实例
ServerLifecycleEvents.SERVER_STARTING.register(server -> { ServerLifecycleEvents.SERVER_STARTING.register(server -> {
timeTracker = new PlayerTimeTracker(server); // Tracker 构造函数不再加载数据 timeTracker = new PlayerTimeTracker(server);
try { try {
webServer = new WebServer(timeTracker, config.getWebPort(), server); webServer = new WebServer(timeTracker, config.getWebPort(), server);
webServer.start(); webServer.start();
@ -68,10 +64,9 @@ public class PlayerTimeMod implements ModInitializer {
} }
}); });
// SERVER_STARTED 阶段加载数据 (此时 UserCache 应该已可用)
ServerLifecycleEvents.SERVER_STARTED.register(server -> { ServerLifecycleEvents.SERVER_STARTED.register(server -> {
if (timeTracker != null) { if (timeTracker != null) {
timeTracker.loadData(); // 在服务器启动完成后加载数据 timeTracker.loadData();
} else { } else {
LOGGER.error("[在线时间] PlayerTimeTracker 未在 SERVER_STARTING 阶段成功初始化!"); LOGGER.error("[在线时间] PlayerTimeTracker 未在 SERVER_STARTING 阶段成功初始化!");
} }
@ -97,15 +92,11 @@ public class PlayerTimeMod implements ModInitializer {
webServer.stop(); webServer.stop();
} }
if (timeTracker != null) { if (timeTracker != null) {
// 在服务器停止前确保所有在线玩家的会话时间被记录
for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) {
timeTracker.onPlayerLeave(player); // onPlayerLeave 会自动保存该玩家数据 timeTracker.onPlayerLeave(player);
} }
// timeTracker.saveAll(); // onPlayerLeave 已经异步保存这里可以考虑是否还需要 saveAll
// 简单的处理是直接调用 saveAll它会覆盖旧数据
timeTracker.saveAll(); timeTracker.saveAll();
} }
// Shutdown the update check executor
updateCheckExecutor.shutdownNow(); updateCheckExecutor.shutdownNow();
}); });
@ -144,11 +135,10 @@ public class PlayerTimeMod implements ModInitializer {
} }
private static int showOnlineTime(ServerCommandSource source, int requestedPage) { private static int showOnlineTime(ServerCommandSource source, int requestedPage) {
// Commands are typically executed after SERVER_STARTED, so UserCache should be available here.
ServerPlayerEntity player = source.getPlayer(); ServerPlayerEntity player = source.getPlayer();
if (player == null) return 0; if (player == null) return 0;
// Use the server's main worker executor for command processing
Util.getMainWorkerExecutor().execute(() -> { Util.getMainWorkerExecutor().execute(() -> {
PlayerTimeTracker tracker = getTimeTracker(); PlayerTimeTracker tracker = getTimeTracker();
if (tracker != null) { if (tracker != null) {
@ -170,18 +160,14 @@ public class PlayerTimeMod implements ModInitializer {
} }
private static int comparePlayTime(String a, String b) { private static int comparePlayTime(String a, String b) {
// This parsing logic is specific to the format generated by getWhitelistedPlayerStats
// Example: "PlayerName: 10h 30m | 7d: 5h 15m | 30d: 20h 45m"
// We need to extract the total time part (e.g., "10h 30m")
try { try {
// Find the first colon and the first pipe
int firstColon = a.indexOf(':'); int firstColon = a.indexOf(':');
int firstPipe = a.indexOf('|'); int firstPipe = a.indexOf('|');
String timeA; String timeA;
if (firstColon > 0) { if (firstColon > 0) {
timeA = (firstPipe > firstColon && firstPipe != -1) ? a.substring(firstColon + 1, firstPipe).trim() : a.substring(firstColon + 1).trim(); timeA = (firstPipe > firstColon && firstPipe != -1) ? a.substring(firstColon + 1, firstPipe).trim() : a.substring(firstColon + 1).trim();
} else { } else {
timeA = a.trim(); // Fallback if format changes unexpectedly timeA = a.trim();
} }
@ -191,26 +177,25 @@ public class PlayerTimeMod implements ModInitializer {
if (firstColonB > 0) { if (firstColonB > 0) {
timeB = (firstPipeB > firstColonB && firstPipeB != -1) ? b.substring(firstColonB + 1, firstPipeB).trim() : b.substring(firstColonB + 1).trim(); timeB = (firstPipeB > firstColonB && firstPipeB != -1) ? b.substring(firstColonB + 1, firstPipeB).trim() : b.substring(firstColonB + 1).trim();
} else { } else {
timeB = b.trim(); // Fallback if format changes unexpectedly timeB = b.trim();
} }
return Long.compare(parseTimeToSeconds(timeB), parseTimeToSeconds(timeA)); // Descending order return Long.compare(parseTimeToSeconds(timeB), parseTimeToSeconds(timeA)); // Descending order
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Error comparing play times: {} vs {}", a, b, e); LOGGER.error("Error comparing play times: {} vs {}", a, b, e);
return 0; // Fallback to equal if parsing fails return 0;
} }
} }
private static long parseTimeToSeconds(String timeStr) { private static long parseTimeToSeconds(String timeStr) {
long seconds = 0; long seconds = 0;
// Handle potential empty or null strings
if (timeStr == null || timeStr.trim().isEmpty()) { if (timeStr == null || timeStr.trim().isEmpty()) {
return 0; return 0;
} }
String[] parts = timeStr.trim().split("\\s+"); // Split by one or more spaces String[] parts = timeStr.trim().split("\\s+");
for (String part : parts) { for (String part : parts) {
if (part.endsWith("h")) { if (part.endsWith("h")) {
try { try {
@ -221,14 +206,11 @@ public class PlayerTimeMod implements ModInitializer {
seconds += Integer.parseInt(part.substring(0, part.length() - 1)) * 60; seconds += Integer.parseInt(part.substring(0, part.length() - 1)) * 60;
} catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) {}
} }
// Ignore other parts like "d:" or numbers without units
} }
return seconds; return seconds;
} }
private static String formatPlayerTime(String name, String timeStr) { private static String formatPlayerTime(String name, String timeStr) {
// timeStr is already formatted like "Total: Xh Ym | 7d: ... | 30d: ..."
// We just need to prepend the player name
return String.format("§e%s§r: %s", name, timeStr); return String.format("§e%s§r: %s", name, timeStr);
} }
@ -277,13 +259,9 @@ public class PlayerTimeMod implements ModInitializer {
player.sendMessage(footer, false); player.sendMessage(footer, false);
} }
/**
* Checks the RSS feed for new releases asynchronously.
*/
private void checkForUpdates() { private void checkForUpdates() {
updateCheckExecutor.submit(() -> { updateCheckExecutor.submit(() -> {
try { try {
// Get current mod version
Optional<ModContainer> modContainer = FabricLoader.getInstance().getModContainer("playertime"); Optional<ModContainer> modContainer = FabricLoader.getInstance().getModContainer("playertime");
if (modContainer.isEmpty()) { if (modContainer.isEmpty()) {
LOGGER.warn("[在线时间] 无法获取 Mod 容器,跳过更新检查。"); LOGGER.warn("[在线时间] 无法获取 Mod 容器,跳过更新检查。");
@ -294,7 +272,6 @@ public class PlayerTimeMod implements ModInitializer {
LOGGER.info("[在线时间] 正在检查更新..."); LOGGER.info("[在线时间] 正在检查更新...");
// Fetch and parse RSS feed
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder(); DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new URL(RSS_FEED_URL).openStream()); Document doc = builder.parse(new URL(RSS_FEED_URL).openStream());
@ -307,14 +284,12 @@ public class PlayerTimeMod implements ModInitializer {
return; return;
} }
// Get the latest item (first one in the feed)
Element latestItem = (Element) itemList.item(0); Element latestItem = (Element) itemList.item(0);
String latestVersionString = latestItem.getElementsByTagName("title").item(0).getTextContent(); String latestVersionString = latestItem.getElementsByTagName("title").item(0).getTextContent();
String latestVersionLink = latestItem.getElementsByTagName("link").item(0).getTextContent(); String latestVersionLink = latestItem.getElementsByTagName("link").item(0).getTextContent();
LOGGER.info("[在线时间] 当前版本: {}, 最新版本 (RSS): {}", currentVersionString, latestVersionString); LOGGER.info("[在线时间] 当前版本: {}, 最新版本 (RSS): {}", currentVersionString, latestVersionString);
// Compare versions
if (isNewerVersion(latestVersionString, currentVersionString)) { if (isNewerVersion(latestVersionString, currentVersionString)) {
LOGGER.warn("=================================================="); LOGGER.warn("==================================================");
LOGGER.warn("[在线时间] 发现新版本!"); LOGGER.warn("[在线时间] 发现新版本!");
@ -338,17 +313,12 @@ public class PlayerTimeMod implements ModInitializer {
}); });
} }
/**
* Compares two version strings (e.g., "1.2.3" vs "1.2.4").
* Returns true if newVersion is newer than currentVersion.
* Assumes versions are dot-separated integers.
*/
private boolean isNewerVersion(String newVersion, String currentVersion) { private boolean isNewerVersion(String newVersion, String currentVersion) {
if (newVersion == null || currentVersion == null || newVersion.isEmpty() || currentVersion.isEmpty()) { if (newVersion == null || currentVersion == null || newVersion.isEmpty() || currentVersion.isEmpty()) {
return false; // Cannot compare return false;
} }
// Clean up version strings - remove potential prefixes like "v"
String cleanNewVersion = newVersion.toLowerCase().startsWith("v") ? newVersion.substring(1) : newVersion; String cleanNewVersion = newVersion.toLowerCase().startsWith("v") ? newVersion.substring(1) : newVersion;
String cleanCurrentVersion = currentVersion.toLowerCase().startsWith("v") ? currentVersion.substring(1) : currentVersion; String cleanCurrentVersion = currentVersion.toLowerCase().startsWith("v") ? currentVersion.substring(1) : currentVersion;
@ -363,15 +333,14 @@ public class PlayerTimeMod implements ModInitializer {
int currentPart = (i < currentParts.length) ? parseIntOrZero(currentParts[i]) : 0; int currentPart = (i < currentParts.length) ? parseIntOrZero(currentParts[i]) : 0;
if (newPart > currentPart) { if (newPart > currentPart) {
return true; // New version is newer return true;
} }
if (newPart < currentPart) { if (newPart < currentPart) {
return false; // New version is older return false;
} }
// If parts are equal, continue to the next part
} }
return false; // Versions are equal return false;
} }
/** /**
@ -381,7 +350,7 @@ public class PlayerTimeMod implements ModInitializer {
try { try {
return Integer.parseInt(s); return Integer.parseInt(s);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
return 0; // Treat non-numeric parts as 0 for comparison return 0;
} }
} }
} }

View File

@ -24,11 +24,9 @@ public class PlayerTimeTracker {
public PlayerTimeTracker(MinecraftServer server) { public PlayerTimeTracker(MinecraftServer server) {
this.server = server; this.server = server;
this.dataFile = server.getRunDirectory().resolve("player_time_data.json"); this.dataFile = server.getRunDirectory().resolve("player_time_data.json");
// loadData() is now called later in ServerLifecycleEvents.SERVER_STARTED // loadData();
// loadData(); // <-- Remove this line
} }
// Make loadData public so it can be called from PlayerTimeMod
public void loadData() { public void loadData() {
if (!Files.exists(dataFile)) { if (!Files.exists(dataFile)) {
PlayerTimeMod.LOGGER.info("[在线时间] 数据文件未找到,跳过加载"); PlayerTimeMod.LOGGER.info("[在线时间] 数据文件未找到,跳过加载");
@ -53,7 +51,6 @@ public class PlayerTimeTracker {
if (data.lastLogin > 0) { if (data.lastLogin > 0) {
PlayerTimeMod.LOGGER.warn( PlayerTimeMod.LOGGER.warn(
// 修改日志直接使用 UUID避免在加载阶段调用 getPlayerName()
"[在线时间] 在数据加载过程中发现玩家UUID{}的最后登录时间大于0{}。将其重置为0。", "[在线时间] 在数据加载过程中发现玩家UUID{}的最后登录时间大于0{}。将其重置为0。",
uuid, data.lastLogin uuid, data.lastLogin
); );
@ -75,7 +72,6 @@ public class PlayerTimeTracker {
} catch (JsonParseException e) { } catch (JsonParseException e) {
PlayerTimeMod.LOGGER.error("[在线时间] 玩家在线时间数据文件格式错误", e); PlayerTimeMod.LOGGER.error("[在线时间] 玩家在线时间数据文件格式错误", e);
} catch (Exception e) { } catch (Exception e) {
// 捕获更广泛的异常以防其他未知错误
PlayerTimeMod.LOGGER.error("[在线时间] 加载玩家在线时间数据时发生未知错误", e); PlayerTimeMod.LOGGER.error("[在线时间] 加载玩家在线时间数据时发生未知错误", e);
} }
} }
@ -96,7 +92,6 @@ public class PlayerTimeTracker {
if (sessionTime > 0) { if (sessionTime > 0) {
data.totalTime += sessionTime; data.totalTime += sessionTime;
// 维护30天滚动窗口
data.rolling30Days.addPlayTime(now, sessionTime); data.rolling30Days.addPlayTime(now, sessionTime);
data.rolling7Days.addPlayTime(now, sessionTime); data.rolling7Days.addPlayTime(now, sessionTime);
@ -124,7 +119,6 @@ public class PlayerTimeTracker {
PlayerTimeStats stats = new PlayerTimeStats(); PlayerTimeStats stats = new PlayerTimeStats();
stats.totalTime = data.totalTime; stats.totalTime = data.totalTime;
// 检查玩家是否当前在线只在在线时才计算
if (data.lastLogin > 0 && server.getPlayerManager().getPlayer(uuid) != null) { if (data.lastLogin > 0 && server.getPlayerManager().getPlayer(uuid) != null) {
long currentSessionTime = now - data.lastLogin; long currentSessionTime = now - data.lastLogin;
if (currentSessionTime > 0) { if (currentSessionTime > 0) {
@ -142,9 +136,7 @@ public class PlayerTimeTracker {
Map<String, String> stats = new LinkedHashMap<>(); Map<String, String> stats = new LinkedHashMap<>();
long now = Instant.now().getEpochSecond(); long now = Instant.now().getEpochSecond();
// 获取白名单玩家UUID集合
Set<UUID> whitelistUuids = new HashSet<>(); Set<UUID> whitelistUuids = new HashSet<>();
// UserCache 应该在 SERVER_STARTED 之后可用
if (server != null && server.getPlayerManager() != null && server.getUserCache() != null) { if (server != null && server.getPlayerManager() != null && server.getUserCache() != null) {
for (String name : server.getPlayerManager().getWhitelist().getNames()) { for (String name : server.getPlayerManager().getWhitelist().getNames()) {
server.getUserCache().findByName(name).ifPresent(profile -> { server.getUserCache().findByName(name).ifPresent(profile -> {
@ -153,18 +145,15 @@ public class PlayerTimeTracker {
} }
} else { } else {
PlayerTimeMod.LOGGER.error("[在线时间] 尝试获取白名单玩家统计时 UserCache 或 PlayerManager 不可用!"); PlayerTimeMod.LOGGER.error("[在线时间] 尝试获取白名单玩家统计时 UserCache 或 PlayerManager 不可用!");
return stats; // 返回空Map避免崩溃 return stats;
} }
// 遍历所有已记录玩家数据
playerData.forEach((uuid, data) -> { playerData.forEach((uuid, data) -> {
// 只处理白名单玩家
if (whitelistUuids.contains(uuid)) { if (whitelistUuids.contains(uuid)) {
String playerName = getPlayerName(uuid); // UserCache 此时应该可用 String playerName = getPlayerName(uuid);
long totalTime = data.totalTime; long totalTime = data.totalTime;
// 检查玩家是否当前在线只在在线时才计算
if (data.lastLogin > 0 && server.getPlayerManager().getPlayer(uuid) != null) { if (data.lastLogin > 0 && server.getPlayerManager().getPlayer(uuid) != null) {
long currentSessionTime = now - data.lastLogin; long currentSessionTime = now - data.lastLogin;
if (currentSessionTime > 0) { if (currentSessionTime > 0) {
@ -187,13 +176,11 @@ public class PlayerTimeTracker {
} }
public String getPlayerName(UUID uuid) { public String getPlayerName(UUID uuid) {
// 这个方法现在只会在 SERVER_STARTED 之后被调用UserCache 应该可用
ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid); ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid);
if (player != null) { if (player != null) {
return player.getName().getString(); return player.getName().getString();
} }
// 确保 UserCache 不为 null 再使用
if (server != null && server.getUserCache() != null) { if (server != null && server.getUserCache() != null) {
Optional<GameProfile> profile = server.getUserCache().getByUuid(uuid); Optional<GameProfile> profile = server.getUserCache().getByUuid(uuid);
if (profile.isPresent()) { if (profile.isPresent()) {
@ -215,7 +202,6 @@ public class PlayerTimeTracker {
}); });
try { try {
// 确保父目录存在
Files.createDirectories(dataFile.getParent()); Files.createDirectories(dataFile.getParent());
try (Writer writer = Files.newBufferedWriter(dataFile)) { try (Writer writer = Files.newBufferedWriter(dataFile)) {
GSON.toJson(root, writer); GSON.toJson(root, writer);
@ -255,21 +241,18 @@ public class PlayerTimeTracker {
root.add(uuid.toString(), GSON.toJsonTree(data)); root.add(uuid.toString(), GSON.toJsonTree(data));
try { try {
// 确保父目录存在
Files.createDirectories(dataFile.getParent()); Files.createDirectories(dataFile.getParent());
try (Writer writer = Files.newBufferedWriter(dataFile)) { try (Writer writer = Files.newBufferedWriter(dataFile)) {
GSON.toJson(root, writer); GSON.toJson(root, writer);
} }
} catch (Exception e) { } catch (Exception e) {
// 异步保存失败时尝试获取玩家名称可能会再次触发 UserCache 问题直接使用 UUID
String playerName = "UUID: " + uuid.toString(); String playerName = "UUID: " + uuid.toString();
// 尝试获取玩家名称但要小心 UserCache 是否可用
try { try {
if (server != null && server.getPlayerManager() != null) { if (server != null && server.getPlayerManager() != null) {
ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid); ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid);
if (player != null) { if (player != null) {
playerName = player.getName().getString(); playerName = player.getName().getString();
} else if (server.getUserCache() != null) { // 只有 UserCache 可用时才尝试 } else if (server.getUserCache() != null) {
Optional<GameProfile> profile = server.getUserCache().getByUuid(uuid); Optional<GameProfile> profile = server.getUserCache().getByUuid(uuid);
if (profile.isPresent()) { if (profile.isPresent()) {
playerName = profile.get().getName(); playerName = profile.get().getName();
@ -277,7 +260,6 @@ public class PlayerTimeTracker {
} }
} }
} catch (Exception nameEx) { } catch (Exception nameEx) {
// Ignore error getting name during error logging
} }
PlayerTimeMod.LOGGER.error("[在线时间] 无法异步保存玩家的在线时间数据 " + playerName, e); PlayerTimeMod.LOGGER.error("[在线时间] 无法异步保存玩家的在线时间数据 " + playerName, e);
@ -326,7 +308,6 @@ public class PlayerTimeTracker {
private void cleanUp(long currentTime) { private void cleanUp(long currentTime) {
long cutoff = currentTime - (days * 24 * 3600L); long cutoff = currentTime - (days * 24 * 3600L);
// 使用迭代器安全地移除元素
entries.removeIf(entry -> entry.timestamp < cutoff); entries.removeIf(entry -> entry.timestamp < cutoff);
} }

View File

@ -69,7 +69,6 @@ public class WebServer {
try { try {
// 白名单 // 白名单
// timeTracker.getWhitelistedPlayerStats() 内部已处理 UserCache null 检查
Map<String, String> stats = timeTracker.getWhitelistedPlayerStats(); Map<String, String> stats = timeTracker.getWhitelistedPlayerStats();
String response = GSON.toJson(stats); String response = GSON.toJson(stats);
sendResponse(exchange, 200, response.getBytes(StandardCharsets.UTF_8), "application/json"); sendResponse(exchange, 200, response.getBytes(StandardCharsets.UTF_8), "application/json");
@ -109,8 +108,7 @@ public class WebServer {
.orElse(null); .orElse(null);
if (is == null) { if (is == null) {
// Fallback to default language if configured language file is not found langCode = "zh_cn";
langCode = "en_us"; // Default fallback
resourcePath = String.format("assets/playertime/lang/%s.json", langCode); resourcePath = String.format("assets/playertime/lang/%s.json", langCode);
String finalResourcePath = resourcePath; String finalResourcePath = resourcePath;
is = FabricLoader.getInstance().getModContainer("playertime") is = FabricLoader.getInstance().getModContainer("playertime")
@ -244,7 +242,6 @@ public class WebServer {
JsonArray whitelistPlayers = new JsonArray(); JsonArray whitelistPlayers = new JsonArray();
Set<UUID> whitelistUuids = new HashSet<>(); Set<UUID> whitelistUuids = new HashSet<>();
// 增加 UserCache null 检查
if (server != null && server.getUserCache() != null) { if (server != null && server.getUserCache() != null) {
for (String name : playerManager.getWhitelist().getNames()) { for (String name : playerManager.getWhitelist().getNames()) {
server.getUserCache().findByName(name).ifPresent(profile -> { server.getUserCache().findByName(name).ifPresent(profile -> {
@ -272,11 +269,10 @@ public class WebServer {
JsonArray topPlayers = new JsonArray(); JsonArray topPlayers = new JsonArray();
timeTracker.getPlayerData().entrySet().stream() timeTracker.getPlayerData().entrySet().stream()
.filter(entry -> whitelistUuids.contains(entry.getKey())) // 只筛选白名单玩家 .filter(entry -> whitelistUuids.contains(entry.getKey()))
.sorted((a, b) -> Long.compare(b.getValue().totalTime, a.getValue().totalTime)) .sorted((a, b) -> Long.compare(b.getValue().totalTime, a.getValue().totalTime))
.limit(3) .limit(3)
.forEach(entry -> { .forEach(entry -> {
// timeTracker.getPlayerName() 内部已处理 UserCache null 检查
JsonObject playerJson = new JsonObject(); JsonObject playerJson = new JsonObject();
playerJson.addProperty("name", timeTracker.getPlayerName(entry.getKey())); playerJson.addProperty("name", timeTracker.getPlayerName(entry.getKey()));
playerJson.addProperty("time", PlayerTimeTracker.formatTime(entry.getValue().totalTime)); // formatTime doesn't need localization playerJson.addProperty("time", PlayerTimeTracker.formatTime(entry.getValue().totalTime)); // formatTime doesn't need localization
@ -312,7 +308,6 @@ public class WebServer {
// 获取白名单玩家UUID集合 // 获取白名单玩家UUID集合
Set<UUID> whitelistUuids = new HashSet<>(); Set<UUID> whitelistUuids = new HashSet<>();
// 增加 UserCache null 检查
if (minecraftServer != null && minecraftServer.getUserCache() != null) { if (minecraftServer != null && minecraftServer.getUserCache() != null) {
for (String name : playerManager.getWhitelist().getNames()) { for (String name : playerManager.getWhitelist().getNames()) {
minecraftServer.getUserCache().findByName(name).ifPresent(profile -> { minecraftServer.getUserCache().findByName(name).ifPresent(profile -> {
@ -370,7 +365,6 @@ public class WebServer {
// 获取白名单玩家UUID集合 // 获取白名单玩家UUID集合
Set<UUID> whitelistUuids = new HashSet<>(); Set<UUID> whitelistUuids = new HashSet<>();
// 增加 UserCache null 检查
if (minecraftServer != null && minecraftServer.getUserCache() != null) { if (minecraftServer != null && minecraftServer.getUserCache() != null) {
for (String name : playerManager.getWhitelist().getNames()) { for (String name : playerManager.getWhitelist().getNames()) {
minecraftServer.getUserCache().findByName(name).ifPresent(profile -> { minecraftServer.getUserCache().findByName(name).ifPresent(profile -> {
@ -409,7 +403,7 @@ public class WebServer {
server.createContext("/api/whitelist", exchange -> { server.createContext("/api/whitelist", exchange -> {
handleCors(exchange); handleCors(exchange);
if ("OPTIONS".equals(exchange.getRequestMethod())) { if ("OPTIONS".equals(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(204, -1); // 204 No Content exchange.sendResponseHeaders(204, -1);
return; return;
} }
@ -422,18 +416,15 @@ public class WebServer {
PlayerManager playerManager = minecraftServer.getPlayerManager(); PlayerManager playerManager = minecraftServer.getPlayerManager();
JsonArray whitelist = new JsonArray(); JsonArray whitelist = new JsonArray();
// 增加 UserCache null 检查
if (minecraftServer != null && minecraftServer.getUserCache() != null) { if (minecraftServer != null && minecraftServer.getUserCache() != null) {
for (String name : playerManager.getWhitelist().getNames()) { for (String name : playerManager.getWhitelist().getNames()) {
JsonObject player = new JsonObject(); JsonObject player = new JsonObject();
player.addProperty("name", name); player.addProperty("name", name);
// 尝试获取UUID
Optional<GameProfile> profile = minecraftServer.getUserCache().findByName(name); Optional<GameProfile> profile = minecraftServer.getUserCache().findByName(name);
if (profile.isPresent()) { if (profile.isPresent()) {
player.addProperty("uuid", profile.get().getId().toString()); player.addProperty("uuid", profile.get().getId().toString());
// 检查是否在线
ServerPlayerEntity onlinePlayer = playerManager.getPlayer(profile.get().getId()); ServerPlayerEntity onlinePlayer = playerManager.getPlayer(profile.get().getId());
player.addProperty("online", onlinePlayer != null); player.addProperty("online", onlinePlayer != null);
} else { } else {
@ -545,7 +536,7 @@ public class WebServer {
} }
try { try {
Path dataFile = timeTracker.getDataFile(); // 从PlayerTimeTracker获取文件路径 Path dataFile = timeTracker.getDataFile();
if (!Files.exists(dataFile)) { if (!Files.exists(dataFile)) {
PlayerTimeMod.LOGGER.warn("[在线时间] 玩家数据文件未找到: {}", dataFile); PlayerTimeMod.LOGGER.warn("[在线时间] 玩家数据文件未找到: {}", dataFile);

View File

@ -42,5 +42,15 @@
"playertime.web.refresh_button": "Refresh", "playertime.web.refresh_button": "Refresh",
"playertime.web.error.load_failed": "Failed to load data, check console" "playertime.web.error.load_failed": "Failed to load data, check console",
"playertime.web.server_status.disk_usage": "Disk Usage",
"playertime.web.server_status.disk_used": "Used",
"playertime.web.server_status.disk_free": "Free",
"playertime.web.server_status.disk_percent": "Usage",
"playertime.web.server_status.processors": "Processors",
"playertime.web.server_status.server_version": "Server Version",
"playertime.web.server_status.disk_total": "Total",
"playertime.web.chart.disk_used": "Used",
"playertime.web.chart.disk_free": "Free"
} }

View File

@ -42,5 +42,15 @@
"playertime.web.refresh_button": "刷新数据", "playertime.web.refresh_button": "刷新数据",
"playertime.web.error.load_failed": "加载数据失败,请检查控制台" "playertime.web.error.load_failed": "加载数据失败,请检查控制台",
"playertime.web.server_status.disk_usage": "磁盘使用",
"playertime.web.server_status.disk_used": "已用",
"playertime.web.server_status.disk_free": "可用",
"playertime.web.server_status.disk_percent": "使用率",
"playertime.web.server_status.processors": "处理器核心",
"playertime.web.server_status.server_version": "服务器版本",
"playertime.web.server_status.disk_total": "总量",
"playertime.web.chart.disk_used": "已用",
"playertime.web.chart.disk_free": "可用"
} }

View File

@ -621,4 +621,93 @@ canvas {
} }
} }
.server-info-stats div {
padding: 5px 0;
border-bottom: 1px solid var(--border-color);
}
.server-info-stats div:last-child {
border-bottom: none;
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 1rem;
}
@media (max-width: 768px) {
.status-grid {
grid-template-columns: 1fr;
}
}
.floating-refresh-btn {
position: fixed;
bottom: 30px;
right: 30px;
z-index: 1000;
transition: transform 0.3s ease;
}
.refresh-btn.floating {
width: 56px;
height: 56px;
border-radius: 50%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.refresh-btn.floating i {
font-size: 1.5rem;
}
.refresh-btn.floating:hover {
transform: scale(1.1);
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.3);
}
.refresh-btn.floating.loading i {
transition: transform 0.5s ease;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.floating-refresh-btn {
bottom: 20px;
right: 20px;
}
.refresh-btn.floating {
width: 50px;
height: 50px;
}
}
@media (max-width: 480px) {
.floating-refresh-btn {
bottom: 15px;
right: 15px;
}
.refresh-btn.floating {
width: 44px;
height: 44px;
}
.refresh-btn.floating i {
font-size: 1.3rem;
}
}

View File

@ -90,6 +90,26 @@
</div> </div>
</div> </div>
<div class="status-item">
<h3 data-lang-key="playertime.web.server_status.disk_usage">磁盘使用</h3>
<canvas id="disk-chart"></canvas>
<div class="memory-stats">
<div><span data-lang-key="playertime.web.server_status.disk_total">总量</span>: <span id="disk-total">0</span> GB</div>
<div><span data-lang-key="playertime.web.server_status.disk_used">已用</span>: <span id="disk-used">0</span> GB</div>
<div><span data-lang-key="playertime.web.server_status.disk_free">可用</span>: <span id="disk-free">0</span> GB</div>
<div><span data-lang-key="playertime.web.server_status.disk_percent">使用率</span>: <span id="disk-percent">0</span>%</div>
</div>
</div>
<div class="status-item">
<h3 data-lang-key="playertime.web.server_status.server_info">服务器信息</h3>
<div class="server-info-stats">
<div><span data-lang-key="playertime.web.server_status.server_version">服务器版本</span>: <span id="server-version">未知</span></div>
<div><span data-lang-key="playertime.web.server_status.processors">处理器核心</span>: <span id="processors">0</span></div>
<div><span data-lang-key="playertime.web.server_status.player_count">在线玩家</span>: <span id="server-player-count">0</span>/<span id="server-max-players">0</span></div>
</div>
</div>
</div> </div>
</div> </div>
<div class="status-item full-width"> <div class="status-item full-width">
@ -127,5 +147,10 @@
<p>Copyright © 2025 <a href="https://git.branulf.top/BRanulf" target="_blank">BRanulf</a></p> <p>Copyright © 2025 <a href="https://git.branulf.top/BRanulf" target="_blank">BRanulf</a></p>
</div> </div>
</footer> </footer>
<div class="floating-refresh-btn">
<button id="floating-refresh-btn" class="refresh-btn floating" title="刷新数据">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</body> </body>
</html> </html>

View File

@ -5,6 +5,9 @@ document.addEventListener('DOMContentLoaded', function() {
let memoryChart = null; let memoryChart = null;
let performanceChart = null; let performanceChart = null;
let lang = {}; let lang = {};
let diskChart = null;
const floatingRefreshBtn = document.getElementById('floating-refresh-btn');
// 获取语言 // 获取语言
const elements = { const elements = {
@ -93,6 +96,29 @@ document.addEventListener('DOMContentLoaded', function() {
} }
} }
if (floatingRefreshBtn) {
floatingRefreshBtn.addEventListener('click', function() {
if (this.classList.contains('loading')) return;
this.classList.add('loading');
loadAllData().finally(() => {
setTimeout(() => {
this.classList.remove('loading');
}, 500);
});
});
}
// window.addEventListener('scroll', function() {
// if (!floatingRefreshBtn) return;
//
// if (window.scrollY > 100) {
// floatingRefreshBtn.style.transform = 'scale(1)';
// } else {
// floatingRefreshBtn.style.transform = 'scale(0.9)';
// }
// });
// 图表 // 图表
function initCharts() { function initCharts() {
@ -221,6 +247,30 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
} }
const diskCtx = document.getElementById('disk-chart')?.getContext('2d');
if (diskCtx) {
diskChart = new Chart(diskCtx, {
type: 'doughnut',
data: {
labels: [getLangString('playertime.web.chart.disk_used'),
getLangString('playertime.web.chart.disk_free')],
datasets: [{
data: [0, 100],
backgroundColor: ['#4361ee', '#e9ecef']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
}
}
}
});
}
} }
function toggleTheme() { function toggleTheme() {
@ -409,6 +459,34 @@ document.addEventListener('DOMContentLoaded', function() {
} }
if (data.disk) {
const totalGB = Math.round(data.disk.total / (1024 * 1024 * 1024));
const usedGB = Math.round(data.disk.used / (1024 * 1024 * 1024));
const freeGB = Math.round(data.disk.free / (1024 * 1024 * 1024));
const percent = Math.round(data.disk.usage_percentage);
if (diskChart) {
diskChart.data.datasets[0].data = [usedGB, freeGB];
diskChart.update();
}
document.getElementById('disk-total').textContent = totalGB;
document.getElementById('disk-used').textContent = usedGB;
document.getElementById('disk-free').textContent = freeGB;
document.getElementById('disk-percent').textContent = percent;
}
// 新增服务器信息
if (data.available_processors) {
document.getElementById('processors').textContent = data.available_processors;
}
if (data.server) {
document.getElementById('server-version').textContent = data.server.version;
document.getElementById('server-player-count').textContent = data.server.player_count;
document.getElementById('server-max-players').textContent = data.server.max_players;
}
if (elements.uptime) elements.uptime.textContent = data.uptime_formatted || '0'; if (elements.uptime) elements.uptime.textContent = data.uptime_formatted || '0';
} }