diff --git a/structs/PARSER.md b/structs/PARSER.md
deleted file mode 100644
index 8d2e394..0000000
--- a/structs/PARSER.md
+++ /dev/null
@@ -1,284 +0,0 @@
-# Packet Definition Parser
-
-Fast XML-like parser for binary packet structures with versioning and conditional fields.
-
-## Basic Syntax
-
-```xml
-
-
-
-
-
-
-
-```
-
-## Field Types
-
-| Type | Size | Description |
-|------|------|-------------|
-| `u8`, `u16`, `u32`, `u64` | 1-8 bytes | Unsigned integers |
-| `i8`, `i16`, `i32`, `i64` | 1-8 bytes | Signed integers |
-| `f32`, `f64`, `double` | 4-8 bytes | Floating point |
-| `str8`, `str16`, `str32` | Variable | Length-prefixed strings |
-| `char` | Fixed | Fixed-size byte array |
-| `color` | 3 bytes | RGB color (r,g,b) |
-| `equip` | 8 bytes | Equipment item |
-| `array` | Variable | Array of substructures |
-
-## Multiple Field Names
-
-```xml
-
-
-```
-
-## Conditional Fields
-
-```xml
-
-
-
-
-```
-
-### Condition Types
-
-**Flag Conditions:**
-- `flag:name` - Flag is set
-- `!flag:name` - Flag not set
-
-**Variable Conditions:**
-- `var:name` - Variable exists and is non-zero
-- `!var:name` - Variable doesn't exist or is zero
-
-**Version Conditions:**
-- `version>=562` - Version comparisons
-- `version<1200` - Supports `>=`, `<=`, `>`, `<`
-
-**Value Comparisons:**
-- `field>=value` - Numeric comparisons
-- `field!=0` - Supports `>=`, `<=`, `>`, `<`, `==`, `!=`
-
-**String Length:**
-- `name!>5` - String longer than 5 chars
-- `name!<=100` - String 100 chars or less
-- Supports `!>`, `!<`, `!>=`, `!<=`, `!=`
-
-**Bitwise Operations:**
-- `field&0x01` - Bitwise AND with hex value
-
-**Complex Logic:**
-- `cond1,cond2` - OR logic (comma-separated)
-- `cond1&cond2` - AND logic (ampersand)
-- `version>=562&level>10` - Multiple conditions
-
-**Array Context:**
-- `item_type_%i!=0` - `%i` substitutes current array index
-
-## Groups
-
-Organize related fields with automatic prefixing:
-
-```xml
-
-
-
-
-
-
-```
-
-## Templates
-
-Define reusable field groups that can be injected into packets:
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-Templates work with groups for prefixing:
-
-```xml
-
-
-
-
-```
-
-## Arrays
-
-```xml
-
-
-
-
-
-```
-
-## Advanced Field Attributes
-
-### Type Switching
-```xml
-
-```
-
-### Oversized Fields
-```xml
-
-
-```
-
-### Field Modifiers
-```xml
-
-
-
-```
-
-## Complete Attribute Reference
-
-| Attribute | Description | Example |
-|-----------|-------------|---------|
-| `name` | Field name(s), comma-separated | `"id,account_id"` |
-| `use` | Template name to inject | `"position"` |
-| `if` | Conditional parsing expression | `"flag:has_guild"` |
-| `size` | Fixed array size for `char` type | `"10"` |
-| `count` | Array size variable | `"var:item_count"` |
-| `substruct` | Reference to substruct | `"ItemInfo"` |
-| `oversized` | Threshold for oversized handling | `"255"` |
-| `type2` | Alternative field type | `"f32"` |
-| `type2_if` | Condition for using type2 | `"stat_type!=6"` |
-| `default` | Default value for initialization | `"0"` |
-| `max_size` | Maximum array size limit | `"100"` |
-| `optional` | Field is optional | `"true"` |
-| `add_to_struct` | Include in packet structure | `"false"` |
-| `add_type` | Type when adding to packet | `"i16"` |
-
-## Reusable Substructs
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-## Multiple Versions
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-```
-
-## Comments
-
-```xml
-
-
-
-
-
-
-```
-
-## Usage
-
-```go
-import "eq2emu/internal/parser"
-
-// Parse PML content
-packets, err := parser.Parse(pmlContent)
-if err != nil {
- log.Fatal(err)
-}
-
-// Get packet definition
-packet := packets["PacketName"]
-
-// Parse binary data with version and flags
-result, err := packet.Parse(data, version, flags)
-if err != nil {
- log.Fatal(err)
-}
-
-// Access parsed fields
-playerID := result["player_id"].(uint32)
-playerName := result["player_name"].(common.EQ2String16).Data
-```
-
-## Complete Example
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
\ No newline at end of file
diff --git a/structs/builder.go b/structs/builder.go
deleted file mode 100644
index 8f17cf4..0000000
--- a/structs/builder.go
+++ /dev/null
@@ -1,454 +0,0 @@
-package packets
-
-import (
- "bytes"
- "encoding/binary"
- "eq2emu/internal/common"
- "eq2emu/internal/packets/parser"
- "fmt"
- "math"
-)
-
-// PacketBuilder constructs packets from data using parsed packet definitions
-type PacketBuilder struct {
- def *parser.PacketDef
- version uint32
- flags uint64
- buf *bytes.Buffer
-}
-
-// NewPacketBuilder creates a new packet builder for the given packet definition
-func NewPacketBuilder(def *parser.PacketDef, version uint32, flags uint64) *PacketBuilder {
- return &PacketBuilder{
- def: def,
- version: version,
- flags: flags,
- buf: new(bytes.Buffer),
- }
-}
-
-// Build constructs a packet from the provided data map
-func (b *PacketBuilder) Build(data map[string]any) ([]byte, error) {
- b.buf.Reset()
-
- err := b.buildStruct(data, b.def)
- if err != nil {
- return nil, err
- }
-
- return b.buf.Bytes(), nil
-}
-
-// buildStruct builds a struct according to the packet definition field order
-func (b *PacketBuilder) buildStruct(data map[string]any, def *parser.PacketDef) error {
- order := b.getVersionOrder(def)
-
- for _, fieldName := range order {
- field, exists := def.Fields[fieldName]
- if !exists {
- continue
- }
-
- // Skip fields based on conditions
- if !b.checkCondition(field.Condition, data) {
- continue
- }
-
- fieldType := field.Type
- if field.Type2 != 0 && b.checkCondition(field.Type2Cond, data) {
- fieldType = field.Type2
- }
-
- value, hasValue := data[fieldName]
- if !hasValue && !field.Optional {
- return fmt.Errorf("required field '%s' not found in data", fieldName)
- }
-
- if hasValue {
- err := b.buildField(value, field, fieldType, fieldName)
- if err != nil {
- return fmt.Errorf("error building field '%s': %w", fieldName, err)
- }
- } else if field.Optional {
- // Write default value for optional fields
- err := b.writeDefaultValue(field, fieldType)
- if err != nil {
- return fmt.Errorf("error writing default value for field '%s': %w", fieldName, err)
- }
- }
- }
-
- return nil
-}
-
-// buildField writes a single field to the packet buffer
-func (b *PacketBuilder) buildField(value any, field parser.FieldDesc, fieldType common.EQ2DataType, fieldName string) error {
- switch fieldType {
- case common.TypeInt8:
- v, ok := value.(uint8)
- if !ok {
- return fmt.Errorf("field '%s' expected uint8, got %T", fieldName, value)
- }
- if field.Oversized > 0 {
- return b.writeOversizedUint8(v, field.Oversized)
- }
- return binary.Write(b.buf, binary.LittleEndian, v)
-
- case common.TypeInt16:
- v, ok := value.(uint16)
- if !ok {
- return fmt.Errorf("field '%s' expected uint16, got %T", fieldName, value)
- }
- if field.Oversized > 0 {
- return b.writeOversizedUint16(v, field.Oversized)
- }
- return binary.Write(b.buf, binary.LittleEndian, v)
-
- case common.TypeInt32:
- v, ok := value.(uint32)
- if !ok {
- return fmt.Errorf("field '%s' expected uint32, got %T", fieldName, value)
- }
- if field.Oversized > 0 {
- return b.writeOversizedUint32(v, field.Oversized)
- }
- return binary.Write(b.buf, binary.LittleEndian, v)
-
- case common.TypeInt64:
- v, ok := value.(uint64)
- if !ok {
- return fmt.Errorf("field '%s' expected uint64, got %T", fieldName, value)
- }
- return binary.Write(b.buf, binary.LittleEndian, v)
-
- case common.TypeSInt8:
- v, ok := value.(int8)
- if !ok {
- return fmt.Errorf("field '%s' expected int8, got %T", fieldName, value)
- }
- return binary.Write(b.buf, binary.LittleEndian, v)
-
- case common.TypeSInt16:
- v, ok := value.(int16)
- if !ok {
- return fmt.Errorf("field '%s' expected int16, got %T", fieldName, value)
- }
- if field.Oversized > 0 {
- return b.writeOversizedSint16(v, field.Oversized)
- }
- return binary.Write(b.buf, binary.LittleEndian, v)
-
- case common.TypeSInt32:
- v, ok := value.(int32)
- if !ok {
- return fmt.Errorf("field '%s' expected int32, got %T", fieldName, value)
- }
- return binary.Write(b.buf, binary.LittleEndian, v)
-
- case common.TypeSInt64:
- v, ok := value.(int64)
- if !ok {
- return fmt.Errorf("field '%s' expected int64, got %T", fieldName, value)
- }
- return binary.Write(b.buf, binary.LittleEndian, v)
-
- case common.TypeString8:
- v, ok := value.(string)
- if !ok {
- return fmt.Errorf("field '%s' expected string, got %T", fieldName, value)
- }
- return b.writeEQ2String8(v)
-
- case common.TypeString16:
- v, ok := value.(string)
- if !ok {
- return fmt.Errorf("field '%s' expected string, got %T", fieldName, value)
- }
- return b.writeEQ2String16(v)
-
- case common.TypeString32:
- v, ok := value.(string)
- if !ok {
- return fmt.Errorf("field '%s' expected string, got %T", fieldName, value)
- }
- return b.writeEQ2String32(v)
-
- case common.TypeChar:
- v, ok := value.([]byte)
- if !ok {
- return fmt.Errorf("field '%s' expected []byte, got %T", fieldName, value)
- }
- return b.writeBytes(v, field.Length)
-
- case common.TypeFloat:
- v, ok := value.(float32)
- if !ok {
- return fmt.Errorf("field '%s' expected float32, got %T", fieldName, value)
- }
- return binary.Write(b.buf, binary.LittleEndian, v)
-
- case common.TypeDouble:
- v, ok := value.(float64)
- if !ok {
- return fmt.Errorf("field '%s' expected float64, got %T", fieldName, value)
- }
- return binary.Write(b.buf, binary.LittleEndian, v)
-
- case common.TypeColor:
- v, ok := value.(common.EQ2Color)
- if !ok {
- return fmt.Errorf("field '%s' expected EQ2Color, got %T", fieldName, value)
- }
- return b.writeEQ2Color(v)
-
- case common.TypeEquipment:
- v, ok := value.(common.EQ2EquipmentItem)
- if !ok {
- return fmt.Errorf("field '%s' expected EQ2EquipmentItem, got %T", fieldName, value)
- }
- return b.writeEQ2Equipment(v)
-
- case common.TypeArray:
- v, ok := value.([]map[string]any)
- if !ok {
- return fmt.Errorf("field '%s' expected []map[string]any, got %T", fieldName, value)
- }
- return b.buildArray(v, field)
-
- default:
- return fmt.Errorf("unsupported field type %d for field '%s'", fieldType, fieldName)
- }
-}
-
-// buildArray builds an array field
-func (b *PacketBuilder) buildArray(items []map[string]any, field parser.FieldDesc) error {
- if field.SubDef == nil {
- return fmt.Errorf("array field missing sub-definition")
- }
-
- size := len(items)
- if field.MaxArraySize > 0 && size > field.MaxArraySize {
- size = field.MaxArraySize
- }
-
- for i := 0; i < size; i++ {
- err := b.buildStruct(items[i], field.SubDef)
- if err != nil {
- return fmt.Errorf("error building array item %d: %w", i, err)
- }
- }
-
- return nil
-}
-
-// Helper methods for writing specific data types
-func (b *PacketBuilder) writeEQ2String8(s string) error {
- data := []byte(s)
- size := len(data)
- if size > math.MaxUint8 {
- size = math.MaxUint8
- }
-
- err := binary.Write(b.buf, binary.LittleEndian, uint8(size))
- if err != nil {
- return err
- }
-
- _, err = b.buf.Write(data[:size])
- return err
-}
-
-func (b *PacketBuilder) writeEQ2String16(s string) error {
- data := []byte(s)
- size := len(data)
- if size > math.MaxUint16 {
- size = math.MaxUint16
- }
-
- err := binary.Write(b.buf, binary.LittleEndian, uint16(size))
- if err != nil {
- return err
- }
-
- _, err = b.buf.Write(data[:size])
- return err
-}
-
-func (b *PacketBuilder) writeEQ2String32(s string) error {
- data := []byte(s)
- size := len(data)
- if size > math.MaxUint32 {
- size = math.MaxUint32
- }
-
- err := binary.Write(b.buf, binary.LittleEndian, uint32(size))
- if err != nil {
- return err
- }
-
- _, err = b.buf.Write(data[:size])
- return err
-}
-
-func (b *PacketBuilder) writeBytes(data []byte, length int) error {
- if length > 0 && len(data) > length {
- data = data[:length]
- }
-
- _, err := b.buf.Write(data)
- return err
-}
-
-func (b *PacketBuilder) writeEQ2Color(color common.EQ2Color) error {
- err := binary.Write(b.buf, binary.LittleEndian, color.Red)
- if err != nil {
- return err
- }
- err = binary.Write(b.buf, binary.LittleEndian, color.Green)
- if err != nil {
- return err
- }
- return binary.Write(b.buf, binary.LittleEndian, color.Blue)
-}
-
-func (b *PacketBuilder) writeEQ2Equipment(item common.EQ2EquipmentItem) error {
- err := binary.Write(b.buf, binary.LittleEndian, item.Type)
- if err != nil {
- return err
- }
- err = b.writeEQ2Color(item.Color)
- if err != nil {
- return err
- }
- return b.writeEQ2Color(item.Highlight)
-}
-
-func (b *PacketBuilder) writeOversizedUint8(value uint8, threshold int) error {
- if int(value) >= threshold {
- err := binary.Write(b.buf, binary.LittleEndian, uint8(255))
- if err != nil {
- return err
- }
- return binary.Write(b.buf, binary.LittleEndian, uint16(value))
- }
- return binary.Write(b.buf, binary.LittleEndian, value)
-}
-
-func (b *PacketBuilder) writeOversizedUint16(value uint16, threshold int) error {
- if int(value) >= threshold {
- err := binary.Write(b.buf, binary.LittleEndian, uint16(65535))
- if err != nil {
- return err
- }
- return binary.Write(b.buf, binary.LittleEndian, uint32(value))
- }
- return binary.Write(b.buf, binary.LittleEndian, value)
-}
-
-func (b *PacketBuilder) writeOversizedUint32(value uint32, threshold int) error {
- if int64(value) >= int64(threshold) {
- err := binary.Write(b.buf, binary.LittleEndian, uint32(4294967295))
- if err != nil {
- return err
- }
- return binary.Write(b.buf, binary.LittleEndian, uint64(value))
- }
- return binary.Write(b.buf, binary.LittleEndian, value)
-}
-
-func (b *PacketBuilder) writeOversizedSint16(value int16, threshold int) error {
- if int(value) >= threshold {
- err := binary.Write(b.buf, binary.LittleEndian, int16(-1))
- if err != nil {
- return err
- }
- return binary.Write(b.buf, binary.LittleEndian, int32(value))
- }
- return binary.Write(b.buf, binary.LittleEndian, value)
-}
-
-func (b *PacketBuilder) writeDefaultValue(field parser.FieldDesc, fieldType common.EQ2DataType) error {
- switch fieldType {
- case common.TypeInt8:
- return binary.Write(b.buf, binary.LittleEndian, uint8(field.DefaultValue))
- case common.TypeInt16:
- return binary.Write(b.buf, binary.LittleEndian, uint16(field.DefaultValue))
- case common.TypeInt32:
- return binary.Write(b.buf, binary.LittleEndian, uint32(field.DefaultValue))
- case common.TypeInt64:
- return binary.Write(b.buf, binary.LittleEndian, uint64(field.DefaultValue))
- case common.TypeSInt8:
- return binary.Write(b.buf, binary.LittleEndian, field.DefaultValue)
- case common.TypeSInt16:
- return binary.Write(b.buf, binary.LittleEndian, int16(field.DefaultValue))
- case common.TypeSInt32:
- return binary.Write(b.buf, binary.LittleEndian, int32(field.DefaultValue))
- case common.TypeSInt64:
- return binary.Write(b.buf, binary.LittleEndian, int64(field.DefaultValue))
- case common.TypeString8:
- return b.writeEQ2String8("")
- case common.TypeString16:
- return b.writeEQ2String16("")
- case common.TypeString32:
- return b.writeEQ2String32("")
- case common.TypeFloat:
- return binary.Write(b.buf, binary.LittleEndian, float32(field.DefaultValue))
- case common.TypeDouble:
- return binary.Write(b.buf, binary.LittleEndian, float64(field.DefaultValue))
- case common.TypeColor:
- color := common.EQ2Color{Red: field.DefaultValue, Green: field.DefaultValue, Blue: field.DefaultValue}
- return b.writeEQ2Color(color)
- case common.TypeChar:
- if field.Length > 0 {
- defaultBytes := make([]byte, field.Length)
- return b.writeBytes(defaultBytes, field.Length)
- }
- }
- return nil
-}
-
-func (b *PacketBuilder) getVersionOrder(def *parser.PacketDef) []string {
- var bestVersion uint32
- for v := range def.Orders {
- if v <= b.version && v > bestVersion {
- bestVersion = v
- }
- }
- return def.Orders[bestVersion]
-}
-
-func (b *PacketBuilder) checkCondition(condition string, data map[string]any) bool {
- if condition == "" {
- return true
- }
-
- // Simple condition evaluation - this would need to be expanded
- // for complex expressions used in the packet definitions
- if value, exists := data[condition]; exists {
- switch v := value.(type) {
- case bool:
- return v
- case int, int8, int16, int32, int64:
- return v != 0
- case uint, uint8, uint16, uint32, uint64:
- return v != 0
- case string:
- return v != ""
- default:
- return true
- }
- }
-
- return false
-}
-
-// BuildPacket is a convenience function that creates a builder and builds a packet in one call
-func BuildPacket(packetName string, data map[string]any, version uint32, flags uint64) ([]byte, error) {
- def, exists := GetPacket(packetName)
- if !exists {
- return nil, fmt.Errorf("packet definition '%s' not found", packetName)
- }
-
- builder := NewPacketBuilder(def, version, flags)
- return builder.Build(data)
-}
diff --git a/structs/builder_test.go b/structs/builder_test.go
deleted file mode 100644
index e4dbbb9..0000000
--- a/structs/builder_test.go
+++ /dev/null
@@ -1,173 +0,0 @@
-package packets
-
-import (
- "eq2emu/internal/common"
- "eq2emu/internal/packets/parser"
- "testing"
-)
-
-func TestPacketBuilderSimpleFields(t *testing.T) {
- // Create a simple packet definition for testing
- def := parser.NewPacketDef(5)
-
- // Add some basic fields
- def.Fields["field1"] = parser.FieldDesc{
- Type: common.TypeInt8,
- }
- def.Fields["field2"] = parser.FieldDesc{
- Type: common.TypeString8,
- }
- def.Fields["field3"] = parser.FieldDesc{
- Type: common.TypeInt32,
- }
-
- // Set field order
- def.Orders[1] = []string{"field1", "field2", "field3"}
-
- // Create test data
- data := map[string]any{
- "field1": uint8(42),
- "field2": "hello",
- "field3": uint32(12345),
- }
-
- // Build packet
- builder := NewPacketBuilder(def, 1, 0)
- packetData, err := builder.Build(data)
- if err != nil {
- t.Errorf("Failed to build simple packet: %v", err)
- return
- }
-
- expectedMinLength := 1 + 1 + 5 + 4 // uint8 + string8(len+data) + uint32
- if len(packetData) < expectedMinLength {
- t.Errorf("Built packet too short: got %d bytes, expected at least %d", len(packetData), expectedMinLength)
- return
- }
-
- t.Logf("Successfully built simple packet (%d bytes)", len(packetData))
-
- // Verify the first byte is correct
- if packetData[0] != 42 {
- t.Errorf("First field incorrect: got %d, expected 42", packetData[0])
- }
-}
-
-func TestPacketBuilderOptionalFields(t *testing.T) {
- // Create a packet definition with optional fields
- def := parser.NewPacketDef(3)
-
- def.Fields["required"] = parser.FieldDesc{
- Type: common.TypeInt8,
- }
- def.Fields["optional"] = parser.FieldDesc{
- Type: common.TypeInt8,
- Optional: true,
- }
-
- def.Orders[1] = []string{"required", "optional"}
-
- // Test with only required field
- data := map[string]any{
- "required": uint8(123),
- }
-
- builder := NewPacketBuilder(def, 1, 0)
- packetData, err := builder.Build(data)
- if err != nil {
- t.Errorf("Failed to build packet with optional fields: %v", err)
- return
- }
-
- expectedLength := 2 // required + optional default
- if len(packetData) != expectedLength {
- t.Errorf("Built packet wrong length: got %d bytes, expected %d", len(packetData), expectedLength)
- return
- }
-
- t.Logf("Successfully built packet with optional fields (%d bytes)", len(packetData))
-}
-
-func TestPacketBuilderMissingRequiredField(t *testing.T) {
- // Create a packet definition
- def := parser.NewPacketDef(2)
-
- def.Fields["required"] = parser.FieldDesc{
- Type: common.TypeInt8,
- }
- def.Fields["also_required"] = parser.FieldDesc{
- Type: common.TypeInt16,
- }
-
- def.Orders[1] = []string{"required", "also_required"}
-
- // Test with missing required field
- data := map[string]any{
- "required": uint8(123),
- // missing "also_required"
- }
-
- builder := NewPacketBuilder(def, 1, 0)
- _, err := builder.Build(data)
- if err == nil {
- t.Error("Expected error for missing required field, but got none")
- return
- }
-
- t.Logf("Correctly detected missing required field: %v", err)
-}
-
-func TestPacketBuilderStringTypes(t *testing.T) {
- // Test different string types
- def := parser.NewPacketDef(3)
-
- def.Fields["str8"] = parser.FieldDesc{
- Type: common.TypeString8,
- }
- def.Fields["str16"] = parser.FieldDesc{
- Type: common.TypeString16,
- }
- def.Fields["str32"] = parser.FieldDesc{
- Type: common.TypeString32,
- }
-
- def.Orders[1] = []string{"str8", "str16", "str32"}
-
- data := map[string]any{
- "str8": "test8",
- "str16": "test16",
- "str32": "test32",
- }
-
- builder := NewPacketBuilder(def, 1, 0)
- packetData, err := builder.Build(data)
- if err != nil {
- t.Errorf("Failed to build packet with strings: %v", err)
- return
- }
-
- // Verify we have some data
- if len(packetData) < 20 { // rough estimate
- t.Errorf("Built packet too short for string data: %d bytes", len(packetData))
- return
- }
-
- t.Logf("Successfully built packet with strings (%d bytes)", len(packetData))
-}
-
-func TestBuildPacketConvenienceFunction(t *testing.T) {
- // Test the convenience function
- data := map[string]any{
- "username": "testuser",
- "password": "testpass",
- }
-
- // This should fail since we don't have a "TestPacket" defined
- _, err := BuildPacket("NonExistentPacket", data, 1, 0)
- if err == nil {
- t.Error("Expected error for non-existent packet, but got none")
- return
- }
-
- t.Logf("Correctly detected non-existent packet: %v", err)
-}
diff --git a/structs/loader.go b/structs/loader.go
deleted file mode 100644
index acb86b1..0000000
--- a/structs/loader.go
+++ /dev/null
@@ -1,146 +0,0 @@
-package packets
-
-import (
- "embed"
- "eq2emu/internal/packets/parser"
- "fmt"
- "log"
- "path"
- "strings"
- "sync"
-)
-
-//go:embed xml/**/*.xml
-var xmlFiles embed.FS
-
-var (
- loadedPackets = make(map[string]*parser.PacketDef)
- loadedMutex sync.RWMutex
- loadError error
-)
-
-func init() {
- err := loadAllPackets()
- if err != nil {
- loadError = err
- log.Printf("Failed to load packet definitions: %v", err)
- }
-}
-
-func loadAllPackets() error {
- entries, err := xmlFiles.ReadDir("xml")
- if err != nil {
- return fmt.Errorf("failed to read xml directory: %w", err)
- }
-
- packets := make(map[string]*parser.PacketDef)
-
- for _, entry := range entries {
- if !entry.IsDir() {
- continue
- }
-
- dirPath := path.Join("xml", entry.Name())
- err := processDirectory(dirPath, packets)
- if err != nil {
- return fmt.Errorf("failed to process directory %s: %w", entry.Name(), err)
- }
- }
-
- loadedMutex.Lock()
- loadedPackets = packets
- loadedMutex.Unlock()
-
- log.Printf("Loaded %d packet definitions", len(packets))
- return nil
-}
-
-func processDirectory(dirPath string, packets map[string]*parser.PacketDef) error {
- entries, err := xmlFiles.ReadDir(dirPath)
- if err != nil {
- return err
- }
-
- for _, entry := range entries {
- entryPath := path.Join(dirPath, entry.Name())
-
- if entry.IsDir() {
- err := processDirectory(entryPath, packets)
- if err != nil {
- return err
- }
- continue
- }
-
- if !strings.HasSuffix(entry.Name(), ".xml") {
- continue
- }
-
- err := processXMLFile(entryPath, packets)
- if err != nil {
- log.Printf("Warning: %s: %v", entryPath, err)
- }
- }
-
- return nil
-}
-
-func processXMLFile(filePath string, packets map[string]*parser.PacketDef) error {
- content, err := xmlFiles.ReadFile(filePath)
- if err != nil {
- return fmt.Errorf("failed to read file: %w", err)
- }
-
- parsedPackets, err := parser.Parse(string(content))
- if err != nil {
- return fmt.Errorf("failed to parse packet def: %w", err)
- }
-
- for name, packet := range parsedPackets {
- if existing, exists := packets[name]; exists {
- log.Printf("Warning: packet '%s' already exists, overwriting", name)
- _ = existing
- }
- packets[name] = packet
- }
-
- return nil
-}
-
-func GetPacket(name string) (*parser.PacketDef, bool) {
- if loadError != nil {
- return nil, false
- }
-
- loadedMutex.RLock()
- defer loadedMutex.RUnlock()
-
- packet, exists := loadedPackets[name]
- return packet, exists
-}
-
-func GetPacketNames() []string {
- if loadError != nil {
- return nil
- }
-
- loadedMutex.RLock()
- defer loadedMutex.RUnlock()
-
- names := make([]string, 0, len(loadedPackets))
- for name := range loadedPackets {
- names = append(names, name)
- }
- return names
-}
-
-func GetPacketCount() int {
- if loadError != nil {
- return 0
- }
-
- loadedMutex.RLock()
- defer loadedMutex.RUnlock()
-
- return len(loadedPackets)
-}
diff --git a/structs/opcode_manager.go b/structs/opcode_manager.go
deleted file mode 100644
index 679b6ec..0000000
--- a/structs/opcode_manager.go
+++ /dev/null
@@ -1,239 +0,0 @@
-package packets
-
-import (
- "fmt"
- "sync"
- "database/sql"
- "log"
-)
-
-// OpcodeManager manages opcode mappings for different client versions
-type OpcodeManager struct {
- versions map[int32]int32 // Maps version range start to end
- opcodes map[int32]map[string]uint16 // Maps version to opcode name->value
- internalOpcodes map[int32]map[uint16]InternalOpcode // Maps version to wire opcode->internal
- mu sync.RWMutex
-}
-
-// NewOpcodeManager creates a new opcode manager
-func NewOpcodeManager() *OpcodeManager {
- return &OpcodeManager{
- versions: make(map[int32]int32),
- opcodes: make(map[int32]map[string]uint16),
- internalOpcodes: make(map[int32]map[uint16]InternalOpcode),
- }
-}
-
-// LoadVersionsFromDB loads version ranges from the database
-func (om *OpcodeManager) LoadVersionsFromDB(db *sql.DB) error {
- om.mu.Lock()
- defer om.mu.Unlock()
-
- query := `SELECT DISTINCT version_range1, version_range2 FROM opcodes`
- rows, err := db.Query(query)
- if err != nil {
- return fmt.Errorf("failed to query versions: %w", err)
- }
- defer rows.Close()
-
- for rows.Next() {
- var start, end int32
- if err := rows.Scan(&start, &end); err != nil {
- return fmt.Errorf("failed to scan version row: %w", err)
- }
- om.versions[start] = end
- }
-
- return rows.Err()
-}
-
-// LoadOpcodesFromDB loads opcodes for all versions from the database
-func (om *OpcodeManager) LoadOpcodesFromDB(db *sql.DB) error {
- om.mu.Lock()
- defer om.mu.Unlock()
-
- for versionStart := range om.versions {
- query := `SELECT name, opcode FROM opcodes
- WHERE ? BETWEEN version_range1 AND version_range2
- ORDER BY version_range1, id`
-
- rows, err := db.Query(query, versionStart)
- if err != nil {
- return fmt.Errorf("failed to query opcodes for version %d: %w", versionStart, err)
- }
-
- opcodes := make(map[string]uint16)
- internal := make(map[uint16]InternalOpcode)
-
- for rows.Next() {
- var name string
- var opcode uint16
- if err := rows.Scan(&name, &opcode); err != nil {
- rows.Close()
- return fmt.Errorf("failed to scan opcode row: %w", err)
- }
- opcodes[name] = opcode
-
- // Map to internal opcodes
- if internalOp, ok := nameToInternalOpcode[name]; ok {
- internal[opcode] = internalOp
- }
- }
- rows.Close()
-
- om.opcodes[versionStart] = opcodes
- om.internalOpcodes[versionStart] = internal
-
- // Silent - we'll log summary later
- }
-
- log.Printf("Loaded opcodes for %d client versions from database", len(om.versions))
- return nil
-}
-
-// LoadDefaultOpcodes loads hardcoded opcodes for when database is not available
-func (om *OpcodeManager) LoadDefaultOpcodes() {
- om.mu.Lock()
- defer om.mu.Unlock()
-
- // Default version range (1119 is a common EQ2 client version)
- defaultVersion := int32(1119)
- om.versions[defaultVersion] = defaultVersion
-
- // These are the default opcodes from the C++ implementation
- opcodes := map[string]uint16{
- "OP_LoginRequestMsg": 0x0001,
- "OP_LoginByNumRequestMsg": 0x0002,
- "OP_WSLoginRequestMsg": 0x0003,
- "OP_ESLoginRequestMsg": 0x0004,
- "OP_LoginReplyMsg": 0x0005,
- "OP_WorldListMsg": 0x0006,
- "OP_WorldStatusChangeMsg": 0x0007,
- "OP_AllWSDescRequestMsg": 0x0008,
- "OP_WSStatusReplyMsg": 0x0009,
- "OP_AllCharactersDescRequestMsg": 0x000A,
- "OP_AllCharactersDescReplyMsg": 0x000B,
- "OP_CreateCharacterRequestMsg": 0x000C,
- "OP_ReskinCharacterRequestMsg": 0x000D,
- "OP_CreateCharacterReplyMsg": 0x000E,
- "OP_WSCreateCharacterRequestMsg": 0x000F,
- "OP_WSCreateCharacterReplyMsg": 0x0010,
- "OP_DeleteCharacterRequestMsg": 0x0011,
- "OP_DeleteCharacterReplyMsg": 0x0012,
- "OP_PlayCharacterRequestMsg": 0x0013,
- "OP_PlayCharacterReplyMsg": 0x0014,
- "OP_ServerPlayCharacterRequestMsg": 0x0015,
- "OP_ServerPlayCharacterReplyMsg": 0x0016,
- "OP_KeymapLoadMsg": 0x0017,
- "OP_KeymapNoneMsg": 0x0018,
- "OP_KeymapDataMsg": 0x0019,
- "OP_KeymapSaveMsg": 0x001A,
- "OP_LSCheckAcctLockMsg": 0x001B,
- "OP_WSAcctLockStatusMsg": 0x001C,
- "OP_LsRequestClientCrashLogMsg": 0x001D,
- "OP_LsClientBaselogReplyMsg": 0x001E,
- "OP_LsClientCrashlogReplyMsg": 0x001F,
- "OP_LsClientAlertlogReplyMsg": 0x0020,
- "OP_LsClientVerifylogReplyMsg": 0x0021,
- "OP_BadLanguageFilter": 0x0022,
- "OP_WSServerLockMsg": 0x0023,
- "OP_WSServerHideMsg": 0x0024,
- "OP_LSServerLockMsg": 0x0025,
- "OP_UpdateCharacterSheetMsg": 0x0026,
- "OP_UpdateInventoryMsg": 0x0027,
- }
-
- internal := make(map[uint16]InternalOpcode)
- for name, opcode := range opcodes {
- if internalOp, ok := nameToInternalOpcode[name]; ok {
- internal[opcode] = internalOp
- }
- }
-
- om.opcodes[defaultVersion] = opcodes
- om.internalOpcodes[defaultVersion] = internal
-
- log.Printf("Loaded default opcodes for version %d", defaultVersion)
-}
-
-// GetOpcodeVersion returns the version range start for a given client version
-// This implements the same logic as the C++ GetOpcodeVersion function
-func (om *OpcodeManager) GetOpcodeVersion(clientVersion int32) int32 {
- om.mu.RLock()
- defer om.mu.RUnlock()
-
- for versionStart, versionEnd := range om.versions {
- if clientVersion >= versionStart && clientVersion <= versionEnd {
- return versionStart
- }
- }
-
- // If no match found, return the client version itself
- return clientVersion
-}
-
-// GetOpcodeByName returns the wire opcode value for a given opcode name and client version
-func (om *OpcodeManager) GetOpcodeByName(clientVersion int32, name string) (uint16, bool) {
- om.mu.RLock()
- defer om.mu.RUnlock()
-
- version := om.GetOpcodeVersion(clientVersion)
- if opcodes, ok := om.opcodes[version]; ok {
- if opcode, ok := opcodes[name]; ok {
- return opcode, true
- }
- }
-
- return 0, false
-}
-
-// GetInternalOpcode converts a wire opcode to an internal opcode for a given client version
-func (om *OpcodeManager) GetInternalOpcode(clientVersion int32, wireOpcode uint16) (InternalOpcode, bool) {
- om.mu.RLock()
- defer om.mu.RUnlock()
-
- version := om.GetOpcodeVersion(clientVersion)
- if internal, ok := om.internalOpcodes[version]; ok {
- if internalOp, ok := internal[wireOpcode]; ok {
- return internalOp, true
- }
- }
-
- return OP_Unknown, false
-}
-
-// GetWireOpcode converts an internal opcode to a wire opcode for a given client version
-func (om *OpcodeManager) GetWireOpcode(clientVersion int32, internalOpcode InternalOpcode) (uint16, bool) {
- om.mu.RLock()
- defer om.mu.RUnlock()
-
- // Get the name for this internal opcode
- name := ""
- for n, op := range nameToInternalOpcode {
- if op == internalOpcode {
- name = n
- break
- }
- }
-
- if name == "" {
- return 0, false
- }
-
- return om.GetOpcodeByName(clientVersion, name)
-}
-
-// nameToInternalOpcode maps opcode names to internal opcodes
-var nameToInternalOpcode = map[string]InternalOpcode{
- "OP_LoginRequestMsg": OP_LoginRequestMsg,
- "OP_LoginByNumRequestMsg": OP_LoginByNumRequestMsg,
- "OP_WSLoginRequestMsg": OP_WSLoginRequestMsg,
- "OP_LoginReplyMsg": OP_LoginReplyMsg,
- "OP_PlayCharacterRequestMsg": OP_PlayCharacterRequest,
- "OP_CreateCharacterRequestMsg": OP_CharacterCreate,
- "OP_DeleteCharacterRequestMsg": OP_CharacterDelete,
- "OP_AllCharactersDescRequestMsg": OP_CharacterList,
- "OP_WorldListMsg": OP_ServerList,
- "OP_WorldStatusChangeMsg": OP_SendServerStatus,
- // Add more mappings as needed
-}
\ No newline at end of file
diff --git a/structs/opcodes.go b/structs/opcodes.go
deleted file mode 100644
index cf9aea1..0000000
--- a/structs/opcodes.go
+++ /dev/null
@@ -1,162 +0,0 @@
-package packets
-
-// InternalOpcode represents the internal opcode enumeration
-type InternalOpcode int32
-
-// Internal opcode constants - these map to the C++ EmuOpcode enum
-const (
- OP_Unknown InternalOpcode = iota
-
- // Login server specific opcodes (from C++ LoginServer)
- OP_Login2
- OP_GetLoginInfo
- OP_LoginInfo
- OP_SessionId
- OP_SessionKey
- OP_Disconnect
- OP_AllFinish
- OP_Ack5
- OP_SendServersFragment
- OP_ServerList
- OP_RequestServerStatus
- OP_SendServerStatus
- OP_Version
- OP_LoginBanner
- OP_PlayCharacterRequest
- OP_CharacterList
- OP_CharacterCreate
- OP_CharacterDelete
-
- // Login and authentication operations
- OP_LoginRequestMsg
- OP_LoginReplyMsg
- OP_LoginByNumRequestMsg
- OP_WSLoginRequestMsg
-
- // Server initialization and zone management
- OP_ESInitMsg
- OP_ESReadyForClientsMsg
- OP_CreateZoneInstanceMsg
- OP_ZoneInstanceCreateReplyMsg
- OP_ZoneInstanceDestroyedMsg
- OP_ExpectClientAsCharacterRequest
- OP_ExpectClientAsCharacterReplyMs
- OP_ZoneInfoMsg
-
- // Character creation and loading
- OP_AllCharactersDescRequestMsg
- OP_AllCharactersDescReplyMsg
- OP_CreateCharacterRequestMsg
- OP_ReskinCharacterRequestMsg
- OP_CreateCharacterReplyMsg
- OP_DeleteCharacterRequestMsg
- OP_DeleteCharacterReplyMsg
- OP_PlayCharacterRequestMsg
- OP_PlayCharacterReplyMsg
-
- // World server communication
- OP_WSCreateCharacterRequestMsg
- OP_WSCreateCharacterReplyMsg
- OP_ServerPlayCharacterRequestMsg
- OP_ServerPlayCharacterReplyMsg
- OP_ExpectClientAsCharacterRequ
- OP_ExpectClientAsCharacterReply
- OP_WorldListMsg
- OP_WorldStatusChangeMsg
- OP_AllWSDescRequestMsg
- OP_WSStatusReplyMsg
- OP_WSAcctLockStatusMsg
- OP_WSServerLockMsg
- OP_WSServerHideMsg
- OP_LSServerLockMsg
-
- // Keymap and configuration
- OP_KeymapLoadMsg
- OP_KeymapNoneMsg
- OP_KeymapDataMsg
- OP_KeymapSaveMsg
-
- // Account and security management
- OP_LSCheckAcctLockMsg
- OP_LsRequestClientCrashLogMsg
- OP_LsClientBaselogReplyMsg
- OP_LsClientCrashlogReplyMsg
- OP_LsClientAlertlogReplyMsg
- OP_LsClientVerifylogReplyMsg
- OP_BadLanguageFilter
-
- // Character sheet and inventory
- OP_UpdateCharacterSheetMsg
- OP_UpdateInventoryMsg
-)
-
-// OpcodeNames provides human-readable names for internal opcodes
-var OpcodeNames = map[InternalOpcode]string{
- OP_Unknown: "OP_Unknown",
-
- // Login server specific opcodes
- OP_Login2: "OP_Login2",
- OP_GetLoginInfo: "OP_GetLoginInfo",
- OP_LoginInfo: "OP_LoginInfo",
- OP_SessionId: "OP_SessionId",
- OP_SessionKey: "OP_SessionKey",
- OP_Disconnect: "OP_Disconnect",
- OP_AllFinish: "OP_AllFinish",
- OP_Ack5: "OP_Ack5",
- OP_SendServersFragment: "OP_SendServersFragment",
- OP_ServerList: "OP_ServerList",
- OP_RequestServerStatus: "OP_RequestServerStatus",
- OP_SendServerStatus: "OP_SendServerStatus",
- OP_Version: "OP_Version",
- OP_LoginBanner: "OP_LoginBanner",
- OP_PlayCharacterRequest: "OP_PlayCharacterRequest",
- OP_CharacterList: "OP_CharacterList",
- OP_CharacterCreate: "OP_CharacterCreate",
- OP_CharacterDelete: "OP_CharacterDelete",
-
- // Login and authentication operations
- OP_LoginRequestMsg: "OP_LoginRequestMsg",
- OP_LoginReplyMsg: "OP_LoginReplyMsg",
- OP_LoginByNumRequestMsg: "OP_LoginByNumRequestMsg",
- OP_WSLoginRequestMsg: "OP_WSLoginRequestMsg",
-
- // Character operations
- OP_AllCharactersDescRequestMsg: "OP_AllCharactersDescRequestMsg",
- OP_AllCharactersDescReplyMsg: "OP_AllCharactersDescReplyMsg",
- OP_CreateCharacterRequestMsg: "OP_CreateCharacterRequestMsg",
- OP_ReskinCharacterRequestMsg: "OP_ReskinCharacterRequestMsg",
- OP_CreateCharacterReplyMsg: "OP_CreateCharacterReplyMsg",
- OP_DeleteCharacterRequestMsg: "OP_DeleteCharacterRequestMsg",
- OP_DeleteCharacterReplyMsg: "OP_DeleteCharacterReplyMsg",
- OP_PlayCharacterRequestMsg: "OP_PlayCharacterRequestMsg",
- OP_PlayCharacterReplyMsg: "OP_PlayCharacterReplyMsg",
-
- // World server communication
- OP_WorldListMsg: "OP_WorldListMsg",
- OP_WorldStatusChangeMsg: "OP_WorldStatusChangeMsg",
- OP_AllWSDescRequestMsg: "OP_AllWSDescRequestMsg",
- OP_WSStatusReplyMsg: "OP_WSStatusReplyMsg",
- OP_WSAcctLockStatusMsg: "OP_WSAcctLockStatusMsg",
- OP_WSServerLockMsg: "OP_WSServerLockMsg",
- OP_WSServerHideMsg: "OP_WSServerHideMsg",
- OP_LSServerLockMsg: "OP_LSServerLockMsg",
-
- // Keymap operations
- OP_KeymapLoadMsg: "OP_KeymapLoadMsg",
- OP_KeymapNoneMsg: "OP_KeymapNoneMsg",
- OP_KeymapDataMsg: "OP_KeymapDataMsg",
- OP_KeymapSaveMsg: "OP_KeymapSaveMsg",
-
- // Account and security
- OP_LSCheckAcctLockMsg: "OP_LSCheckAcctLockMsg",
- OP_LsRequestClientCrashLogMsg: "OP_LsRequestClientCrashLogMsg",
- OP_LsClientBaselogReplyMsg: "OP_LsClientBaselogReplyMsg",
- OP_LsClientCrashlogReplyMsg: "OP_LsClientCrashlogReplyMsg",
- OP_LsClientAlertlogReplyMsg: "OP_LsClientAlertlogReplyMsg",
- OP_LsClientVerifylogReplyMsg: "OP_LsClientVerifylogReplyMsg",
- OP_BadLanguageFilter: "OP_BadLanguageFilter",
-
- // Character sheet and inventory
- OP_UpdateCharacterSheetMsg: "OP_UpdateCharacterSheetMsg",
- OP_UpdateInventoryMsg: "OP_UpdateInventoryMsg",
-}
\ No newline at end of file
diff --git a/structs/parser/context.go b/structs/parser/context.go
deleted file mode 100644
index 9eeb027..0000000
--- a/structs/parser/context.go
+++ /dev/null
@@ -1,447 +0,0 @@
-package parser
-
-import (
- "encoding/binary"
- "eq2emu/internal/common"
- "strconv"
- "strings"
- "unsafe"
-)
-
-type ParseContext struct {
- data []byte
- offset int
- version uint32
- flags uint64
- vars map[string]any
- arrayStack []int
-}
-
-func NewContext(data []byte, version uint32, flags uint64) *ParseContext {
- return &ParseContext{
- data: data,
- version: version,
- flags: flags,
- vars: make(map[string]any),
- }
-}
-
-// Unsigned integer readers
-func (ctx *ParseContext) readUint8() uint8 {
- val := ctx.data[ctx.offset]
- ctx.offset++
- return val
-}
-
-func (ctx *ParseContext) readUint16() uint16 {
- val := binary.LittleEndian.Uint16(ctx.data[ctx.offset:])
- ctx.offset += 2
- return val
-}
-
-func (ctx *ParseContext) readUint32() uint32 {
- val := binary.LittleEndian.Uint32(ctx.data[ctx.offset:])
- ctx.offset += 4
- return val
-}
-
-func (ctx *ParseContext) readUint64() uint64 {
- val := binary.LittleEndian.Uint64(ctx.data[ctx.offset:])
- ctx.offset += 8
- return val
-}
-
-// Signed integer readers
-func (ctx *ParseContext) readSint8() int8 {
- val := int8(ctx.data[ctx.offset])
- ctx.offset++
- return val
-}
-
-func (ctx *ParseContext) readSint16() int16 {
- val := int16(binary.LittleEndian.Uint16(ctx.data[ctx.offset:]))
- ctx.offset += 2
- return val
-}
-
-func (ctx *ParseContext) readSint32() int32 {
- val := int32(binary.LittleEndian.Uint32(ctx.data[ctx.offset:]))
- ctx.offset += 4
- return val
-}
-
-func (ctx *ParseContext) readSint64() int64 {
- val := int64(binary.LittleEndian.Uint64(ctx.data[ctx.offset:]))
- ctx.offset += 8
- return val
-}
-
-// Oversized readers
-func (ctx *ParseContext) readOversizedUint8(threshold int) uint8 {
- if ctx.data[ctx.offset] == byte(threshold) {
- ctx.offset++
- return ctx.readUint8()
- }
- return ctx.readUint8()
-}
-
-func (ctx *ParseContext) readOversizedUint16(threshold int) uint16 {
- if ctx.data[ctx.offset] == byte(threshold) {
- ctx.offset++
- return ctx.readUint16()
- }
- return uint16(ctx.readUint8())
-}
-
-func (ctx *ParseContext) readOversizedUint32(threshold int) uint32 {
- if ctx.data[ctx.offset] == byte(threshold) {
- ctx.offset++
- return ctx.readUint32()
- }
- return uint32(ctx.readUint16())
-}
-
-func (ctx *ParseContext) readOversizedSint16(threshold int) int16 {
- val := int8(ctx.data[ctx.offset])
- if val == int8(threshold) || val == int8(-threshold) {
- ctx.offset++
- return ctx.readSint16()
- }
- return int16(ctx.readSint8())
-}
-
-func (ctx *ParseContext) readEQ2String8() common.EQ2String8 {
- size := ctx.readSint8()
- data := string(ctx.data[ctx.offset : ctx.offset+int(size)])
- ctx.offset += int(size)
- return common.EQ2String8{
- Size: size,
- Data: data,
- }
-}
-
-func (ctx *ParseContext) readEQ2String16() common.EQ2String16 {
- size := ctx.readSint16()
- data := string(ctx.data[ctx.offset : ctx.offset+int(size)])
- ctx.offset += int(size)
- return common.EQ2String16{
- Size: size,
- Data: data,
- }
-}
-
-func (ctx *ParseContext) readEQ2String32() common.EQ2String32 {
- size := ctx.readSint32()
- data := string(ctx.data[ctx.offset : ctx.offset+int(size)])
- ctx.offset += int(size)
- return common.EQ2String32{
- Size: size,
- Data: data,
- }
-}
-
-func (ctx *ParseContext) readEQ2Color() common.EQ2Color {
- return common.EQ2Color{
- Red: ctx.readSint8(),
- Green: ctx.readSint8(),
- Blue: ctx.readSint8(),
- }
-}
-
-func (ctx *ParseContext) readEQ2Equipment() common.EQ2EquipmentItem {
- return common.EQ2EquipmentItem{
- Type: ctx.readUint16(),
- Color: ctx.readEQ2Color(),
- Highlight: ctx.readEQ2Color(),
- }
-}
-
-func (ctx *ParseContext) readBytes(count int) []byte {
- val := make([]byte, count)
- copy(val, ctx.data[ctx.offset:ctx.offset+count])
- ctx.offset += count
- return val
-}
-
-func (ctx *ParseContext) readFloat32() float32 {
- val := ctx.readUint32()
- return *(*float32)(unsafe.Pointer(&val))
-}
-
-func (ctx *ParseContext) readFloat64() float64 {
- val := ctx.readUint64()
- return *(*float64)(unsafe.Pointer(&val))
-}
-
-func (ctx *ParseContext) setVar(name string, value any) {
- ctx.vars[name] = value
-}
-
-func (ctx *ParseContext) pushArrayIndex(index int) {
- ctx.arrayStack = append(ctx.arrayStack, index)
-}
-
-func (ctx *ParseContext) popArrayIndex() {
- if len(ctx.arrayStack) > 0 {
- ctx.arrayStack = ctx.arrayStack[:len(ctx.arrayStack)-1]
- }
-}
-
-// Condition evaluation methods
-func (ctx *ParseContext) checkCondition(condition string) bool {
- if condition == "" {
- return true
- }
-
- // Handle comma-separated OR conditions
- if strings.Contains(condition, ",") {
- parts := strings.Split(condition, ",")
- for _, part := range parts {
- if ctx.evaluateCondition(strings.TrimSpace(part)) {
- return true
- }
- }
- return false
- }
-
- // Handle AND conditions with &
- if strings.Contains(condition, "&") && !strings.Contains(condition, "0x") {
- parts := strings.Split(condition, "&")
- for _, part := range parts {
- if !ctx.evaluateCondition(strings.TrimSpace(part)) {
- return false
- }
- }
- return true
- }
-
- return ctx.evaluateCondition(condition)
-}
-
-func (ctx *ParseContext) evaluateCondition(condition string) bool {
- // Flag conditions: flag:name or !flag:name
- if strings.HasPrefix(condition, "flag:") {
- flagName := condition[5:]
- return (ctx.flags & ctx.getFlagValue(flagName)) != 0
- }
- if strings.HasPrefix(condition, "!flag:") {
- flagName := condition[6:]
- return (ctx.flags & ctx.getFlagValue(flagName)) == 0
- }
-
- // Variable conditions: var:name or !var:name (with %i support)
- if strings.HasPrefix(condition, "var:") {
- varName := ctx.resolveVariableName(condition[4:])
- return ctx.hasVar(varName)
- }
- if strings.HasPrefix(condition, "!var:") {
- varName := ctx.resolveVariableName(condition[5:])
- return !ctx.hasVar(varName)
- }
-
- // Version comparisons
- if strings.HasPrefix(condition, "version") {
- return ctx.evaluateVersionCondition(condition)
- }
-
- // Bitwise AND: header_flag&0x01 (with %i support)
- if strings.Contains(condition, "&0x") {
- parts := strings.SplitN(condition, "&", 2)
- varName := ctx.resolveVariableName(parts[0])
- hexValue, _ := strconv.ParseUint(parts[1], 0, 64)
- varValue := ctx.getVarValue(varName)
- return (varValue & hexValue) != 0
- }
-
- // String length operators: name!>5, name!<=10 (with %i support)
- stringOps := []string{"!>=", "!<=", "!>", "!<", "!="}
- for _, op := range stringOps {
- if idx := strings.Index(condition, op); idx > 0 {
- varName := ctx.resolveVariableName(condition[:idx])
- valueStr := condition[idx+len(op):]
- return ctx.evaluateStringLength(varName, valueStr, op)
- }
- }
-
- // Comparison operators: >=, <=, >, <, ==, != (with %i support)
- compOps := []string{">=", "<=", ">", "<", "==", "!="}
- for _, op := range compOps {
- if idx := strings.Index(condition, op); idx > 0 {
- varName := ctx.resolveVariableName(condition[:idx])
- valueStr := condition[idx+len(op):]
- return ctx.evaluateComparison(varName, valueStr, op)
- }
- }
-
- // Simple variable existence (with %i support)
- resolvedName := ctx.resolveVariableName(condition)
- return ctx.hasVar(resolvedName)
-}
-
-func (ctx *ParseContext) evaluateVersionCondition(condition string) bool {
- if strings.Contains(condition, ">=") {
- versionStr := condition[strings.Index(condition, ">=")+2:]
- targetVersion, _ := strconv.ParseUint(versionStr, 10, 32)
- return ctx.version >= uint32(targetVersion)
- }
- if strings.Contains(condition, "<=") {
- versionStr := condition[strings.Index(condition, "<=")+2:]
- targetVersion, _ := strconv.ParseUint(versionStr, 10, 32)
- return ctx.version <= uint32(targetVersion)
- }
- if strings.Contains(condition, ">") {
- versionStr := condition[strings.Index(condition, ">")+1:]
- targetVersion, _ := strconv.ParseUint(versionStr, 10, 32)
- return ctx.version > uint32(targetVersion)
- }
- if strings.Contains(condition, "<") {
- versionStr := condition[strings.Index(condition, "<")+1:]
- targetVersion, _ := strconv.ParseUint(versionStr, 10, 32)
- return ctx.version < uint32(targetVersion)
- }
- return false
-}
-
-func (ctx *ParseContext) evaluateStringLength(varName, valueStr, op string) bool {
- resolvedName := ctx.resolveVariableName(varName)
- str := ctx.getStringVar(resolvedName)
- targetLen, _ := strconv.Atoi(valueStr)
- strLen := len(str)
-
- switch op {
- case "!>":
- return strLen > targetLen
- case "!<":
- return strLen < targetLen
- case "!>=":
- return strLen >= targetLen
- case "!<=":
- return strLen <= targetLen
- case "!=":
- return strLen != targetLen
- }
- return false
-}
-
-func (ctx *ParseContext) evaluateComparison(varName, valueStr, op string) bool {
- resolvedName := ctx.resolveVariableName(varName)
- varValue := ctx.getVarValue(resolvedName)
- targetValue, _ := strconv.ParseUint(valueStr, 0, 64)
-
- switch op {
- case ">=":
- return varValue >= targetValue
- case "<=":
- return varValue <= targetValue
- case ">":
- return varValue > targetValue
- case "<":
- return varValue < targetValue
- case "==":
- return varValue == targetValue
- case "!=":
- return varValue != targetValue
- }
- return false
-}
-
-func (ctx *ParseContext) hasVar(name string) bool {
- if val, exists := ctx.vars[name]; exists {
- switch v := val.(type) {
- case uint8, uint16, uint32, uint64, int8, int16, int32, int64:
- return ctx.getVarValue(name) != 0
- case string:
- return v != ""
- case common.EQ2String8:
- return v.Data != ""
- case common.EQ2String16:
- return v.Data != ""
- case common.EQ2String32:
- return v.Data != ""
- default:
- return true
- }
- }
- return false
-}
-
-func (ctx *ParseContext) getVarValue(name string) uint64 {
- if val, exists := ctx.vars[name]; exists {
- switch v := val.(type) {
- case uint8:
- return uint64(v)
- case uint16:
- return uint64(v)
- case uint32:
- return uint64(v)
- case uint64:
- return v
- case int8:
- return uint64(v)
- case int16:
- return uint64(v)
- case int32:
- return uint64(v)
- case int64:
- return uint64(v)
- }
- }
- return 0
-}
-
-func (ctx *ParseContext) getStringVar(name string) string {
- if val, exists := ctx.vars[name]; exists {
- switch v := val.(type) {
- case string:
- return v
- case common.EQ2String8:
- return v.Data
- case common.EQ2String16:
- return v.Data
- case common.EQ2String32:
- return v.Data
- }
- }
- return ""
-}
-
-func (ctx *ParseContext) getFlagValue(flagName string) uint64 {
- flagMap := map[string]uint64{
- "loot": 0x01,
- "has_equipment": 0x02,
- "no_colors": 0x04,
- }
- if val, exists := flagMap[flagName]; exists {
- return val
- }
- return 0
-}
-
-func (ctx *ParseContext) getArraySize(condition string) int {
- if strings.HasPrefix(condition, "var:") {
- varName := condition[4:]
- return int(ctx.getVarValue(varName))
- }
- return 0
-}
-
-func (ctx *ParseContext) resolveVariableName(name string) string {
- // Handle %i substitution for array contexts
- if strings.Contains(name, "%i") && len(ctx.arrayStack) > 0 {
- currentIndex := ctx.arrayStack[len(ctx.arrayStack)-1]
- return strings.ReplaceAll(name, "%i", strconv.Itoa(currentIndex))
- }
- return name
-}
-
-func (ctx *ParseContext) setVarWithArrayIndex(name string, value any) {
- // Always set the base variable name
- ctx.vars[name] = value
-
- // If we're in an array context, also set the indexed variable
- if len(ctx.arrayStack) > 0 {
- currentIndex := ctx.arrayStack[len(ctx.arrayStack)-1]
- indexedName := name + "_" + strconv.Itoa(currentIndex)
- ctx.vars[indexedName] = value
- }
-}
diff --git a/structs/parser/parser.go b/structs/parser/parser.go
deleted file mode 100644
index 8b54f34..0000000
--- a/structs/parser/parser.go
+++ /dev/null
@@ -1,1199 +0,0 @@
-package parser
-
-import (
- "eq2emu/internal/common"
- "fmt"
- "strconv"
- "strings"
- "unicode"
-)
-
-// Parser is a single-pass recursive descent parser for packet definitions
-type Parser struct {
- input string
- pos int
- line int
- col int
- substructs map[string]*PacketDef
- templates map[string]*PacketDef
- tagStack []string
-}
-
-// NewParser creates a new string-based parser
-func NewParser(input string) *Parser {
- return &Parser{
- input: input,
- line: 1,
- col: 1,
- substructs: make(map[string]*PacketDef),
- templates: make(map[string]*PacketDef),
- tagStack: make([]string, 0, 16),
- }
-}
-
-// peek returns the current character without advancing
-func (p *Parser) peek() rune {
- if p.pos >= len(p.input) {
- return 0
- }
- return rune(p.input[p.pos])
-}
-
-// peekN looks ahead n characters
-func (p *Parser) peekN(n int) rune {
- pos := p.pos + n
- if pos >= len(p.input) {
- return 0
- }
- return rune(p.input[pos])
-}
-
-// advance moves to the next character and returns it
-func (p *Parser) advance() rune {
- if p.pos >= len(p.input) {
- return 0
- }
-
- ch := rune(p.input[p.pos])
- p.pos++
-
- if ch == '\n' {
- p.line++
- p.col = 1
- } else {
- p.col++
- }
-
- return ch
-}
-
-// skipWhitespace skips whitespace characters
-func (p *Parser) skipWhitespace() {
- for p.pos < len(p.input) && unicode.IsSpace(p.peek()) {
- p.advance()
- }
-}
-
-// consumeChar consumes the expected character, returns true if found
-func (p *Parser) consumeChar(expected rune) bool {
- if p.peek() == expected {
- p.advance()
- return true
- }
- return false
-}
-
-// consumeString consumes the expected string, returns true if found
-func (p *Parser) consumeString(expected string) bool {
- if p.pos+len(expected) > len(p.input) {
- return false
- }
-
- if p.input[p.pos:p.pos+len(expected)] == expected {
- for range expected {
- p.advance()
- }
- return true
- }
- return false
-}
-
-// parseIdentifier parses an identifier (alphanumeric + underscore)
-func (p *Parser) parseIdentifier() (string, error) {
- start := p.pos
-
- if p.pos >= len(p.input) || (!unicode.IsLetter(p.peek()) && p.peek() != '_') {
- return "", fmt.Errorf("expected identifier at line %d, col %d", p.line, p.col)
- }
-
- for p.pos < len(p.input) {
- ch := p.peek()
- if unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == '-' {
- p.advance()
- } else {
- break
- }
- }
-
- return p.input[start:p.pos], nil
-}
-
-// parseQuotedString parses a quoted string value
-func (p *Parser) parseQuotedString() (string, error) {
- quote := p.peek()
- if quote != '"' && quote != '\'' {
- return "", fmt.Errorf("expected quoted string at line %d, col %d", p.line, p.col)
- }
-
- p.advance() // consume opening quote
- start := p.pos
-
- for p.pos < len(p.input) && p.peek() != quote {
- if p.peek() == '\\' {
- p.advance() // consume backslash
- if p.pos < len(p.input) {
- p.advance() // consume escaped character
- }
- } else {
- p.advance()
- }
- }
-
- if p.pos >= len(p.input) {
- return "", fmt.Errorf("unclosed quoted string at line %d", p.line)
- }
-
- value := p.input[start:p.pos]
- p.advance() // consume closing quote
- return value, nil
-}
-
-// parseAttributes parses tag attributes
-func (p *Parser) parseAttributes() (map[string]string, error) {
- attrs := make(map[string]string)
-
- for {
- p.skipWhitespace()
-
- // Check for end of tag
- if p.pos >= len(p.input) || p.peek() == '>' || (p.peek() == '/' && p.peekN(1) == '>') {
- break
- }
-
- // Parse attribute name
- name, err := p.parseIdentifier()
- if err != nil {
- return nil, err
- }
-
- p.skipWhitespace()
- if !p.consumeChar('=') {
- return nil, fmt.Errorf("expected '=' after attribute name '%s' at line %d", name, p.line)
- }
-
- p.skipWhitespace()
-
- // Parse attribute value
- value, err := p.parseQuotedString()
- if err != nil {
- return nil, err
- }
-
- attrs[name] = strings.TrimSpace(value)
- }
-
- return attrs, nil
-}
-
-// parseComment skips over comment content
-func (p *Parser) parseComment() error {
- // We're at the start of
- for p.pos+2 < len(p.input) {
- if p.consumeString("-->") {
- return nil
- }
- p.advance()
- }
-
- return fmt.Errorf("unclosed comment at line %d", p.line)
-}
-
-// Tag represents a parsed XML tag
-type Tag struct {
- Name string
- Attributes map[string]string
- SelfClosing bool
- IsClosing bool
-}
-
-// parseTag parses an opening, closing, or self-closing tag
-func (p *Parser) parseTag() (*Tag, error) {
- if !p.consumeChar('<') {
- return nil, fmt.Errorf("expected '<' at line %d", p.line)
- }
-
- // Check for comment
- if p.peek() == '!' && p.peekN(1) == '-' && p.peekN(2) == '-' {
- // We already consumed the '<', so parse comment from here
- if !p.consumeString("!--") {
- return nil, fmt.Errorf("expected '
- for p.pos+2 < len(p.input) {
- if p.consumeString("-->") {
- return p.parseTag() // recursively get the next tag
- }
- p.advance()
- }
-
- return nil, fmt.Errorf("unclosed comment at line %d", p.line)
- }
-
- tag := &Tag{}
-
- // Check for closing tag
- if p.consumeChar('/') {
- tag.IsClosing = true
- name, err := p.parseIdentifier()
- if err != nil {
- return nil, err
- }
- tag.Name = name
-
- p.skipWhitespace()
- if !p.consumeChar('>') {
- return nil, fmt.Errorf("expected '>' after closing tag '%s' at line %d", name, p.line)
- }
- return tag, nil
- }
-
- // Parse tag name
- name, err := p.parseIdentifier()
- if err != nil {
- return nil, err
- }
- tag.Name = name
-
- // Parse attributes
- attrs, err := p.parseAttributes()
- if err != nil {
- return nil, err
- }
- tag.Attributes = attrs
-
- p.skipWhitespace()
-
- // Check for self-closing
- if p.consumeString("/>") {
- tag.SelfClosing = true
- } else if p.consumeChar('>') {
- // Regular opening tag
- } else {
- return nil, fmt.Errorf("expected '>' or '/>' after tag '%s' at line %d", name, p.line)
- }
-
- return tag, nil
-}
-
-// pushTag pushes a tag name onto the stack for validation
-func (p *Parser) pushTag(tag string) {
- p.tagStack = append(p.tagStack, tag)
-}
-
-// popTag pops and validates the closing tag
-func (p *Parser) popTag(expectedTag string) error {
- if len(p.tagStack) == 0 {
- return fmt.Errorf("unexpected closing tag '%s' at line %d", expectedTag, p.line)
- }
-
- lastTag := p.tagStack[len(p.tagStack)-1]
- if lastTag != expectedTag {
- return fmt.Errorf("mismatched closing tag: expected '%s', got '%s' at line %d", lastTag, expectedTag, p.line)
- }
-
- p.tagStack = p.tagStack[:len(p.tagStack)-1]
- return nil
-}
-
-// validateAllTagsClosed checks for unclosed tags
-func (p *Parser) validateAllTagsClosed() error {
- if len(p.tagStack) > 0 {
- return fmt.Errorf("unclosed tag '%s'", p.tagStack[len(p.tagStack)-1])
- }
- return nil
-}
-
-// Helper functions for robust attribute parsing
-func (p *Parser) parseIntAttribute(value string, attributeName string) (int, error) {
- if value == "" {
- return 0, fmt.Errorf("empty %s attribute at line %d", attributeName, p.line)
- }
-
- cleanValue := strings.TrimSpace(value)
- if cleanValue == "" {
- return 0, fmt.Errorf("empty %s attribute value '%s' at line %d", attributeName, value, p.line)
- }
-
- result, err := strconv.Atoi(cleanValue)
- if err != nil {
- return 0, fmt.Errorf("invalid %s value '%s' at line %d: %v", attributeName, value, p.line, err)
- }
-
- return result, nil
-}
-
-func (p *Parser) parseUintAttribute(value string, attributeName string) (uint32, error) {
- if value == "" {
- return 0, fmt.Errorf("empty %s attribute at line %d", attributeName, p.line)
- }
-
- cleanValue := strings.TrimSpace(value)
- if cleanValue == "" {
- return 0, fmt.Errorf("empty %s attribute value '%s' at line %d", attributeName, value, p.line)
- }
-
- result, err := strconv.ParseUint(cleanValue, 10, 32)
- if err != nil {
- return 0, fmt.Errorf("invalid %s value '%s' at line %d: %v", attributeName, value, p.line, err)
- }
-
- return uint32(result), nil
-}
-
-func (p *Parser) parseInt8Attribute(value string, attributeName string) (int8, error) {
- if value == "" {
- return 0, fmt.Errorf("empty %s attribute at line %d", attributeName, p.line)
- }
-
- cleanValue := strings.TrimSpace(value)
- if cleanValue == "" {
- return 0, fmt.Errorf("empty %s attribute value '%s' at line %d", attributeName, value, p.line)
- }
-
- result, err := strconv.ParseInt(cleanValue, 10, 8)
- if err != nil {
- return 0, fmt.Errorf("invalid %s value '%s' at line %d: %v", attributeName, value, p.line, err)
- }
-
- return int8(result), nil
-}
-
-// parseFieldNames parses comma-separated field names
-func (p *Parser) parseFieldNames(nameAttr string) []string {
- if nameAttr == "" {
- return nil
- }
-
- // Fast path for single name
- if !strings.Contains(nameAttr, ",") {
- name := strings.TrimSpace(nameAttr)
- if name != "" {
- return []string{name}
- }
- return nil
- }
-
- // Parse multiple names
- names := strings.Split(nameAttr, ",")
- result := make([]string, 0, len(names))
- for _, name := range names {
- if trimmed := strings.TrimSpace(name); trimmed != "" {
- result = append(result, trimmed)
- }
- }
-
- return result
-}
-
-// Parse parses the entire packet definition document
-func (p *Parser) Parse() (map[string]*PacketDef, error) {
- packets := make(map[string]*PacketDef)
-
- for p.pos < len(p.input) {
- p.skipWhitespace()
- if p.pos >= len(p.input) {
- break
- }
-
- if p.peek() != '<' {
- return nil, fmt.Errorf("expected '<' at line %d", p.line)
- }
-
- // Handle comments at the top level
- if p.peek() == '<' && p.peekN(1) == '!' && p.peekN(2) == '-' && p.peekN(3) == '-' {
- err := p.parseComment()
- if err != nil {
- return nil, err
- }
- continue
- }
-
- tag, err := p.parseTag()
- if err != nil {
- return nil, err
- }
-
- switch tag.Name {
- case "packet":
- name := tag.Attributes["name"]
- packet, err := p.parsePacket(tag)
- if err != nil {
- return nil, err
- }
- if name != "" {
- packets[name] = packet
- }
-
- case "substruct":
- name := tag.Attributes["name"]
- substruct, err := p.parseSubstruct(tag)
- if err != nil {
- return nil, err
- }
- if name != "" {
- p.substructs[name] = substruct
- }
-
- case "template":
- name := tag.Attributes["name"]
- if name != "" {
- err := p.parseTemplateDefinition(tag, name)
- if err != nil {
- return nil, err
- }
- }
-
- default:
- return nil, fmt.Errorf("unexpected top-level tag '%s' at line %d", tag.Name, p.line)
- }
- }
-
- if err := p.validateAllTagsClosed(); err != nil {
- return nil, err
- }
-
- return packets, nil
-}
-
-// parsePacket parses a packet element
-func (p *Parser) parsePacket(openTag *Tag) (*PacketDef, error) {
- packetDef := NewPacketDef(16)
-
- if openTag.SelfClosing {
- return packetDef, nil
- }
-
- p.pushTag("packet")
-
- for {
- p.skipWhitespace()
- if p.pos >= len(p.input) {
- return nil, fmt.Errorf("unexpected end of input in packet")
- }
-
- if p.peek() != '<' {
- return nil, fmt.Errorf("expected '<' at line %d", p.line)
- }
-
- // Handle comments
- if p.peek() == '<' && p.peekN(1) == '!' && p.peekN(2) == '-' && p.peekN(3) == '-' {
- err := p.parseComment()
- if err != nil {
- return nil, err
- }
- continue
- }
-
- tag, err := p.parseTag()
- if err != nil {
- return nil, err
- }
-
- if tag.IsClosing {
- if tag.Name != "packet" {
- return nil, fmt.Errorf("expected closing tag 'packet', got '%s' at line %d", tag.Name, p.line)
- }
- if err := p.popTag("packet"); err != nil {
- return nil, err
- }
- break
- }
-
- if tag.Name == "version" {
- err := p.parseVersion(tag, packetDef)
- if err != nil {
- return nil, err
- }
- } else {
- return nil, fmt.Errorf("unexpected tag '%s' in packet at line %d", tag.Name, p.line)
- }
- }
-
- return packetDef, nil
-}
-
-// parseSubstruct parses a substruct element
-func (p *Parser) parseSubstruct(openTag *Tag) (*PacketDef, error) {
- packetDef := NewPacketDef(16)
-
- if openTag.SelfClosing {
- return packetDef, nil
- }
-
- p.pushTag("substruct")
-
- // Check if this substruct contains version elements
- hasVersions := false
- for {
- p.skipWhitespace()
- if p.pos >= len(p.input) {
- return nil, fmt.Errorf("unexpected end of input in substruct")
- }
-
- if p.peek() != '<' {
- return nil, fmt.Errorf("expected '<' at line %d", p.line)
- }
-
- tag, err := p.parseTag()
- if err != nil {
- return nil, err
- }
-
- if tag.IsClosing {
- if tag.Name != "substruct" {
- return nil, fmt.Errorf("expected closing tag 'substruct', got '%s' at line %d", tag.Name, p.line)
- }
- if err := p.popTag("substruct"); err != nil {
- return nil, err
- }
- break
- }
-
- if tag.Name == "version" {
- hasVersions = true
- err := p.parseVersion(tag, packetDef)
- if err != nil {
- return nil, err
- }
- } else if hasVersions {
- return nil, fmt.Errorf("unexpected tag '%s' after version in substruct at line %d", tag.Name, p.line)
- } else {
- // No versions found, parse as direct elements
- fieldOrder := make([]string, 0)
- err := p.parseElement(tag, packetDef, &fieldOrder, "")
- if err != nil {
- return nil, err
- }
-
- // Continue parsing remaining elements
- for {
- p.skipWhitespace()
- if p.pos >= len(p.input) || p.peek() != '<' {
- break
- }
-
- nextTag, err := p.parseTag()
- if err != nil {
- return nil, err
- }
-
- if nextTag.IsClosing {
- if nextTag.Name != "substruct" {
- return nil, fmt.Errorf("expected closing tag 'substruct', got '%s' at line %d", nextTag.Name, p.line)
- }
- if err := p.popTag("substruct"); err != nil {
- return nil, err
- }
- break
- }
-
- err = p.parseElement(nextTag, packetDef, &fieldOrder, "")
- if err != nil {
- return nil, err
- }
- }
-
- // Set field order for version 1
- packetDef.Orders[1] = make([]string, len(fieldOrder))
- copy(packetDef.Orders[1], fieldOrder)
- break
- }
- }
-
- return packetDef, nil
-}
-
-// parseVersion parses a version element
-func (p *Parser) parseVersion(openTag *Tag, packetDef *PacketDef) error {
- version := uint32(1)
- if v := openTag.Attributes["number"]; v != "" {
- if parsed, err := p.parseUintAttribute(v, "number"); err == nil {
- version = parsed
- }
- // Don't fail on invalid version numbers, just use default
- }
-
- fieldOrder := make([]string, 0)
-
- if openTag.SelfClosing {
- packetDef.Orders[version] = fieldOrder
- return nil
- }
-
- p.pushTag("version")
-
- for {
- p.skipWhitespace()
- if p.pos >= len(p.input) {
- return fmt.Errorf("unexpected end of input in version")
- }
-
- if p.peek() != '<' {
- return fmt.Errorf("expected '<' at line %d", p.line)
- }
-
- // Handle comments
- if p.peek() == '<' && p.peekN(1) == '!' && p.peekN(2) == '-' && p.peekN(3) == '-' {
- err := p.parseComment()
- if err != nil {
- return err
- }
- continue
- }
-
- tag, err := p.parseTag()
- if err != nil {
- return err
- }
-
- if tag.IsClosing {
- if tag.Name != "version" {
- return fmt.Errorf("expected closing tag 'version', got '%s' at line %d", tag.Name, p.line)
- }
- if err := p.popTag("version"); err != nil {
- return err
- }
- break
- }
-
- err = p.parseElement(tag, packetDef, &fieldOrder, "")
- if err != nil {
- return err
- }
- }
-
- packetDef.Orders[version] = make([]string, len(fieldOrder))
- copy(packetDef.Orders[version], fieldOrder)
- return nil
-}
-
-// parseElement parses any element (field, array, group, template, substruct reference)
-func (p *Parser) parseElement(tag *Tag, packetDef *PacketDef, fieldOrder *[]string, prefix string) error {
- switch tag.Name {
- case "group":
- return p.parseGroup(tag, packetDef, fieldOrder, prefix)
- case "array":
- return p.parseArray(tag, packetDef, fieldOrder, prefix)
- case "template":
- return p.parseTemplateUsage(tag, packetDef, fieldOrder, prefix)
- case "substruct":
- return p.parseSubstructReference(tag, packetDef, fieldOrder, prefix)
- case "item":
- return p.parseItemField(tag, packetDef, fieldOrder, prefix)
- default:
- // Try to parse as a field
- return p.parseField(tag, packetDef, fieldOrder, prefix)
- }
-}
-
-// parseGroup parses a group element
-func (p *Parser) parseGroup(openTag *Tag, packetDef *PacketDef, fieldOrder *[]string, prefix string) error {
- groupPrefix := prefix
- if name := openTag.Attributes["name"]; name != "" {
- if prefix == "" {
- groupPrefix = name + "_"
- } else {
- groupPrefix = prefix + name + "_"
- }
- }
-
- if openTag.SelfClosing {
- return nil
- }
-
- p.pushTag("group")
-
- for {
- p.skipWhitespace()
- if p.pos >= len(p.input) {
- return fmt.Errorf("unexpected end of input in group")
- }
-
- if p.peek() != '<' {
- return fmt.Errorf("expected '<' at line %d", p.line)
- }
-
- tag, err := p.parseTag()
- if err != nil {
- return err
- }
-
- if tag.IsClosing {
- if tag.Name != "group" {
- return fmt.Errorf("expected closing tag 'group', got '%s' at line %d", tag.Name, p.line)
- }
- if err := p.popTag("group"); err != nil {
- return err
- }
- break
- }
-
- err = p.parseElement(tag, packetDef, fieldOrder, groupPrefix)
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
-// parseArray parses an array element
-func (p *Parser) parseArray(openTag *Tag, packetDef *PacketDef, fieldOrder *[]string, prefix string) error {
- var arrayName string
- if prefix == "" {
- arrayName = openTag.Attributes["name"]
- } else {
- arrayName = prefix + openTag.Attributes["name"]
- }
-
- fieldDesc := FieldDesc{
- Type: common.TypeArray,
- Condition: openTag.Attributes["count"],
- AddToStruct: true, // Default to true
- }
-
- if ifCond := openTag.Attributes["if"]; ifCond != "" {
- fieldDesc.Condition = combineConditions(fieldDesc.Condition, ifCond)
- }
-
- // Parse additional attributes
- if maxSize := openTag.Attributes["max_size"]; maxSize != "" {
- if m, err := p.parseIntAttribute(maxSize, "max_size"); err == nil {
- fieldDesc.MaxArraySize = m
- } else {
- return err
- }
- }
-
- if optional := openTag.Attributes["optional"]; optional == "true" {
- fieldDesc.Optional = true
- }
-
- if addToStruct := openTag.Attributes["add_to_struct"]; addToStruct == "false" {
- fieldDesc.AddToStruct = false
- }
-
- // Handle substruct reference
- if substruct := openTag.Attributes["substruct"]; substruct != "" {
- if subDef, exists := p.substructs[substruct]; exists {
- fieldDesc.SubDef = subDef
- }
- }
-
- // Arrays with substruct references or explicit self-closing syntax are self-closing
- if openTag.SelfClosing || fieldDesc.SubDef != nil {
- packetDef.Fields[arrayName] = fieldDesc
- *fieldOrder = append(*fieldOrder, arrayName)
- return nil
- }
-
- p.pushTag("array")
-
- // Handle direct child elements as substruct fields
- if fieldDesc.SubDef == nil {
- subDef := NewPacketDef(16)
- subOrder := make([]string, 0)
-
- for {
- p.skipWhitespace()
- if p.pos >= len(p.input) {
- return fmt.Errorf("unexpected end of input in array")
- }
-
- if p.peek() != '<' {
- return fmt.Errorf("expected '<' at line %d", p.line)
- }
-
- tag, err := p.parseTag()
- if err != nil {
- return err
- }
-
- if tag.IsClosing {
- if tag.Name != "array" {
- return fmt.Errorf("expected closing tag 'array', got '%s' at line %d", tag.Name, p.line)
- }
- if err := p.popTag("array"); err != nil {
- return err
- }
- break
- }
-
- err = p.parseElement(tag, subDef, &subOrder, "")
- if err != nil {
- return err
- }
- }
-
- // Only create substruct if we actually have fields
- if len(subOrder) > 0 {
- subDef.Orders[1] = make([]string, len(subOrder))
- copy(subDef.Orders[1], subOrder)
- fieldDesc.SubDef = subDef
- }
- }
-
- packetDef.Fields[arrayName] = fieldDesc
- *fieldOrder = append(*fieldOrder, arrayName)
- return nil
-}
-
-// combineConditions combines two conditions with AND logic - using existing function
-
-// parseField parses a field element
-func (p *Parser) parseField(openTag *Tag, packetDef *PacketDef, fieldOrder *[]string, prefix string) error {
- dataType, exists := getDataType(openTag.Name)
- if !exists {
- return fmt.Errorf("unknown field type '%s' at line %d", openTag.Name, p.line)
- }
-
- nameAttr := openTag.Attributes["name"]
- if nameAttr == "" {
- return fmt.Errorf("field missing name attribute at line %d", p.line)
- }
-
- names := p.parseFieldNames(nameAttr)
- for _, name := range names {
- var fullName string
- if prefix == "" {
- fullName = name
- } else {
- fullName = prefix + name
- }
-
- fieldDesc := FieldDesc{
- Type: dataType,
- Condition: openTag.Attributes["if"],
- AddToStruct: true, // Default to true
- AddType: dataType,
- }
-
- // Parse size attribute
- if size := openTag.Attributes["size"]; size != "" {
- if s, err := p.parseIntAttribute(size, "size"); err == nil {
- fieldDesc.Length = s
- } else {
- return err
- }
- }
-
- // Parse oversized attribute
- if oversized := openTag.Attributes["oversized"]; oversized != "" {
- if o, err := p.parseIntAttribute(oversized, "oversized"); err == nil {
- fieldDesc.Oversized = o
- } else {
- return err
- }
- }
-
- // Parse type2 attributes
- if type2 := openTag.Attributes["type2"]; type2 != "" {
- if t2, exists := getDataType(type2); exists {
- fieldDesc.Type2 = t2
- fieldDesc.Type2Cond = openTag.Attributes["type2_if"]
- }
- }
-
- // Parse default value
- if defaultVal := openTag.Attributes["default"]; defaultVal != "" {
- if d, err := p.parseInt8Attribute(defaultVal, "default"); err == nil {
- fieldDesc.DefaultValue = d
- } else {
- return err
- }
- }
-
- // Parse max_size
- if maxSize := openTag.Attributes["max_size"]; maxSize != "" {
- if m, err := p.parseIntAttribute(maxSize, "max_size"); err == nil {
- fieldDesc.MaxArraySize = m
- } else {
- return err
- }
- }
-
- // Parse optional
- if optional := openTag.Attributes["optional"]; optional == "true" {
- fieldDesc.Optional = true
- }
-
- // Parse add_to_struct
- if addToStruct := openTag.Attributes["add_to_struct"]; addToStruct == "false" {
- fieldDesc.AddToStruct = false
- }
-
- // Parse add_type
- if addType := openTag.Attributes["add_type"]; addType != "" {
- if at, exists := getDataType(addType); exists {
- fieldDesc.AddType = at
- }
- }
-
- packetDef.Fields[fullName] = fieldDesc
- *fieldOrder = append(*fieldOrder, fullName)
- }
-
- return nil
-}
-
-// parseTemplateDefinition parses a template definition
-func (p *Parser) parseTemplateDefinition(openTag *Tag, templateName string) error {
- templateDef := NewPacketDef(16)
- fieldOrder := make([]string, 0)
-
- if openTag.SelfClosing {
- templateDef.Orders[1] = fieldOrder
- p.templates[templateName] = templateDef
- return nil
- }
-
- p.pushTag("template")
-
- for {
- p.skipWhitespace()
- if p.pos >= len(p.input) {
- return fmt.Errorf("unexpected end of input in template")
- }
-
- if p.peek() != '<' {
- return fmt.Errorf("expected '<' at line %d", p.line)
- }
-
- tag, err := p.parseTag()
- if err != nil {
- return err
- }
-
- if tag.IsClosing {
- if tag.Name != "template" {
- return fmt.Errorf("expected closing tag 'template', got '%s' at line %d", tag.Name, p.line)
- }
- if err := p.popTag("template"); err != nil {
- return err
- }
- break
- }
-
- err = p.parseElement(tag, templateDef, &fieldOrder, "")
- if err != nil {
- return err
- }
- }
-
- templateDef.Orders[1] = make([]string, len(fieldOrder))
- copy(templateDef.Orders[1], fieldOrder)
- p.templates[templateName] = templateDef
- return nil
-}
-
-// parseTemplateUsage parses template usage
-func (p *Parser) parseTemplateUsage(tag *Tag, packetDef *PacketDef, fieldOrder *[]string, prefix string) error {
- // Template usage:
- if templateUse := tag.Attributes["use"]; templateUse != "" {
- template, exists := p.templates[templateUse]
- if !exists {
- return fmt.Errorf("undefined template '%s' at line %d", templateUse, p.line)
- }
-
- // Inject all template fields into current packet
- if templateOrder, exists := template.Orders[1]; exists {
- for _, fieldName := range templateOrder {
- if fieldDesc, exists := template.Fields[fieldName]; exists {
- // Apply prefix if provided
- fullName := fieldName
- if prefix != "" {
- fullName = prefix + fieldName
- }
-
- packetDef.Fields[fullName] = fieldDesc
- *fieldOrder = append(*fieldOrder, fullName)
- }
- }
- }
-
- return nil
- }
-
- return fmt.Errorf("template missing 'use' attribute at line %d", p.line)
-}
-
-// parseSubstructReference parses a substruct reference like
-func (p *Parser) parseSubstructReference(tag *Tag, packetDef *PacketDef, fieldOrder *[]string, prefix string) error {
- nameAttr := tag.Attributes["name"]
- useAttr := tag.Attributes["use"]
-
- if nameAttr == "" {
- return fmt.Errorf("substruct reference missing 'name' attribute at line %d", p.line)
- }
-
- var fullName string
- if prefix == "" {
- fullName = nameAttr
- } else {
- fullName = prefix + nameAttr
- }
-
- fieldDesc := FieldDesc{
- Type: common.TypeArray, // Substructs are treated as arrays
- AddToStruct: true,
- }
-
- // Handle substruct reference with 'use' attribute
- if useAttr != "" {
- if subDef, exists := p.substructs[useAttr]; exists {
- fieldDesc.SubDef = subDef
- } else {
- // If substruct doesn't exist yet, it might be defined later
- // For now, we'll create a placeholder
- }
- }
-
- packetDef.Fields[fullName] = fieldDesc
- *fieldOrder = append(*fieldOrder, fullName)
- return nil
-}
-
-// parseItemField parses an item field like -
-func (p *Parser) parseItemField(tag *Tag, packetDef *PacketDef, fieldOrder *[]string, prefix string) error {
- nameAttr := tag.Attributes["name"]
- if nameAttr == "" {
- return fmt.Errorf("item field missing 'name' attribute at line %d", p.line)
- }
-
- var fullName string
- if prefix == "" {
- fullName = nameAttr
- } else {
- fullName = prefix + nameAttr
- }
-
- fieldDesc := FieldDesc{
- Type: common.TypeItem,
- Condition: tag.Attributes["if"],
- AddToStruct: true,
- AddType: common.TypeItem,
- }
-
- packetDef.Fields[fullName] = fieldDesc
- *fieldOrder = append(*fieldOrder, fullName)
- return nil
-}
-
-// Parse function that uses the new string parser
-func Parse(content string) (map[string]*PacketDef, error) {
- parser := NewParser(content)
- return parser.Parse()
-}
-
-// Type mapping using corrected type constants
-var typeMap = map[string]common.EQ2DataType{
- "u8": common.TypeInt8,
- "u16": common.TypeInt16,
- "u32": common.TypeInt32,
- "u64": common.TypeInt64,
- "i8": common.TypeSInt8,
- "i16": common.TypeSInt16,
- "i32": common.TypeSInt32,
- "i64": common.TypeSInt64,
- "f32": common.TypeFloat,
- "f64": common.TypeDouble,
- "double": common.TypeDouble,
- "str8": common.TypeString8,
- "str16": common.TypeString16,
- "str32": common.TypeString32,
- "char": common.TypeChar,
- "color": common.TypeColor,
- "equip": common.TypeEquipment,
- "array": common.TypeArray,
-}
-
-// combineConditions combines two conditions with AND logic
-func combineConditions(cond1, cond2 string) string {
- if cond1 == "" {
- return cond2
- }
- if cond2 == "" {
- return cond1
- }
- return cond1 + "&" + cond2
-}
-
-// Fast type lookup using first character
-func getDataType(tag string) (common.EQ2DataType, bool) {
- if len(tag) == 0 {
- return 0, false
- }
-
- switch tag[0] {
- case 'u':
- switch tag {
- case "u8":
- return common.TypeInt8, true
- case "u16":
- return common.TypeInt16, true
- case "u32":
- return common.TypeInt32, true
- case "u64":
- return common.TypeInt64, true
- }
- case 'i':
- switch tag {
- case "i8":
- return common.TypeSInt8, true
- case "i16":
- return common.TypeSInt16, true
- case "i32":
- return common.TypeSInt32, true
- case "i64":
- return common.TypeSInt64, true
- }
- case 's':
- switch tag {
- case "str8":
- return common.TypeString8, true
- case "str16":
- return common.TypeString16, true
- case "str32":
- return common.TypeString32, true
- }
- case 'f':
- switch tag {
- case "f32":
- return common.TypeFloat, true
- case "f64":
- return common.TypeDouble, true
- }
- case 'd':
- if tag == "double" {
- return common.TypeDouble, true
- }
- case 'c':
- switch tag {
- case "char":
- return common.TypeChar, true
- case "color":
- return common.TypeColor, true
- }
- case 'e':
- if tag == "equip" {
- return common.TypeEquipment, true
- }
- case 'a':
- if tag == "array" {
- return common.TypeArray, true
- }
- }
-
- // Fallback to map lookup
- if dataType, exists := typeMap[tag]; exists {
- return dataType, true
- }
-
- return 0, false
-}
diff --git a/structs/parser/parser_test.go b/structs/parser/parser_test.go
deleted file mode 100644
index cd43902..0000000
--- a/structs/parser/parser_test.go
+++ /dev/null
@@ -1,933 +0,0 @@
-package parser
-
-import (
- "eq2emu/internal/common"
- "testing"
-)
-
-func TestBasicParsing(t *testing.T) {
- pml := `
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["Test"]
- if packet == nil {
- t.Fatal("Test packet not found")
- }
-
- // Check fields
- if len(packet.Fields) != 3 {
- t.Errorf("Expected 3 fields, got %d", len(packet.Fields))
- }
-
- if packet.Fields["player_id"].Type != common.TypeSInt32 {
- t.Error("player_id should be TypeSInt32")
- }
-
- if packet.Fields["player_name"].Type != common.TypeString16 {
- t.Error("player_name should be TypeString16")
- }
-
- if packet.Fields["skin_color"].Type != common.TypeColor {
- t.Error("skin_color should be TypeColor")
- }
-
- // Check order
- order := packet.Orders[1]
- expected := []string{"player_id", "player_name", "skin_color"}
- if !equalSlices(order, expected) {
- t.Errorf("Expected order %v, got %v", expected, order)
- }
-}
-
-func TestFloat64Support(t *testing.T) {
- pml := `
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["FloatTest"]
- if packet.Fields["position_x"].Type != common.TypeFloat {
- t.Error("position_x should be TypeFloat")
- }
- if packet.Fields["precise_value"].Type != common.TypeDouble {
- t.Error("precise_value should be TypeDouble")
- }
- if packet.Fields["legacy_double"].Type != common.TypeDouble {
- t.Error("legacy_double should be TypeDouble")
- }
-}
-
-func TestOversizedFields(t *testing.T) {
- pml := `
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["OversizedTest"]
- if packet.Fields["small_count"].Oversized != 0 {
- t.Error("small_count should not be oversized")
- }
- if packet.Fields["num_words"].Oversized != 255 {
- t.Errorf("num_words oversized should be 255, got %d", packet.Fields["num_words"].Oversized)
- }
- if packet.Fields["large_value"].Oversized != 65535 {
- t.Errorf("large_value oversized should be 65535, got %d", packet.Fields["large_value"].Oversized)
- }
-}
-
-func TestType2Support(t *testing.T) {
- pml := `
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["Type2Test"]
- statValue := packet.Fields["stat_value"]
- if statValue.Type != common.TypeSInt32 {
- t.Error("stat_value primary type should be TypeSInt32")
- }
- if statValue.Type2 != common.TypeFloat {
- t.Error("stat_value type2 should be TypeFloat")
- }
- if statValue.Type2Cond != "stat_type!=6" {
- t.Errorf("Expected type2_if 'stat_type!=6', got '%s'", statValue.Type2Cond)
- }
-
- anotherField := packet.Fields["another_field"]
- if anotherField.Type2 != common.TypeSInt32 {
- t.Error("another_field type2 should be TypeSInt32")
- }
- if anotherField.Type2Cond != "" {
- t.Error("another_field should have empty type2_if")
- }
-}
-
-func TestAdvancedFieldAttributes(t *testing.T) {
- pml := `
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["AttributeTest"]
-
- dataArray := packet.Fields["data_array"]
- if dataArray.Length != 10 {
- t.Errorf("Expected size 10, got %d", dataArray.Length)
- }
- if dataArray.DefaultValue != 5 {
- t.Errorf("Expected default 5, got %d", dataArray.DefaultValue)
- }
-
- optionalText := packet.Fields["optional_text"]
- if optionalText.Condition != "var:has_text" {
- t.Errorf("Expected condition 'var:has_text', got '%s'", optionalText.Condition)
- }
- if !optionalText.Optional {
- t.Error("optional_text should be optional")
- }
-
- hiddenField := packet.Fields["hidden_field"]
- if hiddenField.AddToStruct {
- t.Error("hidden_field should not be added to struct")
- }
- if hiddenField.AddType != common.TypeSInt16 {
- t.Error("hidden_field add_type should be TypeSInt16")
- }
-}
-
-func TestArrayMaxSize(t *testing.T) {
- pml := `
-
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["ArrayMaxTest"]
- itemsField := packet.Fields["items"]
-
- if itemsField.MaxArraySize != 100 {
- t.Errorf("Expected max_size 100, got %d", itemsField.MaxArraySize)
- }
-}
-
-func TestArrayOptionalAttributes(t *testing.T) {
- pml := `
-
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["ArrayOptionalTest"]
- itemsField := packet.Fields["optional_items"]
-
- if !itemsField.Optional {
- t.Error("optional_items should be optional")
- }
- if itemsField.AddToStruct {
- t.Error("optional_items should not be added to struct")
- }
-}
-
-func TestMultipleVersions(t *testing.T) {
- pml := `
-
-
-
-
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["MultiVersion"]
- if packet == nil {
- t.Fatal("MultiVersion packet not found")
- }
-
- // Check both versions exist
- if len(packet.Orders) != 2 {
- t.Errorf("Expected 2 versions, got %d", len(packet.Orders))
- }
-
- v1Order := packet.Orders[1]
- v562Order := packet.Orders[562]
-
- if len(v1Order) != 2 {
- t.Errorf("Version 1 should have 2 fields, got %d", len(v1Order))
- }
-
- if len(v562Order) != 3 {
- t.Errorf("Version 562 should have 3 fields, got %d", len(v562Order))
- }
-}
-
-func TestArrayParsing(t *testing.T) {
- pml := `
-
-
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["ArrayTest"]
- itemsField := packet.Fields["items"]
-
- if itemsField.Type != common.TypeArray {
- t.Error("items should be TypeArray")
- }
-
- if itemsField.Condition != "var:item_count" {
- t.Errorf("Expected condition 'var:item_count', got '%s'", itemsField.Condition)
- }
-
- if itemsField.SubDef == nil {
- t.Fatal("SubDef should not be nil")
- }
-
- // Check substruct fields
- if len(itemsField.SubDef.Fields) != 2 {
- t.Errorf("Expected 2 substruct fields, got %d", len(itemsField.SubDef.Fields))
- }
-
- if itemsField.SubDef.Fields["item_id"].Type != common.TypeSInt32 {
- t.Error("item_id should be TypeSInt32")
- }
-}
-
-func TestConditionalParsing(t *testing.T) {
- pml := `
-
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["ConditionalTest"]
-
- if packet.Fields["guild_name"].Condition != "flag:has_guild" {
- t.Errorf("guild_name condition wrong: %s", packet.Fields["guild_name"].Condition)
- }
-
- if packet.Fields["enhancement"].Condition != "item_type!=0" {
- t.Errorf("enhancement condition wrong: %s", packet.Fields["enhancement"].Condition)
- }
-
- if packet.Fields["aura"].Condition != "special_flags&0x01" {
- t.Errorf("aura condition wrong: %s", packet.Fields["aura"].Condition)
- }
-}
-
-func TestCommaFieldNames(t *testing.T) {
- pml := `
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["CommaTest"]
-
- expectedFields := []string{"player_id", "account_id", "pos_x", "pos_y", "pos_z"}
- if len(packet.Fields) != len(expectedFields) {
- t.Errorf("Expected %d fields, got %d", len(expectedFields), len(packet.Fields))
- }
-
- for _, field := range expectedFields {
- if _, exists := packet.Fields[field]; !exists {
- t.Errorf("Field %s not found", field)
- }
- }
-}
-
-func TestSubstructReference(t *testing.T) {
- pml := `
-
-
-
-
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["SubstructTest"]
- itemsField := packet.Fields["items"]
-
- if itemsField.SubDef == nil {
- t.Fatal("SubDef should not be nil for referenced substruct")
- }
-
- if len(itemsField.SubDef.Fields) != 2 {
- t.Errorf("Expected 2 substruct fields, got %d", len(itemsField.SubDef.Fields))
- }
-}
-
-func TestFieldAttributes(t *testing.T) {
- pml := `
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["AttributeTest"]
-
- if packet.Fields["data_array"].Length != 10 {
- t.Errorf("Expected size 10, got %d", packet.Fields["data_array"].Length)
- }
-
- if packet.Fields["optional_text"].Condition != "var:has_text" {
- t.Errorf("Expected condition 'var:has_text', got '%s'", packet.Fields["optional_text"].Condition)
- }
-}
-
-func TestComments(t *testing.T) {
- pml := `
-
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["CommentTest"]
- if len(packet.Fields) != 2 {
- t.Errorf("Comments should not affect parsing, expected 2 fields, got %d", len(packet.Fields))
- }
-}
-
-func TestBinaryParsingFloat64(t *testing.T) {
- // Test binary parsing with float64
- pml := `
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- // Create test data: 8 bytes representing float64 value 123.456
- testData := []byte{0x77, 0xbe, 0x9f, 0x1a, 0x2f, 0xdd, 0x5e, 0x40} // 123.456 in little-endian
-
- result, err := packets["BinaryFloat64"].Parse(testData, 1, 0)
- if err != nil {
- t.Fatalf("Binary parse failed: %v", err)
- }
-
- if val, ok := result["precise_value"].(float64); !ok {
- t.Error("precise_value should be float64")
- } else if val < 123.0 || val > 124.0 { // Rough check
- t.Errorf("Expected value around 123.456, got %f", val)
- }
-}
-
-func TestBinaryParsingOversized(t *testing.T) {
- // Test oversized field parsing
- pml := `
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- // Test data: normal 16-bit value (100), then oversized marker (255) + 16-bit value (1000)
- testData := []byte{0x64, 0x00, 0xff, 0xe8, 0x03} // 100, 255, 1000
-
- result, err := packets["BinaryOversized"].Parse(testData, 1, 0)
- if err != nil {
- t.Fatalf("Binary parse failed: %v", err)
- }
-
- if val := result["normal_value"].(int16); val != 100 {
- t.Errorf("Expected normal_value 100, got %d", val)
- }
-
- if val := result["oversized_value"].(int16); val != 1000 {
- t.Errorf("Expected oversized_value 1000, got %d", val)
- }
-}
-
-func TestBinaryParsingType2(t *testing.T) {
- // Test type2 switching in binary parsing
- pml := `
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- // Test with stat_type = 6 (should use float)
- testData1 := []byte{0x06, 0x00, 0x00, 0x20, 0x41} // stat_type=6, float 10.0
-
- result1, err := packets["BinaryType2"].Parse(testData1, 1, 0)
- if err != nil {
- t.Fatalf("Binary parse failed: %v", err)
- }
-
- if statType := result1["stat_type"].(int8); statType != 6 {
- t.Errorf("Expected stat_type 6, got %d", statType)
- }
-
- // Note: The actual type switching logic depends on conditions.go implementation
- // This test verifies the parsing structure is correct
-}
-
-func TestBinaryParsingArrayMaxSize(t *testing.T) {
- // Test array max size limit
- pml := `
-
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- // Test data: count=5, but max_size=2 should limit to 2 items
- testData := []byte{0x05, 0x01, 0x00, 0x02, 0x00} // count=5, item1=1, item2=2
-
- result, err := packets["BinaryArrayMax"].Parse(testData, 1, 0)
- if err != nil {
- t.Fatalf("Binary parse failed: %v", err)
- }
-
- items := result["items"].([]map[string]any)
- if len(items) != 2 {
- t.Errorf("Expected 2 items due to max_size, got %d", len(items))
- }
-}
-
-func TestArrayIndexConditions(t *testing.T) {
- // Test array index substitution in conditions
- pml := `
-
-
-
-
-
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["ArrayConditionTest"]
-
- // Verify fields were parsed correctly
- statsField := packet.Fields["stats"]
- if statsField.Type != common.TypeArray {
- t.Error("stats should be TypeArray")
- }
-
- if statsField.SubDef == nil {
- t.Fatal("SubDef should not be nil")
- }
-
- // Check that conditions were preserved
- modifiedValue := statsField.SubDef.Fields["modified_value"]
- if modifiedValue.Condition != "stat_type_%i>=1&stat_type_%i<=5" {
- t.Errorf("Expected 'stat_type_%%i>=1&stat_type_%%i<=5', got '%s'", modifiedValue.Condition)
- }
-
- percentage := statsField.SubDef.Fields["percentage"]
- if percentage.Condition != "stat_type_%i==6" {
- t.Errorf("Expected 'stat_type_%%i==6', got '%s'", percentage.Condition)
- }
-
- description := statsField.SubDef.Fields["description"]
- if description.Condition != "!var:stat_type_%i" {
- t.Errorf("Expected '!var:stat_type_%%i', got '%s'", description.Condition)
- }
-}
-
-func TestArrayIndexBinaryParsing(t *testing.T) {
- // Test that array index conditions work during binary parsing
- pml := `
-
-
-
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- // Test data: 2 stats, first with type=5, second with type=6
- // stat_count=2, stat1: type=5, base=100, stat2: type=6, base=200, percentage=1.5
- testData := []byte{
- 0x02, // stat_count = 2
- 0x05, 0x64, 0x00, 0x00, 0x00, // stat 1: type=5, base=100 (no percentage)
- 0x06, 0xC8, 0x00, 0x00, 0x00, // stat 2: type=6, base=200
- 0x00, 0x00, 0xC0, 0x3F, // percentage=1.5 (float32)
- }
-
- result, err := packets["ArrayConditionBinary"].Parse(testData, 1, 0)
- if err != nil {
- t.Fatalf("Binary parse failed: %v", err)
- }
-
- stats := result["stats"].([]map[string]any)
- if len(stats) != 2 {
- t.Fatalf("Expected 2 stats, got %d", len(stats))
- }
-
- // First stat (type=5) should not have percentage field
- stat1 := stats[0]
- if stat1["stat_type"].(int8) != 5 {
- t.Errorf("Expected stat_type 5, got %d", stat1["stat_type"])
- }
- if _, hasPercentage := stat1["percentage"]; hasPercentage {
- t.Error("Stat type 5 should not have percentage field")
- }
-
- // Second stat (type=6) should have percentage field
- stat2 := stats[1]
- if stat2["stat_type"].(int8) != 6 {
- t.Errorf("Expected stat_type 6, got %d", stat2["stat_type"])
- }
- if percentage, hasPercentage := stat2["percentage"]; !hasPercentage {
- t.Error("Stat type 6 should have percentage field")
- } else if percentage.(float32) < 1.4 || percentage.(float32) > 1.6 {
- t.Errorf("Expected percentage around 1.5, got %f", percentage)
- }
-}
-
-func TestComplexArrayConditions(t *testing.T) {
- // Test complex conditions with array indices
- pml := `
-
-
-
-
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- packet := packets["ComplexArrayTest"]
- itemsField := packet.Fields["items"]
-
- enhancement := itemsField.SubDef.Fields["enhancement"]
- if enhancement.Condition != "item_type_%i!=0&item_flags_%i&0x01" {
- t.Errorf("Enhancement condition wrong: %s", enhancement.Condition)
- }
-
- specialColor := itemsField.SubDef.Fields["special_color"]
- if specialColor.Condition != "item_type_%i>=100,item_flags_%i&0x02" {
- t.Errorf("Special color condition wrong: %s", specialColor.Condition)
- }
-}
-
-func TestErrorHandling(t *testing.T) {
- testCases := []struct {
- name string
- pml string
- }{
- {"Unclosed tag", ""},
- {"Invalid XML", ""},
- {"Missing quotes", ""},
- {"Invalid oversized", ""},
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- _, err := Parse(tc.pml)
- if err == nil {
- t.Error("Expected error but got none")
- }
- })
- }
-}
-
-func TestTemplateDefinitionAndUsage(t *testing.T) {
- pml := `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `
-
- packets, err := Parse(pml)
- if err != nil {
- t.Fatalf("Parse failed: %v", err)
- }
-
- // Test basic template injection
- playerPacket := packets["PlayerUpdate"]
- if playerPacket == nil {
- t.Fatal("PlayerUpdate packet not found")
- }
-
- // Check that template fields were injected
- expectedFields := []string{"player_id", "x", "y", "z", "heading", "level", "skin_color", "hair_color", "eye_color", "face_file", "hair_file", "guild_name"}
- if len(playerPacket.Fields) != len(expectedFields) {
- t.Errorf("Expected %d fields, got %d", len(expectedFields), len(playerPacket.Fields))
- }
-
- // Verify specific template fields exist with correct types
- if playerPacket.Fields["x"].Type != common.TypeFloat {
- t.Error("x should be TypeFloat from position template")
- }
- if playerPacket.Fields["heading"].Type != common.TypeFloat {
- t.Error("heading should be TypeFloat from position template")
- }
- if playerPacket.Fields["skin_color"].Type != common.TypeColor {
- t.Error("skin_color should be TypeColor from appearance template")
- }
- if playerPacket.Fields["face_file"].Type != common.TypeString16 {
- t.Error("face_file should be TypeString16 from appearance template")
- }
-
- // Check field ordering preserves template injection points
- order := playerPacket.Orders[1]
- expectedOrder := []string{"player_id", "x", "y", "z", "heading", "level", "skin_color", "hair_color", "eye_color", "face_file", "hair_file", "guild_name"}
- if !equalSlices(order, expectedOrder) {
- t.Errorf("Expected order %v, got %v", expectedOrder, order)
- }
-
- // Test template with group prefixes
- groupedPacket := packets["GroupedTemplate"]
- if groupedPacket == nil {
- t.Fatal("GroupedTemplate packet not found")
- }
-
- // Check prefixed fields from templates
- if groupedPacket.Fields["current_x"].Type != common.TypeFloat {
- t.Error("current_x should exist from prefixed template")
- }
- if groupedPacket.Fields["target_heading"].Type != common.TypeFloat {
- t.Error("target_heading should exist from prefixed template")
- }
-
- // Verify grouped template field order
- groupedOrder := groupedPacket.Orders[1]
- expectedGroupedOrder := []string{"entity_id", "current_x", "current_y", "current_z", "current_heading", "target_x", "target_y", "target_z", "target_heading"}
- if !equalSlices(groupedOrder, expectedGroupedOrder) {
- t.Errorf("Expected grouped order %v, got %v", expectedGroupedOrder, groupedOrder)
- }
-}
-
-func BenchmarkSimplePacket(b *testing.B) {
- pml := `
-
-
-
-
-
-
- `
-
- for b.Loop() {
- _, err := Parse(pml)
- if err != nil {
- b.Fatal(err)
- }
- }
-}
-
-func BenchmarkMediumPacket(b *testing.B) {
- pml := `
-
-
-
-
-
-
-
- `
-
- for b.Loop() {
- _, err := Parse(pml)
- if err != nil {
- b.Fatal(err)
- }
- }
-}
-
-func BenchmarkLargePacket(b *testing.B) {
- pml := `
-
-
-
-
-
-
-
- `
-
- for b.Loop() {
- _, err := Parse(pml)
- if err != nil {
- b.Fatal(err)
- }
- }
-}
-
-func BenchmarkArrayPacket(b *testing.B) {
- pml := `
-
-
-
-
-
-
-
-
-
- `
-
- for b.Loop() {
- _, err := Parse(pml)
- if err != nil {
- b.Fatal(err)
- }
- }
-}
-
-func BenchmarkMultiVersionPacket(b *testing.B) {
- pml := `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `
-
- for b.Loop() {
- _, err := Parse(pml)
- if err != nil {
- b.Fatal(err)
- }
- }
-}
-
-// Helper function to compare slices
-func equalSlices(a, b []string) bool {
- if len(a) != len(b) {
- return false
- }
- for i, v := range a {
- if v != b[i] {
- return false
- }
- }
- return true
-}
diff --git a/structs/parser/structs.go b/structs/parser/structs.go
deleted file mode 100644
index a5a7741..0000000
--- a/structs/parser/structs.go
+++ /dev/null
@@ -1,135 +0,0 @@
-package parser
-
-import "eq2emu/internal/common"
-
-// PacketDef defines a complete packet structure with versioned field ordering
-type PacketDef struct {
- Fields map[string]FieldDesc // Field definitions by name
- Orders map[uint32][]string // Field order by version number
-}
-
-// Creates packet definition with estimated capacity
-func NewPacketDef(estimatedFields int) *PacketDef {
- return &PacketDef{
- Fields: make(map[string]FieldDesc, estimatedFields),
- Orders: make(map[uint32][]string, 4),
- }
-}
-
-func (def *PacketDef) Parse(data []byte, version uint32, flags uint64) (map[string]any, error) {
- ctx := NewContext(data, version, flags)
- return def.parseStruct(ctx)
-}
-
-func (def *PacketDef) parseStruct(ctx *ParseContext) (map[string]any, error) {
- result := make(map[string]any)
- order := def.getVersionOrder(ctx.version)
-
- for _, fieldName := range order {
- field := def.Fields[fieldName]
-
- if !ctx.checkCondition(field.Condition) {
- continue
- }
-
- fieldType := field.Type
- if field.Type2 != 0 && ctx.checkCondition(field.Type2Cond) {
- fieldType = field.Type2
- }
-
- value := def.parseField(ctx, field, fieldType, fieldName)
- result[fieldName] = value
- ctx.setVarWithArrayIndex(fieldName, value)
- }
-
- return result, nil
-}
-
-func (def *PacketDef) parseField(ctx *ParseContext, field FieldDesc, fieldType common.EQ2DataType, fieldName string) any {
- switch fieldType {
- case common.TypeInt8:
- if field.Oversized > 0 {
- return ctx.readOversizedUint8(field.Oversized)
- }
- return ctx.readUint8()
- case common.TypeInt16:
- if field.Oversized > 0 {
- return ctx.readOversizedUint16(field.Oversized)
- }
- return ctx.readUint16()
- case common.TypeInt32:
- if field.Oversized > 0 {
- return ctx.readOversizedUint32(field.Oversized)
- }
- return ctx.readUint32()
- case common.TypeInt64:
- return ctx.readUint64()
- case common.TypeSInt8:
- return ctx.readSint8()
- case common.TypeSInt16:
- if field.Oversized > 0 {
- return ctx.readOversizedSint16(field.Oversized)
- }
- return ctx.readSint16()
- case common.TypeSInt32:
- return ctx.readSint32()
- case common.TypeSInt64:
- return ctx.readSint64()
- case common.TypeString8:
- return ctx.readEQ2String8()
- case common.TypeString16:
- return ctx.readEQ2String16()
- case common.TypeString32:
- return ctx.readEQ2String32()
- case common.TypeChar:
- return ctx.readBytes(field.Length)
- case common.TypeFloat:
- return ctx.readFloat32()
- case common.TypeDouble:
- return ctx.readFloat64()
- case common.TypeColor:
- return ctx.readEQ2Color()
- case common.TypeEquipment:
- return ctx.readEQ2Equipment()
- case common.TypeArray:
- size := ctx.getArraySize(field.Condition)
- if field.MaxArraySize > 0 && size > field.MaxArraySize {
- size = field.MaxArraySize
- }
- result := make([]map[string]any, size)
- for i := 0; i < size; i++ {
- ctx.pushArrayIndex(i)
- item, _ := field.SubDef.parseStruct(ctx)
- result[i] = item
- ctx.popArrayIndex()
- }
- return result
- }
- return nil
-}
-
-func (def *PacketDef) getVersionOrder(version uint32) []string {
- var bestVersion uint32
- for v := range def.Orders {
- if v <= version && v > bestVersion {
- bestVersion = v
- }
- }
- return def.Orders[bestVersion]
-}
-
-// FieldDesc describes a single packet field
-type FieldDesc struct {
- Type common.EQ2DataType // Primary data type
- Condition string // Conditional parsing expression
- Length int // Array length or size for fixed-size fields
- SubDef *PacketDef // Nested packet definition for arrays
- Type2 common.EQ2DataType // Alternative data type for conditional parsing
- Type2Cond string // Condition for using Type2
- Oversized int // Threshold for oversized field handling
- DefaultValue int8 // Default value for initialization
- MaxArraySize int // Maximum allowed array size
- Optional bool // Whether this field is optional
- AddToStruct bool // Whether to include in packet structure
- AddType common.EQ2DataType // Type to use when adding to packet
-}
diff --git a/structs/parser_test.go b/structs/parser_test.go
deleted file mode 100644
index 630b5c9..0000000
--- a/structs/parser_test.go
+++ /dev/null
@@ -1,370 +0,0 @@
-package packets
-
-import (
- "eq2emu/internal/packets/parser"
- "fmt"
- "path/filepath"
- "sort"
- "strings"
- "testing"
-)
-
-func TestParseAllXMLDefinitions(t *testing.T) {
- // Get all available packet names
- packetNames := GetPacketNames()
- if len(packetNames) == 0 {
- t.Fatal("No packet definitions loaded")
- }
-
- sort.Strings(packetNames)
-
- var failed []string
- var passed []string
- var skipped []string
-
- t.Logf("Testing %d packet definitions...", len(packetNames))
-
- for _, name := range packetNames {
- t.Run(name, func(t *testing.T) {
- def, exists := GetPacket(name)
- if !exists {
- t.Errorf("Packet definition '%s' not found", name)
- failed = append(failed, name)
- return
- }
-
- // Validate packet definition structure
- err := validatePacketDefinition(name, def)
- if err != nil {
- t.Errorf("Invalid packet definition '%s': %v", name, err)
- failed = append(failed, name)
- return
- }
-
- // Try to create sample data and test parsing round-trip
- err = testPacketRoundTrip(name, def)
- if err != nil {
- if strings.Contains(err.Error(), "complex condition") {
- t.Logf("Skipping '%s': %v", name, err)
- skipped = append(skipped, name)
- return
- }
- t.Errorf("Round-trip test failed for '%s': %v", name, err)
- failed = append(failed, name)
- return
- }
-
- passed = append(passed, name)
- t.Logf("Successfully validated '%s'", name)
- })
- }
-
- // Summary report
- t.Logf("\n=== PACKET VALIDATION SUMMARY ===")
- t.Logf("Total packets: %d", len(packetNames))
- t.Logf("Passed: %d", len(passed))
- t.Logf("Failed: %d", len(failed))
- t.Logf("Skipped (complex conditions): %d", len(skipped))
-
- if len(failed) > 0 {
- t.Logf("\nFailed packets:")
- for _, name := range failed {
- t.Logf(" - %s", name)
- }
- }
-
- if len(skipped) > 0 {
- t.Logf("\nSkipped packets (complex conditions):")
- for _, name := range skipped {
- t.Logf(" - %s", name)
- }
- }
-
- // Report by category
- categories := make(map[string]int)
- for _, name := range passed {
- category := getPacketCategory(name)
- categories[category]++
- }
-
- t.Logf("\nPassed packets by category:")
- var cats []string
- for cat := range categories {
- cats = append(cats, cat)
- }
- sort.Strings(cats)
- for _, cat := range cats {
- t.Logf(" %s: %d", cat, categories[cat])
- }
-
- // Only fail the test if we have actual parsing failures, not skipped ones
- if len(failed) > 0 {
- t.Errorf("%d packet definitions failed validation", len(failed))
- }
-}
-
-func TestPacketBuilderBasicFunctionality(t *testing.T) {
- // Test with a simple packet that we know should work
- testPackets := []string{
- "LoginRequest",
- "PlayRequest",
- "CreateCharacter",
- }
-
- for _, packetName := range testPackets {
- t.Run(packetName, func(t *testing.T) {
- def, exists := GetPacket(packetName)
- if !exists {
- t.Skipf("Packet '%s' not found - may not exist in current definitions", packetName)
- return
- }
-
- // Create minimal test data
- data := createMinimalTestData(def)
-
- // Test builder - just ensure it can build without error
- builder := NewPacketBuilder(def, 1, 0)
- packetData, err := builder.Build(data)
- if err != nil {
- t.Errorf("Failed to build packet '%s': %v", packetName, err)
- return
- }
-
- if len(packetData) == 0 {
- t.Errorf("Built packet '%s' is empty", packetName)
- return
- }
-
- t.Logf("Successfully built packet '%s' (%d bytes)", packetName, len(packetData))
-
- // Skip parsing back for now due to complex conditions - just test building
- })
- }
-}
-
-func TestPacketDefinitionCoverage(t *testing.T) {
- // Test that we have reasonable coverage of packet types
- packetNames := GetPacketNames()
-
- categories := map[string]int{
- "login": 0,
- "world": 0,
- "item": 0,
- "spawn": 0,
- "common": 0,
- "other": 0,
- }
-
- for _, name := range packetNames {
- category := getPacketCategory(name)
- if count, exists := categories[category]; exists {
- categories[category] = count + 1
- } else {
- categories["other"]++
- }
- }
-
- t.Logf("Packet coverage by category:")
- for cat, count := range categories {
- t.Logf(" %s: %d packets", cat, count)
- }
-
- // Ensure we have some packets in each major category
- if categories["world"] == 0 {
- t.Error("No world packets found")
- }
- if categories["login"] == 0 {
- t.Error("No login packets found")
- }
- if categories["item"] == 0 {
- t.Error("No item packets found")
- }
-}
-
-// Helper functions
-
-func validatePacketDefinition(name string, def *parser.PacketDef) error {
- if def == nil {
- return fmt.Errorf("packet definition is nil")
- }
-
- if len(def.Fields) == 0 {
- return fmt.Errorf("packet has no fields defined")
- }
-
- if len(def.Orders) == 0 {
- return fmt.Errorf("packet has no field ordering defined")
- }
-
- // Check that all fields referenced in orders exist
- for version, order := range def.Orders {
- for _, fieldName := range order {
- if _, exists := def.Fields[fieldName]; !exists {
- return fmt.Errorf("field '%s' referenced in version %d order but not defined", fieldName, version)
- }
- }
- }
-
- // Validate field definitions
- for fieldName, field := range def.Fields {
- err := validateFieldDefinition(fieldName, field)
- if err != nil {
- return fmt.Errorf("invalid field '%s': %w", fieldName, err)
- }
- }
-
- return nil
-}
-
-func validateFieldDefinition(name string, field parser.FieldDesc) error {
- // Check that field type is valid
- if field.Type < 0 || field.Type > 25 { // Adjust range based on actual enum
- return fmt.Errorf("invalid field type %d", field.Type)
- }
-
- // If Type2 is set, Type2Cond should also be set
- if field.Type2 != 0 && field.Type2Cond == "" {
- return fmt.Errorf("field has Type2 but no Type2Cond")
- }
-
- return nil
-}
-
-func testPacketRoundTrip(name string, def *parser.PacketDef) error {
- // Skip packets with complex conditions that would be hard to satisfy
- if hasComplexConditions(def) {
- return fmt.Errorf("complex condition detected - skipping round-trip test")
- }
-
- // For now, just validate the structure without round-trip testing
- // This is safer until we have better condition handling
- return nil
-}
-
-func hasComplexConditions(def *parser.PacketDef) bool {
- for _, field := range def.Fields {
- if strings.Contains(field.Condition, ">=") ||
- strings.Contains(field.Condition, "<=") ||
- strings.Contains(field.Condition, "!=") ||
- strings.Contains(field.Condition, "&&") ||
- strings.Contains(field.Condition, "||") ||
- strings.Contains(field.Type2Cond, ">=") ||
- strings.Contains(field.Type2Cond, "<=") {
- return true
- }
- }
- return false
-}
-
-func createMinimalTestData(def *parser.PacketDef) map[string]any {
- data := make(map[string]any)
-
- // Get the field order for version 1 (or first available version)
- var version uint32 = 1
- if len(def.Orders) > 0 {
- for v := range def.Orders {
- version = v
- break
- }
- }
-
- order, exists := def.Orders[version]
- if !exists && len(def.Orders) > 0 {
- // Take first available version
- for v, o := range def.Orders {
- version = v
- order = o
- break
- }
- }
-
- for _, fieldName := range order {
- field, fieldExists := def.Fields[fieldName]
- if !fieldExists {
- continue
- }
-
- // Skip fields with complex conditions
- if field.Condition != "" &&
- (strings.Contains(field.Condition, ">=") ||
- strings.Contains(field.Condition, "<=") ||
- strings.Contains(field.Condition, "!=")) {
- continue
- }
-
- // Create minimal test data based on field type
- switch field.Type {
- case 1: // TypeInt8
- data[fieldName] = uint8(1)
- case 2: // TypeInt16
- data[fieldName] = uint16(1)
- case 3: // TypeInt32
- data[fieldName] = uint32(1)
- case 4: // TypeInt64
- data[fieldName] = uint64(1)
- case 5: // TypeFloat
- data[fieldName] = float32(1.0)
- case 6: // TypeDouble
- data[fieldName] = float64(1.0)
- case 8: // TypeSInt8
- data[fieldName] = int8(1)
- case 9: // TypeSInt16
- data[fieldName] = int16(1)
- case 10: // TypeSInt32
- data[fieldName] = int32(1)
- case 12: // TypeChar
- if field.Length > 0 {
- data[fieldName] = make([]byte, field.Length)
- } else {
- data[fieldName] = []byte("test")
- }
- case 13, 14, 15: // String types
- data[fieldName] = "test"
- case 17: // TypeArray
- data[fieldName] = []map[string]any{}
- case 25: // TypeSInt64
- data[fieldName] = int64(1)
- }
- }
-
- return data
-}
-
-func getPacketCategory(packetName string) string {
- name := strings.ToLower(packetName)
-
- if strings.Contains(name, "login") || strings.Contains(name, "play") ||
- strings.Contains(name, "world") && (strings.Contains(name, "list") || strings.Contains(name, "update")) {
- return "login"
- }
-
- if strings.Contains(name, "item") || strings.Contains(name, "inventory") ||
- strings.Contains(name, "merchant") || strings.Contains(name, "loot") {
- return "item"
- }
-
- if strings.Contains(name, "spawn") || strings.Contains(name, "position") {
- return "spawn"
- }
-
- if strings.Contains(name, "character") || strings.Contains(name, "create") {
- return "common"
- }
-
- // Determine by file path if we have it
- dir := filepath.Dir(packetName)
- switch {
- case strings.Contains(dir, "login"):
- return "login"
- case strings.Contains(dir, "world"):
- return "world"
- case strings.Contains(dir, "item"):
- return "item"
- case strings.Contains(dir, "spawn"):
- return "spawn"
- case strings.Contains(dir, "common"):
- return "common"
- default:
- return "world" // Most packets are world packets
- }
-}
diff --git a/structs/reader.go b/structs/reader.go
deleted file mode 100644
index e0b444e..0000000
--- a/structs/reader.go
+++ /dev/null
@@ -1,255 +0,0 @@
-package packets
-
-import (
- "encoding/binary"
- "eq2emu/internal/common"
- "eq2emu/internal/packets/parser"
- "fmt"
- "io"
- "math"
-)
-
-// PacketReader reads packet data based on packet definitions
-type PacketReader struct {
- data []byte
- pos int
-}
-
-// NewPacketReader creates a new packet reader
-func NewPacketReader(data []byte) *PacketReader {
- return &PacketReader{
- data: data,
- pos: 0,
- }
-}
-
-// ParsePacketFields parses packet data using a packet definition
-func ParsePacketFields(data []byte, packetName string, version uint32) (map[string]any, error) {
- def, exists := GetPacket(packetName)
- if !exists {
- return nil, fmt.Errorf("packet definition '%s' not found", packetName)
- }
-
- reader := NewPacketReader(data)
- return reader.parseStruct(def, version)
-}
-
-// parseStruct parses a struct according to packet definition
-func (r *PacketReader) parseStruct(def *parser.PacketDef, version uint32) (map[string]any, error) {
- result := make(map[string]any)
-
- // Get field order for this version
- order := r.getVersionOrder(def, version)
-
- for _, fieldName := range order {
- field, exists := def.Fields[fieldName]
- if !exists {
- continue
- }
-
- // For simplicity, skip conditional fields for now
- if field.Condition != "" {
- continue
- }
-
- fieldType := field.Type
- if field.Type2 != 0 {
- fieldType = field.Type2
- }
-
- value, err := r.readField(field, fieldType, fieldName, result)
- if err != nil {
- return nil, fmt.Errorf("error reading field '%s': %w", fieldName, err)
- }
-
- if value != nil {
- result[fieldName] = value
- }
- }
-
- return result, nil
-}
-
-// readField reads a single field from the packet data
-func (r *PacketReader) readField(field parser.FieldDesc, fieldType common.EQ2DataType, fieldName string, context map[string]any) (any, error) {
- switch fieldType {
- case common.TypeInt8:
- return r.readUint8()
- case common.TypeInt16:
- return r.readUint16()
- case common.TypeInt32:
- return r.readUint32()
- case common.TypeInt64:
- return r.readUint64()
- case common.TypeSInt8:
- return r.readInt8()
- case common.TypeSInt16:
- return r.readInt16()
- case common.TypeSInt32:
- return r.readInt32()
- case common.TypeSInt64:
- return r.readInt64()
- case common.TypeString8:
- return r.readEQ2String8()
- case common.TypeString16:
- return r.readEQ2String16()
- case common.TypeString32:
- return r.readEQ2String32()
- case common.TypeFloat:
- return r.readFloat32()
- case common.TypeDouble:
- return r.readFloat64()
- case common.TypeChar:
- if field.Length > 0 {
- return r.readBytes(field.Length)
- }
- return nil, fmt.Errorf("char field '%s' has no length specified", fieldName)
- default:
- // For unsupported types, skip the field
- return nil, nil
- }
-}
-
-// Low-level read functions
-func (r *PacketReader) readUint8() (uint8, error) {
- if r.pos+1 > len(r.data) {
- return 0, io.EOF
- }
- value := r.data[r.pos]
- r.pos++
- return value, nil
-}
-
-func (r *PacketReader) readInt8() (int8, error) {
- value, err := r.readUint8()
- return int8(value), err
-}
-
-func (r *PacketReader) readUint16() (uint16, error) {
- if r.pos+2 > len(r.data) {
- return 0, io.EOF
- }
- value := binary.LittleEndian.Uint16(r.data[r.pos:])
- r.pos += 2
- return value, nil
-}
-
-func (r *PacketReader) readInt16() (int16, error) {
- value, err := r.readUint16()
- return int16(value), err
-}
-
-func (r *PacketReader) readUint32() (uint32, error) {
- if r.pos+4 > len(r.data) {
- return 0, io.EOF
- }
- value := binary.LittleEndian.Uint32(r.data[r.pos:])
- r.pos += 4
- return value, nil
-}
-
-func (r *PacketReader) readInt32() (int32, error) {
- value, err := r.readUint32()
- return int32(value), err
-}
-
-func (r *PacketReader) readUint64() (uint64, error) {
- if r.pos+8 > len(r.data) {
- return 0, io.EOF
- }
- value := binary.LittleEndian.Uint64(r.data[r.pos:])
- r.pos += 8
- return value, nil
-}
-
-func (r *PacketReader) readInt64() (int64, error) {
- value, err := r.readUint64()
- return int64(value), err
-}
-
-func (r *PacketReader) readFloat32() (float32, error) {
- if r.pos+4 > len(r.data) {
- return 0, io.EOF
- }
- bits := binary.LittleEndian.Uint32(r.data[r.pos:])
- r.pos += 4
- return math.Float32frombits(bits), nil
-}
-
-func (r *PacketReader) readFloat64() (float64, error) {
- if r.pos+8 > len(r.data) {
- return 0, io.EOF
- }
- bits := binary.LittleEndian.Uint64(r.data[r.pos:])
- r.pos += 8
- return math.Float64frombits(bits), nil
-}
-
-func (r *PacketReader) readBytes(n int) ([]byte, error) {
- if r.pos+n > len(r.data) {
- return nil, io.EOF
- }
- data := make([]byte, n)
- copy(data, r.data[r.pos:r.pos+n])
- r.pos += n
- return data, nil
-}
-
-func (r *PacketReader) readEQ2String8() (string, error) {
- length, err := r.readUint8()
- if err != nil {
- return "", err
- }
- if length == 0 {
- return "", nil
- }
- data, err := r.readBytes(int(length))
- if err != nil {
- return "", err
- }
- return string(data), nil
-}
-
-func (r *PacketReader) readEQ2String16() (string, error) {
- length, err := r.readUint16()
- if err != nil {
- return "", err
- }
- if length == 0 {
- return "", nil
- }
- data, err := r.readBytes(int(length))
- if err != nil {
- return "", err
- }
- return string(data), nil
-}
-
-func (r *PacketReader) readEQ2String32() (string, error) {
- length, err := r.readUint32()
- if err != nil {
- return "", err
- }
- if length == 0 {
- return "", nil
- }
- data, err := r.readBytes(int(length))
- if err != nil {
- return "", err
- }
- return string(data), nil
-}
-
-// getVersionOrder returns the field order for the specified version
-func (r *PacketReader) getVersionOrder(def *parser.PacketDef, version uint32) []string {
- var bestVersion uint32
- for v := range def.Orders {
- if v <= version && v > bestVersion {
- bestVersion = v
- }
- }
- if order, exists := def.Orders[bestVersion]; exists {
- return order
- }
- return []string{}
-}
\ No newline at end of file