2025-06-05 09:48:53 +08:00

588 lines
25 KiB
Java

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<String, String> 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<String, String> 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<UUID> 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<UUID> 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<UUID> 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<GameProfile> 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);
}
}