Skip to content

Commit 08771f0

Browse files
Add PacketLogger module (#5989)
1 parent 3331ab8 commit 08771f0

File tree

2 files changed

+301
-0
lines changed

2 files changed

+301
-0
lines changed

src/main/java/meteordevelopment/meteorclient/systems/modules/Modules.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ private void initMisc() {
575575
add(new Notebot());
576576
add(new Notifier());
577577
add(new PacketCanceller());
578+
add(new PacketLogger());
578579
add(new ServerSpoof());
579580
add(new SoundBlocker());
580581
add(new Spam());
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
/*
2+
* This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client).
3+
* Copyright (c) Meteor Development.
4+
*/
5+
6+
package meteordevelopment.meteorclient.systems.modules.misc;
7+
8+
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
9+
import meteordevelopment.meteorclient.MeteorClient;
10+
import meteordevelopment.meteorclient.events.packets.PacketEvent;
11+
import meteordevelopment.meteorclient.settings.*;
12+
import meteordevelopment.meteorclient.systems.modules.Categories;
13+
import meteordevelopment.meteorclient.systems.modules.Module;
14+
import meteordevelopment.meteorclient.utils.network.PacketUtils;
15+
import meteordevelopment.orbit.EventHandler;
16+
import meteordevelopment.orbit.EventPriority;
17+
import net.minecraft.network.packet.Packet;
18+
import org.jspecify.annotations.NullMarked;
19+
import org.jspecify.annotations.Nullable;
20+
21+
import java.io.BufferedWriter;
22+
import java.io.IOException;
23+
import java.nio.charset.StandardCharsets;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.nio.file.StandardOpenOption;
27+
import java.time.LocalDateTime;
28+
import java.time.format.DateTimeFormatter;
29+
import java.util.ArrayList;
30+
import java.util.Comparator;
31+
import java.util.List;
32+
import java.util.Set;
33+
34+
@NullMarked
35+
public class PacketLogger extends Module {
36+
private final SettingGroup sgGeneral = settings.getDefaultGroup();
37+
private final SettingGroup sgOutput = settings.createGroup("Output");
38+
39+
private final Setting<Set<Class<? extends Packet<?>>>> s2cPackets = sgGeneral.add(new PacketListSetting.Builder()
40+
.name("S2C-packets")
41+
.description("Server-to-client packets to log.")
42+
.filter(aClass -> PacketUtils.getS2CPackets().contains(aClass))
43+
.build()
44+
);
45+
46+
private final Setting<Set<Class<? extends Packet<?>>>> c2sPackets = sgGeneral.add(new PacketListSetting.Builder()
47+
.name("C2S-packets")
48+
.description("Client-to-server packets to log.")
49+
.filter(aClass -> PacketUtils.getC2SPackets().contains(aClass))
50+
.build()
51+
);
52+
53+
private final Setting<Boolean> showTimestamp = sgOutput.add(new BoolSetting.Builder()
54+
.name("show-timestamp")
55+
.description("Show timestamp for each logged packet.")
56+
.defaultValue(true)
57+
.build()
58+
);
59+
60+
private final Setting<Boolean> showPacketData = sgOutput.add(new BoolSetting.Builder()
61+
.name("show-packet-data")
62+
.description("Show the packet's toString() data for debugging.")
63+
.defaultValue(false)
64+
.build()
65+
);
66+
67+
private final Setting<Boolean> showCount = sgOutput.add(new BoolSetting.Builder()
68+
.name("show-count")
69+
.description("Show how many times each packet type has been logged.")
70+
.defaultValue(true)
71+
.build()
72+
);
73+
74+
private final Setting<Boolean> showSummary = sgOutput.add(new BoolSetting.Builder()
75+
.name("show-summary")
76+
.description("Show final packet count summary when module is deactivated.")
77+
.defaultValue(true)
78+
.build()
79+
);
80+
81+
private final Setting<Boolean> logToChat = sgOutput.add(new BoolSetting.Builder()
82+
.name("log-to-chat")
83+
.description("Log packets to chat.")
84+
.defaultValue(true)
85+
.build()
86+
);
87+
88+
// File logging settings
89+
90+
private final Setting<Boolean> logToFile = sgOutput.add(new BoolSetting.Builder()
91+
.name("log-to-file")
92+
.description("Save packet logs to a file in the meteor-client folder.")
93+
.defaultValue(false)
94+
.build()
95+
);
96+
97+
private final Setting<Integer> flushInterval = sgOutput.add(new IntSetting.Builder()
98+
.name("flush-interval")
99+
.description("How often to flush logs to disk (in seconds).")
100+
.defaultValue(1)
101+
.min(1)
102+
.sliderMax(10)
103+
.visible(logToFile::get)
104+
.build()
105+
);
106+
107+
private final Setting<Integer> maxFileSizeMB = sgOutput.add(new IntSetting.Builder()
108+
.name("max-file-size-mb")
109+
.description("Maximum size per log file in MB. Creates new file when exceeded.")
110+
.defaultValue(10)
111+
.min(1)
112+
.sliderMax(100)
113+
.visible(logToFile::get)
114+
.build()
115+
);
116+
117+
private final Setting<Integer> maxTotalLogsMB = sgOutput.add(new IntSetting.Builder()
118+
.name("max-total-logs-mb")
119+
.description("Maximum total disk space for all packet logs in MB. Deletes oldest when exceeded.")
120+
.defaultValue(50)
121+
.min(1)
122+
.sliderMax(500)
123+
.visible(logToFile::get)
124+
.build()
125+
);
126+
127+
private static final Path PACKET_LOGS_DIR = MeteorClient.FOLDER.toPath().resolve("packet-logs");
128+
private static final int LINE_SEPARATOR_BYTES = System.lineSeparator().getBytes(StandardCharsets.UTF_8).length;
129+
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
130+
private static final DateTimeFormatter FILE_NAME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
131+
private final Reference2IntOpenHashMap<Class<? extends Packet<?>>> packetCounts = new Reference2IntOpenHashMap<>();
132+
private @Nullable BufferedWriter fileWriter;
133+
private long lastFlushMs;
134+
private long currentFileSizeBytes;
135+
private int currentFileIndex;
136+
private @Nullable LocalDateTime sessionStartTime;
137+
138+
public PacketLogger() {
139+
super(Categories.Misc, "packet-logger", "Allows you to log certain packets.");
140+
runInMainMenu = true;
141+
}
142+
143+
@Override
144+
public void onActivate() {
145+
closeFileWriter();
146+
147+
packetCounts.clear();
148+
lastFlushMs = System.currentTimeMillis();
149+
sessionStartTime = LocalDateTime.now();
150+
currentFileIndex = 0;
151+
currentFileSizeBytes = 0;
152+
153+
if (logToFile.get()) {
154+
try {
155+
Files.createDirectories(PACKET_LOGS_DIR);
156+
cleanupOldLogs();
157+
openNewLogFile();
158+
} catch (IOException e) {
159+
error("Failed to initialize packet logging: %s", e.getMessage());
160+
fileWriter = null;
161+
}
162+
}
163+
}
164+
165+
@Override
166+
public void onDeactivate() {
167+
if (showSummary.get() && !packetCounts.isEmpty()) {
168+
logSummary();
169+
}
170+
closeFileWriter();
171+
}
172+
173+
private void logPacket(String direction, Packet<?> packet) {
174+
if (!logToChat.get() && !logToFile.get()) return;
175+
176+
@SuppressWarnings("unchecked")
177+
Class<? extends Packet<?>> packetClass = (Class<? extends Packet<?>>) packet.getClass();
178+
179+
// Update count
180+
packetCounts.addTo(packetClass, 1);
181+
182+
// Build log message
183+
StringBuilder msg = new StringBuilder(128);
184+
if (showTimestamp.get()) msg.append("[").append(LocalDateTime.now().format(TIME_FORMATTER)).append("] ");
185+
msg.append(direction).append(" ").append(PacketUtils.getName(packetClass));
186+
if (showCount.get()) msg.append(" (#").append(packetCounts.getInt(packetClass)).append(")");
187+
if (showPacketData.get()) msg.append("\n Data: ").append(packet);
188+
189+
// Log to chat and/or file
190+
String line = msg.toString();
191+
if (logToChat.get()) info(line);
192+
if (logToFile.get()) writeLine(line);
193+
}
194+
195+
private void logSummary() {
196+
int totalPackets = packetCounts.values().intStream().sum();
197+
198+
List<String> lines = new ArrayList<>();
199+
lines.add("--- SUMMARY ---");
200+
lines.add("Final packet counts (total " + totalPackets + "):");
201+
202+
packetCounts.reference2IntEntrySet().stream()
203+
.sorted((a, b) -> Integer.compare(b.getIntValue(), a.getIntValue()))
204+
.forEach(e -> lines.add(" %s: %d".formatted(PacketUtils.getName(e.getKey()), e.getIntValue())));
205+
206+
for (String line : lines) {
207+
if (logToChat.get()) info(line);
208+
if (logToFile.get()) writeLine(line);
209+
}
210+
}
211+
212+
private void writeLine(String line) {
213+
if (fileWriter == null) return;
214+
215+
try {
216+
int lineBytes = line.getBytes(StandardCharsets.UTF_8).length + LINE_SEPARATOR_BYTES;
217+
if (currentFileSizeBytes + lineBytes > maxFileSizeMB.get() * 1024L * 1024L) openNewLogFile();
218+
219+
fileWriter.write(line);
220+
fileWriter.newLine();
221+
currentFileSizeBytes += lineBytes;
222+
223+
long now = System.currentTimeMillis();
224+
if (now - lastFlushMs >= flushInterval.get() * 1000L) {
225+
fileWriter.flush();
226+
lastFlushMs = now;
227+
}
228+
} catch (IOException e) {
229+
error("Failed to write to packet log file: %s. File logging disabled.", e.getMessage());
230+
closeFileWriter();
231+
}
232+
}
233+
234+
private void openNewLogFile() throws IOException {
235+
if (fileWriter != null) fileWriter.close();
236+
if (sessionStartTime == null) sessionStartTime = LocalDateTime.now();
237+
238+
String fileName = "packets-%s-%d.log".formatted(sessionStartTime.format(FILE_NAME_FORMATTER), currentFileIndex++);
239+
fileWriter = Files.newBufferedWriter(
240+
PACKET_LOGS_DIR.resolve(fileName),
241+
StandardCharsets.UTF_8,
242+
StandardOpenOption.CREATE,
243+
StandardOpenOption.WRITE
244+
);
245+
currentFileSizeBytes = 0;
246+
cleanupOldLogs();
247+
}
248+
249+
private void closeFileWriter() {
250+
if (fileWriter != null) {
251+
try {
252+
fileWriter.flush();
253+
fileWriter.close();
254+
} catch (IOException ignored) {
255+
// Safe to ignore on close or rotation
256+
}
257+
fileWriter = null;
258+
}
259+
}
260+
261+
/**
262+
* Cleans up old log files if total size exceeds the maximum limit.
263+
* Deletes oldest files first.
264+
*/
265+
private void cleanupOldLogs() throws IOException {
266+
long maxBytes = maxTotalLogsMB.get() * 1024L * 1024L;
267+
List<LogFileEntry> logFiles = new ArrayList<>();
268+
try (var stream = Files.list(PACKET_LOGS_DIR)) {
269+
for (Path p : stream.toList()) {
270+
String name = p.getFileName().toString();
271+
if (!name.startsWith("packets-") || !name.endsWith(".log")) continue;
272+
try {
273+
logFiles.add(new LogFileEntry(p, Files.size(p), Files.getLastModifiedTime(p).toMillis()));
274+
} catch (IOException ignored) {
275+
// Skip files that can't be accessed
276+
}
277+
}
278+
}
279+
280+
logFiles.sort(Comparator.comparingLong(LogFileEntry::lastModified));
281+
long totalSize = 0;
282+
for (LogFileEntry entry : logFiles) {
283+
totalSize += entry.size();
284+
if (totalSize > maxBytes) Files.deleteIfExists(entry.path());
285+
}
286+
}
287+
288+
private record LogFileEntry(Path path, long size, long lastModified) {
289+
}
290+
291+
@EventHandler(priority = EventPriority.HIGHEST + 1)
292+
private void onReceivePacket(PacketEvent.Receive event) {
293+
if (s2cPackets.get().contains(event.packet.getClass())) logPacket("<- S2C", event.packet);
294+
}
295+
296+
@EventHandler(priority = EventPriority.HIGHEST + 1)
297+
private void onSendPacket(PacketEvent.Send event) {
298+
if (c2sPackets.get().contains(event.packet.getClass())) logPacket("-> C2S", event.packet);
299+
}
300+
}

0 commit comments

Comments
 (0)