修bug修bug
This commit is contained in:
parent
eef2bbebad
commit
0d9e2db5ee
@ -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
|
||||
|
@ -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<String, String> 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<String> 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> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<String, JsonElement> 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<UUID> 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<GameProfile> profile = server.getUserCache().getByUuid(uuid);
|
||||
if (profile.isPresent()) {
|
||||
return profile.get().getName();
|
||||
// 确保 UserCache 不为 null 再使用
|
||||
if (server != null && server.getUserCache() != null) {
|
||||
Optional<GameProfile> 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<String, JsonElement> 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<GameProfile> 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 {
|
||||
|
@ -69,11 +69,12 @@ public class WebServer {
|
||||
|
||||
try {
|
||||
// 白名单
|
||||
// timeTracker.getWhitelistedPlayerStats() 内部已处理 UserCache null 检查
|
||||
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);
|
||||
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");
|
||||
}
|
||||
});
|
||||
@ -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<UUID> 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<UUID> 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<UUID> 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<GameProfile> profile = minecraftServer.getUserCache().findByName(name);
|
||||
if (profile.isPresent()) {
|
||||
player.addProperty("uuid", profile.get().getId().toString());
|
||||
// 尝试获取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);
|
||||
// 检查是否在线
|
||||
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");
|
||||
}
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user