diff --git a/defs/gen/registration.go b/defs/gen/registration.go new file mode 100644 index 0000000..07ea943 --- /dev/null +++ b/defs/gen/registration.go @@ -0,0 +1,175 @@ +//go:build ignore + +package main + +import ( + "encoding/xml" + "fmt" + "io" + "log" + "os" + "path/filepath" + "sort" + "strconv" + "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"` +} + +// Registration represents a packet registration entry +type Registration struct { + StructName string + TypeName string + Version uint16 + OpcodeName string +} + +// RegistrationData holds all registration data for template +type RegistrationData struct { + PackageName string + Registrations []Registration +} + +const registrationTemplate = `// Code generated by go generate; DO NOT EDIT. +// This file was generated from XML packet definitions + +package {{.PackageName}} + +import ( + "reflect" + "git.sharkk.net/EQ2/Protocol/defs/generated" +) + +// initializePacketMappings loads all packet structure definitions +func (pm *PacketManager) initializePacketMappings() { +{{range .Registrations}} pm.registerStruct("{{.StructName}}", {{.Version}}, "{{.OpcodeName}}", reflect.TypeOf(generated.{{.TypeName}}{})) +{{end}}} +` + +func main() { + // Process all XML files + xmlDir := filepath.Join(".", "xml") + outputFile := "registrations_generated.go" + + registrations := []Registration{} + + // Process common.xml + commonFile := filepath.Join(xmlDir, "common.xml") + if regs, err := processXMLFile(commonFile); err == nil { + registrations = append(registrations, regs...) + } else { + log.Printf("Warning: Could not process common.xml: %v", err) + } + + // Process login.xml + loginFile := filepath.Join(xmlDir, "login.xml") + if regs, err := processXMLFile(loginFile); err == nil { + registrations = append(registrations, regs...) + } else { + log.Printf("Warning: Could not process login.xml: %v", err) + } + + // Sort registrations by struct name, then version + sort.Slice(registrations, func(i, j int) bool { + if registrations[i].StructName != registrations[j].StructName { + return registrations[i].StructName < registrations[j].StructName + } + return registrations[i].Version < registrations[j].Version + }) + + // Generate the registration code + if err := generateRegistrationCode(registrations, outputFile); err != nil { + log.Fatalf("Failed to generate registration code: %v", err) + } + + log.Printf("Generated %s with %d registrations", outputFile, len(registrations)) +} + +func processXMLFile(filename string) ([]Registration, 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 in root element if needed + xmlContent := string(data) + if !strings.Contains(xmlContent, "") { + xmlContent = "\n" + xmlContent + "\n" + } + + var def XMLDefinition + if err := xml.Unmarshal([]byte(xmlContent), &def); err != nil { + return nil, fmt.Errorf("failed to parse XML: %v", err) + } + + registrations := []Registration{} + + for _, s := range def.Structs { + if s.OpcodeName == "" { + continue // Skip structs without opcodes + } + + version, err := strconv.ParseUint(s.ClientVersion, 10, 16) + if err != nil { + log.Printf("Warning: Invalid version %s for %s", s.ClientVersion, s.Name) + continue + } + + // Determine the Go type name (remove underscores from struct names) + typeName := strings.ReplaceAll(s.Name, "_", "") + if version > 1 { + typeName = fmt.Sprintf("%sV%d", typeName, version) + } + + registrations = append(registrations, Registration{ + StructName: s.Name, + TypeName: typeName, + Version: uint16(version), + OpcodeName: s.OpcodeName, + }) + } + + return registrations, nil +} + +func generateRegistrationCode(registrations []Registration, outputFile string) error { + tmpl, err := template.New("registration").Parse(registrationTemplate) + if err != nil { + return fmt.Errorf("failed to parse template: %v", err) + } + + out, err := os.Create(outputFile) + if err != nil { + return fmt.Errorf("failed to create output file: %v", err) + } + defer out.Close() + + data := RegistrationData{ + PackageName: "defs", + Registrations: registrations, + } + + if err := tmpl.Execute(out, data); err != nil { + return fmt.Errorf("failed to execute template: %v", err) + } + + return nil +} \ No newline at end of file diff --git a/defs/manager.go b/defs/manager.go new file mode 100644 index 0000000..0d6e1cf --- /dev/null +++ b/defs/manager.go @@ -0,0 +1,297 @@ +package defs + +//go:generate go run gen/registration.go + +import ( + "fmt" + "reflect" + "sort" + "strings" + "sync" + + "git.sharkk.net/EQ2/Protocol/opcodes" +) + +// PacketStruct represents a packet structure definition +type PacketStruct struct { + Name string + Version uint16 + OpcodeName string + StructType reflect.Type +} + +// PacketManager manages packet structure lookups for specific client versions +type PacketManager struct { + version uint16 + structs map[string][]*PacketStruct // map[name][]versions + opcodeToName map[opcodes.EmuOpcode]string + nameToStruct map[string]*PacketStruct // cached lookups for this version + mu sync.RWMutex +} + +// NewPacketManager creates a new packet manager for the specified version +func NewPacketManager(version uint16) *PacketManager { + pm := &PacketManager{ + version: version, + structs: make(map[string][]*PacketStruct), + opcodeToName: make(map[opcodes.EmuOpcode]string), + nameToStruct: make(map[string]*PacketStruct), + } + pm.initializePacketMappings() + return pm +} + +// initializePacketMappings is generated in registrations_generated.go + +// registerStruct adds a packet structure definition +func (pm *PacketManager) registerStruct(name string, version uint16, opcodeName string, structType reflect.Type) { + ps := &PacketStruct{ + Name: name, + Version: version, + OpcodeName: opcodeName, + StructType: structType, + } + + pm.mu.Lock() + defer pm.mu.Unlock() + + if _, exists := pm.structs[name]; !exists { + pm.structs[name] = make([]*PacketStruct, 0) + } + pm.structs[name] = append(pm.structs[name], ps) + + // Map opcode to struct name for quick lookup + // We'll need to add this mapping based on the actual opcode constants + // For now, we map based on the opcode name string + pm.mapOpcodeToStruct(opcodeName, name) +} + +// mapOpcodeToStruct maps opcode names to struct names +// This should be updated when we have a proper opcode name mapping +func (pm *PacketManager) mapOpcodeToStruct(opcodeName string, structName string) { + // Map common opcodes - this will be expanded as needed + switch opcodeName { + case "OP_CreateCharacterRequestMsg": + pm.opcodeToName[opcodes.OP_CreateCharacterRequestMsg] = structName + case "OP_BadLanguageFilter": + pm.opcodeToName[opcodes.OP_BadLanguageFilter] = structName + // Login opcodes would need to be handled differently since they use LS_ prefix + // We'll need to decide how to handle login vs world opcodes + } +} + +// GetStruct returns the best matching packet struct for the given name and version +// It finds the highest version that is <= the requested version (like C++ code) +func (pm *PacketManager) GetStruct(name string, version uint16) *PacketStruct { + pm.mu.RLock() + defer pm.mu.RUnlock() + + // Check cache first + cacheKey := fmt.Sprintf("%s_%d", name, version) + if cached, ok := pm.nameToStruct[cacheKey]; ok { + return cached + } + + versions, exists := pm.structs[name] + if !exists { + return nil + } + + var bestMatch *PacketStruct + for _, ps := range versions { + // Find the highest version that's <= requested version + if ps.Version <= version { + if bestMatch == nil || ps.Version > bestMatch.Version { + bestMatch = ps + } + } + } + + if bestMatch != nil { + // Cache the result + pm.nameToStruct[cacheKey] = bestMatch + } + + return bestMatch +} + +// GetStructByVersion returns an exact version match (like C++ getStructByVersion) +func (pm *PacketManager) GetStructByVersion(name string, version uint16) *PacketStruct { + pm.mu.RLock() + defer pm.mu.RUnlock() + + versions, exists := pm.structs[name] + if !exists { + return nil + } + + for _, ps := range versions { + if ps.Version == version { + return ps + } + } + + return nil +} + +// GetStructByOpcode returns the packet struct for a given opcode +func (pm *PacketManager) GetStructByOpcode(opcode opcodes.EmuOpcode) *PacketStruct { + pm.mu.RLock() + defer pm.mu.RUnlock() + + name, exists := pm.opcodeToName[opcode] + if !exists { + return nil + } + + return pm.GetStruct(name, pm.version) +} + +// CreateInstance creates a new instance of the packet struct +func (pm *PacketManager) CreateInstance(name string) interface{} { + ps := pm.GetStruct(name, pm.version) + if ps == nil { + return nil + } + + return reflect.New(ps.StructType).Interface() +} + +// CreateInstanceByOpcode creates a new instance by opcode +func (pm *PacketManager) CreateInstanceByOpcode(opcode opcodes.EmuOpcode) interface{} { + ps := pm.GetStructByOpcode(opcode) + if ps == nil { + return nil + } + + return reflect.New(ps.StructType).Interface() +} + +// GetAvailableVersions returns all versions available for a struct name +func (pm *PacketManager) GetAvailableVersions(name string) []uint16 { + pm.mu.RLock() + defer pm.mu.RUnlock() + + versions, exists := pm.structs[name] + if !exists { + return nil + } + + var result []uint16 + for _, ps := range versions { + result = append(result, ps.Version) + } + + sort.Slice(result, func(i, j int) bool { + return result[i] < result[j] + }) + + return result +} + +// GetAllStructNames returns all registered struct names +func (pm *PacketManager) GetAllStructNames() []string { + pm.mu.RLock() + defer pm.mu.RUnlock() + + var names []string + for name := range pm.structs { + names = append(names, name) + } + + sort.Strings(names) + return names +} + +// DumpStructs returns a formatted string of all loaded structs for debugging +func (pm *PacketManager) DumpStructs() string { + pm.mu.RLock() + defer pm.mu.RUnlock() + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("Packet Manager for version %d:\n", pm.version)) + sb.WriteString("=====================================\n") + + names := pm.GetAllStructNames() + for _, name := range names { + versions := pm.GetAvailableVersions(name) + sb.WriteString(fmt.Sprintf("%s: versions %v\n", name, versions)) + + // Show which version would be selected + if ps := pm.GetStruct(name, pm.version); ps != nil { + sb.WriteString(fmt.Sprintf(" -> Selected version %d for client %d\n", ps.Version, pm.version)) + } + } + + return sb.String() +} + +// GetStats returns statistics about loaded packet structs +func (pm *PacketManager) GetStats() map[string]int { + pm.mu.RLock() + defer pm.mu.RUnlock() + + totalStructs := 0 + for _, versions := range pm.structs { + totalStructs += len(versions) + } + + return map[string]int{ + "version": int(pm.version), + "unique_structs": len(pm.structs), + "total_versions": totalStructs, + "cached_lookups": len(pm.nameToStruct), + } +} + +// Global managers cache +var ( + managers = make(map[uint16]*PacketManager) + managersMu sync.RWMutex +) + +// GetManager returns packet manager for a version +func GetManager(version uint16) *PacketManager { + managersMu.RLock() + if mgr, ok := managers[version]; ok { + managersMu.RUnlock() + return mgr + } + managersMu.RUnlock() + + managersMu.Lock() + defer managersMu.Unlock() + + // Double-check after acquiring write lock + if mgr, ok := managers[version]; ok { + return mgr + } + + mgr := NewPacketManager(version) + managers[version] = mgr + return mgr +} + +// ClearCache clears the cached managers (useful for testing) +func ClearCache() { + managersMu.Lock() + defer managersMu.Unlock() + managers = make(map[uint16]*PacketManager) +} + +// GetLoadedVersions returns all versions that have managers loaded +func GetLoadedVersions() []uint16 { + managersMu.RLock() + defer managersMu.RUnlock() + + var versions []uint16 + for v := range managers { + versions = append(versions, v) + } + + sort.Slice(versions, func(i, j int) bool { + return versions[i] < versions[j] + }) + + return versions +} \ No newline at end of file diff --git a/defs/registrations_generated.go b/defs/registrations_generated.go new file mode 100644 index 0000000..15cb709 --- /dev/null +++ b/defs/registrations_generated.go @@ -0,0 +1,56 @@ +// Code generated by go generate; DO NOT EDIT. +// This file was generated from XML packet definitions + +package defs + +import ( + "reflect" + "git.sharkk.net/EQ2/Protocol/defs/generated" +) + +// initializePacketMappings loads all packet structure definitions +func (pm *PacketManager) initializePacketMappings() { + pm.registerStruct("BadLanguageFilter", 1, "OP_BadLanguageFilter", reflect.TypeOf(generated.BadLanguageFilter{})) + pm.registerStruct("CreateCharacter", 1, "OP_CreateCharacterRequestMsg", reflect.TypeOf(generated.CreateCharacter{})) + pm.registerStruct("CreateCharacter", 373, "OP_CreateCharacterRequestMsg", reflect.TypeOf(generated.CreateCharacterV373{})) + pm.registerStruct("CreateCharacter", 546, "OP_CreateCharacterRequestMsg", reflect.TypeOf(generated.CreateCharacterV546{})) + pm.registerStruct("CreateCharacter", 561, "OP_CreateCharacterRequestMsg", reflect.TypeOf(generated.CreateCharacterV561{})) + pm.registerStruct("CreateCharacter", 562, "OP_CreateCharacterRequestMsg", reflect.TypeOf(generated.CreateCharacterV562{})) + pm.registerStruct("CreateCharacter", 869, "OP_CreateCharacterRequestMsg", reflect.TypeOf(generated.CreateCharacterV869{})) + pm.registerStruct("CreateCharacter", 1096, "OP_CreateCharacterRequestMsg", reflect.TypeOf(generated.CreateCharacterV1096{})) + pm.registerStruct("CreateCharacter", 57080, "OP_CreateCharacterRequestMsg", reflect.TypeOf(generated.CreateCharacterV57080{})) + pm.registerStruct("CreateCharacter", 60085, "OP_CreateCharacterRequestMsg", reflect.TypeOf(generated.CreateCharacterV60085{})) + pm.registerStruct("CreateCharacter", 64659, "OP_CreateCharacterRequestMsg", reflect.TypeOf(generated.CreateCharacterV64659{})) + pm.registerStruct("CreateCharacter", 65534, "OP_CreateCharacterRequestMsg", reflect.TypeOf(generated.CreateCharacterV65534{})) + pm.registerStruct("LS_CreateCharacterReply", 1, "OP_CreateCharacterReplyMsg", reflect.TypeOf(generated.LSCreateCharacterReply{})) + pm.registerStruct("LS_CreateCharacterReply", 1189, "OP_CreateCharacterReplyMsg", reflect.TypeOf(generated.LSCreateCharacterReplyV1189{})) + pm.registerStruct("LS_CreateCharacterReply", 60085, "OP_CreateCharacterReplyMsg", reflect.TypeOf(generated.LSCreateCharacterReplyV60085{})) + pm.registerStruct("LS_DeleteCharacterRequest", 1, "OP_DeleteCharacterRequestMsg", reflect.TypeOf(generated.LSDeleteCharacterRequest{})) + pm.registerStruct("LS_DeleteCharacterResponse", 1, "OP_DeleteCharacterReplyMsg", reflect.TypeOf(generated.LSDeleteCharacterResponse{})) + pm.registerStruct("LS_LoginReplyMsg", 1, "OP_LoginReplyMsg", reflect.TypeOf(generated.LSLoginReplyMsg{})) + pm.registerStruct("LS_LoginReplyMsg", 284, "OP_LoginReplyMsg", reflect.TypeOf(generated.LSLoginReplyMsgV284{})) + pm.registerStruct("LS_LoginReplyMsg", 843, "OP_LoginReplyMsg", reflect.TypeOf(generated.LSLoginReplyMsgV843{})) + pm.registerStruct("LS_LoginReplyMsg", 1096, "OP_LoginReplyMsg", reflect.TypeOf(generated.LSLoginReplyMsgV1096{})) + pm.registerStruct("LS_LoginReplyMsg", 1142, "OP_LoginReplyMsg", reflect.TypeOf(generated.LSLoginReplyMsgV1142{})) + pm.registerStruct("LS_LoginReplyMsg", 1188, "OP_LoginReplyMsg", reflect.TypeOf(generated.LSLoginReplyMsgV1188{})) + pm.registerStruct("LS_LoginReplyMsg", 57080, "OP_LoginReplyMsg", reflect.TypeOf(generated.LSLoginReplyMsgV57080{})) + pm.registerStruct("LS_LoginReplyMsg", 60100, "OP_LoginReplyMsg", reflect.TypeOf(generated.LSLoginReplyMsgV60100{})) + pm.registerStruct("LS_LoginReplyMsg", 63181, "OP_LoginReplyMsg", reflect.TypeOf(generated.LSLoginReplyMsgV63181{})) + pm.registerStruct("LS_LoginReplyMsg", 65534, "OP_LoginReplyMsg", reflect.TypeOf(generated.LSLoginReplyMsgV65534{})) + pm.registerStruct("LS_LoginRequest", 1, "OP_LoginRequestMsg", reflect.TypeOf(generated.LSLoginRequest{})) + pm.registerStruct("LS_LoginRequest", 562, "OP_LoginRequestMsg", reflect.TypeOf(generated.LSLoginRequestV562{})) + pm.registerStruct("LS_LoginRequest", 1208, "OP_LoginRequestMsg", reflect.TypeOf(generated.LSLoginRequestV1208{})) + pm.registerStruct("LS_PlayRequest", 1, "OP_PlayCharacterRequestMsg", reflect.TypeOf(generated.LSPlayRequest{})) + pm.registerStruct("LS_PlayRequest", 284, "OP_PlayCharacterRequestMsg", reflect.TypeOf(generated.LSPlayRequestV284{})) + pm.registerStruct("LS_PlayResponse", 1, "OP_PlayCharacterReplyMsg", reflect.TypeOf(generated.LSPlayResponse{})) + pm.registerStruct("LS_PlayResponse", 1096, "OP_PlayCharacterReplyMsg", reflect.TypeOf(generated.LSPlayResponseV1096{})) + pm.registerStruct("LS_PlayResponse", 60085, "OP_PlayCharacterReplyMsg", reflect.TypeOf(generated.LSPlayResponseV60085{})) + pm.registerStruct("LS_PlayResponse", 60099, "OP_PlayCharacterReplyMsg", reflect.TypeOf(generated.LSPlayResponseV60099{})) + pm.registerStruct("LS_WorldList", 1, "OP_WorldListMsg", reflect.TypeOf(generated.LSWorldList{})) + pm.registerStruct("LS_WorldList", 373, "OP_WorldListMsg", reflect.TypeOf(generated.LSWorldListV373{})) + pm.registerStruct("LS_WorldList", 546, "OP_WorldListMsg", reflect.TypeOf(generated.LSWorldListV546{})) + pm.registerStruct("LS_WorldList", 562, "OP_WorldListMsg", reflect.TypeOf(generated.LSWorldListV562{})) + pm.registerStruct("LS_WorldList", 60114, "OP_WorldListMsg", reflect.TypeOf(generated.LSWorldListV60114{})) + pm.registerStruct("LS_WorldList", 65534, "OP_WorldListMsg", reflect.TypeOf(generated.LSWorldListV65534{})) + pm.registerStruct("LS_WorldUpdate", 1, "OP_WorldStatusChangeMsg", reflect.TypeOf(generated.LSWorldUpdate{})) +}