From d6f0ab6784ef9a5d8608cba1badaf6cfebf471f7 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Fri, 25 Jul 2025 23:16:30 -0500 Subject: [PATCH] split packet parser into modules --- internal/packets/parser/array_parser.go | 214 +++++ internal/packets/parser/conditions.go | 135 +++ internal/packets/parser/field_tag.go | 123 +++ internal/packets/parser/helpers.go | 191 ++++ internal/packets/parser/parser.go | 959 +------------------- internal/packets/parser/parser_test.go | 424 +++++++++ internal/packets/parser/type_readers.go | 435 +++++++++ internal/packets/parser/version_registry.go | 71 ++ 8 files changed, 1643 insertions(+), 909 deletions(-) create mode 100644 internal/packets/parser/array_parser.go create mode 100644 internal/packets/parser/conditions.go create mode 100644 internal/packets/parser/field_tag.go create mode 100644 internal/packets/parser/helpers.go create mode 100644 internal/packets/parser/parser_test.go create mode 100644 internal/packets/parser/type_readers.go create mode 100644 internal/packets/parser/version_registry.go diff --git a/internal/packets/parser/array_parser.go b/internal/packets/parser/array_parser.go new file mode 100644 index 0000000..01f3a36 --- /dev/null +++ b/internal/packets/parser/array_parser.go @@ -0,0 +1,214 @@ +package parser + +import ( + "fmt" + "reflect" +) + +// readArray handles array parsing with full substruct support +func (p *Parser) readArray(field reflect.Value, fieldTag *FieldTag) error { + var arraySize int + if fieldTag.ArraySizeVar != "" { + arraySize = p.getDynamicLength(fieldTag.ArraySizeVar) + } else { + size, err := p.readUint32() + if err != nil { + return err + } + arraySize = int(size) + } + + if arraySize == 0 { + return nil + } + + if field.Kind() == reflect.Slice { + elemType := field.Type().Elem() + slice := reflect.MakeSlice(field.Type(), arraySize, arraySize) + + p.arrayStack = append(p.arrayStack, ArrayContext{ + elementType: elemType, + totalSize: arraySize, + sizeVariable: fieldTag.ArraySizeVar, + }) + + for i := 0; i < arraySize; i++ { + p.arrayStack[len(p.arrayStack)-1].currentIndex = i + elem := slice.Index(i) + + if elemType.Kind() == reflect.Struct { + if err := p.parseStructElement(elem, elemType); err != nil { + p.arrayStack = p.arrayStack[:len(p.arrayStack)-1] + return fmt.Errorf("array element %d: %w", i, err) + } + } else { + if err := p.readPrimitiveArrayElement(elem, elemType); err != nil { + p.arrayStack = p.arrayStack[:len(p.arrayStack)-1] + return fmt.Errorf("array element %d: %w", i, err) + } + } + } + + p.arrayStack = p.arrayStack[:len(p.arrayStack)-1] + field.Set(slice) + } + + return nil +} + +// readSubstruct handles substruct parsing +func (p *Parser) readSubstruct(field reflect.Value, length int) error { + if field.Kind() == reflect.Slice { + elemType := field.Type().Elem() + slice := reflect.MakeSlice(field.Type(), length, length) + + for i := 0; i < length; i++ { + elem := slice.Index(i) + if err := p.parseStructElement(elem, elemType); err != nil { + return fmt.Errorf("substruct element %d: %w", i, err) + } + } + + field.Set(slice) + return nil + } else if field.Kind() == reflect.Struct { + return p.parseStructElement(field, field.Type()) + } else if field.Kind() == reflect.Ptr && field.Type().Elem().Kind() == reflect.Struct { + structType := field.Type().Elem() + newStruct := reflect.New(structType) + if err := p.parseStructElement(newStruct.Elem(), structType); err != nil { + return err + } + field.Set(newStruct) + return nil + } + + return fmt.Errorf("substruct field must be struct, slice of structs, or pointer to struct") +} + +// parseStructElement parses a single struct element +func (p *Parser) parseStructElement(elem reflect.Value, elemType reflect.Type) error { + oldStruct := p.currentStruct + oldCache := p.fieldCache + + p.currentStruct = elem + p.fieldCache = make(map[string]any) + p.structStack = append(p.structStack, elem) + + for i := 0; i < elem.NumField(); i++ { + field := elem.Field(i) + fieldType := elemType.Field(i) + + if !field.CanSet() { + continue + } + + tag := fieldType.Tag.Get("eq2") + if tag == "" || tag == "-" { + continue + } + + if err := p.parseField(field, tag); err != nil { + p.structStack = p.structStack[:len(p.structStack)-1] + p.currentStruct = oldStruct + p.fieldCache = oldCache + return fmt.Errorf("field %s: %w", fieldType.Name, err) + } + + if field.CanInterface() { + p.fieldCache[fieldType.Name] = field.Interface() + } + } + + p.structStack = p.structStack[:len(p.structStack)-1] + p.currentStruct = oldStruct + p.fieldCache = oldCache + + return nil +} + +// readPrimitiveArrayElement reads primitive array elements +func (p *Parser) readPrimitiveArrayElement(elem reflect.Value, elemType reflect.Type) error { + switch elemType.Kind() { + case reflect.Uint8: + val, err := p.readUint8() + if err != nil { + return err + } + elem.SetUint(uint64(val)) + case reflect.Uint16: + val, err := p.readUint16() + if err != nil { + return err + } + elem.SetUint(uint64(val)) + case reflect.Uint32: + val, err := p.readUint32() + if err != nil { + return err + } + elem.SetUint(uint64(val)) + case reflect.Uint64: + val, err := p.readUint64() + if err != nil { + return err + } + elem.SetUint(val) + case reflect.Int8: + val, err := p.readUint8() + if err != nil { + return err + } + elem.SetInt(int64(int8(val))) + case reflect.Int16: + val, err := p.readUint16() + if err != nil { + return err + } + elem.SetInt(int64(int16(val))) + case reflect.Int32: + val, err := p.readUint32() + if err != nil { + return err + } + elem.SetInt(int64(int32(val))) + case reflect.Int64: + val, err := p.readUint64() + if err != nil { + return err + } + elem.SetInt(int64(val)) + case reflect.Float32: + val, err := p.readFloat32() + if err != nil { + return err + } + elem.SetFloat(float64(val)) + case reflect.Float64: + val, err := p.readFloat64() + if err != nil { + return err + } + elem.SetFloat(val) + default: + return fmt.Errorf("unsupported primitive array element type: %v", elemType.Kind()) + } + + return nil +} + +// GetCurrentArrayIndex returns current array index for nested parsing +func (p *Parser) GetCurrentArrayIndex() int { + if len(p.arrayStack) > 0 { + return p.arrayStack[len(p.arrayStack)-1].currentIndex + } + return -1 +} + +// GetCurrentArraySize returns current array size +func (p *Parser) GetCurrentArraySize() int { + if len(p.arrayStack) > 0 { + return p.arrayStack[len(p.arrayStack)-1].totalSize + } + return 0 +} diff --git a/internal/packets/parser/conditions.go b/internal/packets/parser/conditions.go new file mode 100644 index 0000000..dec7a4a --- /dev/null +++ b/internal/packets/parser/conditions.go @@ -0,0 +1,135 @@ +package parser + +import ( + "reflect" + "strings" +) + +// evaluateAllConditions checks all conditional logic +func (p *Parser) evaluateAllConditions(fieldTag *FieldTag, field reflect.Value) bool { + if fieldTag.Condition != nil && !p.evaluateCondition(fieldTag.Condition) { + return false + } + + if fieldTag.IfVariableSet != "" && !p.isVariableSet(fieldTag.IfVariableSet) { + return false + } + + if fieldTag.IfVariableNotSet != "" && p.isVariableSet(fieldTag.IfVariableNotSet) { + return false + } + + if fieldTag.IfFlag != "" && !p.flags[fieldTag.IfFlag] { + return false + } + + if fieldTag.IfFlagNotSet != "" && p.flags[fieldTag.IfFlagNotSet] { + return false + } + + if fieldTag.IfEquals != "" { + parts := strings.Split(fieldTag.IfEquals, "=") + if len(parts) == 2 && !p.evaluateEqualsCondition(parts[0], parts[1]) { + return false + } + } + + if fieldTag.IfNotEquals != "" { + parts := strings.Split(fieldTag.IfNotEquals, "=") + if len(parts) == 2 && p.evaluateEqualsCondition(parts[0], parts[1]) { + return false + } + } + + return true +} + +// evaluateCondition evaluates conditions for conditional fields +func (p *Parser) evaluateCondition(condition *FieldCondition) bool { + if condition.Type == "simple" { + return p.isVariableSet(condition.Variable) + } + + cachedValue, exists := p.fieldCache[condition.Variable] + if !exists { + if p.currentStruct.IsValid() { + if structField := p.currentStruct.FieldByName(condition.Variable); structField.IsValid() { + cachedValue = structField.Interface() + } else { + return false + } + } else { + return false + } + } + + compareValue, err := p.convertValue(condition.Value, cachedValue) + if err != nil { + return false + } + + return p.compareValues(cachedValue, compareValue, condition.Operator) +} + +// isVariableSet checks if a variable exists and has a truthy value +func (p *Parser) isVariableSet(variable string) bool { + if cachedValue, exists := p.fieldCache[variable]; exists { + return p.isTruthy(cachedValue) + } + + if p.currentStruct.IsValid() { + if field := p.currentStruct.FieldByName(variable); field.IsValid() { + return p.isTruthy(field.Interface()) + } + } + + return false +} + +// evaluateEqualsCondition checks field equality +func (p *Parser) evaluateEqualsCondition(variable, value string) bool { + cachedValue, exists := p.fieldCache[variable] + if !exists && p.currentStruct.IsValid() { + if field := p.currentStruct.FieldByName(variable); field.IsValid() { + cachedValue = field.Interface() + exists = true + } + } + + if !exists { + return false + } + + compareValue, err := p.convertValue(value, cachedValue) + if err != nil { + return false + } + + return p.compareValues(cachedValue, compareValue, "==") +} + +// evaluateType2Criteria evaluates type2 criteria for alternative types +func (p *Parser) evaluateType2Criteria(criteria string) bool { + operators := []string{"!=", "==", ">=", "<=", ">", "<"} + + for _, op := range operators { + if idx := strings.Index(criteria, op); idx > 0 { + fieldName := strings.TrimSpace(criteria[:idx]) + valueStr := strings.TrimSpace(criteria[idx+len(op):]) + + cachedValue, exists := p.fieldCache[fieldName] + if !exists { + return false + } + + compareValue, err := p.convertValue(valueStr, cachedValue) + if err != nil { + return false + } + + return p.compareValues(cachedValue, compareValue, op) + } + } + + return false +} diff --git a/internal/packets/parser/field_tag.go b/internal/packets/parser/field_tag.go new file mode 100644 index 0000000..8f1b52c --- /dev/null +++ b/internal/packets/parser/field_tag.go @@ -0,0 +1,123 @@ +package parser + +import ( + "strconv" + "strings" +) + +// FieldTag represents parsed tag parameters +type FieldTag struct { + Type string + Length int + DynamicLen string + Condition *FieldCondition + MaxSize int + SkipOversized bool + OversizedValue int + OversizedByte byte + Type2 string + Type2Criteria string + ArraySizeVar string + IfVariableSet string + IfVariableNotSet string + IfFlag string + IfFlagNotSet string + IfEquals string + IfNotEquals string + Optional bool +} + +// FieldCondition represents XML conditional logic +type FieldCondition struct { + Type string // "if_set", "if_not_set", "if_flag", etc. + Variable string + Value string + Operator string +} + +// parseFieldTag extracts all tag parameters +func (p *Parser) parseFieldTag(tag string) *FieldTag { + parts := strings.Split(tag, ",") + fieldTag := &FieldTag{ + Type: parts[0], + Length: 1, + MaxSize: -1, + OversizedValue: -1, + OversizedByte: 127, + } + + for _, part := range parts[1:] { + p.parseTagParameter(fieldTag, part) + } + + return fieldTag +} + +// parseTagParameter processes individual tag parameters +func (p *Parser) parseTagParameter(fieldTag *FieldTag, part string) { + switch { + case strings.HasPrefix(part, "len="): + lenVal := part[4:] + if l, err := strconv.Atoi(lenVal); err == nil { + fieldTag.Length = l + } else { + fieldTag.DynamicLen = lenVal + } + case strings.HasPrefix(part, "if="): + fieldTag.Condition = p.parseCondition(part[3:]) + case strings.HasPrefix(part, "maxsize="): + if m, err := strconv.Atoi(part[8:]); err == nil { + fieldTag.MaxSize = m + } + case part == "skipoversized": + fieldTag.SkipOversized = true + case strings.HasPrefix(part, "oversized="): + if o, err := strconv.Atoi(part[10:]); err == nil { + fieldTag.OversizedValue = o + } + case strings.HasPrefix(part, "oversizedbyte="): + if o, err := strconv.Atoi(part[14:]); err == nil { + fieldTag.OversizedByte = byte(o) + } + case strings.HasPrefix(part, "type2="): + fieldTag.Type2 = part[6:] + case strings.HasPrefix(part, "type2criteria="): + fieldTag.Type2Criteria = part[14:] + case strings.HasPrefix(part, "arraysize="): + fieldTag.ArraySizeVar = part[10:] + case strings.HasPrefix(part, "ifvariableset="): + fieldTag.IfVariableSet = part[14:] + case strings.HasPrefix(part, "ifvariablenotset="): + fieldTag.IfVariableNotSet = part[17:] + case strings.HasPrefix(part, "ifflag="): + fieldTag.IfFlag = part[7:] + case strings.HasPrefix(part, "ifflagnotset="): + fieldTag.IfFlagNotSet = part[13:] + case strings.HasPrefix(part, "ifequals="): + fieldTag.IfEquals = part[9:] + case strings.HasPrefix(part, "ifnotequals="): + fieldTag.IfNotEquals = part[12:] + case part == "optional": + fieldTag.Optional = true + } +} + +// parseCondition creates FieldCondition from condition string +func (p *Parser) parseCondition(condition string) *FieldCondition { + operators := []string{"!=", "==", ">=", "<=", ">", "<", "&", "|"} + + for _, op := range operators { + if idx := strings.Index(condition, op); idx > 0 { + return &FieldCondition{ + Variable: strings.TrimSpace(condition[:idx]), + Value: strings.TrimSpace(condition[idx+len(op):]), + Operator: op, + } + } + } + + return &FieldCondition{ + Type: "simple", + Variable: condition, + } +} diff --git a/internal/packets/parser/helpers.go b/internal/packets/parser/helpers.go new file mode 100644 index 0000000..d529f85 --- /dev/null +++ b/internal/packets/parser/helpers.go @@ -0,0 +1,191 @@ +package parser + +import ( + "reflect" + "strconv" +) + +// getDynamicLength gets length from another field with stack support +func (p *Parser) getDynamicLength(fieldName string) int { + if cachedValue, exists := p.fieldCache[fieldName]; exists { + return p.valueToInt(cachedValue) + } + + if p.currentStruct.IsValid() { + if field := p.currentStruct.FieldByName(fieldName); field.IsValid() { + return p.valueToInt(field.Interface()) + } + } + + for i := len(p.structStack) - 1; i >= 0; i-- { + if field := p.structStack[i].FieldByName(fieldName); field.IsValid() { + return p.valueToInt(field.Interface()) + } + } + + return 0 +} + +// convertValue converts string to appropriate type for comparison +func (p *Parser) convertValue(valueStr string, reference any) (any, error) { + switch reference.(type) { + case uint8, int8: + if val, err := strconv.ParseInt(valueStr, 0, 8); err == nil { + return uint8(val), nil + } + case uint16, int16: + if val, err := strconv.ParseInt(valueStr, 0, 16); err == nil { + return uint16(val), nil + } + case uint32, int32: + if val, err := strconv.ParseInt(valueStr, 0, 32); err == nil { + return uint32(val), nil + } + case uint64, int64: + if val, err := strconv.ParseInt(valueStr, 0, 64); err == nil { + return uint64(val), nil + } + case float32: + if val, err := strconv.ParseFloat(valueStr, 32); err == nil { + return float32(val), nil + } + case float64: + if val, err := strconv.ParseFloat(valueStr, 64); err == nil { + return val, nil + } + case string: + return valueStr, nil + } + + if val, err := strconv.ParseInt(valueStr, 0, 32); err == nil { + return int(val), nil + } + + return valueStr, nil +} + +// compareValues performs comparison between two values +func (p *Parser) compareValues(a, b any, op string) bool { + aVal := p.valueToInt64(a) + bVal := p.valueToInt64(b) + + switch op { + case "==": + return aVal == bVal + case "!=": + return aVal != bVal + case ">": + return aVal > bVal + case ">=": + return aVal >= bVal + case "<": + return aVal < bVal + case "<=": + return aVal <= bVal + case "&": + return (aVal & bVal) != 0 + case "|": + return (aVal | bVal) != 0 + } + + return false +} + +// valueToInt converts various types to int +func (p *Parser) valueToInt(v any) int { + switch val := v.(type) { + case uint8: + return int(val) + case int8: + return int(val) + case uint16: + return int(val) + case int16: + return int(val) + case uint32: + return int(val) + case int32: + return int(val) + case uint64: + return int(val) + case int64: + return int(val) + case int: + return val + } + return 0 +} + +// valueToInt64 converts various types to int64 for comparison +func (p *Parser) valueToInt64(v any) int64 { + switch val := v.(type) { + case uint8: + return int64(val) + case int8: + return int64(val) + case uint16: + return int64(val) + case int16: + return int64(val) + case uint32: + return int64(val) + case int32: + return int64(val) + case uint64: + return int64(val) + case int64: + return val + case int: + return int64(val) + case float32: + return int64(val) + case float64: + return int64(val) + } + return 0 +} + +// isTruthy checks if a value is truthy +func (p *Parser) isTruthy(v any) bool { + switch val := v.(type) { + case bool: + return val + case uint8, int8, uint16, int16, uint32, int32, uint64, int64, int: + return p.valueToInt64(val) != 0 + case string: + return val != "" + } + return false +} + +// handleOversizedField handles fields that exceed their oversized value +func (p *Parser) handleOversizedField(field reflect.Value, oversizedByte byte, length int) error { + if field.Kind() == reflect.Slice { + slice := reflect.MakeSlice(field.Type(), 1, 1) + if slice.Len() > 0 { + elem := slice.Index(0) + if elem.CanSet() { + switch elem.Kind() { + case reflect.Uint8: + elem.SetUint(uint64(oversizedByte)) + case reflect.Int8: + elem.SetInt(int64(int8(oversizedByte))) + default: + elem.SetUint(uint64(oversizedByte)) + } + } + } + field.Set(slice) + } else { + switch field.Kind() { + case reflect.Uint8: + field.SetUint(uint64(oversizedByte)) + case reflect.Int8: + field.SetInt(int64(int8(oversizedByte))) + default: + field.SetUint(uint64(oversizedByte)) + } + } + + return nil +} diff --git a/internal/packets/parser/parser.go b/internal/packets/parser/parser.go index db258a2..3580703 100644 --- a/internal/packets/parser/parser.go +++ b/internal/packets/parser/parser.go @@ -2,12 +2,9 @@ package parser import ( "encoding/binary" - "eq2emu/internal/common" "fmt" "io" "reflect" - "strconv" - "strings" "unsafe" ) @@ -16,85 +13,37 @@ type Parser struct { data []byte offset int endian binary.ByteOrder - fieldCache map[string]any // Cache parsed field values for conditions - currentStruct reflect.Value // Current struct being parsed + fieldCache map[string]any + currentStruct reflect.Value + flags map[string]bool + structStack []reflect.Value + arrayStack []ArrayContext +} + +// ArrayContext tracks array parsing state +type ArrayContext struct { + elementType reflect.Type + currentIndex int + totalSize int + sizeVariable string } // NewParser creates a new EQ2 packet parser func NewParser(data []byte) *Parser { return &Parser{ - data: data, - offset: 0, - endian: binary.LittleEndian, - fieldCache: make(map[string]any), + data: data, + offset: 0, + endian: binary.LittleEndian, + fieldCache: make(map[string]any), + flags: make(map[string]bool), + structStack: make([]reflect.Value, 0), + arrayStack: make([]ArrayContext, 0), } } -// VersionRegistry manages version-specific struct types -type VersionRegistry struct { - structs map[string]map[string]reflect.Type // [structName][version] = Type -} - -func NewVersionRegistry() *VersionRegistry { - return &VersionRegistry{ - structs: make(map[string]map[string]reflect.Type), - } -} - -// RegisterStruct registers a struct type for a specific version -func (vr *VersionRegistry) RegisterStruct(name, version string, structType reflect.Type) { - if vr.structs[name] == nil { - vr.structs[name] = make(map[string]reflect.Type) - } - vr.structs[name][version] = structType -} - -// GetStruct returns the appropriate struct type for a version -func (vr *VersionRegistry) GetStruct(name, version string) (reflect.Type, bool) { - if versions, exists := vr.structs[name]; exists { - if structType, exists := versions[version]; exists { - return structType, true - } - // Fallback to nearest lower version - return vr.findNearestVersion(name, version) - } - return nil, false -} - -// findNearestVersion finds the closest version <= requested version -func (vr *VersionRegistry) findNearestVersion(name, targetVersion string) (reflect.Type, bool) { - versions := vr.structs[name] - var bestVersion string - var bestType reflect.Type - - for version, structType := range versions { - if version <= targetVersion && version > bestVersion { - bestVersion = version - bestType = structType - } - } - - return bestType, bestVersion != "" -} - -// ParseWithVersion parses using version-specific struct -func (p *Parser) ParseWithVersion(registry *VersionRegistry, structName, version string) (any, error) { - structType, exists := registry.GetStruct(structName, version) - if !exists { - return nil, fmt.Errorf("no struct found for %s version %s", structName, version) - } - - // Create new instance - ptr := reflect.New(structType) - elem := ptr.Elem() - - // Parse into the struct - err := p.ParseStruct(ptr.Interface()) - if err != nil { - return nil, err - } - - return elem.Interface(), nil +// SetFlag sets a flag for conditional parsing +func (p *Parser) SetFlag(name string, value bool) { + p.flags[name] = value } // ParseStruct reads data into a struct using reflection and tags @@ -107,7 +56,7 @@ func (p *Parser) ParseStruct(v any) error { elem := val.Elem() typ := elem.Type() p.currentStruct = elem - p.fieldCache = make(map[string]any) // Reset cache for new struct + p.fieldCache = make(map[string]any) for i := 0; i < elem.NumField(); i++ { field := elem.Field(i) @@ -126,7 +75,6 @@ func (p *Parser) ParseStruct(v any) error { return fmt.Errorf("field %s: %w", fieldType.Name, err) } - // Cache the field value for conditional evaluation if field.CanInterface() { p.fieldCache[fieldType.Name] = field.Interface() } @@ -137,89 +85,47 @@ func (p *Parser) ParseStruct(v any) error { // parseField processes individual struct fields based on their tags func (p *Parser) parseField(field reflect.Value, tag string) error { - parts := strings.Split(tag, ",") - typeStr := parts[0] + fieldTag := p.parseFieldTag(tag) - var length int = 1 - var condition string - var maxSize int = -1 - var skipOversized bool - var dynamicLen string - var oversizedValue int = -1 - var oversizedByte byte = 127 - var type2 string - var type2Criteria string - - // Parse additional tag options - for _, part := range parts[1:] { - if strings.HasPrefix(part, "len=") { - lenVal := part[4:] - if l, err := strconv.Atoi(lenVal); err == nil { - length = l - } else { - // Dynamic length reference to another field - dynamicLen = lenVal - } - } else if strings.HasPrefix(part, "if=") { - condition = part[3:] - } else if strings.HasPrefix(part, "maxsize=") { - if m, err := strconv.Atoi(part[8:]); err == nil { - maxSize = m - } - } else if part == "skipoversized" { - skipOversized = true - } else if strings.HasPrefix(part, "oversized=") { - if o, err := strconv.Atoi(part[10:]); err == nil { - oversizedValue = o - } - } else if strings.HasPrefix(part, "oversizedbyte=") { - if o, err := strconv.Atoi(part[14:]); err == nil { - oversizedByte = byte(o) - } - } else if strings.HasPrefix(part, "type2=") { - type2 = part[6:] - } else if strings.HasPrefix(part, "type2criteria=") { - type2Criteria = part[14:] - } - } - - // Skip field if condition not met - if condition != "" && !p.evaluateCondition(condition, field) { + if !p.evaluateAllConditions(fieldTag, field) { return nil } - // Handle dynamic length - if dynamicLen != "" { - if dynLen := p.getDynamicLength(dynamicLen); dynLen > 0 { + length := fieldTag.Length + if fieldTag.DynamicLen != "" { + if dynLen := p.getDynamicLength(fieldTag.DynamicLen); dynLen > 0 { length = dynLen - // Apply max size limit - if maxSize > 0 && length > maxSize { - if skipOversized { - length = maxSize + if fieldTag.MaxSize > 0 && length > fieldTag.MaxSize { + if fieldTag.SkipOversized { + length = fieldTag.MaxSize } else { - return fmt.Errorf("length %d exceeds maximum %d", length, maxSize) + return fmt.Errorf("length %d exceeds maximum %d", length, fieldTag.MaxSize) } } } } - // Handle oversized values - if oversizedValue > 0 && length > oversizedValue { - if skipOversized { - length = oversizedValue + if fieldTag.OversizedValue > 0 && length > fieldTag.OversizedValue { + if fieldTag.SkipOversized { + length = fieldTag.OversizedValue } else { - // Use oversized byte value - return p.handleOversizedField(field, oversizedByte, length) + return p.handleOversizedField(field, fieldTag.OversizedByte, length) } } - // Choose type based on type2 criteria - actualType := typeStr - if type2 != "" && type2Criteria != "" && p.evaluateType2Criteria(type2Criteria) { - actualType = type2 + actualType := fieldTag.Type + if fieldTag.Type2 != "" && fieldTag.Type2Criteria != "" { + if p.evaluateType2Criteria(fieldTag.Type2Criteria) { + actualType = fieldTag.Type2 + } } - switch actualType { + return p.parseFieldByType(field, actualType, length, fieldTag) +} + +// parseFieldByType handles type-specific parsing +func (p *Parser) parseFieldByType(field reflect.Value, fieldType string, length int, fieldTag *FieldTag) error { + switch fieldType { case "int8": return p.readInt8(field, length) case "int16": @@ -253,519 +159,14 @@ func (p *Parser) parseField(field reflect.Value, tag string) error { case "equipment", "EQ2_EquipmentItem": return p.readEquipment(field, length) case "array": - return p.readArray(field) + return p.readArray(field, fieldTag) case "substruct": return p.readSubstruct(field, length) default: - return fmt.Errorf("unknown type: %s", actualType) + return fmt.Errorf("unknown type: %s", fieldType) } } -// Type-specific readers -func (p *Parser) readInt8(field reflect.Value, length int) error { - if length == 1 { - val, err := p.readUint8() - if err != nil { - return err - } - field.SetUint(uint64(val)) - return nil - } - - slice := make([]uint8, length) - for i := 0; i < length; i++ { - val, err := p.readUint8() - if err != nil { - return err - } - slice[i] = val - } - field.Set(reflect.ValueOf(slice)) - return nil -} - -func (p *Parser) readInt16(field reflect.Value, length int) error { - if length == 1 { - val, err := p.readUint16() - if err != nil { - return err - } - field.SetUint(uint64(val)) - return nil - } - - slice := make([]uint16, length) - for i := 0; i < length; i++ { - val, err := p.readUint16() - if err != nil { - return err - } - slice[i] = val - } - field.Set(reflect.ValueOf(slice)) - return nil -} - -func (p *Parser) readInt32(field reflect.Value, length int) error { - if length == 1 { - val, err := p.readUint32() - if err != nil { - return err - } - field.SetUint(uint64(val)) - return nil - } - - slice := make([]uint32, length) - for i := 0; i < length; i++ { - val, err := p.readUint32() - if err != nil { - return err - } - slice[i] = val - } - field.Set(reflect.ValueOf(slice)) - return nil -} - -func (p *Parser) readInt64(field reflect.Value, length int) error { - if length == 1 { - val, err := p.readUint64() - if err != nil { - return err - } - field.SetUint(val) - return nil - } - - slice := make([]uint64, length) - for i := 0; i < length; i++ { - val, err := p.readUint64() - if err != nil { - return err - } - slice[i] = val - } - field.Set(reflect.ValueOf(slice)) - return nil -} - -func (p *Parser) readSInt8(field reflect.Value, length int) error { - if length == 1 { - val, err := p.readUint8() - if err != nil { - return err - } - field.SetInt(int64(int8(val))) - return nil - } - - slice := make([]int8, length) - for i := 0; i < length; i++ { - val, err := p.readUint8() - if err != nil { - return err - } - slice[i] = int8(val) - } - field.Set(reflect.ValueOf(slice)) - return nil -} - -func (p *Parser) readSInt16(field reflect.Value, length int) error { - if length == 1 { - val, err := p.readUint16() - if err != nil { - return err - } - field.SetInt(int64(int16(val))) - return nil - } - - slice := make([]int16, length) - for i := 0; i < length; i++ { - val, err := p.readUint16() - if err != nil { - return err - } - slice[i] = int16(val) - } - field.Set(reflect.ValueOf(slice)) - return nil -} - -func (p *Parser) readSInt32(field reflect.Value, length int) error { - if length == 1 { - val, err := p.readUint32() - if err != nil { - return err - } - field.SetInt(int64(int32(val))) - return nil - } - - slice := make([]int32, length) - for i := 0; i < length; i++ { - val, err := p.readUint32() - if err != nil { - return err - } - slice[i] = int32(val) - } - field.Set(reflect.ValueOf(slice)) - return nil -} - -func (p *Parser) readSInt64(field reflect.Value, length int) error { - if length == 1 { - val, err := p.readUint64() - if err != nil { - return err - } - field.SetInt(int64(val)) - return nil - } - - slice := make([]int64, length) - for i := 0; i < length; i++ { - val, err := p.readUint64() - if err != nil { - return err - } - slice[i] = int64(val) - } - field.Set(reflect.ValueOf(slice)) - return nil -} - -func (p *Parser) readChar(field reflect.Value, length int) error { - if length == 1 { - val, err := p.readUint8() - if err != nil { - return err - } - field.SetUint(uint64(val)) - return nil - } - - slice := make([]byte, length) - err := p.readBytes(slice) - if err != nil { - return err - } - field.SetBytes(slice) - return nil -} - -func (p *Parser) readFloat(field reflect.Value, length int) error { - if length == 1 { - val, err := p.readFloat32() - if err != nil { - return err - } - field.SetFloat(float64(val)) - return nil - } - - slice := make([]float32, length) - for i := 0; i < length; i++ { - val, err := p.readFloat32() - if err != nil { - return err - } - slice[i] = val - } - field.Set(reflect.ValueOf(slice)) - return nil -} - -func (p *Parser) readDouble(field reflect.Value, length int) error { - if length == 1 { - val, err := p.readFloat64() - if err != nil { - return err - } - field.SetFloat(val) - return nil - } - - slice := make([]float64, length) - for i := 0; i < length; i++ { - val, err := p.readFloat64() - if err != nil { - return err - } - slice[i] = val - } - field.Set(reflect.ValueOf(slice)) - return nil -} - -func (p *Parser) readString8(field reflect.Value) error { - size, err := p.readUint8() - if err != nil { - return err - } - - data := make([]byte, size) - err = p.readBytes(data) - if err != nil { - return err - } - - str8 := common.EQ2String8{ - Size: size, - Data: string(data), - } - field.Set(reflect.ValueOf(str8)) - return nil -} - -func (p *Parser) readString16(field reflect.Value) error { - size, err := p.readUint16() - if err != nil { - return err - } - - data := make([]byte, size) - err = p.readBytes(data) - if err != nil { - return err - } - - str16 := common.EQ2String16{ - Size: size, - Data: string(data), - } - field.Set(reflect.ValueOf(str16)) - return nil -} - -func (p *Parser) readString32(field reflect.Value) error { - size, err := p.readUint32() - if err != nil { - return err - } - - data := make([]byte, size) - err = p.readBytes(data) - if err != nil { - return err - } - - str32 := common.EQ2String32{ - Size: size, - Data: string(data), - } - field.Set(reflect.ValueOf(str32)) - return nil -} - -func (p *Parser) readColor(field reflect.Value, length int) error { - if length == 1 { - color := common.EQ2Color{} - var err error - color.Red, err = p.readUint8() - if err != nil { - return err - } - color.Green, err = p.readUint8() - if err != nil { - return err - } - color.Blue, err = p.readUint8() - if err != nil { - return err - } - field.Set(reflect.ValueOf(color)) - return nil - } - - slice := make([]common.EQ2Color, length) - for i := 0; i < length; i++ { - var err error - slice[i].Red, err = p.readUint8() - if err != nil { - return err - } - slice[i].Green, err = p.readUint8() - if err != nil { - return err - } - slice[i].Blue, err = p.readUint8() - if err != nil { - return err - } - } - field.Set(reflect.ValueOf(slice)) - return nil -} - -func (p *Parser) readEquipment(field reflect.Value, length int) error { - if length == 1 { - equipment := common.EQ2EquipmentItem{} - var err error - equipment.Type, err = p.readUint16() - if err != nil { - return err - } - - equipment.Color.Red, err = p.readUint8() - if err != nil { - return err - } - equipment.Color.Green, err = p.readUint8() - if err != nil { - return err - } - equipment.Color.Blue, err = p.readUint8() - if err != nil { - return err - } - - equipment.Highlight.Red, err = p.readUint8() - if err != nil { - return err - } - equipment.Highlight.Green, err = p.readUint8() - if err != nil { - return err - } - equipment.Highlight.Blue, err = p.readUint8() - if err != nil { - return err - } - - field.Set(reflect.ValueOf(equipment)) - return nil - } - - slice := make([]common.EQ2EquipmentItem, length) - for i := 0; i < length; i++ { - var err error - slice[i].Type, err = p.readUint16() - if err != nil { - return err - } - - slice[i].Color.Red, err = p.readUint8() - if err != nil { - return err - } - slice[i].Color.Green, err = p.readUint8() - if err != nil { - return err - } - slice[i].Color.Blue, err = p.readUint8() - if err != nil { - return err - } - - slice[i].Highlight.Red, err = p.readUint8() - if err != nil { - return err - } - slice[i].Highlight.Green, err = p.readUint8() - if err != nil { - return err - } - slice[i].Highlight.Blue, err = p.readUint8() - if err != nil { - return err - } - } - field.Set(reflect.ValueOf(slice)) - return nil -} - -func (p *Parser) readArray(field reflect.Value) error { - // For arrays, read the size first then parse elements - size, err := p.readUint32() - if err != nil { - return err - } - - // This would need to be implemented based on the specific array type - // For now, just skip the specified number of bytes - p.offset += int(size) - return nil -} - -func (p *Parser) readSubstruct(field reflect.Value, length int) error { - if field.Kind() == reflect.Slice { - // Create slice for multiple substructs - elemType := field.Type().Elem() - slice := reflect.MakeSlice(field.Type(), length, length) - - for i := 0; i < length; i++ { - elem := slice.Index(i) - if err := p.parseSubstructElement(elem, elemType); err != nil { - return fmt.Errorf("substruct element %d: %w", i, err) - } - } - - field.Set(slice) - return nil - } else if field.Kind() == reflect.Struct { - // Single substruct - return p.parseSubstructElement(field, field.Type()) - } else if field.Kind() == reflect.Ptr && field.Type().Elem().Kind() == reflect.Struct { - // Pointer to struct - structType := field.Type().Elem() - newStruct := reflect.New(structType) - if err := p.parseSubstructElement(newStruct.Elem(), structType); err != nil { - return err - } - field.Set(newStruct) - return nil - } - - return fmt.Errorf("substruct field must be struct, slice of structs, or pointer to struct") -} - -func (p *Parser) parseSubstructElement(elem reflect.Value, elemType reflect.Type) error { - // Save current state - oldStruct := p.currentStruct - oldCache := p.fieldCache - - // Set up for nested parsing - p.currentStruct = elem - p.fieldCache = make(map[string]any) - - // Parse all fields of the substruct - for i := 0; i < elem.NumField(); i++ { - field := elem.Field(i) - fieldType := elemType.Field(i) - - if !field.CanSet() { - continue - } - - tag := fieldType.Tag.Get("eq2") - if tag == "" || tag == "-" { - continue - } - - if err := p.parseField(field, tag); err != nil { - // Restore state before returning error - p.currentStruct = oldStruct - p.fieldCache = oldCache - return fmt.Errorf("field %s: %w", fieldType.Name, err) - } - - // Cache the field value for conditional evaluation - if field.CanInterface() { - p.fieldCache[fieldType.Name] = field.Interface() - } - } - - // Restore state - p.currentStruct = oldStruct - p.fieldCache = oldCache - - return nil -} - // Low-level read methods func (p *Parser) readUint8() (uint8, error) { if p.offset >= len(p.data) { @@ -828,266 +229,6 @@ func (p *Parser) readBytes(buf []byte) error { return nil } -// evaluateCondition evaluates conditions for conditional fields -func (p *Parser) evaluateCondition(condition string, field reflect.Value) bool { - // Handle simple comparisons like "Channel==1", "Count>0", "Flag&4" - - // Parse condition operators - operators := []string{"==", "!=", ">=", "<=", ">", "<", "&", "|"} - - for _, op := range operators { - if idx := strings.Index(condition, op); idx > 0 { - fieldName := strings.TrimSpace(condition[:idx]) - valueStr := strings.TrimSpace(condition[idx+len(op):]) - - // Get field value from cache - cachedValue, exists := p.fieldCache[fieldName] - if !exists { - // Try to get from current struct - if p.currentStruct.IsValid() { - if structField := p.currentStruct.FieldByName(fieldName); structField.IsValid() { - cachedValue = structField.Interface() - } else { - return false - } - } else { - return false - } - } - - // Convert comparison value - compareValue, err := p.convertValue(valueStr, cachedValue) - if err != nil { - return false - } - - return p.compareValues(cachedValue, compareValue, op) - } - } - - // Simple boolean field check - if cachedValue, exists := p.fieldCache[condition]; exists { - return p.isTruthy(cachedValue) - } - - return true -} - -// getDynamicLength gets length from another field -func (p *Parser) getDynamicLength(fieldName string) int { - if cachedValue, exists := p.fieldCache[fieldName]; exists { - return p.valueToInt(cachedValue) - } - - // Try current struct - if p.currentStruct.IsValid() { - if field := p.currentStruct.FieldByName(fieldName); field.IsValid() { - return p.valueToInt(field.Interface()) - } - } - - return 0 -} - -// convertValue converts string to appropriate type for comparison -func (p *Parser) convertValue(valueStr string, reference any) (any, error) { - switch reference.(type) { - case uint8, int8: - if val, err := strconv.ParseInt(valueStr, 0, 8); err == nil { - return uint8(val), nil - } - case uint16, int16: - if val, err := strconv.ParseInt(valueStr, 0, 16); err == nil { - return uint16(val), nil - } - case uint32, int32: - if val, err := strconv.ParseInt(valueStr, 0, 32); err == nil { - return uint32(val), nil - } - case uint64, int64: - if val, err := strconv.ParseInt(valueStr, 0, 64); err == nil { - return uint64(val), nil - } - case float32: - if val, err := strconv.ParseFloat(valueStr, 32); err == nil { - return float32(val), nil - } - case float64: - if val, err := strconv.ParseFloat(valueStr, 64); err == nil { - return val, nil - } - case string: - return valueStr, nil - } - - // Try as int for bitwise operations - if val, err := strconv.ParseInt(valueStr, 0, 32); err == nil { - return int(val), nil - } - - return valueStr, nil -} - -// compareValues performs comparison between two values -func (p *Parser) compareValues(a, b any, op string) bool { - // Convert to comparable types - aVal := p.valueToInt64(a) - bVal := p.valueToInt64(b) - - switch op { - case "==": - return aVal == bVal - case "!=": - return aVal != bVal - case ">": - return aVal > bVal - case ">=": - return aVal >= bVal - case "<": - return aVal < bVal - case "<=": - return aVal <= bVal - case "&": - return (aVal & bVal) != 0 - case "|": - return (aVal | bVal) != 0 - } - - return false -} - -// valueToInt converts various types to int -func (p *Parser) valueToInt(v any) int { - switch val := v.(type) { - case uint8: - return int(val) - case int8: - return int(val) - case uint16: - return int(val) - case int16: - return int(val) - case uint32: - return int(val) - case int32: - return int(val) - case uint64: - return int(val) - case int64: - return int(val) - case int: - return val - } - return 0 -} - -// valueToInt64 converts various types to int64 for comparison -func (p *Parser) valueToInt64(v any) int64 { - switch val := v.(type) { - case uint8: - return int64(val) - case int8: - return int64(val) - case uint16: - return int64(val) - case int16: - return int64(val) - case uint32: - return int64(val) - case int32: - return int64(val) - case uint64: - return int64(val) - case int64: - return val - case int: - return int64(val) - case float32: - return int64(val) - case float64: - return int64(val) - } - return 0 -} - -// isTruthy checks if a value is truthy -func (p *Parser) isTruthy(v any) bool { - switch val := v.(type) { - case bool: - return val - case uint8, int8, uint16, int16, uint32, int32, uint64, int64, int: - return p.valueToInt64(val) != 0 - case string: - return val != "" - } - return false -} - -// evaluateType2Criteria evaluates type2 criteria for alternative types -func (p *Parser) evaluateType2Criteria(criteria string) bool { - // Handle criteria like "stat_type!=6" - operators := []string{"!=", "==", ">=", "<=", ">", "<"} - - for _, op := range operators { - if idx := strings.Index(criteria, op); idx > 0 { - fieldName := strings.TrimSpace(criteria[:idx]) - valueStr := strings.TrimSpace(criteria[idx+len(op):]) - - // Get field value from cache - cachedValue, exists := p.fieldCache[fieldName] - if !exists { - return false - } - - // Convert comparison value - compareValue, err := p.convertValue(valueStr, cachedValue) - if err != nil { - return false - } - - return p.compareValues(cachedValue, compareValue, op) - } - } - - return false -} - -// handleOversizedField handles fields that exceed their oversized value -func (p *Parser) handleOversizedField(field reflect.Value, oversizedByte byte, length int) error { - // For oversized fields, we typically write the oversized byte value - // and skip the actual data - if field.Kind() == reflect.Slice { - // Create slice with oversized byte values - slice := reflect.MakeSlice(field.Type(), 1, 1) - if slice.Len() > 0 { - elem := slice.Index(0) - if elem.CanSet() { - switch elem.Kind() { - case reflect.Uint8: - elem.SetUint(uint64(oversizedByte)) - case reflect.Int8: - elem.SetInt(int64(int8(oversizedByte))) - default: - elem.SetUint(uint64(oversizedByte)) - } - } - } - field.Set(slice) - } else { - // Single value - switch field.Kind() { - case reflect.Uint8: - field.SetUint(uint64(oversizedByte)) - case reflect.Int8: - field.SetInt(int64(int8(oversizedByte))) - default: - field.SetUint(uint64(oversizedByte)) - } - } - - return nil -} - // Position tracking func (p *Parser) Offset() int { return p.offset diff --git a/internal/packets/parser/parser_test.go b/internal/packets/parser/parser_test.go new file mode 100644 index 0000000..2d2f854 --- /dev/null +++ b/internal/packets/parser/parser_test.go @@ -0,0 +1,424 @@ +package parser + +import ( + "eq2emu/internal/common" + "reflect" + "testing" +) + +type BasicPacket struct { + Channel uint8 `eq2:"int8"` + Count uint16 `eq2:"int16"` + MessageType uint8 `eq2:"int8,if=Channel==1"` +} + +type ArrayPacket struct { + ItemCount uint16 `eq2:"int16"` + Items []ItemDescription `eq2:"array,arraysize=ItemCount"` +} + +type ItemDescription struct { + InfoHeader WS_ExamineInfoHeader `eq2:"substruct"` + Info BaseItemDescription `eq2:"substruct"` + ItemType uint8 `eq2:"int8"` +} + +type WS_ExamineInfoHeader struct { + Unknown1 uint32 `eq2:"int32"` + Unknown2 uint16 `eq2:"int16"` +} + +type BaseItemDescription struct { + Name common.EQ2String16 `eq2:"string16"` + Category common.EQ2String8 `eq2:"string8"` +} + +type ConditionalPacket struct { + HasRewards uint8 `eq2:"int8"` + RewardData *RewardInfo `eq2:"substruct,ifvariableset=HasRewards"` + CompleteFlag uint8 `eq2:"int8"` + ClassicSound uint8 `eq2:"int8,ifvariableset=CompleteFlag"` +} + +type RewardInfo struct { + RewardType uint8 `eq2:"int8"` + Amount uint32 `eq2:"int32"` +} + +type OversizedPacket struct { + LargeDataCount uint16 `eq2:"int16"` + LargeData []byte `eq2:"char,len=LargeDataCount,maxsize=10,skipoversized"` +} + +type TypeSwitchPacket struct { + StatType uint8 `eq2:"int8"` + StatValue any `eq2:"int32,type2=float,type2criteria=StatType!=6"` +} + +type FlagPacket struct { + Equipment []common.EQ2EquipmentItem `eq2:"equipment,len=2,ifflag=has_equipment"` + Colors []common.EQ2Color `eq2:"color,len=3,ifflagnotset=no_colors"` +} + +type ComplexPacket struct { + GroupCount uint8 `eq2:"int8"` + Groups []Group `eq2:"array,arraysize=GroupCount"` +} + +type Group struct { + MemberCount uint16 `eq2:"int16"` + Members []Member `eq2:"array,arraysize=MemberCount"` +} + +type Member struct { + Name common.EQ2String16 `eq2:"string16"` + Level uint8 `eq2:"int8"` + Class uint8 `eq2:"int8"` +} + +// Version-specific structs +type PacketV1 struct { + Field1 uint8 `eq2:"int8"` + Field2 uint16 `eq2:"int16"` +} + +type PacketV2 struct { + Field1 uint8 `eq2:"int8"` + Field2 uint16 `eq2:"int16"` + NewField uint32 `eq2:"int32"` +} + +func TestBasicParsing(t *testing.T) { + // Basic packet: Channel=1, Count=100, MessageType=5 + data := []byte{0x01, 0x64, 0x00, 0x05} + parser := NewParser(data) + + var packet BasicPacket + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.Channel != 1 { + t.Errorf("Expected Channel=1, got %d", packet.Channel) + } + if packet.Count != 100 { + t.Errorf("Expected Count=100, got %d", packet.Count) + } + if packet.MessageType != 5 { + t.Errorf("Expected MessageType=5, got %d", packet.MessageType) + } +} + +func TestConditionalSkip(t *testing.T) { + // Channel=2 (not 1), so MessageType should be skipped + data := []byte{0x02, 0x64, 0x00} + parser := NewParser(data) + + var packet BasicPacket + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.Channel != 2 { + t.Errorf("Expected Channel=2, got %d", packet.Channel) + } + if packet.MessageType != 0 { + t.Errorf("Expected MessageType=0 (skipped), got %d", packet.MessageType) + } +} + +func TestArrayParsing(t *testing.T) { + // ItemCount=2, then 2 items + data := []byte{ + 0x02, 0x00, // ItemCount = 2 + // Item 1 + 0x01, 0x00, 0x00, 0x00, // InfoHeader.Unknown1 = 1 + 0x02, 0x00, // InfoHeader.Unknown2 = 2 + 0x04, 0x00, 't', 'e', 's', 't', // Name: "test" + 0x03, 'c', 'a', 't', // Category: "cat" + 0x05, // ItemType = 5 + // Item 2 + 0x03, 0x00, 0x00, 0x00, // InfoHeader.Unknown1 = 3 + 0x04, 0x00, // InfoHeader.Unknown2 = 4 + 0x05, 0x00, 'i', 't', 'e', 'm', '2', // Name: "item2" + 0x04, 't', 'y', 'p', 'e', // Category: "type" + 0x06, // ItemType = 6 + } + + parser := NewParser(data) + var packet ArrayPacket + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.ItemCount != 2 { + t.Errorf("Expected ItemCount=2, got %d", packet.ItemCount) + } + if len(packet.Items) != 2 { + t.Errorf("Expected 2 items, got %d", len(packet.Items)) + } + + if packet.Items[0].Info.Name.Data != "test" { + t.Errorf("Expected first item name 'test', got '%s'", packet.Items[0].Info.Name.Data) + } + if packet.Items[1].ItemType != 6 { + t.Errorf("Expected second item type 6, got %d", packet.Items[1].ItemType) + } +} + +func TestConditionalFields(t *testing.T) { + // HasRewards=1, RewardData present, CompleteFlag=1, ClassicSound present + data := []byte{ + 0x01, // HasRewards = 1 + 0x02, // RewardType = 2 + 0x64, 0x00, 0x00, 0x00, // Amount = 100 + 0x01, // CompleteFlag = 1 + 0x05, // ClassicSound = 5 + } + + parser := NewParser(data) + var packet ConditionalPacket + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.HasRewards != 1 { + t.Errorf("Expected HasRewards=1, got %d", packet.HasRewards) + } + if packet.RewardData == nil { + t.Error("Expected RewardData to be present") + } else { + if packet.RewardData.RewardType != 2 { + t.Errorf("Expected RewardType=2, got %d", packet.RewardData.RewardType) + } + if packet.RewardData.Amount != 100 { + t.Errorf("Expected Amount=100, got %d", packet.RewardData.Amount) + } + } + if packet.ClassicSound != 5 { + t.Errorf("Expected ClassicSound=5, got %d", packet.ClassicSound) + } +} + +func TestOversizedHandling(t *testing.T) { + // LargeDataCount=15 (exceeds maxsize=10), should be truncated + data := []byte{ + 0x0F, 0x00, // LargeDataCount = 15 + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, + 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, // 15 bytes of data + } + + parser := NewParser(data) + var packet OversizedPacket + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.LargeDataCount != 15 { + t.Errorf("Expected LargeDataCount=15, got %d", packet.LargeDataCount) + } + if len(packet.LargeData) != 10 { + t.Errorf("Expected LargeData length=10 (truncated), got %d", len(packet.LargeData)) + } +} + +func TestTypeSwitching(t *testing.T) { + // StatType=3 (not 6), so StatValue should be parsed as float + data := []byte{ + 0x03, // StatType = 3 + 0x00, 0x00, 0x80, 0x3F, // float32: 1.0 + } + + parser := NewParser(data) + var packet TypeSwitchPacket + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.StatType != 3 { + t.Errorf("Expected StatType=3, got %d", packet.StatType) + } + + // Should be parsed as float32 since StatType != 6 + floatVal, ok := packet.StatValue.(float32) + if !ok { + t.Errorf("Expected StatValue to be float32, got %T", packet.StatValue) + } else if floatVal != 1.0 { + t.Errorf("Expected StatValue=1.0, got %f", floatVal) + } +} + +func TestFlagBasedConditionals(t *testing.T) { + // Equipment data (2 items) + data := []byte{ + // Equipment item 1 + 0x01, 0x00, // Type = 1 + 0xFF, 0x00, 0x00, // Color: Red=255, Green=0, Blue=0 + 0x00, 0xFF, 0x00, // Highlight: Red=0, Green=255, Blue=0 + // Equipment item 2 + 0x02, 0x00, // Type = 2 + 0x00, 0x00, 0xFF, // Color: Red=0, Green=0, Blue=255 + 0xFF, 0xFF, 0x00, // Highlight: Red=255, Green=255, Blue=0 + // Colors (3 items) + 0x80, 0x80, 0x80, // Color 1: Gray + 0x40, 0x40, 0x40, // Color 2: Dark gray + 0xC0, 0xC0, 0xC0, // Color 3: Light gray + } + + parser := NewParser(data) + parser.SetFlag("has_equipment", true) + // no_colors flag is not set, so colors should be parsed + + var packet FlagPacket + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if len(packet.Equipment) != 2 { + t.Errorf("Expected 2 equipment items, got %d", len(packet.Equipment)) + } + if len(packet.Colors) != 3 { + t.Errorf("Expected 3 colors, got %d", len(packet.Colors)) + } + + if packet.Equipment[0].Type != 1 { + t.Errorf("Expected first equipment type 1, got %d", packet.Equipment[0].Type) + } + if packet.Colors[0].Red != 0x80 { + t.Errorf("Expected first color red=128, got %d", packet.Colors[0].Red) + } +} + +func TestComplexNestedArrays(t *testing.T) { + // GroupCount=1, Group has MemberCount=2 + data := []byte{ + 0x01, // GroupCount = 1 + 0x02, 0x00, // MemberCount = 2 + // Member 1 + 0x05, 0x00, 'A', 'l', 'i', 'c', 'e', // Name: "Alice" + 0x0A, // Level = 10 + 0x01, // Class = 1 + // Member 2 + 0x03, 0x00, 'B', 'o', 'b', // Name: "Bob" + 0x0F, // Level = 15 + 0x02, // Class = 2 + } + + parser := NewParser(data) + var packet ComplexPacket + err := parser.ParseStruct(&packet) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + + if packet.GroupCount != 1 { + t.Errorf("Expected GroupCount=1, got %d", packet.GroupCount) + } + if len(packet.Groups) != 1 { + t.Errorf("Expected 1 group, got %d", len(packet.Groups)) + } + if len(packet.Groups[0].Members) != 2 { + t.Errorf("Expected 2 members, got %d", len(packet.Groups[0].Members)) + } + + if packet.Groups[0].Members[0].Name.Data != "Alice" { + t.Errorf("Expected first member 'Alice', got '%s'", packet.Groups[0].Members[0].Name.Data) + } + if packet.Groups[0].Members[1].Level != 15 { + t.Errorf("Expected second member level 15, got %d", packet.Groups[0].Members[1].Level) + } +} + +func TestVersionRegistry(t *testing.T) { + registry := NewVersionRegistry() + + // Register different versions + registry.RegisterStruct("TestPacket", "1.0", reflect.TypeOf(PacketV1{})) + registry.RegisterStruct("TestPacket", "2.0", reflect.TypeOf(PacketV2{})) + + // Test version 1.0 + data1 := []byte{0x01, 0x64, 0x00} // Field1=1, Field2=100 + parser1 := NewParser(data1) + + result1, err := parser1.ParseWithVersion(registry, "TestPacket", "1.0") + if err != nil { + t.Fatalf("Parse v1.0 error: %v", err) + } + + v1Packet, ok := result1.(PacketV1) + if !ok { + t.Fatal("Expected PacketV1 type") + } + if v1Packet.Field1 != 1 || v1Packet.Field2 != 100 { + t.Errorf("V1 packet values incorrect: Field1=%d, Field2=%d", v1Packet.Field1, v1Packet.Field2) + } + + // Test version 2.0 + data2 := []byte{0x02, 0xC8, 0x00, 0x90, 0x01, 0x00, 0x00} // Field1=2, Field2=200, NewField=400 + parser2 := NewParser(data2) + + result2, err := parser2.ParseWithVersion(registry, "TestPacket", "2.0") + if err != nil { + t.Fatalf("Parse v2.0 error: %v", err) + } + + v2Packet, ok := result2.(PacketV2) + if !ok { + t.Fatal("Expected PacketV2 type") + } + if v2Packet.Field1 != 2 || v2Packet.Field2 != 200 || v2Packet.NewField != 400 { + t.Errorf("V2 packet values incorrect: Field1=%d, Field2=%d, NewField=%d", + v2Packet.Field1, v2Packet.Field2, v2Packet.NewField) + } +} + +func TestVersionFallback(t *testing.T) { + registry := NewVersionRegistry() + registry.RegisterStruct("TestPacket", "1.0", reflect.TypeOf(PacketV1{})) + registry.RegisterStruct("TestPacket", "2.0", reflect.TypeOf(PacketV2{})) + + // Request version 1.5 (doesn't exist), should fall back to 1.0 + data := []byte{0x01, 0x64, 0x00} + parser := NewParser(data) + + result, err := parser.ParseWithVersion(registry, "TestPacket", "1.5") + if err != nil { + t.Fatalf("Parse with fallback error: %v", err) + } + + // Should get v1.0 (nearest lower version) + _, ok := result.(PacketV1) + if !ok { + t.Error("Expected fallback to PacketV1") + } +} + +func TestParserPosition(t *testing.T) { + data := []byte{0x01, 0x02, 0x03, 0x04} + parser := NewParser(data) + + if parser.Offset() != 0 { + t.Errorf("Initial offset should be 0, got %d", parser.Offset()) + } + + parser.readUint8() // Read one byte + if parser.Offset() != 1 { + t.Errorf("After reading 1 byte, offset should be 1, got %d", parser.Offset()) + } + + if parser.Remaining() != 3 { + t.Errorf("After reading 1 byte, remaining should be 3, got %d", parser.Remaining()) + } + + parser.SetOffset(2) + if parser.Offset() != 2 { + t.Errorf("After SetOffset(2), offset should be 2, got %d", parser.Offset()) + } +} diff --git a/internal/packets/parser/type_readers.go b/internal/packets/parser/type_readers.go new file mode 100644 index 0000000..9fd87f8 --- /dev/null +++ b/internal/packets/parser/type_readers.go @@ -0,0 +1,435 @@ +package parser + +import ( + "eq2emu/internal/common" + "reflect" +) + +// readInt8 handles uint8/int8 reading +func (p *Parser) readInt8(field reflect.Value, length int) error { + if length == 1 { + val, err := p.readUint8() + if err != nil { + return err + } + field.SetUint(uint64(val)) + return nil + } + + slice := make([]uint8, length) + for i := 0; i < length; i++ { + val, err := p.readUint8() + if err != nil { + return err + } + slice[i] = val + } + field.Set(reflect.ValueOf(slice)) + return nil +} + +func (p *Parser) readInt16(field reflect.Value, length int) error { + if length == 1 { + val, err := p.readUint16() + if err != nil { + return err + } + field.SetUint(uint64(val)) + return nil + } + + slice := make([]uint16, length) + for i := 0; i < length; i++ { + val, err := p.readUint16() + if err != nil { + return err + } + slice[i] = val + } + field.Set(reflect.ValueOf(slice)) + return nil +} + +func (p *Parser) readInt32(field reflect.Value, length int) error { + if length == 1 { + val, err := p.readUint32() + if err != nil { + return err + } + + // Handle interface{} fields + if field.Kind() == reflect.Interface { + field.Set(reflect.ValueOf(val)) + } else { + field.SetUint(uint64(val)) + } + return nil + } + + slice := make([]uint32, length) + for i := 0; i < length; i++ { + val, err := p.readUint32() + if err != nil { + return err + } + slice[i] = val + } + field.Set(reflect.ValueOf(slice)) + return nil +} + +func (p *Parser) readInt64(field reflect.Value, length int) error { + if length == 1 { + val, err := p.readUint64() + if err != nil { + return err + } + field.SetUint(val) + return nil + } + + slice := make([]uint64, length) + for i := 0; i < length; i++ { + val, err := p.readUint64() + if err != nil { + return err + } + slice[i] = val + } + field.Set(reflect.ValueOf(slice)) + return nil +} + +func (p *Parser) readSInt8(field reflect.Value, length int) error { + if length == 1 { + val, err := p.readUint8() + if err != nil { + return err + } + field.SetInt(int64(int8(val))) + return nil + } + + slice := make([]int8, length) + for i := 0; i < length; i++ { + val, err := p.readUint8() + if err != nil { + return err + } + slice[i] = int8(val) + } + field.Set(reflect.ValueOf(slice)) + return nil +} + +func (p *Parser) readSInt16(field reflect.Value, length int) error { + if length == 1 { + val, err := p.readUint16() + if err != nil { + return err + } + field.SetInt(int64(int16(val))) + return nil + } + + slice := make([]int16, length) + for i := 0; i < length; i++ { + val, err := p.readUint16() + if err != nil { + return err + } + slice[i] = int16(val) + } + field.Set(reflect.ValueOf(slice)) + return nil +} + +func (p *Parser) readSInt32(field reflect.Value, length int) error { + if length == 1 { + val, err := p.readUint32() + if err != nil { + return err + } + field.SetInt(int64(int32(val))) + return nil + } + + slice := make([]int32, length) + for i := 0; i < length; i++ { + val, err := p.readUint32() + if err != nil { + return err + } + slice[i] = int32(val) + } + field.Set(reflect.ValueOf(slice)) + return nil +} + +func (p *Parser) readSInt64(field reflect.Value, length int) error { + if length == 1 { + val, err := p.readUint64() + if err != nil { + return err + } + field.SetInt(int64(val)) + return nil + } + + slice := make([]int64, length) + for i := 0; i < length; i++ { + val, err := p.readUint64() + if err != nil { + return err + } + slice[i] = int64(val) + } + field.Set(reflect.ValueOf(slice)) + return nil +} + +func (p *Parser) readChar(field reflect.Value, length int) error { + if length == 1 { + val, err := p.readUint8() + if err != nil { + return err + } + field.SetUint(uint64(val)) + return nil + } + + slice := make([]byte, length) + err := p.readBytes(slice) + if err != nil { + return err + } + field.SetBytes(slice) + return nil +} + +func (p *Parser) readFloat(field reflect.Value, length int) error { + if length == 1 { + val, err := p.readFloat32() + if err != nil { + return err + } + + // Handle interface{} fields + if field.Kind() == reflect.Interface { + field.Set(reflect.ValueOf(val)) + } else { + field.SetFloat(float64(val)) + } + return nil + } + + slice := make([]float32, length) + for i := 0; i < length; i++ { + val, err := p.readFloat32() + if err != nil { + return err + } + slice[i] = val + } + field.Set(reflect.ValueOf(slice)) + return nil +} + +func (p *Parser) readDouble(field reflect.Value, length int) error { + if length == 1 { + val, err := p.readFloat64() + if err != nil { + return err + } + field.SetFloat(val) + return nil + } + + slice := make([]float64, length) + for i := 0; i < length; i++ { + val, err := p.readFloat64() + if err != nil { + return err + } + slice[i] = val + } + field.Set(reflect.ValueOf(slice)) + return nil +} + +func (p *Parser) readString8(field reflect.Value) error { + size, err := p.readUint8() + if err != nil { + return err + } + + data := make([]byte, size) + err = p.readBytes(data) + if err != nil { + return err + } + + str8 := common.EQ2String8{ + Size: size, + Data: string(data), + } + field.Set(reflect.ValueOf(str8)) + return nil +} + +func (p *Parser) readString16(field reflect.Value) error { + size, err := p.readUint16() + if err != nil { + return err + } + + data := make([]byte, size) + err = p.readBytes(data) + if err != nil { + return err + } + + str16 := common.EQ2String16{ + Size: size, + Data: string(data), + } + field.Set(reflect.ValueOf(str16)) + return nil +} + +func (p *Parser) readString32(field reflect.Value) error { + size, err := p.readUint32() + if err != nil { + return err + } + + data := make([]byte, size) + err = p.readBytes(data) + if err != nil { + return err + } + + str32 := common.EQ2String32{ + Size: size, + Data: string(data), + } + field.Set(reflect.ValueOf(str32)) + return nil +} + +func (p *Parser) readColor(field reflect.Value, length int) error { + if length == 1 { + color := common.EQ2Color{} + var err error + color.Red, err = p.readUint8() + if err != nil { + return err + } + color.Green, err = p.readUint8() + if err != nil { + return err + } + color.Blue, err = p.readUint8() + if err != nil { + return err + } + field.Set(reflect.ValueOf(color)) + return nil + } + + slice := make([]common.EQ2Color, length) + for i := 0; i < length; i++ { + var err error + slice[i].Red, err = p.readUint8() + if err != nil { + return err + } + slice[i].Green, err = p.readUint8() + if err != nil { + return err + } + slice[i].Blue, err = p.readUint8() + if err != nil { + return err + } + } + field.Set(reflect.ValueOf(slice)) + return nil +} + +func (p *Parser) readEquipment(field reflect.Value, length int) error { + if length == 1 { + equipment := common.EQ2EquipmentItem{} + var err error + equipment.Type, err = p.readUint16() + if err != nil { + return err + } + + equipment.Color.Red, err = p.readUint8() + if err != nil { + return err + } + equipment.Color.Green, err = p.readUint8() + if err != nil { + return err + } + equipment.Color.Blue, err = p.readUint8() + if err != nil { + return err + } + + equipment.Highlight.Red, err = p.readUint8() + if err != nil { + return err + } + equipment.Highlight.Green, err = p.readUint8() + if err != nil { + return err + } + equipment.Highlight.Blue, err = p.readUint8() + if err != nil { + return err + } + + field.Set(reflect.ValueOf(equipment)) + return nil + } + + slice := make([]common.EQ2EquipmentItem, length) + for i := 0; i < length; i++ { + var err error + slice[i].Type, err = p.readUint16() + if err != nil { + return err + } + + slice[i].Color.Red, err = p.readUint8() + if err != nil { + return err + } + slice[i].Color.Green, err = p.readUint8() + if err != nil { + return err + } + slice[i].Color.Blue, err = p.readUint8() + if err != nil { + return err + } + + slice[i].Highlight.Red, err = p.readUint8() + if err != nil { + return err + } + slice[i].Highlight.Green, err = p.readUint8() + if err != nil { + return err + } + slice[i].Highlight.Blue, err = p.readUint8() + if err != nil { + return err + } + } + field.Set(reflect.ValueOf(slice)) + return nil +} diff --git a/internal/packets/parser/version_registry.go b/internal/packets/parser/version_registry.go new file mode 100644 index 0000000..2a8759f --- /dev/null +++ b/internal/packets/parser/version_registry.go @@ -0,0 +1,71 @@ +package parser + +import ( + "fmt" + "reflect" +) + +// VersionRegistry manages version-specific struct types +type VersionRegistry struct { + structs map[string]map[string]reflect.Type // [structName][version] = Type +} + +// NewVersionRegistry creates a new version registry +func NewVersionRegistry() *VersionRegistry { + return &VersionRegistry{ + structs: make(map[string]map[string]reflect.Type), + } +} + +// RegisterStruct registers a struct type for a specific version +func (vr *VersionRegistry) RegisterStruct(name, version string, structType reflect.Type) { + if vr.structs[name] == nil { + vr.structs[name] = make(map[string]reflect.Type) + } + vr.structs[name][version] = structType +} + +// GetStruct returns the appropriate struct type for a version +func (vr *VersionRegistry) GetStruct(name, version string) (reflect.Type, bool) { + if versions, exists := vr.structs[name]; exists { + if structType, exists := versions[version]; exists { + return structType, true + } + return vr.findNearestVersion(name, version) + } + return nil, false +} + +// findNearestVersion finds the closest version <= requested version +func (vr *VersionRegistry) findNearestVersion(name, targetVersion string) (reflect.Type, bool) { + versions := vr.structs[name] + var bestVersion string + var bestType reflect.Type + + for version, structType := range versions { + if version <= targetVersion && version > bestVersion { + bestVersion = version + bestType = structType + } + } + + return bestType, bestVersion != "" +} + +// ParseWithVersion parses using version-specific struct +func (p *Parser) ParseWithVersion(registry *VersionRegistry, structName, version string) (any, error) { + structType, exists := registry.GetStruct(structName, version) + if !exists { + return nil, fmt.Errorf("no struct found for %s version %s", structName, version) + } + + ptr := reflect.New(structType) + elem := ptr.Elem() + + err := p.ParseStruct(ptr.Interface()) + if err != nil { + return nil, err + } + + return elem.Interface(), nil +}