delete old struct parser
This commit is contained in:
parent
0b8c794692
commit
f8236efffd
@ -1,284 +0,0 @@
|
||||
# Packet Definition Parser
|
||||
|
||||
Fast XML-like parser for binary packet structures with versioning and conditional fields.
|
||||
|
||||
## Basic Syntax
|
||||
|
||||
```xml
|
||||
<packet name="PacketName">
|
||||
<version number="1">
|
||||
<i32 name="player_id">
|
||||
<str16 name="player_name">
|
||||
<color name="skin_color">
|
||||
</version>
|
||||
</packet>
|
||||
```
|
||||
|
||||
## 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
|
||||
<u32 name="player_id,account_id">
|
||||
<f32 name="pos_x,pos_y,pos_z">
|
||||
```
|
||||
|
||||
## Conditional Fields
|
||||
|
||||
```xml
|
||||
<str16 name="guild_name" if="flag:has_guild">
|
||||
<u8 name="enhancement" if="item_type!=0">
|
||||
<color name="aura" if="special_flags&0x01">
|
||||
<str16 name="description" if="description!>0">
|
||||
```
|
||||
|
||||
### 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
|
||||
<group name="appearance">
|
||||
<color name="skin_color,hair_color,eye_color">
|
||||
<str16 name="face_file,hair_file">
|
||||
</group>
|
||||
<!-- Creates: appearance_skin_color, appearance_hair_color, etc. -->
|
||||
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
Define reusable field groups that can be injected into packets:
|
||||
|
||||
```xml
|
||||
<!-- Define template -->
|
||||
<template name="position">
|
||||
<f32 name="x,y,z">
|
||||
<f32 name="heading">
|
||||
</template>
|
||||
|
||||
<template name="appearance">
|
||||
<color name="skin_color,hair_color,eye_color">
|
||||
<str16 name="face_file,hair_file">
|
||||
</template>
|
||||
|
||||
<!-- Use templates in packets -->
|
||||
<packet name="PlayerUpdate">
|
||||
<version number="1">
|
||||
<u32 name="player_id">
|
||||
<template use="position">
|
||||
<u8 name="level">
|
||||
<template use="appearance">
|
||||
</version>
|
||||
</packet>
|
||||
```
|
||||
|
||||
Templates work with groups for prefixing:
|
||||
|
||||
```xml
|
||||
<group name="current">
|
||||
<template use="position">
|
||||
</group>
|
||||
<!-- Creates: current_x, current_y, current_z, current_heading -->
|
||||
```
|
||||
|
||||
## Arrays
|
||||
|
||||
```xml
|
||||
<u8 name="item_count">
|
||||
<array name="items" count="var:item_count" max_size="100">
|
||||
<u32 name="item_id">
|
||||
<str16 name="item_name">
|
||||
</array>
|
||||
```
|
||||
|
||||
## Advanced Field Attributes
|
||||
|
||||
### Type Switching
|
||||
```xml
|
||||
<u32 name="stat_value" type2="f32" type2_if="stat_type==6">
|
||||
```
|
||||
|
||||
### Oversized Fields
|
||||
```xml
|
||||
<u16 name="large_count" oversized="255">
|
||||
<u32 name="huge_value" oversized="65535">
|
||||
```
|
||||
|
||||
### Field Modifiers
|
||||
```xml
|
||||
<u8 name="data_array" size="10" default="5">
|
||||
<str16 name="optional_text" optional="true">
|
||||
<u32 name="hidden_field" add_to_struct="false" add_type="i16">
|
||||
```
|
||||
|
||||
## 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
|
||||
<substruct name="ItemInfo">
|
||||
<u32 name="item_id">
|
||||
<str16 name="item_name">
|
||||
<u8 name="rarity">
|
||||
</substruct>
|
||||
|
||||
<packet name="Inventory">
|
||||
<version number="1">
|
||||
<u8 name="count">
|
||||
<array name="items" count="var:count" substruct="ItemInfo">
|
||||
</version>
|
||||
</packet>
|
||||
```
|
||||
|
||||
## Multiple Versions
|
||||
|
||||
```xml
|
||||
<packet name="PlayerInfo">
|
||||
<version number="1">
|
||||
<u32 name="id">
|
||||
<str16 name="name">
|
||||
</version>
|
||||
<version number="562">
|
||||
<u32 name="id">
|
||||
<str16 name="name">
|
||||
<color name="skin_color">
|
||||
</version>
|
||||
</packet>
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
```xml
|
||||
<!-- This is a comment -->
|
||||
<packet name="Test"> <!-- Inline comment -->
|
||||
<version number="1">
|
||||
<u32 name="id"> <!-- Field comment -->
|
||||
</version>
|
||||
</packet>
|
||||
```
|
||||
|
||||
## 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
|
||||
<!-- Define reusable templates -->
|
||||
<template name="position">
|
||||
<f32 name="x,y,z">
|
||||
<f32 name="heading">
|
||||
</template>
|
||||
|
||||
<template name="appearance">
|
||||
<color name="skin_color,hair_color,eye_color">
|
||||
<str16 name="face_file,hair_file">
|
||||
</template>
|
||||
|
||||
<substruct name="StatBonus">
|
||||
<u8 name="stat_type">
|
||||
<u32 name="base_value" type2="f32" type2_if="stat_type==6">
|
||||
<u16 name="bonus_value" if="stat_type!=6">
|
||||
</substruct>
|
||||
|
||||
<packet name="PlayerStats">
|
||||
<version number="562">
|
||||
<u32 name="player_id,account_id">
|
||||
<str16 name="player_name">
|
||||
<u8 name="level,race,class">
|
||||
<template use="position">
|
||||
<template use="appearance" if="version>=562">
|
||||
<str16 name="guild_name" if="flag:has_guild">
|
||||
<u32 name="guild_id" if="flag:has_guild&level>=10">
|
||||
<u8 name="stat_count">
|
||||
<array name="stats" count="var:stat_count" max_size="50" substruct="StatBonus">
|
||||
<u32 name="special_flags">
|
||||
<str16 name="special_ability" if="special_flags&0x01">
|
||||
<color name="aura_color" if="special_flags&0x02">
|
||||
<str16 name="description" if="description!>0">
|
||||
</version>
|
||||
</packet>
|
||||
```
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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",
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,933 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"eq2emu/internal/common"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBasicParsing(t *testing.T) {
|
||||
pml := `<packet name="Test">
|
||||
<version number="1">
|
||||
<i32 name="player_id">
|
||||
<str16 name="player_name">
|
||||
<color name="skin_color">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="FloatTest">
|
||||
<version number="1">
|
||||
<f32 name="position_x">
|
||||
<f64 name="precise_value">
|
||||
<double name="legacy_double">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="OversizedTest">
|
||||
<version number="1">
|
||||
<i8 name="small_count">
|
||||
<i16 name="num_words" oversized="255">
|
||||
<i32 name="large_value" oversized="65535">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="Type2Test">
|
||||
<version number="1">
|
||||
<i8 name="stat_type">
|
||||
<i32 name="stat_value" type2="f32" type2_if="stat_type!=6">
|
||||
<i16 name="another_field" type2="i32">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="AttributeTest">
|
||||
<version number="1">
|
||||
<i8 name="data_array" size="10" default="5">
|
||||
<str16 name="optional_text" if="var:has_text" optional="true">
|
||||
<i32 name="hidden_field" add_to_struct="false" add_type="i16">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="ArrayMaxTest">
|
||||
<version number="1">
|
||||
<i8 name="item_count">
|
||||
<array name="items" count="var:item_count" max_size="100">
|
||||
<i32 name="item_id">
|
||||
</array>
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="ArrayOptionalTest">
|
||||
<version number="1">
|
||||
<i8 name="count">
|
||||
<array name="optional_items" count="var:count" optional="true" add_to_struct="false">
|
||||
<i32 name="id">
|
||||
</array>
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="MultiVersion">
|
||||
<version number="1">
|
||||
<i32 name="id">
|
||||
<str16 name="name">
|
||||
</version>
|
||||
<version number="562">
|
||||
<i32 name="id">
|
||||
<str16 name="name">
|
||||
<color name="color">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="ArrayTest">
|
||||
<version number="1">
|
||||
<i8 name="item_count">
|
||||
<array name="items" count="var:item_count">
|
||||
<i32 name="item_id">
|
||||
<str16 name="item_name">
|
||||
</array>
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="ConditionalTest">
|
||||
<version number="1">
|
||||
<i32 name="player_id">
|
||||
<str16 name="guild_name" if="flag:has_guild">
|
||||
<i8 name="enhancement" if="item_type!=0">
|
||||
<color name="aura" if="special_flags&0x01">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="CommaTest">
|
||||
<version number="1">
|
||||
<i32 name="player_id,account_id">
|
||||
<f32 name="pos_x,pos_y,pos_z">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<substruct name="ItemInfo">
|
||||
<i32 name="item_id">
|
||||
<str16 name="item_name">
|
||||
</substruct>
|
||||
|
||||
<packet name="SubstructTest">
|
||||
<version number="1">
|
||||
<i8 name="count">
|
||||
<array name="items" count="var:count" substruct="ItemInfo">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="AttributeTest">
|
||||
<version number="1">
|
||||
<i8 name="data_array" size="10">
|
||||
<str16 name="optional_text" if="var:has_text">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<!-- This is a comment -->
|
||||
<packet name="CommentTest">
|
||||
<!-- Another comment -->
|
||||
<version number="1">
|
||||
<i32 name="id"> <!-- Inline comment -->
|
||||
<str16 name="name">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="BinaryFloat64">
|
||||
<version number="1">
|
||||
<f64 name="precise_value">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="BinaryOversized">
|
||||
<version number="1">
|
||||
<i16 name="normal_value">
|
||||
<i16 name="oversized_value" oversized="255">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="BinaryType2">
|
||||
<version number="1">
|
||||
<i8 name="stat_type">
|
||||
<i32 name="stat_value" type2="f32" type2_if="stat_type==6">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="BinaryArrayMax">
|
||||
<version number="1">
|
||||
<i8 name="item_count">
|
||||
<array name="items" count="var:item_count" max_size="2">
|
||||
<i16 name="item_id">
|
||||
</array>
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="ArrayConditionTest">
|
||||
<version number="1">
|
||||
<i8 name="stat_count">
|
||||
<array name="stats" count="var:stat_count">
|
||||
<i8 name="stat_type">
|
||||
<i32 name="base_value">
|
||||
<i32 name="modified_value" if="stat_type_%i>=1&stat_type_%i<=5">
|
||||
<f32 name="percentage" if="stat_type_%i==6">
|
||||
<str16 name="description" if="!var:stat_type_%i">
|
||||
</array>
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="ArrayConditionBinary">
|
||||
<version number="1">
|
||||
<i8 name="stat_count">
|
||||
<array name="stats" count="var:stat_count">
|
||||
<i8 name="stat_type">
|
||||
<i32 name="base_value">
|
||||
<f32 name="percentage" if="stat_type_%i==6">
|
||||
</array>
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="ComplexArrayTest">
|
||||
<version number="1">
|
||||
<i8 name="item_count">
|
||||
<array name="items" count="var:item_count">
|
||||
<i16 name="item_type">
|
||||
<i32 name="item_flags">
|
||||
<i8 name="enhancement" if="item_type_%i!=0&item_flags_%i&0x01">
|
||||
<color name="special_color" if="item_type_%i>=100,item_flags_%i&0x02">
|
||||
</array>
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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", "<packet name=\"Test\"><version number=\"1\"><i32 name=\"id\">"},
|
||||
{"Invalid XML", "<packet><version><i32></packet>"},
|
||||
{"Missing quotes", "<packet name=Test><version number=1></version></packet>"},
|
||||
{"Invalid oversized", "<packet name=\"Test\"><version number=\"1\"><i32 name=\"id\" oversized=\"abc\"></version></packet>"},
|
||||
}
|
||||
|
||||
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 := `<!-- Define reusable templates -->
|
||||
<template name="position">
|
||||
<f32 name="x,y,z">
|
||||
<f32 name="heading">
|
||||
</template>
|
||||
|
||||
<template name="appearance">
|
||||
<color name="skin_color,hair_color,eye_color">
|
||||
<str16 name="face_file,hair_file">
|
||||
</template>
|
||||
|
||||
<!-- Use templates in packet -->
|
||||
<packet name="PlayerUpdate">
|
||||
<version number="1">
|
||||
<i32 name="player_id">
|
||||
<template use="position">
|
||||
<i8 name="level">
|
||||
<template use="appearance">
|
||||
<str16 name="guild_name" if="flag:has_guild">
|
||||
</version>
|
||||
</packet>
|
||||
|
||||
<!-- Test template with group prefix -->
|
||||
<packet name="GroupedTemplate">
|
||||
<version number="1">
|
||||
<i32 name="entity_id">
|
||||
<group name="current">
|
||||
<template use="position">
|
||||
</group>
|
||||
<group name="target">
|
||||
<template use="position">
|
||||
</group>
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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 := `<packet name="Simple">
|
||||
<version number="1">
|
||||
<i32 name="id">
|
||||
<str16 name="name">
|
||||
<i8 name="level">
|
||||
<f32 name="x,y,z">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
for b.Loop() {
|
||||
_, err := Parse(pml)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMediumPacket(b *testing.B) {
|
||||
pml := `<packet name="Medium">
|
||||
<version number="1">
|
||||
<i32 name="id,account_id,session_id">
|
||||
<str16 name="name,guild_name">
|
||||
<i8 name="level,race,class">
|
||||
<f32 name="x,y,z,heading">
|
||||
<color name="skin_color,hair_color">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
for b.Loop() {
|
||||
_, err := Parse(pml)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLargePacket(b *testing.B) {
|
||||
pml := `<packet name="Large">
|
||||
<version number="1">
|
||||
<i32 name="a1,a2,a3,a4,a5,a6,a7,a8,a9,a10">
|
||||
<str16 name="s1,s2,s3,s4,s5">
|
||||
<f32 name="f1,f2,f3,f4,f5,f6,f7,f8">
|
||||
<color name="c1,c2,c3,c4">
|
||||
<i8 name="b1,b2,b3,b4,b5,b6,b7,b8,b9,b10,b11,b12">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
for b.Loop() {
|
||||
_, err := Parse(pml)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkArrayPacket(b *testing.B) {
|
||||
pml := `<packet name="ArrayPacket">
|
||||
<version number="1">
|
||||
<i8 name="count">
|
||||
<array name="items" count="var:count">
|
||||
<i32 name="id">
|
||||
<str16 name="name">
|
||||
<color name="color">
|
||||
<f32 name="x,y,z">
|
||||
</array>
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
for b.Loop() {
|
||||
_, err := Parse(pml)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMultiVersionPacket(b *testing.B) {
|
||||
pml := `<packet name="MultiVersion">
|
||||
<version number="1">
|
||||
<i32 name="id">
|
||||
<str16 name="name">
|
||||
</version>
|
||||
<version number="100">
|
||||
<i32 name="id">
|
||||
<str16 name="name">
|
||||
<i8 name="level">
|
||||
</version>
|
||||
<version number="500">
|
||||
<i32 name="id">
|
||||
<str16 name="name">
|
||||
<i8 name="level">
|
||||
<color name="color">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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{}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user