# Adding New Packet Parsers This guide explains how to implement parsers for new GCNet packet types. ## Architecture Overview ``` ┌──────────────────────────────────────────────────────────────┐ │ PacketProcessor │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ PacketParserFactory │ │ │ │ ┌─────────────────┐ ┌────────────────────────────┐ │ │ │ │ │ KeyExchangeParser│ │ VerifyAccountReqParser │ │ │ │ │ └─────────────────┘ └────────────────────────────┘ │ │ │ │ Strategy Pattern │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ PacketContext │ │ │ │ rawPayload() → full decrypted bytes │ │ content() → header stripped, decompressed if flagged │ │ isCompressed() → whether original had compression flag │ │ │ └──────────────────────────────────────────────────────────────┘ ``` ### Design Patterns | Pattern | Class | Purpose | |---------|-------|---------| | **Strategy** | `PacketParser` | Each opcode has its own parsing algorithm | | **Factory** | `PacketParserFactory` | Creates the correct parser per opcode | | **Context** | `PacketContext` | Immutable object with processed content | | **Registry** | `Opcode` enum | Maps numbers to names + direction hints | | **Null Object** | `GenericPayloadParser` | Fallback for unimplemented opcodes | | **Marker** | `CompressedPayloadParser` | Declares a parser needs decompressed content | ## Step-by-Step: Adding a New Parser ### 1. Define the Opcode ```java // In com.gcemu.gcpp.packets.Opcode PLAYER_JOIN_REQ(50, "PLAYER_JOIN_REQ", "Player join request", Direction.CLIENT_TO_SERVER), ``` ### 2. Create the Parser ```java package com.gcemu.gcpp.packets.parsers; import com.gcemu.gcpp.packets.*; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; /** * Parser for PLAYER_JOIN_REQ (opcode 50). * *

Content Structure (offset 0, header already stripped):

