diff --git a/internal/packets/login/BadLanguageFilter.xml b/internal/packets/login/BadLanguageFilter.xml
index 660c7e6..0163c29 100644
--- a/internal/packets/login/BadLanguageFilter.xml
+++ b/internal/packets/login/BadLanguageFilter.xml
@@ -2,9 +2,7 @@
-
-
-
+
\ No newline at end of file
diff --git a/internal/packets/parser/parser.go b/internal/packets/parser/parser.go
index 3c0efde..ca4ab1a 100644
--- a/internal/packets/parser/parser.go
+++ b/internal/packets/parser/parser.go
@@ -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:
+ if templateName := attrs["name"]; templateName != "" {
+ return p.parseTemplateDefinition(templateName)
+ }
+
+ // Template usage:
+ 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
diff --git a/internal/packets/parser/parser_test.go b/internal/packets/parser/parser_test.go
index e1afb25..ec3ecfe 100644
--- a/internal/packets/parser/parser_test.go
+++ b/internal/packets/parser/parser_test.go
@@ -720,6 +720,102 @@ func TestErrorHandling(t *testing.T) {
}
}
+func TestTemplateDefinitionAndUsage(t *testing.T) {
+ pml := `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+
+ 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 := `
@@ -738,27 +834,53 @@ func BenchmarkSimplePacket(b *testing.B) {
}
}
-func BenchmarkComplexPacketWithNewFeatures(b *testing.B) {
- pml := `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+func BenchmarkMediumPacket(b *testing.B) {
+ pml := `
+
+
+
+
+
+
+
+ `
+
+ for b.Loop() {
+ _, err := Parse(pml)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkLargePacket(b *testing.B) {
+ pml := `
+
+
+
+
+
+
+
+ `
+
+ for b.Loop() {
+ _, err := Parse(pml)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func BenchmarkArrayPacket(b *testing.B) {
+ pml := `
+
+
+
+
+
+
+
`
@@ -771,6 +893,33 @@ func BenchmarkComplexPacketWithNewFeatures(b *testing.B) {
}
}
+func BenchmarkMultiVersionPacket(b *testing.B) {
+ pml := `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+
+ 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) {
diff --git a/internal/packets/parser/structs.go b/internal/packets/parser/structs.go
index 356db6c..9251f4d 100644
--- a/internal/packets/parser/structs.go
+++ b/internal/packets/parser/structs.go
@@ -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)