2025-06-09 18:27:05 +08:00

357 lines
14 KiB
Java

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<String, String> stats = tracker.getWhitelistedPlayerStats();
List<String> 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<String> 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> 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;
}
}
}