diff --git a/gradle.properties b/gradle.properties index c701e7c..8c1213f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ minecraft_version=1.21.4 yarn_mappings=1.21.4+build.8 loader_version=0.16.10 # Mod Properties -mod_version=1.14.514.132 +mod_version=1.14.514.133 maven_group=org.example1 archives_base_name=ServerPlayerOnlineTracker # Dependencies diff --git a/src/main/java/com/example/playertime/PlayerTimeMod.java b/src/main/java/com/example/playertime/PlayerTimeMod.java index 01c024a..d657354 100644 --- a/src/main/java/com/example/playertime/PlayerTimeMod.java +++ b/src/main/java/com/example/playertime/PlayerTimeMod.java @@ -6,6 +6,8 @@ import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.fabricmc.loader.api.Version; import net.minecraft.server.command.CommandManager; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; @@ -16,9 +18,18 @@ import net.minecraft.util.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.DocumentBuilder; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.w3c.dom.Element; +import java.net.URL; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.stream.Collectors; public class PlayerTimeMod implements ModInitializer { @@ -28,6 +39,12 @@ public class PlayerTimeMod implements ModInitializer { private static ModConfig config; public static LocalizationManager localizationManager; + // RSS Feed URL for releases + 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(); + + @Override public void onInitialize() { config = new ModConfig(FabricLoader.getInstance().getConfigDir()); @@ -36,8 +53,12 @@ public class PlayerTimeMod implements ModInitializer { try { LOGGER.info("[在线时间] 初始化 玩家在线时长视奸Mod"); + // Check for updates asynchronously on mod initialization + checkForUpdates(); + + // 在 SERVER_STARTING 阶段创建 Tracker 和 WebServer 实例 ServerLifecycleEvents.SERVER_STARTING.register(server -> { - timeTracker = new PlayerTimeTracker(server); + timeTracker = new PlayerTimeTracker(server); // Tracker 构造函数不再加载数据 try { webServer = new WebServer(timeTracker, config.getWebPort(), server); webServer.start(); @@ -47,6 +68,16 @@ public class PlayerTimeMod implements ModInitializer { } }); + // 在 SERVER_STARTED 阶段加载数据 (此时 UserCache 应该已可用) + ServerLifecycleEvents.SERVER_STARTED.register(server -> { + if (timeTracker != null) { + timeTracker.loadData(); // 在服务器启动完成后加载数据 + } else { + LOGGER.error("[在线时间] PlayerTimeTracker 未在 SERVER_STARTING 阶段成功初始化!"); + } + }); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { if (timeTracker != null) { timeTracker.onPlayerJoin(handler.player); @@ -66,11 +97,16 @@ public class PlayerTimeMod implements ModInitializer { webServer.stop(); } if (timeTracker != null) { + // 在服务器停止前,确保所有在线玩家的会话时间被记录 for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { - timeTracker.onPlayerLeave(player); + timeTracker.onPlayerLeave(player); // onPlayerLeave 会自动保存该玩家数据 } + // timeTracker.saveAll(); // onPlayerLeave 已经异步保存,这里可以考虑是否还需要 saveAll + // 简单的处理是直接调用 saveAll,它会覆盖旧数据 timeTracker.saveAll(); } + // Shutdown the update check executor + updateCheckExecutor.shutdownNow(); }); } catch (Exception e) { @@ -108,10 +144,12 @@ public class PlayerTimeMod implements ModInitializer { } 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(); if (player == null) return 0; - CompletableFuture.runAsync(() -> { + // Use the server's main worker executor for command processing + Util.getMainWorkerExecutor().execute(() -> { PlayerTimeTracker tracker = getTimeTracker(); if (tracker != null) { Map stats = tracker.getWhitelistedPlayerStats(); @@ -122,40 +160,79 @@ public class PlayerTimeMod implements ModInitializer { .collect(Collectors.toList()); sendPaginatedMessage(player, sorted, requestedPage); + } else { + player.sendMessage(Text.literal(localizationManager.getString("playertime.command.error.not_initialized")), false); } - }, Util.getMainWorkerExecutor()); + }); + return 1; } private static int comparePlayTime(String a, String b) { - String timeA = a.substring(a.indexOf(':') + 1).trim().split(" \\| ")[0]; - String timeB = b.substring(b.indexOf(':') + 1).trim().split(" \\| ")[0]; + // 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 { + // Find the first colon and the first pipe + int firstColon = a.indexOf(':'); + int firstPipe = a.indexOf('|'); + String timeA; + if (firstColon > 0) { + timeA = (firstPipe > firstColon && firstPipe != -1) ? a.substring(firstColon + 1, firstPipe).trim() : a.substring(firstColon + 1).trim(); + } else { + timeA = a.trim(); // Fallback if format changes unexpectedly + } - return Long.compare(parseTimeToSeconds(timeB), parseTimeToSeconds(timeA)); + + int firstColonB = b.indexOf(':'); + int firstPipeB = b.indexOf('|'); + String timeB; + if (firstColonB > 0) { + timeB = (firstPipeB > firstColonB && firstPipeB != -1) ? b.substring(firstColonB + 1, firstPipeB).trim() : b.substring(firstColonB + 1).trim(); + } else { + timeB = b.trim(); // Fallback if format changes unexpectedly + } + + + return Long.compare(parseTimeToSeconds(timeB), parseTimeToSeconds(timeA)); // Descending order + } catch (Exception e) { + LOGGER.error("Error comparing play times: {} vs {}", a, b, e); + return 0; // Fallback to equal if parsing fails + } } + private static long parseTimeToSeconds(String timeStr) { long seconds = 0; - String[] parts = timeStr.split(" "); + // Handle potential empty or null strings + if (timeStr == null || timeStr.trim().isEmpty()) { + return 0; + } + + String[] parts = timeStr.trim().split("\\s+"); // Split by one or more spaces for (String part : parts) { if (part.endsWith("h")) { try { - seconds += Integer.parseInt(part.replace("h", "")) * 3600; + seconds += Integer.parseInt(part.substring(0, part.length() - 1)) * 3600; } catch (NumberFormatException ignored) {} } else if (part.endsWith("m")) { try { - seconds += Integer.parseInt(part.replace("m", "")) * 60; + seconds += Integer.parseInt(part.substring(0, part.length() - 1)) * 60; } catch (NumberFormatException ignored) {} } + // Ignore other parts like "d:" or numbers without units } return seconds; } 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); } + private static void sendPaginatedMessage(ServerPlayerEntity player, List lines, int page) { int pageSize = 10; int totalPages = (lines.size() + pageSize - 1) / pageSize; @@ -166,6 +243,11 @@ public class PlayerTimeMod implements ModInitializer { player.sendMessage(Text.literal(localizationManager.getString("playertime.command.title", page, totalPages)), false); + if (lines.isEmpty()) { + player.sendMessage(Text.literal(localizationManager.getString("playertime.command.empty_stats")), false); + return; + } + for (int i = from; i < to; i++) { player.sendMessage(Text.literal(lines.get(i)), false); } @@ -194,4 +276,112 @@ public class PlayerTimeMod implements ModInitializer { player.sendMessage(footer, false); } + + /** + * Checks the RSS feed for new releases asynchronously. + */ + private void checkForUpdates() { + updateCheckExecutor.submit(() -> { + try { + // Get current mod version + Optional modContainer = FabricLoader.getInstance().getModContainer("playertime"); + if (modContainer.isEmpty()) { + LOGGER.warn("[在线时间] 无法获取 Mod 容器,跳过更新检查。"); + return; + } + Version currentVersion = modContainer.get().getMetadata().getVersion(); + String currentVersionString = currentVersion.getFriendlyString(); + + LOGGER.info("[在线时间] 正在检查更新..."); + + // Fetch and parse RSS feed + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(new URL(RSS_FEED_URL).openStream()); + doc.getDocumentElement().normalize(); + + NodeList itemList = doc.getElementsByTagName("item"); + + if (itemList.getLength() == 0) { + LOGGER.warn("[在线时间] RSS Feed 中没有找到任何发布项。"); + return; + } + + // Get the latest item (first one in the feed) + Element latestItem = (Element) itemList.item(0); + String latestVersionString = latestItem.getElementsByTagName("title").item(0).getTextContent(); + String latestVersionLink = latestItem.getElementsByTagName("link").item(0).getTextContent(); + + LOGGER.info("[在线时间] 当前版本: {}, 最新版本 (RSS): {}", currentVersionString, latestVersionString); + + // Compare versions + if (isNewerVersion(latestVersionString, currentVersionString)) { + LOGGER.warn("=================================================="); + LOGGER.warn("[在线时间] 发现新版本!"); + LOGGER.warn("[在线时间] 当前版本: {}", currentVersionString); + LOGGER.warn("[在线时间] 最新版本: {}", latestVersionString); + LOGGER.warn("[在线时间] 下载链接: {}", latestVersionLink); + LOGGER.warn("=================================================="); + } else { + LOGGER.info("[在线时间] 当前已是最新版本(也有可能检查失败)。"); + } + + } catch (javax.xml.parsers.ParserConfigurationException e) { + LOGGER.error("[在线时间] 更新检查失败: XML 解析器配置错误", e); + } catch (org.xml.sax.SAXException e) { + LOGGER.error("[在线时间] 更新检查失败: XML 解析错误", e); + } catch (java.io.IOException e) { + LOGGER.error("[在线时间] 更新检查失败: 网络或文件读取错误", e); + } catch (Exception e) { + LOGGER.error("[在线时间] 更新检查时发生未知错误", e); + } + }); + } + + /** + * 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) { + if (newVersion == null || currentVersion == null || newVersion.isEmpty() || currentVersion.isEmpty()) { + return false; // Cannot compare + } + + // Clean up version strings - remove potential prefixes like "v" + String cleanNewVersion = newVersion.toLowerCase().startsWith("v") ? newVersion.substring(1) : newVersion; + String cleanCurrentVersion = currentVersion.toLowerCase().startsWith("v") ? currentVersion.substring(1) : currentVersion; + + + String[] newParts = cleanNewVersion.split("\\."); + String[] currentParts = cleanCurrentVersion.split("\\."); + + int maxLength = Math.max(newParts.length, currentParts.length); + + for (int i = 0; i < maxLength; i++) { + int newPart = (i < newParts.length) ? parseIntOrZero(newParts[i]) : 0; + int currentPart = (i < currentParts.length) ? parseIntOrZero(currentParts[i]) : 0; + + if (newPart > currentPart) { + return true; // New version is newer + } + if (newPart < currentPart) { + return false; // New version is older + } + // If parts are equal, continue to the next part + } + + return false; // Versions are equal + } + + /** + * Parses a string part of a version into an integer, returning 0 if parsing fails. + */ + private int parseIntOrZero(String s) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return 0; // Treat non-numeric parts as 0 for comparison + } + } } diff --git a/src/main/java/com/example/playertime/PlayerTimeTracker.java b/src/main/java/com/example/playertime/PlayerTimeTracker.java index fb85f41..7f69229 100644 --- a/src/main/java/com/example/playertime/PlayerTimeTracker.java +++ b/src/main/java/com/example/playertime/PlayerTimeTracker.java @@ -24,9 +24,63 @@ public class PlayerTimeTracker { public PlayerTimeTracker(MinecraftServer server) { this.server = server; this.dataFile = server.getRunDirectory().resolve("player_time_data.json"); - loadData(); + // loadData() is now called later in ServerLifecycleEvents.SERVER_STARTED + // loadData(); // <-- Remove this line } + // Make loadData public so it can be called from PlayerTimeMod + public void loadData() { + if (!Files.exists(dataFile)) { + PlayerTimeMod.LOGGER.info("[在线时间] 数据文件未找到,跳过加载"); + return; + } + + try (Reader reader = Files.newBufferedReader(dataFile)) { + JsonElement jsonElement = JsonParser.parseReader(reader); + + if (jsonElement == null || !jsonElement.isJsonObject()) { + PlayerTimeMod.LOGGER.warn("[在线时间] 数据文件为空或格式错误,跳过加载"); + return; + } + + JsonObject root = jsonElement.getAsJsonObject(); + int resetCount = 0; + + for (Map.Entry entry : root.entrySet()) { + try { + UUID uuid = UUID.fromString(entry.getKey()); + PlayerTimeData data = GSON.fromJson(entry.getValue(), PlayerTimeData.class); + + if (data.lastLogin > 0) { + PlayerTimeMod.LOGGER.warn( + // 修改日志,直接使用 UUID,避免在加载阶段调用 getPlayerName() + "[在线时间] 在数据加载过程中发现玩家(UUID:{})的最后登录时间大于0({})。将其重置为0。", + uuid, data.lastLogin + ); + data.lastLogin = 0; + resetCount++; + } + + playerData.put(uuid, data); + } catch (IllegalArgumentException e) { + PlayerTimeMod.LOGGER.error("[在线时间] I数据文件中的 UUID 格式无效: " + entry.getKey(), e); + } catch (JsonParseException e) { + PlayerTimeMod.LOGGER.error("[在线时间] 解析玩家数据失败(UUID: " + entry.getKey() + ")", e); + } + } + PlayerTimeMod.LOGGER.info("[在线时间] 成功加载了 {} 名玩家的数据,重置了 {} 名玩家的上次登录时间", playerData.size(), resetCount); + + } catch (IOException e) { + PlayerTimeMod.LOGGER.error("[在线时间] 无法读取玩家在线时间数据文件", e); + } catch (JsonParseException e) { + PlayerTimeMod.LOGGER.error("[在线时间] 玩家在线时间数据文件格式错误", e); + } catch (Exception e) { + // 捕获更广泛的异常,以防其他未知错误 + PlayerTimeMod.LOGGER.error("[在线时间] 加载玩家在线时间数据时发生未知错误", e); + } + } + + public void onPlayerJoin(ServerPlayerEntity player) { PlayerTimeData data = playerData.computeIfAbsent(player.getUuid(), uuid -> new PlayerTimeData()); data.lastLogin = Instant.now().getEpochSecond(); @@ -90,17 +144,24 @@ public class PlayerTimeTracker { // 获取白名单玩家UUID集合 Set whitelistUuids = new HashSet<>(); - for (String name : server.getPlayerManager().getWhitelist().getNames()) { - server.getUserCache().findByName(name).ifPresent(profile -> { - whitelistUuids.add(profile.getId()); - }); + // UserCache 应该在 SERVER_STARTED 之后可用 + if (server != null && server.getPlayerManager() != null && server.getUserCache() != null) { + for (String name : server.getPlayerManager().getWhitelist().getNames()) { + server.getUserCache().findByName(name).ifPresent(profile -> { + whitelistUuids.add(profile.getId()); + }); + } + } else { + PlayerTimeMod.LOGGER.error("[在线时间] 尝试获取白名单玩家统计时 UserCache 或 PlayerManager 不可用!"); + return stats; // 返回空Map避免崩溃 } + // 遍历所有已记录玩家数据 playerData.forEach((uuid, data) -> { // 只处理白名单玩家 if (whitelistUuids.contains(uuid)) { - String playerName = getPlayerName(uuid); + String playerName = getPlayerName(uuid); // UserCache 此时应该可用 long totalTime = data.totalTime; // 检查玩家是否当前在线,只在在线时才计算 @@ -126,68 +187,26 @@ public class PlayerTimeTracker { } public String getPlayerName(UUID uuid) { + // 这个方法现在只会在 SERVER_STARTED 之后被调用,UserCache 应该可用 ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid); if (player != null) { return player.getName().getString(); } - Optional profile = server.getUserCache().getByUuid(uuid); - if (profile.isPresent()) { - return profile.get().getName(); + // 确保 UserCache 不为 null 再使用 + if (server != null && server.getUserCache() != null) { + Optional profile = server.getUserCache().getByUuid(uuid); + if (profile.isPresent()) { + return profile.get().getName(); + } + } else { + PlayerTimeMod.LOGGER.warn("[在线时间] 尝试通过 UUID 获取玩家名称时 UserCache 不可用: {}", uuid); } + return "Unknown"; } - private void loadData() { - if (!Files.exists(dataFile)) { - PlayerTimeMod.LOGGER.info("[在线时间] 数据文件未找到,跳过加载"); - return; - } - - try (Reader reader = Files.newBufferedReader(dataFile)) { - JsonElement jsonElement = JsonParser.parseReader(reader); - - if (jsonElement == null || !jsonElement.isJsonObject()) { - PlayerTimeMod.LOGGER.warn("[在线时间] 数据文件为空或格式错误,跳过加载"); - return; - } - - JsonObject root = jsonElement.getAsJsonObject(); - int resetCount = 0; - - for (Map.Entry entry : root.entrySet()) { - try { - UUID uuid = UUID.fromString(entry.getKey()); - PlayerTimeData data = GSON.fromJson(entry.getValue(), PlayerTimeData.class); - - if (data.lastLogin > 0) { - PlayerTimeMod.LOGGER.warn( - "[在线时间] 在数据加载过程中发现玩家{}(UUID:{})的最后登录时间大于0({})。将其重置为0。", - getPlayerName(uuid), uuid, data.lastLogin - ); - data.lastLogin = 0; - resetCount++; - } - - playerData.put(uuid, data); - } catch (IllegalArgumentException e) { - PlayerTimeMod.LOGGER.error("[在线时间] I数据文件中的 UUID 格式无效: " + entry.getKey(), e); - } catch (JsonParseException e) { - PlayerTimeMod.LOGGER.error("[在线时间] 解析玩家数据失败(UUID: " + entry.getKey() + ")", e); - } - } - PlayerTimeMod.LOGGER.info("[在线时间] 成功加载了 {} 名玩家的数据,重置了 {} 名玩家的上次登录时间", playerData.size(), resetCount); - - } catch (IOException e) { - PlayerTimeMod.LOGGER.error("[在线时间] 无法读取玩家在线时间数据文件", e); - } catch (JsonParseException e) { - PlayerTimeMod.LOGGER.error("[在线时间] 玩家在线时间数据文件格式错误", e); - } catch (Exception e) { - PlayerTimeMod.LOGGER.error("[在线时间] 加载玩家在线时间数据时发生未知错误", e); - } - } - public void saveAll() { PlayerTimeMod.LOGGER.info("[在线时间] 开始保存所有玩家数据..."); JsonObject root = new JsonObject(); @@ -195,8 +214,12 @@ public class PlayerTimeTracker { root.add(uuid.toString(), GSON.toJsonTree(data)); }); - try (Writer writer = Files.newBufferedWriter(dataFile)) { - GSON.toJson(root, writer); + try { + // 确保父目录存在 + Files.createDirectories(dataFile.getParent()); + try (Writer writer = Files.newBufferedWriter(dataFile)) { + GSON.toJson(root, writer); + } PlayerTimeMod.LOGGER.info("[在线时间] 所有玩家数据已成功保存"); } catch (Exception e) { PlayerTimeMod.LOGGER.error("[在线时间] 无法保存所有玩家在线时间数据", e); @@ -231,10 +254,33 @@ public class PlayerTimeTracker { root.add(uuid.toString(), GSON.toJsonTree(data)); - try (Writer writer = Files.newBufferedWriter(dataFile)) { - GSON.toJson(root, writer); + try { + // 确保父目录存在 + Files.createDirectories(dataFile.getParent()); + try (Writer writer = Files.newBufferedWriter(dataFile)) { + GSON.toJson(root, writer); + } } catch (Exception e) { - PlayerTimeMod.LOGGER.error("[在线时间] 无法异步保存玩家的在线时间数据 " + getPlayerName(uuid) + " (UUID: " + uuid + ")", e); + // 异步保存失败时,尝试获取玩家名称可能会再次触发 UserCache 问题,直接使用 UUID + String playerName = "UUID: " + uuid.toString(); + // 尝试获取玩家名称,但要小心 UserCache 是否可用 + try { + if (server != null && server.getPlayerManager() != null) { + ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid); + if (player != null) { + playerName = player.getName().getString(); + } else if (server.getUserCache() != null) { // 只有 UserCache 可用时才尝试 + Optional profile = server.getUserCache().getByUuid(uuid); + if (profile.isPresent()) { + playerName = profile.get().getName(); + } + } + } + } catch (Exception nameEx) { + // Ignore error getting name during error logging + } + + PlayerTimeMod.LOGGER.error("[在线时间] 无法异步保存玩家的在线时间数据 " + playerName, e); } }); } @@ -280,9 +326,8 @@ public class PlayerTimeTracker { private void cleanUp(long currentTime) { long cutoff = currentTime - (days * 24 * 3600L); - while (!entries.isEmpty() && entries.get(0).timestamp < cutoff) { - entries.remove(0); - } + // 使用迭代器安全地移除元素 + entries.removeIf(entry -> entry.timestamp < cutoff); } private static class TimeEntry { diff --git a/src/main/java/com/example/playertime/WebServer.java b/src/main/java/com/example/playertime/WebServer.java index e58bf18..f0e64b7 100644 --- a/src/main/java/com/example/playertime/WebServer.java +++ b/src/main/java/com/example/playertime/WebServer.java @@ -69,11 +69,12 @@ public class WebServer { try { // 白名单 + // timeTracker.getWhitelistedPlayerStats() 内部已处理 UserCache null 检查 Map stats = timeTracker.getWhitelistedPlayerStats(); - String response = GSON.toJson(stats); + String response = GSON.toJson(stats); sendResponse(exchange, 200, response.getBytes(StandardCharsets.UTF_8), "application/json"); } catch (Exception e) { - PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to get stats data", e); + PlayerTimeMod.LOGGER.error("[在线时间] 获取统计数据失败", e); sendResponse(exchange, 500, "Internal Server Error"); } }); @@ -124,11 +125,11 @@ public class WebServer { .orElse(null); if (is == null) { - PlayerTimeMod.LOGGER.error("[PlayerTime] Default language file (en_us.json) not found!"); + PlayerTimeMod.LOGGER.error("[在线时间] 默认语言文件(zh_cn.json)未找到!"); sendResponse(exchange, 500, "Language file not found"); return; } - PlayerTimeMod.LOGGER.warn("[PlayerTime] Configured language file ({}.json) not found, using default (en_us.json).", PlayerTimeMod.getConfig().getLanguage()); + PlayerTimeMod.LOGGER.warn("[在线时间] 配置的语言文件({}.json)未找到,正在使用默认(zh_cn.json)。", PlayerTimeMod.getConfig().getLanguage()); } ByteArrayOutputStream buffer = new ByteArrayOutputStream(); @@ -141,13 +142,13 @@ public class WebServer { is.close(); sendResponse(exchange, 200, buffer.toByteArray(), "application/json"); - PlayerTimeMod.LOGGER.debug("[PlayerTime] Served language file: {}", resourcePath); + PlayerTimeMod.LOGGER.debug("[在线时间] 提供的语言文件: {}", resourcePath); } catch (IOException e) { - PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to read language file", e); + PlayerTimeMod.LOGGER.error("[在线时间] 无法读取语言文件", e); sendResponse(exchange, 500, "Error reading language file"); } catch (Exception e) { - PlayerTimeMod.LOGGER.error("[PlayerTime] An unknown error occurred while processing language request", e); + PlayerTimeMod.LOGGER.error("[在线时间] 处理语言请求时发生未知错误", e); sendResponse(exchange, 500, "Internal Server Error"); } }); @@ -157,7 +158,7 @@ public class WebServer { server.createContext("/", exchange -> { try { String requestPath = exchange.getRequestURI().getPath(); - String resourceFileName; + String resourceFileName; if (requestPath.equals("/")) { resourceFileName = "index.html"; @@ -185,7 +186,7 @@ public class WebServer { if (is == null) { - PlayerTimeMod.LOGGER.warn("[PlayerTime] Static resource not found: {}", resourcePath); + PlayerTimeMod.LOGGER.warn("[在线时间] 找不到静态资源: {}", resourcePath); sendResponse(exchange, 404, "Not Found"); return; } @@ -209,16 +210,16 @@ public class WebServer { is.close(); sendResponse(exchange, 200, buffer.toByteArray(), contentType); - PlayerTimeMod.LOGGER.debug("[PlayerTime] Served static file: {}", resourcePath); + PlayerTimeMod.LOGGER.debug("[在线时间] 提供静态文件: {}", resourcePath); } catch (Exception e) { - PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to serve static resource", e); + PlayerTimeMod.LOGGER.error("[在线时间] 无法提供静态资源", e); sendResponse(exchange, 500, "Internal Server Error"); } }); - // 没啥用了 + // 没啥用了 (根据注释,这个API可能不再使用,但保留并增加健壮性) server.createContext("/api/widget-data", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { @@ -243,12 +244,18 @@ public class WebServer { JsonArray whitelistPlayers = new JsonArray(); Set whitelistUuids = new HashSet<>(); - for (String name : playerManager.getWhitelist().getNames()) { - server.getUserCache().findByName(name).ifPresent(profile -> { - whitelistUuids.add(profile.getId()); - }); + // 增加 UserCache null 检查 + if (server != null && server.getUserCache() != null) { + for (String name : playerManager.getWhitelist().getNames()) { + server.getUserCache().findByName(name).ifPresent(profile -> { + whitelistUuids.add(profile.getId()); + }); + } + } else { + PlayerTimeMod.LOGGER.warn("[在线时间] UserCache 不可用,无法获取白名单UUID用于 widget-data"); } + for (ServerPlayerEntity player : playerManager.getPlayerList()) { UUID uuid = player.getUuid(); if (whitelistUuids.contains(uuid)) { @@ -269,6 +276,7 @@ public class WebServer { .sorted((a, b) -> Long.compare(b.getValue().totalTime, a.getValue().totalTime)) .limit(3) .forEach(entry -> { + // timeTracker.getPlayerName() 内部已处理 UserCache null 检查 JsonObject playerJson = new JsonObject(); playerJson.addProperty("name", timeTracker.getPlayerName(entry.getKey())); playerJson.addProperty("time", PlayerTimeTracker.formatTime(entry.getValue().totalTime)); // formatTime doesn't need localization @@ -280,7 +288,7 @@ public class WebServer { sendResponse(exchange, 200, GSON.toJson(response).getBytes(StandardCharsets.UTF_8), "application/json"); } catch (Exception e) { - PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to get widget data", e); + PlayerTimeMod.LOGGER.error("[在线时间] 获取小部件数据失败", e); sendResponse(exchange, 500, "Internal Server Error"); } }); @@ -304,12 +312,18 @@ public class WebServer { // 获取白名单玩家UUID集合 Set whitelistUuids = new HashSet<>(); - for (String name : playerManager.getWhitelist().getNames()) { - minecraftServer.getUserCache().findByName(name).ifPresent(profile -> { - whitelistUuids.add(profile.getId()); - }); + // 增加 UserCache null 检查 + if (minecraftServer != null && minecraftServer.getUserCache() != null) { + for (String name : playerManager.getWhitelist().getNames()) { + minecraftServer.getUserCache().findByName(name).ifPresent(profile -> { + whitelistUuids.add(profile.getId()); + }); + } + } else { + PlayerTimeMod.LOGGER.warn("[在线时间] UserCache 不可用,无法获取白名单UUID用于 online-players"); } + // 分类玩家 JsonArray whitelistedPlayers = new JsonArray(); JsonArray nonWhitelistedPlayers = new JsonArray(); @@ -332,7 +346,7 @@ public class WebServer { sendResponse(exchange, 200, GSON.toJson(response).getBytes(StandardCharsets.UTF_8), "application/json"); } catch (Exception e) { - PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to get online players list", e); + PlayerTimeMod.LOGGER.error("[在线时间] 获取在线玩家列表失败", e); sendResponse(exchange, 500, "Internal Server Error"); } }); @@ -356,12 +370,18 @@ public class WebServer { // 获取白名单玩家UUID集合 Set whitelistUuids = new HashSet<>(); - for (String name : playerManager.getWhitelist().getNames()) { - minecraftServer.getUserCache().findByName(name).ifPresent(profile -> { - whitelistUuids.add(profile.getId()); - }); + // 增加 UserCache null 检查 + if (minecraftServer != null && minecraftServer.getUserCache() != null) { + for (String name : playerManager.getWhitelist().getNames()) { + minecraftServer.getUserCache().findByName(name).ifPresent(profile -> { + whitelistUuids.add(profile.getId()); + }); + } + } else { + PlayerTimeMod.LOGGER.warn("[在线时间] UserCache 不可用,无法获取白名单UUID用于 player-count"); } + // 分类计数 int whitelistedCount = 0; int nonWhitelistedCount = 0; @@ -380,16 +400,16 @@ public class WebServer { sendResponse(exchange, 200, GSON.toJson(response).getBytes(StandardCharsets.UTF_8), "application/json"); } catch (Exception e) { - PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to get player count", e); + PlayerTimeMod.LOGGER.error("[在线时间] 获取玩家数量失败", e); sendResponse(exchange, 500, "Internal Server Error"); } }); - // 白名单玩家(还有用吗?) + // 白名单玩家 server.createContext("/api/whitelist", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { - exchange.sendResponseHeaders(204, -1); + exchange.sendResponseHeaders(204, -1); // 204 No Content return; } @@ -402,28 +422,34 @@ public class WebServer { PlayerManager playerManager = minecraftServer.getPlayerManager(); JsonArray whitelist = new JsonArray(); - for (String name : playerManager.getWhitelist().getNames()) { - JsonObject player = new JsonObject(); - player.addProperty("name", name); + // 增加 UserCache null 检查 + if (minecraftServer != null && minecraftServer.getUserCache() != null) { + for (String name : playerManager.getWhitelist().getNames()) { + JsonObject player = new JsonObject(); + player.addProperty("name", name); - // 尝试获取UUID - Optional profile = minecraftServer.getUserCache().findByName(name); - if (profile.isPresent()) { - player.addProperty("uuid", profile.get().getId().toString()); + // 尝试获取UUID + Optional profile = minecraftServer.getUserCache().findByName(name); + if (profile.isPresent()) { + player.addProperty("uuid", profile.get().getId().toString()); - // 检查是否在线 - ServerPlayerEntity onlinePlayer = playerManager.getPlayer(profile.get().getId()); - player.addProperty("online", onlinePlayer != null); - } else { - player.addProperty("online", false); + // 检查是否在线 + ServerPlayerEntity onlinePlayer = playerManager.getPlayer(profile.get().getId()); + player.addProperty("online", onlinePlayer != null); + } else { + player.addProperty("online", false); + } + + whitelist.add(player); } - - whitelist.add(player); + } else { + PlayerTimeMod.LOGGER.warn("[在线时间] UserCache 不可用,无法获取白名单列表"); } + sendResponse(exchange, 200, GSON.toJson(whitelist).getBytes(StandardCharsets.UTF_8), "application/json"); } catch (Exception e) { - PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to get whitelist", e); + PlayerTimeMod.LOGGER.error("[在线时间] 获取白名单失败", e); sendResponse(exchange, 500, "Internal Server Error"); } }); @@ -500,7 +526,7 @@ public class WebServer { sendResponse(exchange, 200, GSON.toJson(status).getBytes(StandardCharsets.UTF_8), "application/json"); } catch (Exception e) { - PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to get server status", e); + PlayerTimeMod.LOGGER.error("[在线时间] 无法获取服务器状态", e); sendResponse(exchange, 500, "Internal Server Error"); } }); @@ -522,7 +548,7 @@ public class WebServer { Path dataFile = timeTracker.getDataFile(); // 从PlayerTimeTracker获取文件路径 if (!Files.exists(dataFile)) { - PlayerTimeMod.LOGGER.warn("[PlayerTime] Player data file not found: {}", dataFile); + PlayerTimeMod.LOGGER.warn("[在线时间] 玩家数据文件未找到: {}", dataFile); sendResponse(exchange, 404, "Data file not found"); return; } @@ -530,13 +556,13 @@ public class WebServer { byte[] fileContent = Files.readAllBytes(dataFile); sendResponse(exchange, 200, fileContent, "application/json"); - PlayerTimeMod.LOGGER.debug("[PlayerTime] Successfully served player data file {}", dataFile); + PlayerTimeMod.LOGGER.debug("[在线时间] 成功提供玩家数据文件 {}", dataFile); } catch (IOException e) { - PlayerTimeMod.LOGGER.error("[PlayerTime] Error reading player data file", e); + PlayerTimeMod.LOGGER.error("[在线时间] 读取玩家数据文件时出错", e); sendResponse(exchange, 500, "Error reading data file"); } catch (Exception e) { - PlayerTimeMod.LOGGER.error("[PlayerTime] An unknown error occurred while processing player data file request", e); + PlayerTimeMod.LOGGER.error("[在线时间] 处理玩家数据文件请求时发生未知错误", e); sendResponse(exchange, 500, "Internal Server Error"); } });