implement missing types, conditions
This commit is contained in:
parent
e310437c1b
commit
c42485f874
396
internal/packets/parser/context.go
Normal file
396
internal/packets/parser/context.go
Normal file
@ -0,0 +1,396 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) readString8() string {
|
||||
length := ctx.readUint8()
|
||||
str := string(ctx.data[ctx.offset : ctx.offset+int(length)])
|
||||
ctx.offset += int(length)
|
||||
return str
|
||||
}
|
||||
|
||||
func (ctx *ParseContext) readString16() string {
|
||||
length := ctx.readUint16()
|
||||
str := string(ctx.data[ctx.offset : ctx.offset+int(length)])
|
||||
ctx.offset += int(length)
|
||||
return str
|
||||
}
|
||||
|
||||
func (ctx *ParseContext) readEQ2String8() common.EQ2String8 {
|
||||
size := ctx.readUint8()
|
||||
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.readUint16()
|
||||
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.readUint32()
|
||||
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.readUint8(),
|
||||
Green: ctx.readUint8(),
|
||||
Blue: ctx.readUint8(),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if strings.HasPrefix(condition, "var:") {
|
||||
varName := condition[4:]
|
||||
return ctx.hasVar(varName)
|
||||
}
|
||||
if strings.HasPrefix(condition, "!var:") {
|
||||
varName := condition[5:]
|
||||
return !ctx.hasVar(varName)
|
||||
}
|
||||
|
||||
// Version comparisons
|
||||
if strings.HasPrefix(condition, "version") {
|
||||
return ctx.evaluateVersionCondition(condition)
|
||||
}
|
||||
|
||||
// String length operators: name!>5, name!<=10
|
||||
stringOps := []string{"!>=", "!<=", "!>", "!<", "!="}
|
||||
for _, op := range stringOps {
|
||||
if idx := strings.Index(condition, op); idx > 0 {
|
||||
varName := condition[:idx]
|
||||
valueStr := condition[idx+len(op):]
|
||||
return ctx.evaluateStringLength(varName, valueStr, op)
|
||||
}
|
||||
}
|
||||
|
||||
// Comparison operators: >=, <=, >, <, ==, !=
|
||||
compOps := []string{">=", "<=", ">", "<", "==", "!="}
|
||||
for _, op := range compOps {
|
||||
if idx := strings.Index(condition, op); idx > 0 {
|
||||
varName := condition[:idx]
|
||||
valueStr := condition[idx+len(op):]
|
||||
return ctx.evaluateComparison(varName, valueStr, op)
|
||||
}
|
||||
}
|
||||
|
||||
// Simple variable existence
|
||||
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:
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
@ -63,7 +63,7 @@ func (l *Lexer) next() byte {
|
||||
// Checks if a tag should be treated as self-closing (using byte comparison)
|
||||
func (l *Lexer) isSelfClosingTag(start, end int) bool {
|
||||
length := end - start
|
||||
if length < 2 || length > 5 {
|
||||
if length < 2 || length > 6 {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -103,6 +103,10 @@ func (l *Lexer) isSelfClosingTag(start, end int) bool {
|
||||
(l.input[start] == 's' && l.input[start+1] == 't' &&
|
||||
l.input[start+2] == 'r' && l.input[start+3] == '3' &&
|
||||
l.input[start+4] == '2')
|
||||
case 6:
|
||||
return (l.input[start] == 'd' && l.input[start+1] == 'o' &&
|
||||
l.input[start+2] == 'u' && l.input[start+3] == 'b' &&
|
||||
l.input[start+4] == 'l' && l.input[start+5] == 'e')
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -58,23 +58,24 @@ type Parser struct {
|
||||
|
||||
// Type mapping for efficient lookup
|
||||
var typeMap = map[string]common.EQ2DataType{
|
||||
"i8": common.TypeInt8,
|
||||
"i16": common.TypeInt16,
|
||||
"i32": common.TypeInt32,
|
||||
"i64": common.TypeInt64,
|
||||
"si8": common.TypeSInt8,
|
||||
"si16": common.TypeSInt16,
|
||||
"si32": common.TypeSInt32,
|
||||
"si64": common.TypeSInt64,
|
||||
"f32": common.TypeFloat,
|
||||
"f64": common.TypeDouble,
|
||||
"str8": common.TypeString8,
|
||||
"str16": common.TypeString16,
|
||||
"str32": common.TypeString32,
|
||||
"char": common.TypeChar,
|
||||
"color": common.TypeColor,
|
||||
"equip": common.TypeEquipment,
|
||||
"array": common.TypeArray,
|
||||
"i8": common.TypeInt8,
|
||||
"i16": common.TypeInt16,
|
||||
"i32": common.TypeInt32,
|
||||
"i64": common.TypeInt64,
|
||||
"si8": common.TypeSInt8,
|
||||
"si16": common.TypeSInt16,
|
||||
"si32": common.TypeSInt32,
|
||||
"si64": common.TypeSInt64,
|
||||
"f32": common.TypeFloat,
|
||||
"f64": common.TypeDouble,
|
||||
"double": common.TypeDouble, // XML compatibility
|
||||
"str8": common.TypeString8,
|
||||
"str16": common.TypeString16,
|
||||
"str32": common.TypeString32,
|
||||
"char": common.TypeChar,
|
||||
"color": common.TypeColor,
|
||||
"equip": common.TypeEquipment,
|
||||
"array": common.TypeArray,
|
||||
}
|
||||
|
||||
// Creates a new parser
|
||||
@ -183,6 +184,10 @@ func getDataType(tag string) (common.EQ2DataType, bool) {
|
||||
case "f64":
|
||||
return common.TypeDouble, true
|
||||
}
|
||||
case 'd':
|
||||
if tag == "double" {
|
||||
return common.TypeDouble, true
|
||||
}
|
||||
case 'c':
|
||||
switch tag {
|
||||
case "char":
|
||||
@ -527,14 +532,32 @@ func (p *Parser) parseArray(packetDef *PacketDef, fieldOrder *[]string, prefix s
|
||||
}
|
||||
|
||||
fieldDesc := FieldDesc{
|
||||
Type: common.TypeArray,
|
||||
Condition: attrs["count"],
|
||||
Type: common.TypeArray,
|
||||
Condition: attrs["count"],
|
||||
AddToStruct: true, // Default to true
|
||||
}
|
||||
|
||||
if ifCond := attrs["if"]; ifCond != "" {
|
||||
fieldDesc.Condition = combineConditions(fieldDesc.Condition, ifCond)
|
||||
}
|
||||
|
||||
// Parse additional attributes
|
||||
if maxSize := attrs["max_size"]; maxSize != "" {
|
||||
if m, err := strconv.Atoi(maxSize); err == nil {
|
||||
fieldDesc.MaxArraySize = m
|
||||
} else {
|
||||
return fmt.Errorf("invalid max_size value '%s' at line %d: %v", maxSize, p.current.Line, err)
|
||||
}
|
||||
}
|
||||
|
||||
if optional := attrs["optional"]; optional == "true" {
|
||||
fieldDesc.Optional = true
|
||||
}
|
||||
|
||||
if addToStruct := attrs["add_to_struct"]; addToStruct == "false" {
|
||||
fieldDesc.AddToStruct = false
|
||||
}
|
||||
|
||||
// Handle substruct reference
|
||||
if substruct := attrs["substruct"]; substruct != "" {
|
||||
if subDef, exists := p.substructs[substruct]; exists {
|
||||
@ -640,13 +663,70 @@ func (p *Parser) parseField(packetDef *PacketDef, fieldOrder *[]string, prefix s
|
||||
}
|
||||
|
||||
fieldDesc := FieldDesc{
|
||||
Type: dataType,
|
||||
Condition: attrs["if"],
|
||||
Type: dataType,
|
||||
Condition: attrs["if"],
|
||||
AddToStruct: true, // Default to true
|
||||
AddType: dataType,
|
||||
}
|
||||
|
||||
// Parse size attribute
|
||||
if size := attrs["size"]; size != "" {
|
||||
if s, err := strconv.Atoi(size); err == nil {
|
||||
fieldDesc.Length = s
|
||||
} else {
|
||||
return fmt.Errorf("invalid size value '%s' at line %d: %v", size, p.current.Line, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse oversized attribute
|
||||
if oversized := attrs["oversized"]; oversized != "" {
|
||||
if o, err := strconv.Atoi(oversized); err == nil {
|
||||
fieldDesc.Oversized = o
|
||||
} else {
|
||||
return fmt.Errorf("invalid oversized value '%s' at line %d: %v", oversized, p.current.Line, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse type2 attributes
|
||||
if type2 := attrs["type2"]; type2 != "" {
|
||||
if t2, exists := getDataType(type2); exists {
|
||||
fieldDesc.Type2 = t2
|
||||
fieldDesc.Type2Cond = attrs["type2_if"]
|
||||
}
|
||||
}
|
||||
|
||||
// Parse default value
|
||||
if defaultVal := attrs["default"]; defaultVal != "" {
|
||||
if d, err := strconv.ParseInt(defaultVal, 10, 8); err == nil {
|
||||
fieldDesc.DefaultValue = int8(d)
|
||||
} else {
|
||||
return fmt.Errorf("invalid default value '%s' at line %d: %v", defaultVal, p.current.Line, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse max_size
|
||||
if maxSize := attrs["max_size"]; maxSize != "" {
|
||||
if m, err := strconv.Atoi(maxSize); err == nil {
|
||||
fieldDesc.MaxArraySize = m
|
||||
} else {
|
||||
return fmt.Errorf("invalid max_size value '%s' at line %d: %v", maxSize, p.current.Line, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse optional
|
||||
if optional := attrs["optional"]; optional == "true" {
|
||||
fieldDesc.Optional = true
|
||||
}
|
||||
|
||||
// Parse add_to_struct
|
||||
if addToStruct := attrs["add_to_struct"]; addToStruct == "false" {
|
||||
fieldDesc.AddToStruct = false
|
||||
}
|
||||
|
||||
// Parse add_type
|
||||
if addType := attrs["add_type"]; addType != "" {
|
||||
if at, exists := getDataType(addType); exists {
|
||||
fieldDesc.AddType = at
|
||||
}
|
||||
}
|
||||
|
||||
@ -663,4 +743,4 @@ func Parse(content string) (map[string]*PacketDef, error) {
|
||||
parser := NewParser(content)
|
||||
defer parser.cleanup()
|
||||
return parser.Parse()
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,187 @@ func TestBasicParsing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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.TypeInt32 {
|
||||
t.Error("stat_value primary type should be TypeInt32")
|
||||
}
|
||||
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.TypeInt32 {
|
||||
t.Error("another_field type2 should be TypeInt32")
|
||||
}
|
||||
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.TypeInt16 {
|
||||
t.Error("hidden_field add_type should be TypeInt16")
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
<substruct>
|
||||
<i32 name="item_id">
|
||||
</substruct>
|
||||
</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">
|
||||
<substruct>
|
||||
<i32 name="id">
|
||||
</substruct>
|
||||
</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">
|
||||
@ -265,6 +446,127 @@ func TestComments(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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"].(uint16); val != 100 {
|
||||
t.Errorf("Expected normal_value 100, got %d", val)
|
||||
}
|
||||
|
||||
if val := result["oversized_value"].(uint16); 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"].(uint8); 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">
|
||||
<substruct>
|
||||
<i16 name="item_id">
|
||||
</substruct>
|
||||
</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 TestErrorHandling(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
@ -273,6 +575,7 @@ func TestErrorHandling(t *testing.T) {
|
||||
{"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 {
|
||||
@ -295,8 +598,7 @@ func BenchmarkSimplePacket(b *testing.B) {
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
_, err := Parse(pml)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
@ -304,7 +606,7 @@ func BenchmarkSimplePacket(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkComplexPacket(b *testing.B) {
|
||||
func BenchmarkComplexPacketWithNewFeatures(b *testing.B) {
|
||||
pml := `<packet name="Complex">
|
||||
<version number="562">
|
||||
<i32 name="player_id,account_id">
|
||||
@ -313,27 +615,27 @@ func BenchmarkComplexPacket(b *testing.B) {
|
||||
<str16 name="guild_name" if="flag:has_guild">
|
||||
<i32 name="guild_id" if="flag:has_guild">
|
||||
<i8 name="equipment_count">
|
||||
<array name="equipment" count="var:equipment_count">
|
||||
<array name="equipment" count="var:equipment_count" max_size="50">
|
||||
<substruct>
|
||||
<i16 name="slot_id,item_type">
|
||||
<color name="primary_color,secondary_color">
|
||||
<i8 name="enhancement_level" if="item_type!=0">
|
||||
<i32 name="stat_value" type2="f32" type2_if="item_type==6" oversized="255">
|
||||
</substruct>
|
||||
</array>
|
||||
<i8 name="stat_count">
|
||||
<array name="stats" count="var:stat_count">
|
||||
<array name="stats" count="var:stat_count" optional="true">
|
||||
<substruct>
|
||||
<i8 name="stat_type">
|
||||
<i32 name="base_value">
|
||||
<i32 name="modified_value" if="stat_type>=1&stat_type<=5">
|
||||
<f32 name="percentage" if="stat_type==6">
|
||||
<f64 name="percentage" if="stat_type==6">
|
||||
</substruct>
|
||||
</array>
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
_, err := Parse(pml)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
@ -341,47 +643,6 @@ func BenchmarkComplexPacket(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLargePacket(b *testing.B) {
|
||||
// Generate a large packet definition
|
||||
pmlBuilder := `<packet name="Large"><version number="1">`
|
||||
for i := 0; i < 100; i++ {
|
||||
pmlBuilder += `<i32 name="field` + string(rune('A'+i%26)) + `">`
|
||||
}
|
||||
pmlBuilder += `</version></packet>`
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := Parse(pmlBuilder)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWithSubstructs(b *testing.B) {
|
||||
pml := `<substruct name="Item">
|
||||
<i32 name="id">
|
||||
<str16 name="name">
|
||||
<i16 name="quantity">
|
||||
</substruct>
|
||||
|
||||
<packet name="WithSubstruct">
|
||||
<version number="1">
|
||||
<i8 name="count">
|
||||
<array name="items" count="var:count" substruct="Item">
|
||||
</version>
|
||||
</packet>`
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
parser := NewParser(pml)
|
||||
_, err := parser.Parse()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to compare slices
|
||||
func equalSlices(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
|
@ -8,13 +8,109 @@ type PacketDef struct {
|
||||
Orders map[uint32][]string // Field order by version number
|
||||
}
|
||||
|
||||
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.setVar(fieldName, value)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (def *PacketDef) parseField(ctx *ParseContext, field FieldDesc, fieldType common.EQ2DataType, fieldName string) any {
|
||||
switch fieldType {
|
||||
case common.TypeInt8, common.TypeSInt8:
|
||||
if field.Oversized > 0 {
|
||||
return ctx.readOversizedUint8(field.Oversized)
|
||||
}
|
||||
return ctx.readUint8()
|
||||
case common.TypeInt16, common.TypeSInt16:
|
||||
if field.Oversized > 0 {
|
||||
return ctx.readOversizedUint16(field.Oversized)
|
||||
}
|
||||
return ctx.readUint16()
|
||||
case common.TypeInt32, common.TypeSInt32:
|
||||
if field.Oversized > 0 {
|
||||
return ctx.readOversizedUint32(field.Oversized)
|
||||
}
|
||||
return ctx.readUint32()
|
||||
case common.TypeInt64, common.TypeSInt64:
|
||||
return ctx.readUint64()
|
||||
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
|
||||
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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user