eq2go/internal/appearances/appearances.go
2025-08-23 16:51:35 -05:00

340 lines
9.1 KiB
Go

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 ""
}