* * * * * *
OffsetSizeTypeDescription
04int (LE)Username byte length
4varstring (UTF-16LE)Username
var4int (LE)Character class
*/ @PacketParser.PacketParserFor(opcode = 50) public class PlayerJoinReqParser implements PacketParser { @Override public ParseResult parse(PacketContext context) { // content() = raw payload minus the 7-byte GCNet header. // If this parser implemented CompressedPayloadParser, the // content would also be zlib-decompressed automatically. byte[] content = context.content(); if (content.length < 4) { return ParseResult.empty(Opcode.PLAYER_JOIN_REQ, "Client -> Server"); } int offset = 0; Map fields = new LinkedHashMap<>(); // Parse username length int nameLen = ByteBuffer.wrap(content, offset, 4) .order(java.nio.ByteOrder.LITTLE_ENDIAN).getInt(); offset += 4; // Parse username if (offset + nameLen <= content.length) { String name = new String(content, offset, nameLen, StandardCharsets.UTF_16LE); fields.put("Username", name); offset += nameLen; } // Parse character class if (offset + 4 <= content.length) { int charClass = ByteBuffer.wrap(content, offset, 4) .order(java.nio.ByteOrder.LITTLE_ENDIAN).getInt(); fields.put("Character Class", String.valueOf(charClass)); } return ParseResult.parsed( Opcode.PLAYER_JOIN_REQ, "Client -> Server", "PLAYER_JOIN_REQ { username: string_utf16, class: int32 }", fields, formatHex(content), "player=\"" + fields.getOrDefault("Username", "?") + "\"" ); } private String formatHex(byte[] data) { var sb = new StringBuilder(); for (int i = 0; i < data.length; i++) { if (i % 16 == 0 && i > 0) sb.append("\n"); sb.append(String.format("%02X ", data[i])); } return sb.toString().trim(); } } ``` **Key rules:** - `context.content()` returns **only content** — no header, no padding - Start parsing at **offset 0** - If the packet was compressed and your parser implements `CompressedPayloadParser`, it's **already decompressed** ### 3. Register the Parser ```java // In PacketParserFactory.registerBuiltInParsers() registerParser(50, new PlayerJoinReqParser()); ``` ## Handling Compressed Packets Some GCNet packets have zlib-compressed content. The framework handles decompression automatically — you just need to **declare intent**. ### Add `CompressedPayloadParser` to Your Parser ```java package com.gcemu.gcpp.packets.parsers; import com.gcemu.gcpp.packets.*; import java.util.LinkedHashMap; import java.util.Map; /** * Parser for a packet with compressed content (e.g., opcode 3). * *

By implementing {@link CompressedPayloadParser}, the framework * decompresses the content before {@code parse()} is called.

*/ @PacketParser.PacketParserFor(opcode = 3) public class ServerContentsParser implements PacketParser, CompressedPayloadParser { @Override public ParseResult parse(PacketContext context) { // context.content() is already: // 1. Stripped of the 7-byte GCNet header // 2. Decompressed via zlib (because compression flag was set) byte[] content = context.content(); // True if the original packet had compression flag = 1 boolean wasCompressed = context.isCompressed(); Map fields = new LinkedHashMap<>(); fields.put("Was Compressed", String.valueOf(wasCompressed)); fields.put("Decompressed Size", String.valueOf(content.length)); // Parse the decompressed content as needed... return ParseResult.parsed( Opcode.valueOf(3), resolveDirection(context), "SERVER_CONTENTS { data: bytes (zlib decompressed) }", fields, formatHex(content), "" ); } private String resolveDirection(PacketContext ctx) { return ctx.isClientToServer() ? "Client -> Server" : "Server -> Client"; } private String formatHex(byte[] data) { var sb = new StringBuilder(); for (int i = 0; i < data.length; i++) { if (i % 16 == 0 && i > 0) sb.append("\n"); sb.append(String.format("%02X ", data[i])); } return sb.toString().trim(); } } ``` ### Content Flow ``` Wire (encrypted): [DES-CBC encrypted: header(7) + [decompressedSize(4) + zlibData...]] │ PacketProcessor.decryptPacket() ▼ [opcode:2][size:4][flag:1][decompressedSize:4][zlibData...] │ ▼ PacketParserFactory detects CompressedPayloadParser (or just strips header if not compressed) │ ▼ PacketContext.content() [Decompressed content bytes — offset 0] │ ▼ Your parser — parse from offset 0 ``` ### `CompressedPayloadParser` vs Plain `PacketParser` | | Plain `PacketParser` | + `CompressedPayloadParser` | |---|---|---| | Header stripping | ❌ No — raw payload | ✅ Stripped | | Decompression | ❌ No | ✅ Automatic | | `context.content()` | Returns full raw payload | Returns processed content | | `context.isCompressed()` | Available | Available | | Best for | Key exchange, small control packets | Large data packets with compressed content | > **Tip:** Always implement `CompressedPayloadParser` for packets that > may have compressed content. For packets that are never compressed > (key exchange, heartbeats), a plain `PacketParser` is fine. ## Direction Detection ```java context.isClientToServer() // dstPort == serverPort context.isServerToClient() // srcPort == serverPort ``` For server port 9501: - `client:51094 -> server:9501` = **Client -> Server** - `server:9501 -> client:51094` = **Server -> Client** ## File Structure ``` src/main/java/com/gcemu/gcpp/packets/ ├── Opcode.java ← Opcode registry ├── PacketContext.java ← Processed content + direction ├── PacketParser.java ← Strategy interface + annotation ├── CompressedPayloadParser.java ← Marker for compressed content ├── PayloadContentExtractor.java ← Header stripping + zlib decompression ├── PacketParserFactory.java ← Factory + registration └── parsers/ ├── GenericPayloadParser.java ← Fallback ├── HeartBeatParser.java ← Opcode 0 ├── KeyExchangeParser.java ← Opcode 1 (uses content()) └── VerifyAccountReqParser.java← Opcode 2 (uses content()) ``` ## Testing ```bash java -jar target/gcnet-decryptor-1.0.0-jar-with-dependencies.jar capture.pcapng 9501 ```