# 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):
*
* | Offset | Size | Type | Description |
* | 0 | 4 | int (LE) | Username byte length |
* | 4 | var | string (UTF-16LE) | Username |
* | var | 4 | int (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
```