340 lines
9.1 KiB
Go
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 ""
|
|
} |