1
0
Protocol/defs/gen/main.go

764 lines
22 KiB
Go

//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), "<?xml") && !strings.HasPrefix(strings.TrimSpace(content), "<structs>") {
content = "<structs>" + content + "</structs>"
}
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 <xml-file> -output <go-file> [-package <name>]\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)
}