gcpp/docs/ADDING_PACKET_PARSERS.md

8.6 KiB

Adding New Packet Parsers

This guide explains how to implement parsers for new GCNet packet types.

Architecture Overview

The packet parsing system uses several design patterns:

┌─────────────────────────────────────────────────────────────────┐
│                      GCNetPacketProcessor                        │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                PacketParserFactory (Factory)                 ││
│  │  ┌────────────┐ ┌────────────┐ ┌──────────────────────────┐ ││
│  │  │ PingParser │ │ PongParser │ │ VerifyAccountRequestParser│ ││
│  │  └────────────┘ └────────────┘ └──────────────────────────┘ ││
│  │                     Strategy Pattern                         ││
│  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                       PacketContext                              │
│  • decrypted payload bytes                                       │
│  • TCP segment (src/dst IP:port)                                 │
│  • server port for direction detection                           │
└─────────────────────────────────────────────────────────────────┘

Design Patterns Used

Pattern Class Purpose
Strategy PacketParser interface Each opcode has its own parsing strategy
Factory PacketParserFactory Creates the correct parser for each opcode
Context PacketContext Immutable context with payload + connection metadata
Registry GCNetOpcode enum Maps opcode numbers to metadata and direction hints
Null Object GenericPayloadParser Fallback for unimplemented opcodes

Step-by-Step: Adding a New Parser

1. Define the Opcode

Add the opcode to GCNetOpcode.java:

// In packets.com.gcpp.GCNetOpcode

/**
 * Example: Player join request.
 * Structure: Player name (string), Character class (int), Level (short)
 */
ENU_PLAYER_JOIN_REQ(50, "ENU_PLAYER_JOIN_REQ", 
    "Player join request", Direction.CLIENT_TO_SERVER),

The Direction enum indicates expected traffic flow:

  • CLIENT_TO_SERVER — Sent from client to server port (dstPort == serverPort)
  • SERVER_TO_CLIENT — Sent from server from server port (srcPort == serverPort)
  • BIDIRECTIONAL — Valid both ways (e.g., PING/PONG)

2. Create the Parser

Create a new class in packets/parsers/:

package com.gcnet.decryptor.packets.parsers;

import packets.com.gcemu.gcpp.GCNetOpcode;
import packets.com.gcemu.gcpp.PacketContext;
import packets.com.gcemu.gcpp.PacketParser;

import java.nio.ByteBuffer;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Parser for ENU_PLAYER_JOIN_REQ packets (opcode 50).
 *
 * <h3>Packet Structure:</h3>
 * <table border="1">
 *   <tr><th>Offset</th><th>Size</th><th>Type</th><th>Description</th></tr>
 *   <tr><td>0</td><td>2</td><td>ushort (BE)</td><td>Opcode (50)</td></tr>
 *   <tr><td>2</td><td>4</td><td>int (BE)</td><td>Content size</td></tr>
 *   <tr><td>6</td><td>1</td><td>bool</td><td>Compression flag</td></tr>
 *   <tr><td>7</td><td>4</td><td>int (LE)</td><td>Player name byte length</td></tr>
 *   <tr><td>11</td><td>var</td><td>string (UTF-16LE)</td><td>Player name</td></tr>
 *   <tr><td>var</td><td>4</td><td>int (LE)</td><td>Character class ID</td></tr>
 *   <tr><td>var</td><td>2</td><td>short (LE)</td><td>Character level</td></tr>
 * </table>
 *
 * <p>Direction: {@link GCNetOpcode.Direction#CLIENT_TO_SERVER}</p>
 */
@PacketParser.PacketParserFor(opcode = 50)
public class PlayerJoinRequestParser implements PacketParser {

    private static final int CONTENT_OFFSET = 7;

    @Override
    public ParseResult parse(PacketContext context) {
        byte[] payload = context.decryptedPayload();

        if (payload.length < CONTENT_OFFSET + 10) {
            return ParseResult.empty(GCNetOpcode.ENU_PLAYER_JOIN_REQ, "Client → Server");
        }

        int offset = CONTENT_OFFSET;
        Map<String, String> fields = new LinkedHashMap<>();

        // Parse player name
        ByteBuffer bb = ByteBuffer.wrap(payload, offset, 4)
                .order(java.nio.ByteOrder.LITTLE_ENDIAN);
        int nameLen = bb.getInt();
        offset += 4;

        if (offset + nameLen <= payload.length) {
            String playerName = new String(payload, offset, nameLen, java.nio.charset.StandardCharsets.UTF_16LE);
            fields.put("Player Name", playerName);
            offset += nameLen;
        }

        // Parse character class
        bb = ByteBuffer.wrap(payload, offset, 4)
                .order(java.nio.ByteOrder.LITTLE_ENDIAN);
        int charClass = bb.getInt();
        fields.put("Character Class", String.valueOf(charClass));
        offset += 4;

        // Parse level
        bb = ByteBuffer.wrap(payload, offset, 2)
                .order(java.nio.ByteOrder.LITTLE_ENDIAN);
        short level = bb.getShort();
        fields.put("Level", String.valueOf(level));

        return ParseResult.parsed(
                GCNetOpcode.ENU_PLAYER_JOIN_REQ,
                "Client → Server",
                "ENU_PLAYER_JOIN_REQ { name: string_utf16_le, class_id: int32, level: int16 }",
                fields,
                formatContentHex(payload),
                "player=\"" + fields.getOrDefault("Player Name", "?") + "\""
        );
    }

    private String formatContentHex(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();
    }
}

3. Register the Parser

Add registration in PacketParserFactory.registerBuiltInParsers():

private void registerBuiltInParsers() {
    // ... existing registrations ...

    // Player Management
    registerParser(50, new PlayerJoinRequestParser());
    registerParser(51, new PlayerJoinAckParser());
}

4. Direction Detection

The PacketContext automatically determines direction:

public boolean isClientToServer() {
    return segment.dstPort() == serverPort;  // Destination is the server
}

public boolean isServerToClient() {
    return segment.srcPort() == serverPort;  // Source is the server
}

For a server on port 9501:

  • 10.0.1.10:51094 → 10.0.1.10:9501Client → Server (dstPort == 9501)
  • 10.0.1.10:9501 → 10.0.1.10:51094Server → Client (srcPort == 9501)

File Structure

src/main/java/com/gcnet/decryptor/packets/
├── GCNetOpcode.java              ← Opcode registry (add new opcodes here)
├── PacketContext.java            ← Immutable context object
├── PacketParser.java             ← Strategy interface + annotation
├── PacketParserFactory.java      ← Factory (register new parsers here)
└── parsers/
    ├── GenericPayloadParser.java ← Fallback for unknown opcodes
    ├── KeyExchangeParser.java    ← Opcode 1
    ├── PingParser.java           ← Opcode 39
    ├── PongParser.java           ← Opcode 40
    ├── VerifyAccountRequestParser.java ← Opcode 34
    └── VerifyAccountAckParser.java     ← Opcode 35

Testing

Run the decryptor with your capture file:

java -jar target/gcnet-decryptor-1.0.0-jar-with-dependencies.jar capture.pcapng 9501

New parsers will automatically be invoked for their registered opcodes, producing structured field output instead of raw hex dumps.