Can read the emulator's packet struct XML files and serialize/deserialize protocol-compatible packet structs with that data.
Find a file
2025-12-27 18:05:52 -06:00
buffer first commit 2025-12-26 11:36:20 -06:00
builder Bring up to parity with C++ packet struct functionality 2025-12-26 13:46:42 -06:00
packet Simplify string handling: remove wrapper type support 2025-12-27 12:03:24 -06:00
parser first commit 2025-12-26 11:36:20 -06:00
xml some indentation in XML 2025-12-27 18:05:52 -06:00
.gitignore Bring up to parity with C++ packet struct functionality 2025-12-26 13:46:42 -06:00
go.mod Bring up to parity with C++ packet struct functionality 2025-12-26 13:46:42 -06:00
README.md interface to any 2025-12-27 11:35:11 -06:00

PacketStruct

This package is the packet structure reader and serde for the special XML format the emulator uses. Meticulously engineered to be byte-for-byte compatible with the C++ implementation.

Current features:

  • Parse XML packet definitions
  • Serialize Go data structures to binary EQ2 packets
  • Deserialize binary packets back to Go structures
  • Support versioned packet definitions for different client versions
  • Handle complex features like arrays, conditional fields, and nested structures

Meh

Personally, not a big fan of the current XML-based versioned packet structure. I spent a lot of time trying to figure out how or whether it was even possible to modify it, but aside from cleaning up the syntax and inconsistencies I couldn't come up with anything. Therefore, this is going to be as hyper-performant as I can make it, but does not reflect my personal taste.

Package Structure

packetstruct/
├── buffer/          Binary I/O operations (little-endian)
├── builder/         XML parsing and template management
├── packet/          Core packet structure and serialization
└── parser/          SAX-style XML parser

Quick Start

Loading Packet Definitions

import "git.sharkk.net/eq2go/packetstruct/builder"

// Create a builder and load XML definitions
sb := builder.NewStructBuilder()
err := sb.ProcessXMLFile("metadata/TestStructs.xml")
if err != nil {
    log.Fatal(err)
}

// Get a packet structure for a specific client version
ps, err := sb.GetStruct("LS_LoginReply", 1096)
if err != nil {
    log.Fatal(err)
}

Setting Field Values

// Set individual fields
ps.SetDataByName("session_id", uint32(12345))
ps.SetDataByName("server_name", "TestServer")
ps.SetDataByName("player_level", uint8(50))

// Set array data
ps.SetArrayDataByName("item_id", int32(1001), 0)  // First element
ps.SetArrayDataByName("item_id", int32(1002), 1)  // Second element

Serializing to Binary

// Serialize to bytes
data, err := ps.Serialize()
if err != nil {
    log.Fatal(err)
}

// Send over network
conn.Write(data)

Deserializing from Binary

// Receive packet data
data := make([]byte, 1024)
n, _ := conn.Read(data)

// Get packet structure template
ps, _ := sb.GetStruct("LS_LoginReply", 1096)

// Deserialize
err := ps.LoadPacketData(data[:n])
if err != nil {
    log.Fatal(err)
}

// Read field values
sessionID, _ := ps.GetUint32ByName("session_id")
serverName, _ := ps.GetStringByName("server_name")

XML Packet Definitions

Basic Structure

<Struct Name="LS_LoginReply" ClientVersion="1096" OpcodeName="LS_LoginReply">
    <Data ElementName="session_id" Type="int32" />
    <Data ElementName="server_name" Type="EQ2_16BitString" />
    <Data ElementName="player_level" Type="int8" />
</Struct>

Arrays

Arrays require a size variable and element template:

<Struct Name="ItemList" ClientVersion="1">
    <Data ElementName="item_count" Type="int8" />
    <Data ElementName="items" Type="Array" ArraySize="item_count">
        <Data ElementName="item_id" Type="int32" />
        <Data ElementName="item_name" Type="EQ2_16BitString" />
        <Data ElementName="quantity" Type="int16" />
    </Data>
</Struct>

Conditional Fields

Fields can be conditionally included based on other field values:

<Data ElementName="has_guild" Type="int8" />
<Data ElementName="guild_name" Type="EQ2_16BitString" IfSet="has_guild" />

Supported conditionals:

  • IfSet - Include if variable is set
  • IfNotSet - Include if variable is not set
  • IfEquals - Include if variable equals value
  • IfNotEquals - Include if variable doesn't equal value
  • IfFlagSet - Include if flag is set
  • IfFlagNotSet - Include if flag is not set

Oversized Encoding

For variable-length integer encoding:

<Data ElementName="value" Type="int16" Size="255" OversizedByte="0xFF" />

This writes 1 byte if value < 255, otherwise writes marker byte (0xFF) + 2-byte value.

Nested Structures

Reference other packet definitions as substructs:

<Data ElementName="player_position" Type="int8" Substruct="Position_Struct" />

Supported Types

All types from the types package:

  • Integers: int8, int16, int32, int64
  • Signed: sint8, sint16, sint32, sint64
  • Floats: float, double
  • Strings: EQ2_8BitString, EQ2_16BitString, EQ2_32BitString
  • Special: char, color, equipment, array, item

Version Management

The package supports multiple versions of the same packet structure:

// Get best matching version for client
ps, err := sb.GetStruct("LS_LoginReply", 1096)

// Check available versions
version, err := sb.GetVersion("LS_LoginReply", 1096)

Version selection follows "latest compatible" logic - finds the highest version ≤ requested version.

Thread Safety

  • StructBuilder is thread-safe for concurrent reads (GetStruct)
  • Individual PacketStruct instances should not be shared across goroutines
  • Clone packets for concurrent use: ps2 := ps.Clone()

Advanced Features

Custom Field Metadata

ds := packet.NewDataStruct()
ds.SetName("player_health")
ds.SetDataType(types.DataStructInt32)
ds.SetDefaultValue(100)
ds.SetOversized(32767)
ds.SetOversizedByte(0xFF)
ps.AddDataStruct(ds)

Array Templates

// Create array element template
template := packet.NewPacketStruct()
template.SetSubPacket(true)
idField := packet.NewDataStruct()
idField.SetName("id")
idField.SetDataType(types.DataStructInt32)
template.AddDataStruct(idField)

// Create array field
arrayField := packet.NewDataStruct()
arrayField.SetName("items")
arrayField.SetDataType(types.DataStructArray)
arrayField.SetArraySizeVariable("count")
arrayField.SetArrayTemplate(template)
ps.AddDataStruct(arrayField)

Flags and Conditionals

// Add flag
ps.AddFlag("has_advanced_stats")

// Check flag
if ps.HasFlag("has_advanced_stats") {
    // Include advanced fields
}

Testing

Run the complete test suite:

cd go/packetstruct
go test ./...

Run comparison tests against C++ implementation:

cd test-harness
./compare.sh

This verifies byte-for-byte compatibility with the C++ emulator.

Performance

  • Zero-copy deserialization where possible
  • Efficient binary I/O with buffered operations
  • Template-based packet creation minimizes allocations
  • Concurrent reads from StructBuilder cache

Examples

See packet/packet_test.go for comprehensive examples including:

  • Simple field serialization
  • Array handling
  • Oversized encoding
  • Multiple type handling
  • Round-trip tests

Compatibility

Fully compatible with the C++ EQ2 emulator packet system. All serialization output matches byte-for-byte with the C++ implementation.

Dependencies

  • git.sharkk.net/eq2go/types - EQ2 type definitions
  • Go 1.25.4 or later

License

Part of the EQ2 Emulator project.