implement packet field templates

This commit is contained in:
Sky Johnson 2025-07-29 08:51:43 -05:00
parent b2a2e9366b
commit 19bc67233b
4 changed files with 282 additions and 36 deletions

View File

@ -2,9 +2,7 @@
<version number="1"> <version number="1">
<i16 name="num_words" oversized="255"> <i16 name="num_words" oversized="255">
<array name="words_array" count="var:num_words"> <array name="words_array" count="var:num_words">
<substruct> <str16 name="word">
<str16 name="word">
</substruct>
</array> </array>
</version> </version>
</packet> </packet>

View File

@ -8,7 +8,6 @@ import (
"sync" "sync"
) )
// Object pools for reducing allocations
var ( var (
fieldOrderPool = sync.Pool{ fieldOrderPool = sync.Pool{
New: func() any { New: func() any {
@ -29,7 +28,6 @@ var (
} }
) )
// String builder for efficient concatenation
type stringBuilder struct { type stringBuilder struct {
buf []byte buf []byte
} }
@ -52,6 +50,7 @@ type Parser struct {
current *Token current *Token
input string input string
substructs map[string]*PacketDef substructs map[string]*PacketDef
templates map[string]*PacketDef
tagStack []string tagStack []string
fieldNames []string fieldNames []string
} }
@ -68,7 +67,7 @@ var typeMap = map[string]common.EQ2DataType{
"si64": common.TypeSInt64, "si64": common.TypeSInt64,
"f32": common.TypeFloat, "f32": common.TypeFloat,
"f64": common.TypeDouble, "f64": common.TypeDouble,
"double": common.TypeDouble, // XML compatibility "double": common.TypeDouble,
"str8": common.TypeString8, "str8": common.TypeString8,
"str16": common.TypeString16, "str16": common.TypeString16,
"str32": common.TypeString32, "str32": common.TypeString32,
@ -84,6 +83,7 @@ func NewParser(input string) *Parser {
lexer: NewLexer(input), lexer: NewLexer(input),
input: input, input: input,
substructs: make(map[string]*PacketDef), substructs: make(map[string]*PacketDef),
templates: make(map[string]*PacketDef),
tagStack: make([]string, 0, 16), tagStack: make([]string, 0, 16),
fieldNames: make([]string, 0, 8), fieldNames: make([]string, 0, 8),
} }
@ -285,14 +285,6 @@ func combineConditions(cond1, cond2 string) string {
return string(buf) return string(buf)
} }
// Creates packet definition with estimated capacity
func NewPacketDef(estimatedFields int) *PacketDef {
return &PacketDef{
Fields: make(map[string]FieldDesc, estimatedFields),
Orders: make(map[uint32][]string, 4),
}
}
// Parses the entire PML document // Parses the entire PML document
func (p *Parser) Parse() (map[string]*PacketDef, error) { func (p *Parser) Parse() (map[string]*PacketDef, error) {
packets := make(map[string]*PacketDef) packets := make(map[string]*PacketDef)
@ -322,6 +314,16 @@ func (p *Parser) Parse() (map[string]*PacketDef, error) {
if name != "" { if name != "" {
p.substructs[name] = substruct p.substructs[name] = substruct
} }
case "template":
name := p.current.Attributes["name"]
if name != "" {
err := p.parseTemplateDefinition(name)
if err != nil {
return nil, err
}
} else {
p.advance()
}
default: default:
p.advance() p.advance()
} }
@ -468,6 +470,11 @@ func (p *Parser) parseElements(packetDef *PacketDef, fieldOrder *[]string, prefi
if err != nil { if err != nil {
return err return err
} }
case "template":
err := p.parseTemplate(packetDef, fieldOrder, prefix)
if err != nil {
return err
}
default: default:
err := p.parseField(packetDef, fieldOrder, prefix) err := p.parseField(packetDef, fieldOrder, prefix)
if err != nil { if err != nil {
@ -483,6 +490,90 @@ func (p *Parser) parseElements(packetDef *PacketDef, fieldOrder *[]string, prefi
return nil return nil
} }
// Handles template definition and usage
func (p *Parser) parseTemplate(packetDef *PacketDef, fieldOrder *[]string, prefix string) error {
attrs := p.current.Attributes
// Template definition: <template name="foo">
if templateName := attrs["name"]; templateName != "" {
return p.parseTemplateDefinition(templateName)
}
// Template usage: <template use="foo">
if templateUse := attrs["use"]; templateUse != "" {
return p.parseTemplateUsage(packetDef, fieldOrder, prefix, templateUse)
}
// No name or use attribute - skip
p.advance()
return nil
}
// Parses template definition and stores it
func (p *Parser) parseTemplateDefinition(templateName string) error {
templateDef := NewPacketDef(16)
fieldOrder := fieldOrderPool.Get().(*[]string)
*fieldOrder = (*fieldOrder)[:0]
defer fieldOrderPool.Put(fieldOrder)
if p.current.Type == TokenSelfCloseTag {
p.advance()
templateDef.Orders[1] = make([]string, len(*fieldOrder))
copy(templateDef.Orders[1], *fieldOrder)
p.templates[templateName] = templateDef
return nil
}
p.pushTag("template")
p.advance()
err := p.parseElements(templateDef, fieldOrder, "")
if err != nil {
return err
}
if p.current.Type == TokenCloseTag && p.current.Tag(p.input) == "template" {
if err := p.popTag("template"); err != nil {
return err
}
p.advance()
} else {
return fmt.Errorf("expected closing tag for template at line %d", p.current.Line)
}
templateDef.Orders[1] = make([]string, len(*fieldOrder))
copy(templateDef.Orders[1], *fieldOrder)
p.templates[templateName] = templateDef
return nil
}
// Injects template fields into current parsing context
func (p *Parser) parseTemplateUsage(packetDef *PacketDef, fieldOrder *[]string, prefix string, templateName string) error {
template, exists := p.templates[templateName]
if !exists {
return fmt.Errorf("undefined template '%s' at line %d", templateName, p.current.Line)
}
// Inject all template fields into current packet
if templateOrder, exists := template.Orders[1]; exists {
for _, fieldName := range templateOrder {
if fieldDesc, exists := template.Fields[fieldName]; exists {
// Apply prefix if provided
fullName := fieldName
if prefix != "" {
fullName = buildFieldName(prefix, fieldName)
}
packetDef.Fields[fullName] = fieldDesc
*fieldOrder = append(*fieldOrder, fullName)
}
}
}
p.advance()
return nil
}
// Handles group elements // Handles group elements
func (p *Parser) parseGroup(packetDef *PacketDef, fieldOrder *[]string, prefix string) error { func (p *Parser) parseGroup(packetDef *PacketDef, fieldOrder *[]string, prefix string) error {
attrs := p.current.Attributes attrs := p.current.Attributes
@ -520,7 +611,7 @@ func (p *Parser) parseGroup(packetDef *PacketDef, fieldOrder *[]string, prefix s
return nil return nil
} }
// Handles array elements - FIXED: Remove redundant substruct wrapper // Handles array elements
func (p *Parser) parseArray(packetDef *PacketDef, fieldOrder *[]string, prefix string) error { func (p *Parser) parseArray(packetDef *PacketDef, fieldOrder *[]string, prefix string) error {
attrs := p.current.Attributes attrs := p.current.Attributes

View File

@ -720,6 +720,102 @@ func TestErrorHandling(t *testing.T) {
} }
} }
func TestTemplateDefinitionAndUsage(t *testing.T) {
pml := `<!-- Define reusable templates -->
<template name="position">
<f32 name="x,y,z">
<f32 name="heading">
</template>
<template name="appearance">
<color name="skin_color,hair_color,eye_color">
<str16 name="face_file,hair_file">
</template>
<!-- Use templates in packet -->
<packet name="PlayerUpdate">
<version number="1">
<i32 name="player_id">
<template use="position">
<i8 name="level">
<template use="appearance">
<str16 name="guild_name" if="flag:has_guild">
</version>
</packet>
<!-- Test template with group prefix -->
<packet name="GroupedTemplate">
<version number="1">
<i32 name="entity_id">
<group name="current">
<template use="position">
</group>
<group name="target">
<template use="position">
</group>
</version>
</packet>`
packets, err := Parse(pml)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
// Test basic template injection
playerPacket := packets["PlayerUpdate"]
if playerPacket == nil {
t.Fatal("PlayerUpdate packet not found")
}
// Check that template fields were injected
expectedFields := []string{"player_id", "x", "y", "z", "heading", "level", "skin_color", "hair_color", "eye_color", "face_file", "hair_file", "guild_name"}
if len(playerPacket.Fields) != len(expectedFields) {
t.Errorf("Expected %d fields, got %d", len(expectedFields), len(playerPacket.Fields))
}
// Verify specific template fields exist with correct types
if playerPacket.Fields["x"].Type != common.TypeFloat {
t.Error("x should be TypeFloat from position template")
}
if playerPacket.Fields["heading"].Type != common.TypeFloat {
t.Error("heading should be TypeFloat from position template")
}
if playerPacket.Fields["skin_color"].Type != common.TypeColor {
t.Error("skin_color should be TypeColor from appearance template")
}
if playerPacket.Fields["face_file"].Type != common.TypeString16 {
t.Error("face_file should be TypeString16 from appearance template")
}
// Check field ordering preserves template injection points
order := playerPacket.Orders[1]
expectedOrder := []string{"player_id", "x", "y", "z", "heading", "level", "skin_color", "hair_color", "eye_color", "face_file", "hair_file", "guild_name"}
if !equalSlices(order, expectedOrder) {
t.Errorf("Expected order %v, got %v", expectedOrder, order)
}
// Test template with group prefixes
groupedPacket := packets["GroupedTemplate"]
if groupedPacket == nil {
t.Fatal("GroupedTemplate packet not found")
}
// Check prefixed fields from templates
if groupedPacket.Fields["current_x"].Type != common.TypeFloat {
t.Error("current_x should exist from prefixed template")
}
if groupedPacket.Fields["target_heading"].Type != common.TypeFloat {
t.Error("target_heading should exist from prefixed template")
}
// Verify grouped template field order
groupedOrder := groupedPacket.Orders[1]
expectedGroupedOrder := []string{"entity_id", "current_x", "current_y", "current_z", "current_heading", "target_x", "target_y", "target_z", "target_heading"}
if !equalSlices(groupedOrder, expectedGroupedOrder) {
t.Errorf("Expected grouped order %v, got %v", expectedGroupedOrder, groupedOrder)
}
}
func BenchmarkSimplePacket(b *testing.B) { func BenchmarkSimplePacket(b *testing.B) {
pml := `<packet name="Simple"> pml := `<packet name="Simple">
<version number="1"> <version number="1">
@ -738,27 +834,53 @@ func BenchmarkSimplePacket(b *testing.B) {
} }
} }
func BenchmarkComplexPacketWithNewFeatures(b *testing.B) { func BenchmarkMediumPacket(b *testing.B) {
pml := `<packet name="Complex"> pml := `<packet name="Medium">
<version number="562"> <version number="1">
<i32 name="player_id,account_id"> <i32 name="id,account_id,session_id">
<str16 name="player_name"> <str16 name="name,guild_name">
<color name="skin_color,hair_color,eye_color"> <i8 name="level,race,class">
<str16 name="guild_name" if="flag:has_guild"> <f32 name="x,y,z,heading">
<i32 name="guild_id" if="flag:has_guild"> <color name="skin_color,hair_color">
<i8 name="equipment_count"> </version>
<array name="equipment" count="var:equipment_count" max_size="50"> </packet>`
<i16 name="slot_id,item_type">
<color name="primary_color,secondary_color"> for b.Loop() {
<i8 name="enhancement_level" if="item_type!=0"> _, err := Parse(pml)
<i32 name="stat_value" type2="f32" type2_if="item_type==6" oversized="255"> if err != nil {
</array> b.Fatal(err)
<i8 name="stat_count"> }
<array name="stats" count="var:stat_count" optional="true"> }
<i8 name="stat_type"> }
<i32 name="base_value">
<i32 name="modified_value" if="stat_type>=1&stat_type<=5"> func BenchmarkLargePacket(b *testing.B) {
<f64 name="percentage" if="stat_type==6"> pml := `<packet name="Large">
<version number="1">
<i32 name="a1,a2,a3,a4,a5,a6,a7,a8,a9,a10">
<str16 name="s1,s2,s3,s4,s5">
<f32 name="f1,f2,f3,f4,f5,f6,f7,f8">
<color name="c1,c2,c3,c4">
<i8 name="b1,b2,b3,b4,b5,b6,b7,b8,b9,b10,b11,b12">
</version>
</packet>`
for b.Loop() {
_, err := Parse(pml)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkArrayPacket(b *testing.B) {
pml := `<packet name="ArrayPacket">
<version number="1">
<i8 name="count">
<array name="items" count="var:count">
<i32 name="id">
<str16 name="name">
<color name="color">
<f32 name="x,y,z">
</array> </array>
</version> </version>
</packet>` </packet>`
@ -771,6 +893,33 @@ func BenchmarkComplexPacketWithNewFeatures(b *testing.B) {
} }
} }
func BenchmarkMultiVersionPacket(b *testing.B) {
pml := `<packet name="MultiVersion">
<version number="1">
<i32 name="id">
<str16 name="name">
</version>
<version number="100">
<i32 name="id">
<str16 name="name">
<i8 name="level">
</version>
<version number="500">
<i32 name="id">
<str16 name="name">
<i8 name="level">
<color name="color">
</version>
</packet>`
for b.Loop() {
_, err := Parse(pml)
if err != nil {
b.Fatal(err)
}
}
}
// Helper function to compare slices // Helper function to compare slices
func equalSlices(a, b []string) bool { func equalSlices(a, b []string) bool {
if len(a) != len(b) { if len(a) != len(b) {

View File

@ -8,6 +8,14 @@ type PacketDef struct {
Orders map[uint32][]string // Field order by version number Orders map[uint32][]string // Field order by version number
} }
// Creates packet definition with estimated capacity
func NewPacketDef(estimatedFields int) *PacketDef {
return &PacketDef{
Fields: make(map[string]FieldDesc, estimatedFields),
Orders: make(map[uint32][]string, 4),
}
}
func (def *PacketDef) Parse(data []byte, version uint32, flags uint64) (map[string]any, error) { func (def *PacketDef) Parse(data []byte, version uint32, flags uint64) (map[string]any, error) {
ctx := NewContext(data, version, flags) ctx := NewContext(data, version, flags)
return def.parseStruct(ctx) return def.parseStruct(ctx)