package com.example.playertime; import com.mojang.brigadier.arguments.IntegerArgumentType; import net.fabricmc.api.ModInitializer; 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; import net.minecraft.text.ClickEvent; import net.minecraft.text.MutableText; import net.minecraft.text.Text; 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 { public static final Logger LOGGER = LoggerFactory.getLogger("PlayerTimeTracker"); private static PlayerTimeTracker timeTracker; private static WebServer webServer; private static ModConfig config; public static LocalizationManager localizationManager; private static final String RSS_FEED_URL = "https://git.branulf.top/Branulf/ServerPlayerOnlineTracker/releases.rss"; private static final ExecutorService updateCheckExecutor = Executors.newSingleThreadExecutor(); @Override public void onInitialize() { config = new ModConfig(FabricLoader.getInstance().getConfigDir()); localizationManager = new LocalizationManager(config.getLanguage()); try { LOGGER.info("[在线时间] 初始化 玩家在线时长视奸Mod"); checkForUpdates(); ServerLifecycleEvents.SERVER_STARTING.register(server -> { timeTracker = new PlayerTimeTracker(server); try { webServer = new WebServer(timeTracker, config.getWebPort(), server); webServer.start(); LOGGER.info("[在线时间] Web服务器在端口 " + config.getWebPort() + "启动"); } catch (Exception e) { LOGGER.error("[在线时间] 无法启动Web服务器", e); } }); 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); } }); ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> { if (timeTracker != null) { timeTracker.onPlayerLeave(handler.player); } }); ServerLifecycleEvents.SERVER_STOPPING.register(server -> { LOGGER.info("[在线时间] 服务器停止 - 正在保存数据"); if (webServer != null) { webServer.stop(); } if (timeTracker != null) { for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { timeTracker.onPlayerLeave(player); } timeTracker.saveAll(); } updateCheckExecutor.shutdownNow(); }); } catch (Exception e) { LOGGER.error("[在线时间] Mod初始化失败", e); throw new RuntimeException("[在线时间] Mod初始化失败 ", e); } registerCommands(); } public static ModConfig getConfig() { return config; } public static PlayerTimeTracker getTimeTracker() { return timeTracker; } public static LocalizationManager getLocalizationManager() { return localizationManager; } public static void registerCommands() { CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { dispatcher.register(CommandManager.literal("onlineTime") .requires(source -> source.hasPermissionLevel(0)) .executes(context -> showOnlineTime(context.getSource(), 1)) .then(CommandManager.argument("page", IntegerArgumentType.integer(1)) .executes(context -> showOnlineTime( context.getSource(), IntegerArgumentType.getInteger(context, "page") )) ) ); }); } private static int showOnlineTime(ServerCommandSource source, int requestedPage) { ServerPlayerEntity player = source.getPlayer(); if (player == null) return 0; Util.getMainWorkerExecutor().execute(() -> { PlayerTimeTracker tracker = getTimeTracker(); if (tracker != null) { Map stats = tracker.getWhitelistedPlayerStats(); List sorted = stats.entrySet().stream() .sorted((a, b) -> comparePlayTime(a.getValue(), b.getValue())) .map(entry -> formatPlayerTime(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); sendPaginatedMessage(player, sorted, requestedPage); } else { player.sendMessage(Text.literal(localizationManager.getString("playertime.command.error.not_initialized")), false); } }); return 1; } private static int comparePlayTime(String a, String b) { try { 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(); } 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(); } return Long.compare(parseTimeToSeconds(timeB), parseTimeToSeconds(timeA)); // Descending order } catch (Exception e) { LOGGER.error("Error comparing play times: {} vs {}", a, b, e); return 0; } } private static long parseTimeToSeconds(String timeStr) { long seconds = 0; if (timeStr == null || timeStr.trim().isEmpty()) { return 0; } String[] parts = timeStr.trim().split("\\s+"); for (String part : parts) { if (part.endsWith("h")) { try { seconds += Integer.parseInt(part.substring(0, part.length() - 1)) * 3600; } catch (NumberFormatException ignored) {} } else if (part.endsWith("m")) { try { seconds += Integer.parseInt(part.substring(0, part.length() - 1)) * 60; } catch (NumberFormatException ignored) {} } } return seconds; } private static String formatPlayerTime(String name, String timeStr) { 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; page = Math.max(1, Math.min(page, totalPages)); int from = (page - 1) * pageSize; int to = Math.min(from + pageSize, lines.size()); 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); } MutableText footer = Text.literal(""); if (page > 1) { int finalPage = page; footer.append(Text.literal(localizationManager.getString("playertime.command.prev_page")) .styled(style -> style.withClickEvent(new ClickEvent( ClickEvent.Action.RUN_COMMAND, "/onlineTime " + (finalPage - 1) ))) .append(" ")); } footer.append(Text.literal(localizationManager.getString("playertime.command.total_players", lines.size()))); if (page < totalPages) { int finalPage1 = page; footer.append(" ").append(Text.literal(localizationManager.getString("playertime.command.next_page")) .styled(style -> style.withClickEvent(new ClickEvent( ClickEvent.Action.RUN_COMMAND, "/onlineTime " + (finalPage1 + 1) )))); } player.sendMessage(footer, false); } private void checkForUpdates() { updateCheckExecutor.submit(() -> { try { 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("[在线时间] 正在检查更新..."); 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; } 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); 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); } }); } private boolean isNewerVersion(String newVersion, String currentVersion) { if (newVersion == null || currentVersion == null || newVersion.isEmpty() || currentVersion.isEmpty()) { return false; } 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; } if (newPart < currentPart) { return false; } } return false; } /** * 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; } } }