//go:build ignore package main import ( "encoding/xml" "flag" "fmt" "io" "log" "os" "path/filepath" "strings" "text/template" ) // XMLDefinition represents the root element containing structs type XMLDefinition struct { XMLName xml.Name `xml:"structs"` Structs []XMLStruct `xml:"struct"` } // XMLStruct represents a packet structure definition type XMLStruct struct { Name string `xml:"name,attr"` ClientVersion string `xml:"clientVersion,attr"` OpcodeName string `xml:"opcodeName,attr"` Fields []XMLData `xml:"data"` } // XMLData represents a field in the packet structure type XMLData struct { Name string `xml:"name,attr"` Type string `xml:"type,attr"` Size int `xml:"size,attr"` ArraySizeVariable string `xml:"arraySizeVariable,attr"` IfVarSet string `xml:"ifVarSet,attr"` IfVarNotSet string `xml:"ifVarNotSet,attr"` OversizedValue int `xml:"oversizedValue,attr"` Children []XMLData `xml:"data"` // For nested array elements } // TypeMapping maps XML types to Go types var TypeMapping = map[string]string{ "int8": "int8", "int16": "int16", "int32": "int32", "int64": "int64", "uint8": "uint8", "uint16": "uint16", "uint32": "uint32", "uint64": "uint64", "i8": "int8", "i16": "int16", "i32": "int32", "i64": "int64", "u8": "uint8", "u16": "uint16", "u32": "uint32", "u64": "uint64", "float": "float32", "double": "float64", "str8": "string", "str16": "string", "str32": "string", "EQ2_32Bit_String": "string", "EQ2_8Bit_String": "string", "EQ2_16Bit_String": "string", "char": "byte", "color": "types.Color", // RGB color as 32-bit value "equipmentItem": "types.EquipmentItem", // Custom type "Array": "array", // Capital A variant } // GoStruct represents the generated Go struct type GoStruct struct { Name string ClientVersion string OpcodeName string Fields []GoField PackageName string } // GoField represents a field in the Go struct type GoField struct { Name string GoName string Type string IsArray bool IsDynamicArray bool ArraySizeVariable string Size int Tag string Comment string IfVarSet string IfVarNotSet string ArrayElements []GoField // For complex array elements (anonymous struct) } // GenerateOutput holds the generated code type GenerateOutput struct { SourceFile string PackageName string Structs []GoStruct NeedsMath bool } // parseXMLFile parses an XML definition file func parseXMLFile(filename string) ([]XMLStruct, error) { file, err := os.Open(filename) if err != nil { return nil, err } defer file.Close() data, err := io.ReadAll(file) if err != nil { return nil, err } // Wrap content in root element if needed content := string(data) if !strings.HasPrefix(strings.TrimSpace(content), "") { content = "" + content + "" } var def XMLDefinition if err := xml.Unmarshal([]byte(content), &def); err != nil { return nil, err } return def.Structs, nil } // toGoName converts snake_case to CamelCase func toGoName(name string) string { parts := strings.Split(name, "_") for i, part := range parts { if len(part) > 0 { parts[i] = strings.ToUpper(part[:1]) + part[1:] } } return strings.Join(parts, "") } // mapSimpleType maps XML type to Go type func mapSimpleType(xmlType string) (string, bool) { goType, ok := TypeMapping[xmlType] if !ok { return "byte", false } return goType, true } // convertArrayChildren converts nested array data to Go fields func convertArrayChildren(children []XMLData) []GoField { fields := make([]GoField, 0, len(children)) for _, child := range children { if child.Type == "array" && len(child.Children) > 0 { // Nested array - recursive nestedFields := convertArrayChildren(child.Children) goField := GoField{ Name: child.Name, GoName: toGoName(child.Name), IsDynamicArray: true, ArraySizeVariable: child.ArraySizeVariable, ArrayElements: nestedFields, } fields = append(fields, goField) } else { // Simple field goType, _ := mapSimpleType(child.Type) isArray := child.Size > 0 goField := GoField{ Name: child.Name, GoName: toGoName(child.Name), Type: goType, IsArray: isArray, Size: child.Size, } if isArray { goField.Type = fmt.Sprintf("[%d]%s", child.Size, goType) } fields = append(fields, goField) } } return fields } // buildAnonymousStructType builds the anonymous struct type string func buildAnonymousStructType(fields []GoField) string { if len(fields) == 0 { return "struct{}" } var sb strings.Builder sb.WriteString("struct {\n") for _, field := range fields { sb.WriteString("\t\t") sb.WriteString(field.GoName) sb.WriteString(" ") if field.IsDynamicArray && len(field.ArrayElements) > 0 { // Nested array sb.WriteString("[]") sb.WriteString(buildAnonymousStructType(field.ArrayElements)) } else if field.IsArray { sb.WriteString(field.Type) } else { sb.WriteString(field.Type) } // Add struct tag sb.WriteString(" `eq2:\"") sb.WriteString(field.Name) if field.Type == "string" { // Determine string type from original XML sb.WriteString(",type:str16") // Default to str16 for now } if field.IsArray && field.Size > 0 { sb.WriteString(fmt.Sprintf(",size:%d", field.Size)) } sb.WriteString("\"`") sb.WriteString("\n") } sb.WriteString("\t}") return sb.String() } // convertToGoStruct converts XML struct definition to Go struct func convertToGoStruct(xmlStruct XMLStruct, packageName string) GoStruct { goStruct := GoStruct{ Name: toGoName(xmlStruct.Name), ClientVersion: xmlStruct.ClientVersion, OpcodeName: xmlStruct.OpcodeName, PackageName: packageName, Fields: make([]GoField, 0, len(xmlStruct.Fields)), } // Add version suffix if needed if xmlStruct.ClientVersion != "" && xmlStruct.ClientVersion != "1" { goStruct.Name = fmt.Sprintf("%sV%s", goStruct.Name, xmlStruct.ClientVersion) } // Track field names to avoid duplicates fieldNames := make(map[string]int) for _, field := range xmlStruct.Fields { if field.Type == "array" || field.Type == "Array" { // Handle array with nested structure if len(field.Children) > 0 { arrayElements := convertArrayChildren(field.Children) structType := buildAnonymousStructType(arrayElements) // Check for duplicate field names and make unique if needed baseName := toGoName(field.Name) finalName := baseName if count, exists := fieldNames[baseName]; exists { finalName = fmt.Sprintf("%s_%d", baseName, count+1) fieldNames[baseName] = count + 1 } else { fieldNames[baseName] = 1 } goField := GoField{ Name: field.Name, GoName: finalName, Type: "[]" + structType, IsDynamicArray: true, ArraySizeVariable: field.ArraySizeVariable, ArrayElements: arrayElements, IfVarSet: field.IfVarSet, IfVarNotSet: field.IfVarNotSet, } // Generate struct tag tag := fmt.Sprintf("`eq2:\"%s", field.Name) if field.ArraySizeVariable != "" { tag += fmt.Sprintf(",sizeVar:%s", field.ArraySizeVariable) } if field.IfVarSet != "" { tag += fmt.Sprintf(",ifSet:%s", field.IfVarSet) } tag += "\"`" goField.Tag = tag goStruct.Fields = append(goStruct.Fields, goField) } else { // Simple array (shouldn't happen but handle it) log.Printf("Warning: array field %s has no children", field.Name) } } else if field.Type == "equipmentItem" { // Handle equipment item arrays // Check for duplicate field names and make unique if needed baseName := toGoName(field.Name) finalName := baseName if count, exists := fieldNames[baseName]; exists { finalName = fmt.Sprintf("%s_%d", baseName, count+1) fieldNames[baseName] = count + 1 } else { fieldNames[baseName] = 1 } goField := GoField{ Name: field.Name, GoName: finalName, Type: fmt.Sprintf("[%d]types.EquipmentItem", field.Size), IsArray: true, Size: field.Size, } tag := fmt.Sprintf("`eq2:\"%s,size:%d\"`", field.Name, field.Size) goField.Tag = tag goStruct.Fields = append(goStruct.Fields, goField) } else { // Regular field goType, _ := mapSimpleType(field.Type) isArray := field.Size > 0 // Check for duplicate field names and make unique if needed baseName := toGoName(field.Name) finalName := baseName if count, exists := fieldNames[baseName]; exists { finalName = fmt.Sprintf("%s_%d", baseName, count+1) fieldNames[baseName] = count + 1 } else { fieldNames[baseName] = 1 } goField := GoField{ Name: field.Name, GoName: finalName, Type: goType, IsArray: isArray, Size: field.Size, IfVarSet: field.IfVarSet, IfVarNotSet: field.IfVarNotSet, } if isArray { goField.Type = fmt.Sprintf("[%d]%s", field.Size, goType) } // Generate struct tag tag := fmt.Sprintf("`eq2:\"%s", field.Name) if field.Type == "str8" || field.Type == "str16" || field.Type == "str32" || field.Type == "EQ2_32Bit_String" || field.Type == "EQ2_16Bit_String" || field.Type == "EQ2_8Bit_String" { tag += fmt.Sprintf(",type:%s", field.Type) } if isArray && field.Size > 0 { tag += fmt.Sprintf(",size:%d", field.Size) } if field.IfVarSet != "" { tag += fmt.Sprintf(",ifSet:%s", field.IfVarSet) } tag += "\"`" goField.Tag = tag // Add comment if needed if strings.HasPrefix(field.Name, "unknown") || strings.HasPrefix(field.Name, "Unknown") { goField.Comment = " // TODO: Identify purpose" } goStruct.Fields = append(goStruct.Fields, goField) } } return goStruct } const structTemplate = `// Code generated by codegen. DO NOT EDIT. // Source: {{.SourceFile}} package {{.PackageName}} import ( "encoding/binary"{{if .NeedsMath}} "math"{{end}} "git.sharkk.net/EQ2/Protocol/types" ) {{range .Structs}} // {{.Name}} represents packet structure for {{if .OpcodeName}}{{.OpcodeName}}{{else}}client version {{.ClientVersion}}{{end}} type {{.Name}} struct { {{- range .Fields}} {{.GoName}} {{.Type}} {{.Tag}}{{if .Comment}} {{.Comment}}{{end}} {{- end}} } // Serialize writes the packet data to the provided buffer func (p *{{.Name}}) Serialize(dest []byte) uint32 { offset := uint32(0) {{range .Fields}} {{- if .IsDynamicArray}} // Write {{.GoName}} array (dynamic size) for _, elem := range p.{{.GoName}} { {{- template "serializeFields" .ArrayElements}} } {{- else if eq .Type "string"}} // Write {{.GoName}} as {{if contains .Tag "str16"}}16-bit{{else if contains .Tag "str32"}}32-bit{{else if contains .Tag "EQ2_32Bit_String"}}32-bit{{else}}8-bit{{end}} length-prefixed string {{- if or (contains .Tag "str16") (contains .Tag "EQ2_16Bit_String")}} binary.LittleEndian.PutUint16(dest[offset:], uint16(len(p.{{.GoName}}))) offset += 2 {{- else if or (contains .Tag "str32") (contains .Tag "EQ2_32Bit_String")}} binary.LittleEndian.PutUint32(dest[offset:], uint32(len(p.{{.GoName}}))) offset += 4 {{- else}} dest[offset] = byte(len(p.{{.GoName}})) offset++ {{- end}} copy(dest[offset:], []byte(p.{{.GoName}})) offset += uint32(len(p.{{.GoName}})) {{- else if .IsArray}} // Write {{.GoName}} array for i := 0; i < {{.Size}}; i++ { {{- if eq (baseType .Type) "float32"}} binary.LittleEndian.PutUint32(dest[offset:], math.Float32bits(p.{{.GoName}}[i])) offset += 4 {{- else if eq (baseType .Type) "float64"}} binary.LittleEndian.PutUint64(dest[offset:], math.Float64bits(p.{{.GoName}}[i])) offset += 8 {{- else if (eq (baseType .Type) "int8")}} dest[offset] = byte(p.{{.GoName}}[i]) offset++ {{- else if or (eq (baseType .Type) "uint8") (eq (baseType .Type) "byte")}} dest[offset] = p.{{.GoName}}[i] offset++ {{- else if or (eq (baseType .Type) "int16") (eq (baseType .Type) "uint16")}} binary.LittleEndian.PutUint16(dest[offset:], uint16(p.{{.GoName}}[i])) offset += 2 {{- else if or (eq (baseType .Type) "int32") (eq (baseType .Type) "uint32")}} binary.LittleEndian.PutUint32(dest[offset:], uint32(p.{{.GoName}}[i])) offset += 4 {{- else if or (eq (baseType .Type) "int64") (eq (baseType .Type) "uint64")}} binary.LittleEndian.PutUint64(dest[offset:], uint64(p.{{.GoName}}[i])) offset += 8 {{- else if eq (baseType .Type) "types.EquipmentItem"}} binary.LittleEndian.PutUint16(dest[offset:], p.{{.GoName}}[i].Type) offset += 2 dest[offset] = p.{{.GoName}}[i].Color.R dest[offset+1] = p.{{.GoName}}[i].Color.G dest[offset+2] = p.{{.GoName}}[i].Color.B offset += 3 dest[offset] = p.{{.GoName}}[i].Highlight.R dest[offset+1] = p.{{.GoName}}[i].Highlight.G dest[offset+2] = p.{{.GoName}}[i].Highlight.B offset += 3 {{- end}} } {{- else}} // Write {{.GoName}} {{- if eq .Type "float32"}} binary.LittleEndian.PutUint32(dest[offset:], math.Float32bits(p.{{.GoName}})) offset += 4 {{- else if eq .Type "float64"}} binary.LittleEndian.PutUint64(dest[offset:], math.Float64bits(p.{{.GoName}})) offset += 8 {{- else if or (eq .Type "int8") (eq .Type "uint8") (eq .Type "byte")}} dest[offset] = byte(p.{{.GoName}}) offset++ {{- else if or (eq .Type "int16") (eq .Type "uint16")}} binary.LittleEndian.PutUint16(dest[offset:], uint16(p.{{.GoName}})) offset += 2 {{- else if eq .Type "types.Color"}} dest[offset] = p.{{.GoName}}.R dest[offset+1] = p.{{.GoName}}.G dest[offset+2] = p.{{.GoName}}.B offset += 3 {{- else if or (eq .Type "int32") (eq .Type "uint32")}} binary.LittleEndian.PutUint32(dest[offset:], uint32(p.{{.GoName}})) offset += 4 {{- else if or (eq .Type "int64") (eq .Type "uint64")}} binary.LittleEndian.PutUint64(dest[offset:], uint64(p.{{.GoName}})) offset += 8 {{- end}} {{- end}} {{end}} return offset } // Size returns the serialized size of the packet func (p *{{.Name}}) Size() uint32 { return types.CalculateSize(p) } {{end}} {{define "serializeFields"}} {{- range .}} {{- if .IsDynamicArray}} // Write nested {{.GoName}} array for _, nestedElem := range elem.{{.GoName}} { {{- template "serializeNestedFields" .ArrayElements}} } {{- else if eq .Type "string"}} // Write {{.GoName}} string field dest[offset] = byte(len(elem.{{.GoName}})) offset++ copy(dest[offset:], []byte(elem.{{.GoName}})) offset += uint32(len(elem.{{.GoName}})) {{- else if .IsArray}} // Write {{.GoName}} array field for i := 0; i < {{.Size}}; i++ { {{- if eq (baseType .Type) "float32"}} binary.LittleEndian.PutUint32(dest[offset:], math.Float32bits(elem.{{.GoName}}[i])) offset += 4 {{- else if eq (baseType .Type) "uint32"}} binary.LittleEndian.PutUint32(dest[offset:], elem.{{.GoName}}[i]) offset += 4 {{- else if eq (baseType .Type) "uint16"}} binary.LittleEndian.PutUint16(dest[offset:], elem.{{.GoName}}[i]) offset += 2 {{- else}} dest[offset] = byte(elem.{{.GoName}}[i]) offset++ {{- end}} } {{- else if eq .Type "float32"}} binary.LittleEndian.PutUint32(dest[offset:], math.Float32bits(elem.{{.GoName}})) offset += 4 {{- else if eq .Type "types.Color"}} dest[offset] = elem.{{.GoName}}.R dest[offset+1] = elem.{{.GoName}}.G dest[offset+2] = elem.{{.GoName}}.B offset += 3 {{- else if eq .Type "uint32"}} binary.LittleEndian.PutUint32(dest[offset:], elem.{{.GoName}}) offset += 4 {{- else if eq .Type "int32"}} binary.LittleEndian.PutUint32(dest[offset:], uint32(elem.{{.GoName}})) offset += 4 {{- else if eq .Type "uint16"}} binary.LittleEndian.PutUint16(dest[offset:], elem.{{.GoName}}) offset += 2 {{- else if eq .Type "int16"}} binary.LittleEndian.PutUint16(dest[offset:], uint16(elem.{{.GoName}})) offset += 2 {{- else if eq .Type "int8"}} dest[offset] = byte(elem.{{.GoName}}) offset++ {{- else if eq .Type "uint8"}} dest[offset] = elem.{{.GoName}} offset++ {{- else}} dest[offset] = byte(elem.{{.GoName}}) offset++ {{- end}} {{- end}} {{end}} {{define "serializeNestedFields"}} {{- range .}} {{- if .IsDynamicArray}} // Write deeply nested {{.GoName}} array for _, deepNested := range nestedElem.{{.GoName}} { // TODO: Handle deeper nesting if needed _ = deepNested } {{- else if eq .Type "string"}} // Write {{.GoName}} string field dest[offset] = byte(len(nestedElem.{{.GoName}})) offset++ copy(dest[offset:], []byte(nestedElem.{{.GoName}})) offset += uint32(len(nestedElem.{{.GoName}})) {{- else if .IsArray}} // Write {{.GoName}} array field for i := 0; i < {{.Size}}; i++ { {{- if eq (baseType .Type) "float32"}} binary.LittleEndian.PutUint32(dest[offset:], math.Float32bits(nestedElem.{{.GoName}}[i])) offset += 4 {{- else if eq (baseType .Type) "uint32"}} binary.LittleEndian.PutUint32(dest[offset:], nestedElem.{{.GoName}}[i]) offset += 4 {{- else if eq (baseType .Type) "uint16"}} binary.LittleEndian.PutUint16(dest[offset:], nestedElem.{{.GoName}}[i]) offset += 2 {{- else}} dest[offset] = nestedElem.{{.GoName}}[i] offset++ {{- end}} } {{- else if eq .Type "float32"}} binary.LittleEndian.PutUint32(dest[offset:], math.Float32bits(nestedElem.{{.GoName}})) offset += 4 {{- else if eq .Type "types.Color"}} dest[offset] = nestedElem.{{.GoName}}.R dest[offset+1] = nestedElem.{{.GoName}}.G dest[offset+2] = nestedElem.{{.GoName}}.B offset += 3 {{- else if eq .Type "uint32"}} binary.LittleEndian.PutUint32(dest[offset:], nestedElem.{{.GoName}}) offset += 4 {{- else if eq .Type "int32"}} binary.LittleEndian.PutUint32(dest[offset:], uint32(nestedElem.{{.GoName}})) offset += 4 {{- else if eq .Type "uint16"}} binary.LittleEndian.PutUint16(dest[offset:], nestedElem.{{.GoName}}) offset += 2 {{- else if eq .Type "int16"}} binary.LittleEndian.PutUint16(dest[offset:], uint16(nestedElem.{{.GoName}})) offset += 2 {{- else if eq .Type "int8"}} dest[offset] = byte(nestedElem.{{.GoName}}) offset++ {{- else if eq .Type "uint8"}} dest[offset] = nestedElem.{{.GoName}} offset++ {{- else}} dest[offset] = byte(nestedElem.{{.GoName}}) offset++ {{- end}} {{- end}} {{end}} ` func contains(s, substr string) bool { return strings.Contains(s, substr) } func baseType(arrayType string) string { // Extract base type from array declaration like "[10]uint32" if strings.HasPrefix(arrayType, "[") { idx := strings.Index(arrayType, "]") if idx > 0 { return arrayType[idx+1:] } } return arrayType } func sizeOf(typeName string) int { // Return the size in bytes for a given type switch typeName { case "int8", "uint8", "byte": return 1 case "int16", "uint16": return 2 case "types.Color": return 3 // RGB: 3 bytes case "int32", "uint32", "float32": return 4 case "int64", "uint64", "float64": return 8 case "types.EquipmentItem": return 8 // 2 bytes type + 3 bytes color + 3 bytes highlight default: return 0 } } func main() { var ( input = flag.String("input", "", "Input XML file or directory") output = flag.String("output", "", "Output Go file or directory") pkgName = flag.String("package", "generated", "Package name for generated code") ) flag.Parse() if *input == "" || *output == "" { fmt.Fprintf(os.Stderr, "Usage: %s -input -output [-package ]\n", os.Args[0]) os.Exit(1) } // Get file info info, err := os.Stat(*input) if err != nil { log.Fatalf("Error accessing input: %v", err) } if info.IsDir() { // Process directory files, err := filepath.Glob(filepath.Join(*input, "*.xml")) if err != nil { log.Fatalf("Error listing XML files: %v", err) } for _, xmlFile := range files { processFile(xmlFile, *output, *pkgName) } } else { // Process single file processFile(*input, *output, *pkgName) } } func processFile(inputFile, outputPath, packageName string) { log.Printf("Processing %s...", inputFile) // Parse XML file structs, err := parseXMLFile(inputFile) if err != nil { log.Printf("Error parsing %s: %v", inputFile, err) return } // Convert to Go structs goStructs := make([]GoStruct, 0, len(structs)) for _, xmlStruct := range structs { goStructs = append(goStructs, convertToGoStruct(xmlStruct, packageName)) } // Determine output file outputFile := outputPath if info, err := os.Stat(outputPath); err == nil && info.IsDir() { base := strings.TrimSuffix(filepath.Base(inputFile), ".xml") outputFile = filepath.Join(outputPath, base+".go") } // Generate code tmpl := template.New("struct") tmpl.Funcs(template.FuncMap{ "contains": contains, "baseType": baseType, "sizeOf": sizeOf, }) // Parse the main template tmpl, err = tmpl.Parse(structTemplate) if err != nil { log.Fatalf("Error parsing template: %v", err) } // Create output file out, err := os.Create(outputFile) if err != nil { log.Fatalf("Error creating output file: %v", err) } defer out.Close() // Check if math package is needed (for float types) needsMath := false for _, goStruct := range goStructs { for _, field := range goStruct.Fields { if strings.Contains(field.Type, "float") { needsMath = true break } } if needsMath { break } } // Execute template data := GenerateOutput{ SourceFile: filepath.Base(inputFile), PackageName: packageName, Structs: goStructs, NeedsMath: needsMath, } if err := tmpl.Execute(out, data); err != nil { log.Fatalf("Error executing template: %v", err) } log.Printf("Generated %s", outputFile) }