From b40a45bf11a8d2c766727c1bb24cff477083650e Mon Sep 17 00:00:00 2001 From: Big-Iron-Cheems <52252627+Big-Iron-Cheems@users.noreply.github.com> Date: Fri, 26 Dec 2025 20:54:43 +0100 Subject: [PATCH 1/3] Add PacketLogger module --- .../meteorclient/systems/modules/Modules.java | 1 + .../systems/modules/misc/PacketLogger.java | 222 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 src/main/java/meteordevelopment/meteorclient/systems/modules/misc/PacketLogger.java diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/Modules.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/Modules.java index 8071b17f4b..68b9d8d62d 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/modules/Modules.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/Modules.java @@ -574,6 +574,7 @@ private void initMisc() { add(new Notebot()); add(new Notifier()); add(new PacketCanceller()); + add(new PacketLogger()); add(new ServerSpoof()); add(new SoundBlocker()); add(new Spam()); diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/PacketLogger.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/PacketLogger.java new file mode 100644 index 0000000000..f146e53a69 --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/PacketLogger.java @@ -0,0 +1,222 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.systems.modules.misc; + +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import meteordevelopment.meteorclient.MeteorClient; +import meteordevelopment.meteorclient.events.packets.PacketEvent; +import meteordevelopment.meteorclient.settings.*; +import meteordevelopment.meteorclient.systems.modules.Categories; +import meteordevelopment.meteorclient.systems.modules.Module; +import meteordevelopment.meteorclient.utils.network.PacketUtils; +import meteordevelopment.orbit.EventHandler; +import meteordevelopment.orbit.EventPriority; +import net.minecraft.network.packet.Packet; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Set; + +@NullMarked +public class PacketLogger extends Module { + private final SettingGroup sgGeneral = settings.getDefaultGroup(); + private final SettingGroup sgOutput = settings.createGroup("Output"); + + private final Setting>>> s2cPackets = sgGeneral.add(new PacketListSetting.Builder() + .name("S2C-packets") + .description("Server-to-client packets to log.") + .filter(aClass -> PacketUtils.getS2CPackets().contains(aClass)) + .build() + ); + + private final Setting>>> c2sPackets = sgGeneral.add(new PacketListSetting.Builder() + .name("C2S-packets") + .description("Client-to-server packets to log.") + .filter(aClass -> PacketUtils.getC2SPackets().contains(aClass)) + .build() + ); + + private final Setting showTimestamp = sgOutput.add(new BoolSetting.Builder() + .name("show-timestamp") + .description("Show timestamp for each logged packet.") + .defaultValue(true) + .build() + ); + + private final Setting showPacketData = sgOutput.add(new BoolSetting.Builder() + .name("show-packet-data") + .description("Show the packet's toString() data for debugging.") + .defaultValue(false) + .build() + ); + + private final Setting showCount = sgOutput.add(new BoolSetting.Builder() + .name("show-count") + .description("Show how many times each packet type has been logged.") + .defaultValue(true) + .build() + ); + + private final Setting logToFile = sgOutput.add(new BoolSetting.Builder() + .name("log-to-file") + .description("Save packet logs to a file in the meteor-client folder.") + .defaultValue(false) + .build() + ); + + private final Setting flushInterval = sgOutput.add(new IntSetting.Builder() + .name("flush-interval") + .description("How often to flush logs to disk (in seconds).") + .defaultValue(1) + .min(1) + .sliderMax(10) + .visible(logToFile::get) + .build() + ); + + private final Setting showSummary = sgOutput.add(new BoolSetting.Builder() + .name("show-summary") + .description("Show final packet count summary when module is deactivated.") + .defaultValue(true) + .build() + ); + + private final Object2IntOpenHashMap>> packetCounts = new Object2IntOpenHashMap<>(); + private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + private final DateTimeFormatter fileNameFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); + private @Nullable BufferedWriter fileWriter; + private long lastFlushMs; + + public PacketLogger() { + super(Categories.Misc, "packet-logger", "Allows you to log certain packets."); + runInMainMenu = true; + } + + @Override + public void onActivate() { + if (fileWriter != null) { + try { + fileWriter.close(); + } catch (IOException e) { + error("Failed to close previous log file: %s", e.getMessage()); + } + fileWriter = null; + } + + packetCounts.clear(); + lastFlushMs = System.currentTimeMillis(); + + if (logToFile.get()) { + try { + Path logPath = MeteorClient.FOLDER.toPath().resolve("packet-logs"); + Files.createDirectories(logPath); + + String fileName = "packets-%s.log".formatted(LocalDateTime.now().format(fileNameFormatter)); + fileWriter = Files.newBufferedWriter(logPath.resolve(fileName), StandardOpenOption.CREATE, StandardOpenOption.WRITE); + info("Logging packets to file: %s", fileName); + } catch (IOException e) { + error("Failed to create packet log file: %s", e.getMessage()); + fileWriter = null; + lastFlushMs = 0; + } + } + } + + @Override + public void onDeactivate() { + if (fileWriter != null) { + try { + fileWriter.flush(); + fileWriter.close(); + info("Closed packet log file."); + } catch (IOException e) { + error("Failed to close packet log file: %s", e.getMessage()); + } + fileWriter = null; + } + + if (showSummary.get() && !packetCounts.isEmpty()) { + info("Final packet counts:"); + packetCounts.object2IntEntrySet().stream() + .sorted((a, b) -> Integer.compare(b.getIntValue(), a.getIntValue())) + .forEach(e -> info(" %s: %d", PacketUtils.getName(e.getKey()), e.getIntValue())); + } + } + + private void logPacket(String direction, Packet packet) { + @SuppressWarnings("unchecked") + Class> packetClass = (Class>) packet.getClass(); + + // Update count + packetCounts.addTo(packetClass, 1); + + // Build log message + StringBuilder msg = new StringBuilder(128); + + if (showTimestamp.get()) { + msg.append("[").append(LocalDateTime.now().format(timeFormatter)).append("] "); + } + + msg.append(direction).append(" "); + msg.append(PacketUtils.getName(packetClass)); + + if (showCount.get()) { + msg.append(" (#").append(packetCounts.getInt(packetClass)).append(")"); + } + + if (showPacketData.get()) { + msg.append("\n Data: ").append(packet); + } + + // Log to chat + info(msg.toString()); + + // Log to file + if (logToFile.get() && fileWriter != null) { + try { + fileWriter.write(msg.toString()); + fileWriter.newLine(); + + // Flush periodically, not on every packet + long now = System.currentTimeMillis(); + long flushIntervalMs = flushInterval.get() * 1000L; + if (now - lastFlushMs >= flushIntervalMs) { + fileWriter.flush(); + lastFlushMs = now; + } + } catch (IOException e) { + error("Failed to write to packet log file: %s. File logging disabled.", e.getMessage()); + try { + fileWriter.close(); + } catch (IOException ignored) { + // Close attempt after error + } + fileWriter = null; + } + } + } + + @EventHandler(priority = EventPriority.HIGHEST + 1) + private void onReceivePacket(PacketEvent.Receive event) { + if (s2cPackets.get().contains(event.packet.getClass())) { + logPacket("<- S2C", event.packet); + } + } + + @EventHandler(priority = EventPriority.HIGHEST + 1) + private void onSendPacket(PacketEvent.Send event) { + if (c2sPackets.get().contains(event.packet.getClass())) { + logPacket("-> C2S", event.packet); + } + } +} From 894ad33c757662f79ad51836dcc92c00cef1373c Mon Sep 17 00:00:00 2001 From: Big-Iron-Cheems <52252627+Big-Iron-Cheems@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:02:17 +0100 Subject: [PATCH 2/3] Address PR reviews - Use `Reference2IntOpenHashMap` for `packetCounts` - Add system to limit log file size and total size to avoid bloating the dir over extended usage - Add optional chat logging - Remove unnecessary info prints TODO: consider a writeLine helper for `fileWriter` --- .../systems/modules/misc/PacketLogger.java | 151 +++++++++++++++--- 1 file changed, 131 insertions(+), 20 deletions(-) diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/PacketLogger.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/PacketLogger.java index f146e53a69..90536bbb70 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/PacketLogger.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/PacketLogger.java @@ -5,7 +5,7 @@ package meteordevelopment.meteorclient.systems.modules.misc; -import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; import meteordevelopment.meteorclient.MeteorClient; import meteordevelopment.meteorclient.events.packets.PacketEvent; import meteordevelopment.meteorclient.settings.*; @@ -20,11 +20,15 @@ import java.io.BufferedWriter; import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; import java.util.Set; @NullMarked @@ -67,6 +71,22 @@ public class PacketLogger extends Module { .build() ); + private final Setting showSummary = sgOutput.add(new BoolSetting.Builder() + .name("show-summary") + .description("Show final packet count summary when module is deactivated.") + .defaultValue(true) + .build() + ); + + private final Setting logToChat = sgOutput.add(new BoolSetting.Builder() + .name("log-to-chat") + .description("Log packets to chat.") + .defaultValue(true) + .build() + ); + + // File logging settings + private final Setting logToFile = sgOutput.add(new BoolSetting.Builder() .name("log-to-file") .description("Save packet logs to a file in the meteor-client folder.") @@ -84,18 +104,37 @@ public class PacketLogger extends Module { .build() ); - private final Setting showSummary = sgOutput.add(new BoolSetting.Builder() - .name("show-summary") - .description("Show final packet count summary when module is deactivated.") - .defaultValue(true) + private final Setting maxFileSizeMB = sgOutput.add(new IntSetting.Builder() + .name("max-file-size-mb") + .description("Maximum size per log file in MB. Creates new file when exceeded.") + .defaultValue(10) + .min(1) + .sliderMax(100) + .visible(logToFile::get) + .build() + ); + + private final Setting maxTotalLogsMB = sgOutput.add(new IntSetting.Builder() + .name("max-total-logs-mb") + .description("Maximum total disk space for all packet logs in MB. Deletes oldest when exceeded.") + .defaultValue(50) + .min(1) + .sliderMax(500) + .visible(logToFile::get) .build() ); - private final Object2IntOpenHashMap>> packetCounts = new Object2IntOpenHashMap<>(); - private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); - private final DateTimeFormatter fileNameFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); + private static final Path PACKET_LOGS_DIR = MeteorClient.FOLDER.toPath().resolve("packet-logs"); + private static final Charset LOG_CHARSET = StandardCharsets.UTF_8; + private static final int LINE_SEPARATOR_BYTES = System.lineSeparator().getBytes(LOG_CHARSET).length; + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + private static final DateTimeFormatter FILE_NAME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); + private final Reference2IntOpenHashMap>> packetCounts = new Reference2IntOpenHashMap<>(); private @Nullable BufferedWriter fileWriter; private long lastFlushMs; + private long currentFileSizeBytes; + private int currentFileIndex; + private @Nullable LocalDateTime sessionStartTime; public PacketLogger() { super(Categories.Misc, "packet-logger", "Allows you to log certain packets."); @@ -115,15 +154,20 @@ public void onActivate() { packetCounts.clear(); lastFlushMs = System.currentTimeMillis(); + sessionStartTime = LocalDateTime.now(); + currentFileIndex = 0; + currentFileSizeBytes = 0; if (logToFile.get()) { try { - Path logPath = MeteorClient.FOLDER.toPath().resolve("packet-logs"); - Files.createDirectories(logPath); - - String fileName = "packets-%s.log".formatted(LocalDateTime.now().format(fileNameFormatter)); - fileWriter = Files.newBufferedWriter(logPath.resolve(fileName), StandardOpenOption.CREATE, StandardOpenOption.WRITE); - info("Logging packets to file: %s", fileName); + Files.createDirectories(PACKET_LOGS_DIR); + cleanupOldLogs(); + + String fileName = "packets-%s-%d.log".formatted( + sessionStartTime.format(FILE_NAME_FORMATTER), + currentFileIndex + ); + fileWriter = Files.newBufferedWriter(PACKET_LOGS_DIR.resolve(fileName), LOG_CHARSET, StandardOpenOption.CREATE, StandardOpenOption.WRITE); } catch (IOException e) { error("Failed to create packet log file: %s", e.getMessage()); fileWriter = null; @@ -138,7 +182,6 @@ public void onDeactivate() { try { fileWriter.flush(); fileWriter.close(); - info("Closed packet log file."); } catch (IOException e) { error("Failed to close packet log file: %s", e.getMessage()); } @@ -146,8 +189,9 @@ public void onDeactivate() { } if (showSummary.get() && !packetCounts.isEmpty()) { - info("Final packet counts:"); - packetCounts.object2IntEntrySet().stream() + int totalPackets = packetCounts.values().intStream().sum(); + info("Final packet counts (total %d):", totalPackets); + packetCounts.reference2IntEntrySet().stream() .sorted((a, b) -> Integer.compare(b.getIntValue(), a.getIntValue())) .forEach(e -> info(" %s: %d", PacketUtils.getName(e.getKey()), e.getIntValue())); } @@ -164,7 +208,7 @@ private void logPacket(String direction, Packet packet) { StringBuilder msg = new StringBuilder(128); if (showTimestamp.get()) { - msg.append("[").append(LocalDateTime.now().format(timeFormatter)).append("] "); + msg.append("[").append(LocalDateTime.now().format(TIME_FORMATTER)).append("] "); } msg.append(direction).append(" "); @@ -179,13 +223,21 @@ private void logPacket(String direction, Packet packet) { } // Log to chat - info(msg.toString()); + if (logToChat.get()) info(msg.toString()); // Log to file if (logToFile.get() && fileWriter != null) { try { - fileWriter.write(msg.toString()); + String line = msg.toString(); + int lineBytes = line.getBytes(LOG_CHARSET).length + LINE_SEPARATOR_BYTES; + + if (currentFileSizeBytes + lineBytes > maxFileSizeMB.get() * 1024L * 1024L) { + rotateLogFile(); + } + + fileWriter.write(line); fileWriter.newLine(); + currentFileSizeBytes += lineBytes; // Flush periodically, not on every packet long now = System.currentTimeMillis(); @@ -206,6 +258,65 @@ private void logPacket(String direction, Packet packet) { } } + /** + * Rotates the current log file when it exceeds the maximum size. + */ + private void rotateLogFile() throws IOException { + fileWriter.flush(); + fileWriter.close(); + + currentFileIndex++; + + String fileName = "packets-%s-%d.log".formatted( + sessionStartTime.format(FILE_NAME_FORMATTER), + currentFileIndex + ); + + fileWriter = Files.newBufferedWriter(PACKET_LOGS_DIR.resolve(fileName), LOG_CHARSET, StandardOpenOption.CREATE, StandardOpenOption.WRITE); + currentFileSizeBytes = 0; + + cleanupOldLogs(); + } + + /** + * Cleans up old log files if total size exceeds the maximum limit. + */ + private void cleanupOldLogs() throws IOException { + long maxBytes = maxTotalLogsMB.get() * 1024L * 1024L; + + List logFiles = new ArrayList<>(); + + try (var stream = Files.list(PACKET_LOGS_DIR)) { + for (Path p : stream.toList()) { + String name = p.getFileName().toString(); + if (!name.startsWith("packets-") || !name.endsWith(".log")) continue; + + try { + logFiles.add(new LogFileEntry( + p, + Files.size(p), + Files.getLastModifiedTime(p).toMillis() + )); + } catch (IOException ignored) { + // Skip unreadable files + } + } + } + + logFiles.sort((a, b) -> Long.compare(b.lastModified(), a.lastModified())); + + long totalSize = 0; + for (LogFileEntry entry : logFiles) { + totalSize += entry.size(); + if (totalSize > maxBytes) { + Files.deleteIfExists(entry.path()); + } + } + } + + private record LogFileEntry(Path path, long size, long lastModified) { + } + @EventHandler(priority = EventPriority.HIGHEST + 1) private void onReceivePacket(PacketEvent.Receive event) { if (s2cPackets.get().contains(event.packet.getClass())) { From 48ec2e8cbdabf0c6c4be2f5666312a2a8921fe3a Mon Sep 17 00:00:00 2001 From: Big-Iron-Cheems <52252627+Big-Iron-Cheems@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:42:46 +0100 Subject: [PATCH 3/3] Refactor file logging logic - early return `logPacket` if both outputs are off - `logSummary` prepares the lines in advance, prints later - centralized `writeLine` to handle IO ops Note: summary won't appear if both outputs are off, this makes sense since `packetCounts` would never be updated in that case. --- .../systems/modules/misc/PacketLogger.java | 181 +++++++----------- 1 file changed, 74 insertions(+), 107 deletions(-) diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/PacketLogger.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/PacketLogger.java index 90536bbb70..f00a21692b 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/PacketLogger.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/misc/PacketLogger.java @@ -20,7 +20,6 @@ import java.io.BufferedWriter; import java.io.IOException; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -28,6 +27,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Set; @@ -125,8 +125,7 @@ public class PacketLogger extends Module { ); private static final Path PACKET_LOGS_DIR = MeteorClient.FOLDER.toPath().resolve("packet-logs"); - private static final Charset LOG_CHARSET = StandardCharsets.UTF_8; - private static final int LINE_SEPARATOR_BYTES = System.lineSeparator().getBytes(LOG_CHARSET).length; + private static final int LINE_SEPARATOR_BYTES = System.lineSeparator().getBytes(StandardCharsets.UTF_8).length; private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); private static final DateTimeFormatter FILE_NAME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); private final Reference2IntOpenHashMap>> packetCounts = new Reference2IntOpenHashMap<>(); @@ -143,14 +142,7 @@ public PacketLogger() { @Override public void onActivate() { - if (fileWriter != null) { - try { - fileWriter.close(); - } catch (IOException e) { - error("Failed to close previous log file: %s", e.getMessage()); - } - fileWriter = null; - } + closeFileWriter(); packetCounts.clear(); lastFlushMs = System.currentTimeMillis(); @@ -162,42 +154,25 @@ public void onActivate() { try { Files.createDirectories(PACKET_LOGS_DIR); cleanupOldLogs(); - - String fileName = "packets-%s-%d.log".formatted( - sessionStartTime.format(FILE_NAME_FORMATTER), - currentFileIndex - ); - fileWriter = Files.newBufferedWriter(PACKET_LOGS_DIR.resolve(fileName), LOG_CHARSET, StandardOpenOption.CREATE, StandardOpenOption.WRITE); + openNewLogFile(); } catch (IOException e) { - error("Failed to create packet log file: %s", e.getMessage()); + error("Failed to initialize packet logging: %s", e.getMessage()); fileWriter = null; - lastFlushMs = 0; } } } @Override public void onDeactivate() { - if (fileWriter != null) { - try { - fileWriter.flush(); - fileWriter.close(); - } catch (IOException e) { - error("Failed to close packet log file: %s", e.getMessage()); - } - fileWriter = null; - } - if (showSummary.get() && !packetCounts.isEmpty()) { - int totalPackets = packetCounts.values().intStream().sum(); - info("Final packet counts (total %d):", totalPackets); - packetCounts.reference2IntEntrySet().stream() - .sorted((a, b) -> Integer.compare(b.getIntValue(), a.getIntValue())) - .forEach(e -> info(" %s: %d", PacketUtils.getName(e.getKey()), e.getIntValue())); + logSummary(); } + closeFileWriter(); } private void logPacket(String direction, Packet packet) { + if (!logToChat.get() && !logToFile.get()) return; + @SuppressWarnings("unchecked") Class> packetClass = (Class>) packet.getClass(); @@ -206,111 +181,107 @@ private void logPacket(String direction, Packet packet) { // Build log message StringBuilder msg = new StringBuilder(128); + if (showTimestamp.get()) msg.append("[").append(LocalDateTime.now().format(TIME_FORMATTER)).append("] "); + msg.append(direction).append(" ").append(PacketUtils.getName(packetClass)); + if (showCount.get()) msg.append(" (#").append(packetCounts.getInt(packetClass)).append(")"); + if (showPacketData.get()) msg.append("\n Data: ").append(packet); + + // Log to chat and/or file + String line = msg.toString(); + if (logToChat.get()) info(line); + if (logToFile.get()) writeLine(line); + } - if (showTimestamp.get()) { - msg.append("[").append(LocalDateTime.now().format(TIME_FORMATTER)).append("] "); - } + private void logSummary() { + int totalPackets = packetCounts.values().intStream().sum(); - msg.append(direction).append(" "); - msg.append(PacketUtils.getName(packetClass)); + List lines = new ArrayList<>(); + lines.add("--- SUMMARY ---"); + lines.add("Final packet counts (total " + totalPackets + "):"); - if (showCount.get()) { - msg.append(" (#").append(packetCounts.getInt(packetClass)).append(")"); - } + packetCounts.reference2IntEntrySet().stream() + .sorted((a, b) -> Integer.compare(b.getIntValue(), a.getIntValue())) + .forEach(e -> lines.add(" %s: %d".formatted(PacketUtils.getName(e.getKey()), e.getIntValue()))); - if (showPacketData.get()) { - msg.append("\n Data: ").append(packet); + for (String line : lines) { + if (logToChat.get()) info(line); + if (logToFile.get()) writeLine(line); } + } - // Log to chat - if (logToChat.get()) info(msg.toString()); - - // Log to file - if (logToFile.get() && fileWriter != null) { - try { - String line = msg.toString(); - int lineBytes = line.getBytes(LOG_CHARSET).length + LINE_SEPARATOR_BYTES; + private void writeLine(String line) { + if (fileWriter == null) return; - if (currentFileSizeBytes + lineBytes > maxFileSizeMB.get() * 1024L * 1024L) { - rotateLogFile(); - } + try { + int lineBytes = line.getBytes(StandardCharsets.UTF_8).length + LINE_SEPARATOR_BYTES; + if (currentFileSizeBytes + lineBytes > maxFileSizeMB.get() * 1024L * 1024L) openNewLogFile(); - fileWriter.write(line); - fileWriter.newLine(); - currentFileSizeBytes += lineBytes; + fileWriter.write(line); + fileWriter.newLine(); + currentFileSizeBytes += lineBytes; - // Flush periodically, not on every packet - long now = System.currentTimeMillis(); - long flushIntervalMs = flushInterval.get() * 1000L; - if (now - lastFlushMs >= flushIntervalMs) { - fileWriter.flush(); - lastFlushMs = now; - } - } catch (IOException e) { - error("Failed to write to packet log file: %s. File logging disabled.", e.getMessage()); - try { - fileWriter.close(); - } catch (IOException ignored) { - // Close attempt after error - } - fileWriter = null; + long now = System.currentTimeMillis(); + if (now - lastFlushMs >= flushInterval.get() * 1000L) { + fileWriter.flush(); + lastFlushMs = now; } + } catch (IOException e) { + error("Failed to write to packet log file: %s. File logging disabled.", e.getMessage()); + closeFileWriter(); } } - /** - * Rotates the current log file when it exceeds the maximum size. - */ - private void rotateLogFile() throws IOException { - fileWriter.flush(); - fileWriter.close(); + private void openNewLogFile() throws IOException { + if (fileWriter != null) fileWriter.close(); + if (sessionStartTime == null) sessionStartTime = LocalDateTime.now(); - currentFileIndex++; - - String fileName = "packets-%s-%d.log".formatted( - sessionStartTime.format(FILE_NAME_FORMATTER), - currentFileIndex + String fileName = "packets-%s-%d.log".formatted(sessionStartTime.format(FILE_NAME_FORMATTER), currentFileIndex++); + fileWriter = Files.newBufferedWriter( + PACKET_LOGS_DIR.resolve(fileName), + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.WRITE ); - - fileWriter = Files.newBufferedWriter(PACKET_LOGS_DIR.resolve(fileName), LOG_CHARSET, StandardOpenOption.CREATE, StandardOpenOption.WRITE); currentFileSizeBytes = 0; - cleanupOldLogs(); } + private void closeFileWriter() { + if (fileWriter != null) { + try { + fileWriter.flush(); + fileWriter.close(); + } catch (IOException ignored) { + // Safe to ignore on close or rotation + } + fileWriter = null; + } + } + /** * Cleans up old log files if total size exceeds the maximum limit. + * Deletes oldest files first. */ private void cleanupOldLogs() throws IOException { long maxBytes = maxTotalLogsMB.get() * 1024L * 1024L; - List logFiles = new ArrayList<>(); - try (var stream = Files.list(PACKET_LOGS_DIR)) { for (Path p : stream.toList()) { String name = p.getFileName().toString(); if (!name.startsWith("packets-") || !name.endsWith(".log")) continue; - try { - logFiles.add(new LogFileEntry( - p, - Files.size(p), - Files.getLastModifiedTime(p).toMillis() - )); + logFiles.add(new LogFileEntry(p, Files.size(p), Files.getLastModifiedTime(p).toMillis())); } catch (IOException ignored) { - // Skip unreadable files + // Skip files that can't be accessed } } } - logFiles.sort((a, b) -> Long.compare(b.lastModified(), a.lastModified())); - + logFiles.sort(Comparator.comparingLong(LogFileEntry::lastModified)); long totalSize = 0; for (LogFileEntry entry : logFiles) { totalSize += entry.size(); - if (totalSize > maxBytes) { - Files.deleteIfExists(entry.path()); - } + if (totalSize > maxBytes) Files.deleteIfExists(entry.path()); } } @@ -319,15 +290,11 @@ private record LogFileEntry(Path path, long size, long lastModified) { @EventHandler(priority = EventPriority.HIGHEST + 1) private void onReceivePacket(PacketEvent.Receive event) { - if (s2cPackets.get().contains(event.packet.getClass())) { - logPacket("<- S2C", event.packet); - } + if (s2cPackets.get().contains(event.packet.getClass())) logPacket("<- S2C", event.packet); } @EventHandler(priority = EventPriority.HIGHEST + 1) private void onSendPacket(PacketEvent.Send event) { - if (c2sPackets.get().contains(event.packet.getClass())) { - logPacket("-> C2S", event.packet); - } + if (c2sPackets.get().contains(event.packet.getClass())) logPacket("-> C2S", event.packet); } }