357 lines
14 KiB
Java
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;
|
|
}
|
|
}
|
|
}
|