package appearances import ( "fmt" "strings" "sync" "eq2emu/internal/database" "eq2emu/internal/packets" ) // Appearance represents a single appearance entry type Appearance struct { ID int32 `json:"id"` Name string `json:"name"` MinClient int16 `json:"min_client"` } // GetID returns the appearance ID func (a *Appearance) GetID() int32 { return a.ID } // GetName returns the appearance name (C++ API compatibility) func (a *Appearance) GetName() string { return a.Name } // GetNameString returns the name as string (C++ API compatibility) func (a *Appearance) GetNameString() string { return a.Name } // GetMinClientVersion returns the minimum client version func (a *Appearance) GetMinClientVersion() int16 { return a.MinClient } // IsCompatibleWithClient checks if appearance is compatible with client version func (a *Appearance) IsCompatibleWithClient(clientVersion int16) bool { return clientVersion >= a.MinClient } // Manager provides centralized appearance management with packet support type Manager struct { appearances map[int32]*Appearance nameIndex map[string][]*Appearance // For GetAppearanceIDsLikeName compatibility mutex sync.RWMutex db *database.Database // Statistics stats struct { AppearancesLoaded int32 PacketsSent int32 PacketErrors int32 } } // NewManager creates a new appearance manager func NewManager(db *database.Database) *Manager { return &Manager{ appearances: make(map[int32]*Appearance), nameIndex: make(map[string][]*Appearance), db: db, } } // LoadAppearances loads all appearances from database (C++ API compatibility) func (m *Manager) LoadAppearances() error { m.mutex.Lock() defer m.mutex.Unlock() query := `SELECT appearance_id, name, min_client_version FROM appearances ORDER BY appearance_id` rows, err := m.db.Query(query) if err != nil { return fmt.Errorf("failed to query appearances: %w", err) } defer rows.Close() // Clear existing data m.appearances = make(map[int32]*Appearance) m.nameIndex = make(map[string][]*Appearance) count := int32(0) for rows.Next() { appearance := &Appearance{} err := rows.Scan(&appearance.ID, &appearance.Name, &appearance.MinClient) if err != nil { return fmt.Errorf("failed to scan appearance: %w", err) } m.appearances[appearance.ID] = appearance m.updateNameIndex(appearance, true) count++ } if err := rows.Err(); err != nil { return fmt.Errorf("error iterating appearance rows: %w", err) } m.stats.AppearancesLoaded = count return nil } // FindAppearanceByID finds appearance by ID (C++ API compatibility) func (m *Manager) FindAppearanceByID(id int32) *Appearance { m.mutex.RLock() defer m.mutex.RUnlock() return m.appearances[id] } // GetAppearanceID gets appearance ID by name (C++ API compatibility) func (m *Manager) GetAppearanceID(name string) int16 { m.mutex.RLock() defer m.mutex.RUnlock() for _, appearance := range m.appearances { if appearance.Name == name { return int16(appearance.ID) } } return 0 // Not found } // GetAppearanceIDsLikeName finds appearance IDs matching name pattern (C++ API compatibility) func (m *Manager) GetAppearanceIDsLikeName(name string, filtered bool) []int16 { m.mutex.RLock() defer m.mutex.RUnlock() var results []int16 searchName := strings.ToLower(name) // Try indexed lookup first if indexed := m.nameIndex[searchName]; len(indexed) > 0 { for _, appearance := range indexed { if !filtered || m.isFilteredAppearance(appearance.Name) { results = append(results, int16(appearance.ID)) } } return results } // Fallback to pattern matching for _, appearance := range m.appearances { if strings.Contains(strings.ToLower(appearance.Name), searchName) { if !filtered || m.isFilteredAppearance(appearance.Name) { results = append(results, int16(appearance.ID)) } } } return results } // GetAppearanceName gets appearance name by ID (C++ API compatibility) func (m *Manager) GetAppearanceName(appearanceID int16) string { m.mutex.RLock() defer m.mutex.RUnlock() if appearance := m.appearances[int32(appearanceID)]; appearance != nil { return appearance.Name } return "" } // ShowDressingRoom builds and sends a dressing room packet (C++ API compatibility) func (m *Manager) ShowDressingRoom(characterID int32, clientVersion uint32, slot uint32, appearanceID int16, itemID int32, itemCRC int32) ([]byte, error) { packet, exists := packets.GetPacket("DressingRoom") if !exists { m.stats.PacketErrors++ return nil, fmt.Errorf("failed to get DressingRoom packet structure: packet not found") } // Build packet data matching C++ implementation data := map[string]interface{}{ "slot": slot, "appearance_id": uint16(appearanceID), "item_id": itemID, "item_crc": itemCRC, "unknown": uint16(0), "rgb": []float32{1.0, 1.0, 1.0}, // Default white color "highlight_rgb": []float32{1.0, 1.0, 1.0}, // Default white highlight "unknown3": uint8(0), "icon": uint16(0), "unknown4": uint32(0), "unknown5": make([]uint8, 10), // Zero-filled array } builder := packets.NewPacketBuilder(packet, clientVersion, 0) packetData, err := builder.Build(data) if err != nil { m.stats.PacketErrors++ return nil, fmt.Errorf("failed to build dressing room packet: %v", err) } m.stats.PacketsSent++ return packetData, nil } // GetDressingRoomPacket builds a dressing room packet (alternative API) func (m *Manager) GetDressingRoomPacket(characterID int32, clientVersion uint32, appearanceData map[string]interface{}) ([]byte, error) { return m.ShowDressingRoom( characterID, clientVersion, appearanceData["slot"].(uint32), appearanceData["appearance_id"].(int16), appearanceData["item_id"].(int32), appearanceData["item_crc"].(int32), ) } // GetAppearanceCount returns total number of appearances func (m *Manager) GetAppearanceCount() int { m.mutex.RLock() defer m.mutex.RUnlock() return len(m.appearances) } // GetStatistics returns current statistics func (m *Manager) GetStatistics() map[string]interface{} { m.mutex.RLock() defer m.mutex.RUnlock() return map[string]interface{}{ "appearances_loaded": m.stats.AppearancesLoaded, "packets_sent": m.stats.PacketsSent, "packet_errors": m.stats.PacketErrors, "total_appearances": int32(len(m.appearances)), } } // GetCompatibleAppearances returns appearances compatible with client version func (m *Manager) GetCompatibleAppearances(clientVersion int16) []*Appearance { m.mutex.RLock() defer m.mutex.RUnlock() var result []*Appearance for _, appearance := range m.appearances { if appearance.IsCompatibleWithClient(clientVersion) { result = append(result, appearance) } } return result } // updateNameIndex updates the name-based search index func (m *Manager) updateNameIndex(appearance *Appearance, add bool) { name := strings.ToLower(appearance.Name) // Index full name if add { m.nameIndex[name] = append(m.nameIndex[name], appearance) } else { if entries := m.nameIndex[name]; entries != nil { for i, entry := range entries { if entry.ID == appearance.ID { m.nameIndex[name] = append(entries[:i], entries[i+1:]...) break } } } } // Index path components for file-like names if strings.Contains(name, "/") { parts := strings.Split(name, "/") for _, part := range parts { if len(part) > 0 { if add { m.nameIndex[part] = append(m.nameIndex[part], appearance) } else { if entries := m.nameIndex[part]; entries != nil { for i, entry := range entries { if entry.ID == appearance.ID { m.nameIndex[part] = append(entries[:i], entries[i+1:]...) break } } } } } } } } // isFilteredAppearance checks if appearance should be filtered out (matches C++ filtering logic) func (m *Manager) isFilteredAppearance(name string) bool { lowerName := strings.ToLower(name) // Filter out ghost, headless, elemental, test, zombie, vampire appearances filterTerms := []string{"ghost", "headless", "elemental", "test", "zombie", "vampire"} for _, term := range filterTerms { if strings.Contains(lowerName, term) { return false } } return true } // Global manager instance var globalManager *Manager // InitializeManager initializes the global appearance manager func InitializeManager(db *database.Database) error { globalManager = NewManager(db) return globalManager.LoadAppearances() } // GetManager returns the global appearance manager func GetManager() *Manager { return globalManager } // Global functions for C++ API compatibility func FindAppearanceByID(id int32) *Appearance { if globalManager != nil { return globalManager.FindAppearanceByID(id) } return nil } func GetAppearanceID(name string) int16 { if globalManager != nil { return globalManager.GetAppearanceID(name) } return 0 } func GetAppearanceIDsLikeName(name string, filtered bool) []int16 { if globalManager != nil { return globalManager.GetAppearanceIDsLikeName(name, filtered) } return nil } func GetAppearanceName(appearanceID int16) string { if globalManager != nil { return globalManager.GetAppearanceName(appearanceID) } return "" }