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">
<i16 name="num_words" oversized="255">
<array name="words_array" count="var:num_words">
<substruct>
<str16 name="word">
</substruct>
<str16 name="word">
</array>
</version>
</packet>

View File

@ -8,7 +8,6 @@ import (
"sync"
)
// Object pools for reducing allocations
var (
fieldOrderPool = sync.Pool{
New: func() any {
@ -29,7 +28,6 @@ var (
}
)
// String builder for efficient concatenation
type stringBuilder struct {
buf []byte
}
@ -52,6 +50,7 @@ type Parser struct {
current *Token
input string
substructs map[string]*PacketDef
templates map[string]*PacketDef
tagStack []string
fieldNames []string
}
@ -68,7 +67,7 @@ var typeMap = map[string]common.EQ2DataType{
"si64": common.TypeSInt64,
"f32": common.TypeFloat,
"f64": common.TypeDouble,
"double": common.TypeDouble, // XML compatibility
"double": common.TypeDouble,
"str8": common.TypeString8,
"str16": common.TypeString16,
"str32": common.TypeString32,
@ -84,6 +83,7 @@ func NewParser(input string) *Parser {
lexer: NewLexer(input),
input: input,
substructs: make(map[string]*PacketDef),
templates: make(map[string]*PacketDef),
tagStack: make([]string, 0, 16),
fieldNames: make([]string, 0, 8),
}
@ -285,14 +285,6 @@ func combineConditions(cond1, cond2 string) string {
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
func (p *Parser) Parse() (map[string]*PacketDef, error) {
packets := make(map[string]*PacketDef)
@ -322,6 +314,16 @@ func (p *Parser) Parse() (map[string]*PacketDef, error) {
if name != "" {
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:
p.advance()
}
@ -468,6 +470,11 @@ func (p *Parser) parseElements(packetDef *PacketDef, fieldOrder *[]string, prefi
if err != nil {
return err
}
case "template":
err := p.parseTemplate(packetDef, fieldOrder, prefix)
if err != nil {
return err
}
default:
err := p.parseField(packetDef, fieldOrder, prefix)
if err != nil {
@ -483,6 +490,90 @@ func (p *Parser) parseElements(packetDef *PacketDef, fieldOrder *[]string, prefi
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
func (p *Parser) parseGroup(packetDef *PacketDef, fieldOrder *[]string, prefix string) error {
attrs := p.current.Attributes
@ -520,7 +611,7 @@ func (p *Parser) parseGroup(packetDef *PacketDef, fieldOrder *[]string, prefix s
return nil
}
// Handles array elements - FIXED: Remove redundant substruct wrapper
// Handles array elements
func (p *Parser) parseArray(packetDef *PacketDef, fieldOrder *[]string, prefix string) error {
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) {
pml := `<packet name="Simple">
<version number="1">
@ -738,27 +834,53 @@ func BenchmarkSimplePacket(b *testing.B) {
}
}
func BenchmarkComplexPacketWithNewFeatures(b *testing.B) {
pml := `<packet name="Complex">
<version number="562">
<i32 name="player_id,account_id">
<str16 name="player_name">
<color name="skin_color,hair_color,eye_color">
<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" max_size="50">
<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">
</array>
<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">
<f64 name="percentage" if="stat_type==6">
func BenchmarkMediumPacket(b *testing.B) {
pml := `<packet name="Medium">
<version number="1">
<i32 name="id,account_id,session_id">
<str16 name="name,guild_name">
<i8 name="level,race,class">
<f32 name="x,y,z,heading">
<color name="skin_color,hair_color">
</version>
</packet>`
for b.Loop() {
_, err := Parse(pml)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkLargePacket(b *testing.B) {
pml := `<packet name="Large">
<version number="1">
<i32 name="a1,a2,a3,a4,a5,a6,a7,a8,a9,a10">
<str16 name="s1,s2,s3,s4,s5">
<f32 name="f1,f2,f3,f4,f5,f6,f7,f8">
<color name="c1,c2,c3,c4">
<i8 name="b1,b2,b3,b4,b5,b6,b7,b8,b9,b10,b11,b12">
</version>
</packet>`
for b.Loop() {
_, err := Parse(pml)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkArrayPacket(b *testing.B) {
pml := `<packet name="ArrayPacket">
<version number="1">
<i8 name="count">
<array name="items" count="var:count">
<i32 name="id">
<str16 name="name">
<color name="color">
<f32 name="x,y,z">
</array>
</version>
</packet>`
@ -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
func equalSlices(a, b []string) bool {
if len(a) != len(b) {

View File

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