package com.example.playertime; import com.google.gson.*; import com.mojang.authlib.GameProfile; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpExchange; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.server.MinecraftServer; import net.minecraft.server.PlayerManager; import net.minecraft.server.network.ServerPlayerEntity; import java.io.*; import java.lang.management.ManagementFactory; import java.net.InetSocketAddress; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.*; import java.util.concurrent.*; import java.util.stream.Collectors; public class WebServer { private final HttpServer server; private final PlayerTimeTracker timeTracker; private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private final ExecutorService executor = Executors.newFixedThreadPool(4); private final MinecraftServer minecraftServer; private static final Map MIME_TYPES = Map.ofEntries( Map.entry("html", "text/html"), Map.entry("css", "text/css"), Map.entry("js", "application/javascript"), Map.entry("json", "application/json"), Map.entry("png", "image/png"), // Added common web asset types Map.entry("jpg", "image/jpeg"), Map.entry("jpeg", "image/jpeg"), Map.entry("gif", "image/gif"), Map.entry("svg", "image/svg+xml"), Map.entry("woff", "application/font-woff"), Map.entry("woff2", "application/font-woff2"), Map.entry("ttf", "application/font-sfnt"), Map.entry("eot", "application/vnd.ms-fontobject") ); public WebServer(PlayerTimeTracker timeTracker, int port, MinecraftServer minecraftServer) throws IOException { this.minecraftServer = minecraftServer; if (port < 1 || port > 65535) { throw new IllegalArgumentException("Invalid port number: " + port); } this.timeTracker = timeTracker; this.server = HttpServer.create(new InetSocketAddress(port), 0); setupContexts(); } private void setupContexts() { server.createContext("/api/stats", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { // 白名单 Map stats = timeTracker.getWhitelistedPlayerStats(); 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); sendResponse(exchange, 500, "Internal Server Error"); } }); // 语言文件内容 server.createContext("/api/lang", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { String langCode = PlayerTimeMod.getConfig().getLanguage(); String resourcePath = String.format("assets/playertime/lang/%s.json", langCode); String finalResourcePath1 = resourcePath; InputStream is = FabricLoader.getInstance().getModContainer("playertime") .flatMap(container -> container.findPath(finalResourcePath1)) .map(path -> { try { return path.toUri().toURL().openStream(); } catch (Exception e) { return null; } }) .orElse(null); if (is == null) { // Fallback to default language if configured language file is not found langCode = "en_us"; // Default fallback resourcePath = String.format("assets/playertime/lang/%s.json", langCode); String finalResourcePath = resourcePath; is = FabricLoader.getInstance().getModContainer("playertime") .flatMap(container -> container.findPath(finalResourcePath)) .map(path -> { try { return path.toUri().toURL().openStream(); } catch (Exception e) { return null; } }) .orElse(null); if (is == null) { PlayerTimeMod.LOGGER.error("[PlayerTime] Default language file (en_us.json) not found!"); 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()); } ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] data = new byte[1024]; int nRead; while ((nRead = is.read(data, 0, data.length)) != -1) { buffer.write(data, 0, nRead); } buffer.flush(); is.close(); sendResponse(exchange, 200, buffer.toByteArray(), "application/json"); PlayerTimeMod.LOGGER.debug("[PlayerTime] Served language file: {}", resourcePath); } catch (IOException e) { PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to read language file", e); sendResponse(exchange, 500, "Error reading language file"); } catch (Exception e) { PlayerTimeMod.LOGGER.error("[PlayerTime] An unknown error occurred while processing language request", e); sendResponse(exchange, 500, "Internal Server Error"); } }); // 静态文件服务 server.createContext("/", exchange -> { try { String requestPath = exchange.getRequestURI().getPath(); String resourceFileName; if (requestPath.equals("/")) { resourceFileName = "index.html"; } else { int lastSlash = requestPath.lastIndexOf('/'); resourceFileName = requestPath.substring(lastSlash + 1); } String resourcePath = "assets/playertime/web" + requestPath; if (requestPath.equals("/")) { resourcePath += resourceFileName; } String finalResourcePath = resourcePath; InputStream is = FabricLoader.getInstance().getModContainer("playertime") .flatMap(container -> container.findPath(finalResourcePath)) .map(p -> { try { return p.toUri().toURL().openStream(); } catch (Exception e) { return null; } }) .orElse(null); if (is == null) { PlayerTimeMod.LOGGER.warn("[PlayerTime] Static resource not found: {}", resourcePath); sendResponse(exchange, 404, "Not Found"); return; } // 确定内容类型,一层保险 String extension = ""; int dotIndex = resourceFileName.lastIndexOf('.'); if (dotIndex > 0 && dotIndex < resourceFileName.length() - 1) { extension = resourceFileName.substring(dotIndex + 1).toLowerCase(); } String contentType = MIME_TYPES.getOrDefault(extension, "application/octet-stream"); // 读取文件内容 ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] data = new byte[1024]; int nRead; while ((nRead = is.read(data, 0, data.length)) != -1) { buffer.write(data, 0, nRead); } buffer.flush(); is.close(); sendResponse(exchange, 200, buffer.toByteArray(), contentType); PlayerTimeMod.LOGGER.debug("[PlayerTime] Served static file: {}", resourcePath); } catch (Exception e) { PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to serve static resource", e); sendResponse(exchange, 500, "Internal Server Error"); } }); // 没啥用了 server.createContext("/api/widget-data", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { MinecraftServer server = minecraftServer; PlayerManager playerManager = server.getPlayerManager(); JsonObject response = new JsonObject(); response.addProperty("onlineCount", playerManager.getCurrentPlayerCount()); JsonArray whitelistPlayers = new JsonArray(); Set whitelistUuids = new HashSet<>(); for (String name : playerManager.getWhitelist().getNames()) { server.getUserCache().findByName(name).ifPresent(profile -> { whitelistUuids.add(profile.getId()); }); } for (ServerPlayerEntity player : playerManager.getPlayerList()) { UUID uuid = player.getUuid(); if (whitelistUuids.contains(uuid)) { PlayerTimeTracker.PlayerTimeStats stats = timeTracker.getPlayerStats(uuid); if (stats != null) { JsonObject playerJson = new JsonObject(); playerJson.addProperty("name", player.getName().getString()); playerJson.addProperty("time", PlayerTimeTracker.formatTime(stats.totalTime)); whitelistPlayers.add(playerJson); } } } response.add("whitelistPlayers", whitelistPlayers); JsonArray topPlayers = new JsonArray(); timeTracker.getPlayerData().entrySet().stream() .filter(entry -> whitelistUuids.contains(entry.getKey())) // 只筛选白名单玩家 .sorted((a, b) -> Long.compare(b.getValue().totalTime, a.getValue().totalTime)) .limit(3) .forEach(entry -> { JsonObject playerJson = new JsonObject(); playerJson.addProperty("name", timeTracker.getPlayerName(entry.getKey())); playerJson.addProperty("time", PlayerTimeTracker.formatTime(entry.getValue().totalTime)); // formatTime doesn't need localization topPlayers.add(playerJson); }); response.add("topPlayers", topPlayers); response.addProperty("timestamp", System.currentTimeMillis()); 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); sendResponse(exchange, 500, "Internal Server Error"); } }); // 在线玩家列表 server.createContext("/api/online-players", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { PlayerManager playerManager = minecraftServer.getPlayerManager(); JsonObject response = new JsonObject(); // 获取白名单玩家UUID集合 Set whitelistUuids = new HashSet<>(); for (String name : playerManager.getWhitelist().getNames()) { minecraftServer.getUserCache().findByName(name).ifPresent(profile -> { whitelistUuids.add(profile.getId()); }); } // 分类玩家 JsonArray whitelistedPlayers = new JsonArray(); JsonArray nonWhitelistedPlayers = new JsonArray(); for (ServerPlayerEntity player : playerManager.getPlayerList()) { UUID uuid = player.getUuid(); JsonObject playerJson = new JsonObject(); playerJson.addProperty("name", player.getName().getString()); playerJson.addProperty("uuid", uuid.toString()); if (whitelistUuids.contains(uuid)) { whitelistedPlayers.add(playerJson); } else { nonWhitelistedPlayers.add(playerJson); } } response.add("whitelisted", whitelistedPlayers); response.add("non_whitelisted", nonWhitelistedPlayers); 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); sendResponse(exchange, 500, "Internal Server Error"); } }); // 玩家计数 server.createContext("/api/player-count", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { PlayerManager playerManager = minecraftServer.getPlayerManager(); JsonObject response = new JsonObject(); // 获取白名单玩家UUID集合 Set whitelistUuids = new HashSet<>(); for (String name : playerManager.getWhitelist().getNames()) { minecraftServer.getUserCache().findByName(name).ifPresent(profile -> { whitelistUuids.add(profile.getId()); }); } // 分类计数 int whitelistedCount = 0; int nonWhitelistedCount = 0; for (ServerPlayerEntity player : playerManager.getPlayerList()) { if (whitelistUuids.contains(player.getUuid())) { whitelistedCount++; } else { nonWhitelistedCount++; } } response.addProperty("total", playerManager.getCurrentPlayerCount()); response.addProperty("whitelisted", whitelistedCount); response.addProperty("non_whitelisted", nonWhitelistedCount); 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); sendResponse(exchange, 500, "Internal Server Error"); } }); // 白名单玩家(还有用吗?) server.createContext("/api/whitelist", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { PlayerManager playerManager = minecraftServer.getPlayerManager(); JsonArray whitelist = new JsonArray(); 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()); // 检查是否在线 ServerPlayerEntity onlinePlayer = playerManager.getPlayer(profile.get().getId()); player.addProperty("online", onlinePlayer != null); } else { player.addProperty("online", false); } whitelist.add(player); } sendResponse(exchange, 200, GSON.toJson(whitelist).getBytes(StandardCharsets.UTF_8), "application/json"); } catch (Exception e) { PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to get whitelist", e); sendResponse(exchange, 500, "Internal Server Error"); } }); // 服务器状态 server.createContext("/api/server-status", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { Runtime runtime = Runtime.getRuntime(); JsonObject status = new JsonObject(); long maxMemory = runtime.maxMemory(); long totalMemory = runtime.totalMemory(); long freeMemory = runtime.freeMemory(); long usedMemory = totalMemory - freeMemory; JsonObject memory = new JsonObject(); memory.addProperty("max", maxMemory); memory.addProperty("total", totalMemory); memory.addProperty("used", usedMemory); memory.addProperty("free", freeMemory); memory.addProperty("usage_percentage", (double) usedMemory / maxMemory * 100); status.add("memory", memory); status.addProperty("available_processors", runtime.availableProcessors()); long uptime = ManagementFactory.getRuntimeMXBean().getUptime(); status.addProperty("uptime", uptime); status.addProperty("uptime_formatted", formatUptime(uptime)); // formatUptime doesn't need localization File diskPartition = new File("."); long totalSpace = diskPartition.getTotalSpace(); long freeSpace = diskPartition.getFreeSpace(); long usableSpace = diskPartition.getUsableSpace(); JsonObject disk = new JsonObject(); disk.addProperty("total", totalSpace); disk.addProperty("free", freeSpace); disk.addProperty("usable", usableSpace); disk.addProperty("usage_percentage", (double) (totalSpace - freeSpace) / totalSpace * 100); status.add("disk", disk); JsonObject serverInfo = new JsonObject(); serverInfo.addProperty("version", minecraftServer.getVersion()); serverInfo.addProperty("player_count", minecraftServer.getCurrentPlayerCount()); serverInfo.addProperty("max_players", minecraftServer.getMaxPlayerCount()); serverInfo.addProperty("average_tick_time_ms", minecraftServer.getAverageTickTime()); long[] tickTimes = minecraftServer.getTickTimes(); if (tickTimes != null && tickTimes.length > 0) { double recentAvgTickTime = Arrays.stream(tickTimes).average().orElse(0) / 1000000.0; serverInfo.addProperty("recent_avg_tick_time_ms", recentAvgTickTime); JsonArray recentTicks = new JsonArray(); int sampleCount = Math.min(10, tickTimes.length); for (int i = 0; i < sampleCount; i++) { recentTicks.add(tickTimes[i] / 1000000.0); } serverInfo.add("recent_tick_samples_ms", recentTicks); } status.add("server", serverInfo); 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); sendResponse(exchange, 500, "Internal Server Error"); } }); // 原始数据文件 server.createContext("/api/playerdata", exchange -> { handleCors(exchange); if ("OPTIONS".equals(exchange.getRequestMethod())) { exchange.sendResponseHeaders(204, -1); // 204 No Content return; } if (!"GET".equals(exchange.getRequestMethod())) { sendResponse(exchange, 405, "Method Not Allowed"); return; } try { Path dataFile = timeTracker.getDataFile(); // 从PlayerTimeTracker获取文件路径 if (!Files.exists(dataFile)) { PlayerTimeMod.LOGGER.warn("[PlayerTime] Player data file not found: {}", dataFile); sendResponse(exchange, 404, "Data file not found"); return; } byte[] fileContent = Files.readAllBytes(dataFile); sendResponse(exchange, 200, fileContent, "application/json"); PlayerTimeMod.LOGGER.debug("[PlayerTime] Successfully served player data file {}", dataFile); } catch (IOException e) { PlayerTimeMod.LOGGER.error("[PlayerTime] Error reading player data file", 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); sendResponse(exchange, 500, "Internal Server Error"); } }); server.setExecutor(executor); } private void handleCors(HttpExchange exchange) { exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "GET, OPTIONS"); exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type"); } private void sendResponse(HttpExchange exchange, int code, String response) throws IOException { sendResponse(exchange, code, response.getBytes(StandardCharsets.UTF_8), "text/plain"); } private void sendResponse(HttpExchange exchange, int code, byte[] response, String contentType) throws IOException { exchange.getResponseHeaders().set("Content-Type", contentType); exchange.sendResponseHeaders(code, response.length); try (OutputStream os = exchange.getResponseBody()) { os.write(response); } } public void start() { server.start(); } public void stop() { server.stop(0); executor.shutdown(); } private String formatUptime(long millis) { long seconds = millis / 1000; long days = seconds / 86400; seconds %= 86400; long hours = seconds / 3600; seconds %= 3600; long minutes = seconds / 60; seconds %= 60; return String.format("%d天 %02d小时 %02d分钟 %02d秒", days, hours, minutes, seconds); } }